一、Open vSwitch 数据包的来源
Open vSwitch 中的数据包有许多种来源:
- 物理网络接口:OVS 可以连接到物理网络设备,并处理从这些设备收到的数据包。这些数据包可能来自外部网络,需要被转发或进一步处理。
- 虚拟网络接口:OVS 可以创建和管理虚拟网络接口,如 OVS 内部端口(vport)或 Linux 网桥端口。这些虚拟接口可能来自虚拟机、容器或其他网络应用程序,数据包也需要被转发或进一步处理。
- 隧道连接:OVS 支持多种隧道协议,如 VXLAN 和 GRE 等,可以从这些隧道收到数据包。这些数据包需要被解封装并转发到正确的目的地。
- 内部生成:OVS 自身可能会生成一些控制和管理类型的数据包,例如用于实现 OpenFlow 协议、OVSDB 协议等。这些数据包需要被特殊处理。
- 用户空间:OVS 可以接收来自用户空间应用程序的数据包,例如通过 AF_PACKET 套接字或 netdev-based 接口。这些数据包需要被转发到适当的目的地。
对于 Open vSwitch 数据包的接收过程而言,来自物理网络接口、虚拟网络接口和隧道连接的数据包更为常见。在代码实现中,对于不同的来源和不同类型的数据包会有不同的接收和处理策略,比如内核模块 openvswitch.ko 在网卡上注册的 netdev_frame_hook() 函数和不同隧道的 rcv 接收函数。这里我们忽略不同的前置接收方式,重点关注 Datapath 模块对数据包的接收过程。
从图中可以看出,无论何种接收方式,最终都会汇集到 netdev_port_receive() 函数,即都需要调用这个函数。所以我们以 netdev_port_receive() 函数为起点,分析 Open vSwitch 数据包的接收过程。
二、数据包接收 netdev_port_receive()
函数 netdev_port_receive() 主要负责处理从网络设备接收的数据包,存储在 ovs-main/datapath/vport-netdev.c 文件中:
/* Must be called with rcu_read_lock. */
void netdev_port_receive(struct sk_buff *skb, struct ip_tunnel_info *tun_info) {
struct vport *vport;
vport = ovs_netdev_get_vport(skb->dev);
if (unlikely(!vport))
goto error;
if (unlikely(skb_warn_if_lro(skb)))
goto error;
/* Make our own copy of the packet.
* Otherwise we will mangle the packet for anyone who came before us (e.g. tcpdump via AF_PACKET). */
skb = skb_share_check(skb, GFP_ATOMIC);
if (unlikely(!skb))
return;
if (skb->dev->type == ARPHRD_ETHER) {
skb_push(skb, ETH_HLEN);
skb_postpush_rcsum(skb, skb->data, ETH_HLEN);
}
ovs_vport_receive(vport, skb, tun_info);
return;
error:
kfree_skb(skb);
}
函数的第一个输入参数 struct sk_buff *skb 代表接收到的数据包,第二个输入参数 struct ip_tunnel_info *tun_info 包含了数据包相关的隧道信息。注意这里的 skb 是一个指向 sk_buff 结构体的指针,代表要被处理的网络数据包,其中 sk_buff 是 Linux 内核中用于表示网络数据包的核心结构体,参见下图:
函数首先通过 vport = ovs_netdev_get_vport(skb->dev) 获取与数据包关联的 vport 对象(代表了一个 OVS 的虚拟端口),并使用 skb = skb_share_check(skb, GFP_ATOMIC) 创建数据包的副本,以免影响其他使用该数据包的实体(比如 tcpdump 或镜像端口)。如果数据包来自以太网设备(ARPHRD_ETHER),则添加以太网头部(ETH_HLEN)并更新校验和。最后调用 ovs_vport_receive(vport, skb, tun_info) 函数将数据包转发给 OVS 虚拟端口进行进一步处理。
三、数据包传递 ovs_vport_receive()
函数 ovs_vport_receive() 用于将接收到的数据包传递到数据平面进行处理,存储在 ovs-main/datapath/vport.c 文件中:
/* Must be called with rcu_read_lock.
* The packet cannot be shared and skb->data should point to the Ethernet header. */
int ovs_vport_receive(struct vport *vport, struct sk_buff *skb, const struct ip_tunnel_info *tun_info) {
struct sw_flow_key key;
int error;
OVS_CB(skb)->input_vport = vport;
OVS_CB(skb)->mru = 0;
OVS_CB(skb)->cutlen = 0;
if (unlikely(dev_net(skb->dev) != ovs_dp_get_net(vport->dp))) {
u32 mark;
mark = skb->mark;
skb_scrub_packet(skb, true);
skb->mark = mark;
tun_info = NULL;
}
ovs_skb_init_inner_protocol(skb);
skb_clear_ovs_gso_cb(skb);
/* Extract flow from 'skb' into 'key'. */
error = ovs_flow_key_extract(tun_info, skb, &key);
if (unlikely(error)) {
kfree_skb(skb);
return error;
}
ovs_dp_process_packet(skb, &key);
return 0;
}
函数的第一个输入参数 struct vport *vport 是在 netdev_port_receive() 函数中关联的 vport 对象,其他参数和 netdev_port_receive() 函数相同。
函数首先设置 OVS_CB(skb) 结构体中的一些字段如 input_vport、mru 和 cutlen,其中 OVS_CB(skb) 结构体主要用于在 sk_buff 结构体的私有数据区域 (skb->cb) 存储 OVS 的相关的信息,定义在 ovs-main/datapath/datapath.h 头文件中:
/* struct ovs_skb_cb - OVS data in skb CB */
struct ovs_skb_cb {
struct vport *input_vport;
u16 mru;
u16 acts_origlen;
u32 cutlen;
};
#define OVS_CB(skb) ((struct ovs_skb_cb *)(skb)->cb)
然后调用 ovs_skb_init_inner_protocol(skb) 函数初始化数据包的内部协议信息,并调用 skb_clear_ovs_gso_cb(skb) 函数清除 skb 中与 OVS 分组服务相关的回调函数。接下来使用 error = ovs_flow_key_extract(tun_info, skb, &key) 从 skb 中提取流的 key 值信息到 key 结构体中,如果提取失败还需要返回错误码。最后,调用 ovs_dp_process_packet(skb, &key) 函数将数据包及其 key 值信息传递到数据平面进行处理。
四、数据包处理 ovs_dp_process_packet()
函数 ovs_dp_process_packet() 主要实现 OVS 数据平面的数据包处理逻辑,存储在 ovs-main/datapath/datapath.c 头文件中:
/* Must be called with rcu_read_lock. */
void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key) {
const struct vport *p = OVS_CB(skb)->input_vport;
struct datapath *dp = p->dp;
struct sw_flow *flow;
struct sw_flow_actions *sf_acts;
struct dp_stats_percpu *stats;
u64 *stats_counter;
u32 n_mask_hit;
int error;
stats = this_cpu_ptr(dp->stats_percpu);
/* Look up flow. */
flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb), &n_mask_hit);
if (unlikely(!flow)) {
struct dp_upcall_info upcall;
memset(&upcall, 0, sizeof(upcall));
upcall.cmd = OVS_PACKET_CMD_MISS;
upcall.portid = ovs_vport_find_upcall_portid(p, skb);
upcall.mru = OVS_CB(skb)->mru;
error = ovs_dp_upcall(dp, skb, key, &upcall, 0);
if (unlikely(error))
kfree_skb(skb);
else
consume_skb(skb);
stats_counter = &stats->n_missed;
goto out;
}
ovs_flow_stats_update(flow, key->tp.flags, skb);
sf_acts = rcu_dereference(flow->sf_acts);
error = ovs_execute_actions(dp, skb, sf_acts, key);
if (unlikely(error))
net_dbg_ratelimited("ovs: action execution error on datapath %s: %d\n", ovs_dp_name(dp), error);
stats_counter = &stats->n_hit;
out:
/* Update datapath statistics. */
u64_stats_update_begin(&stats->syncp);
(*stats_counter)++;
stats->n_mask_hit += n_mask_hit;
u64_stats_update_end(&stats->syncp);
}
函数的第一个输入参数 struct sk_buff *skb 代表接收到的数据包,第二个参数 struct sw_flow_key *key 是在 ovs_vport_receive() 函数中调用 ovs_flow_key_extract() 函数获取的 key 值信息。
函数首先使用 flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb), &n_mask_hit),以根据数据包的 hash 值和 key 结构体在数据平面的流表中查找匹配的流表项 flow。此时根据是否成功匹配流表分别采用的不同的数据包处理策略。
新流处理(数据包首次处理):如果找不到匹配的流表项,则会创建一个 upcall 结构体,包含 OVS_PACKET_CMD_MISS 命令和其他必要信息。然后调用 ovs_dp_upcall(dp, skb, key, &upcall, 0) 函数,将数据包发送到 vswitchd 守护进程进行处理。这里对应 Open vSwitch 数据包处理流程中的慢速路径。
已知流处理(数据包非首次处理):如果存在匹配的流表项,则更新流表项的统计信息(如包计数和标志位),并获取与流表项关联的动作集 sf_acts。接下来调用 ovs_execute_actions(dp, skb, sf_acts, key) 函数,执行与该流关联的数据包处理动作。这里对应 Open vSwitch 数据包处理流程中的快速路径。
最后,更新数据平面的统计信息。这里使用 u64_stats_update_begin() 和 u64_stats_update_end() 原子更新的方式,避免出现数据的不一致。
五、同步机制
细心的你可能发现了,在数据包接收的整个流程中,每个函数最开头的注释都提到了:
/* Must be called with rcu_read_lock. */
这里的 rcu_read_lock() 是 Linux 内核中实现读-写锁的一种机制,即 RCU(Read-Copy-Update),具有以下特点:
- rcu_read_lock() 通过 rcu_read_lock() 和 rcu_read_unlock() 来标记和保护临界区资源,并且在受保护的临界区内,读取线程可以安全地访问数据结构,不会观察到中间状态。
- RCU 允许读取线程并发地访问共享数据,而不会被写入线程的修改所干扰(这种无锁的并发读写机制主要是通过延迟释放旧版本的数据结构来实现的)
- 相比传统的读写锁,RCU 在读取密集型场景下能提供更好的性能,因为读取线程不需要获取锁,减少了竞争和上下文切换
总的来说,Open vSwitch 通过 RCU 机制确保了在处理数据包时,不会观察到数据结构的中间状态,从而保证数据包处理的正确性。
Tips:细心的你可能又发现了,在数据包接收的整个流程中,出现过很多关于 error 的判断,并且对于所有 error 的处理方式都差不多,即丢弃数据包并释放空间。也就是说 Open vSwitch 在数据包接收的过程中是没有反馈机制的,出现问题直接丢掉。其实这是非常合理的,因为对于数据包的传输而言,网络层主要提供的是不可靠的服务,可靠的服务会在链路层和传输层实现。
总结:
本文介绍了 Open vSwitch 数据包接收过程的源码实现,其中函数 ovs_dp_process_packet() 是 Datapath 模块进行数据包处理的核心函数,非常重要!
数据包的接收过程的调用关系如上图所示,并以此函数作为终点,但是对于 upcall 调用和数据包匹配和发送等内容而言,则将以此函数作为起点来讨论。
由于本人水平有限,以上内容如有不足之处欢迎大家指正(评论区/私信均可)。
参考资料:
Open vSwitch 官网
Open vSwitch 源代码 GitHub
关键数据结构 sk_buff_skb 的 freeback 字段-CSDN博客
Open vSwitch v2.17.10 LTS 源代码
如何提高 Linux RCU 实时性-望获OS
Linux 网络协议栈 ovs 收发包 - 简书
OVS - 数据包处理流程 ovs 的工作流程-CSDN博客
Open vSwitch 2.3.90 源码阅读笔记(上) | SDNLAB