一、reinject 机制
在 Open vSwitch 的数据包转发过程中,当数据包在 Datapath 模块无法完全处理时,会通过 upcall 调用将数据包交给用户空间的 vswitchd 守护进程进行进一步的处理。当 vswitchd 守护进程完成处理后,会将收到的数据包从用户空间发送回内核空间,让 Datapath 模块根据最新的流表重新匹配,并执行相应的行为。此外,这里的 reinject 过程也是通过 Netlink 实现内核空间和用户空间之间消息传递的。
二、数据包接收执行 ovs_packet_cmd_execute()
函数 ovs_packet_cmd_execute() 是 Datapath 模块实现基于流表转发的核心部分,存储在 ovs-main/datapath/datapath.c 文件中:
static int ovs_packet_cmd_execute(struct sk_buff *skb, struct genl_info *info) {
struct ovs_header *ovs_header = info->userhdr;
struct net *net = sock_net(skb->sk);
struct nlattr **a = info->attrs;
struct sw_flow_actions *acts;
struct sk_buff *packet;
struct sw_flow *flow;
struct sw_flow_actions *sf_acts;
struct datapath *dp;
struct vport *input_vport;
u16 mru = 0;
u64 hash;
int len;
int err;
bool log = !a[OVS_PACKET_ATTR_PROBE];
err = -EINVAL;
if (!a[OVS_PACKET_ATTR_PACKET] || !a[OVS_PACKET_ATTR_KEY] || !a[OVS_PACKET_ATTR_ACTIONS])
goto err;
len = nla_len(a[OVS_PACKET_ATTR_PACKET]);
packet = __dev_alloc_skb(NET_IP_ALIGN + len, GFP_KERNEL);
err = -ENOMEM;
if (!packet)
goto err;
skb_reserve(packet, NET_IP_ALIGN);
nla_memcpy(__skb_put(packet, len), a[OVS_PACKET_ATTR_PACKET], len);
/* Set packet's mru */
if (a[OVS_PACKET_ATTR_MRU]) {
mru = nla_get_u16(a[OVS_PACKET_ATTR_MRU]);
packet->ignore_df = 1;
}
OVS_CB(packet)->mru = mru;
if (a[OVS_PACKET_ATTR_HASH]) {
hash = nla_get_u64(a[OVS_PACKET_ATTR_HASH]);
__skb_set_hash(packet, hash & 0xFFFFFFFFULL, !!(hash & OVS_PACKET_HASH_SW_BIT), !!(hash & OVS_PACKET_HASH_L4_BIT));
}
/* Build an sw_flow for sending this packet. */
flow = ovs_flow_alloc();
err = PTR_ERR(flow);
if (IS_ERR(flow))
goto err_kfree_skb;
err = ovs_flow_key_extract_userspace(net, a[OVS_PACKET_ATTR_KEY], packet, &flow->key, log);
if (err)
goto err_flow_free;
err = ovs_nla_copy_actions(net, a[OVS_PACKET_ATTR_ACTIONS], &flow->key, &acts, log);
if (err)
goto err_flow_free;
rcu_assign_pointer(flow->sf_acts, acts);
packet->priority = flow->key.phy.priority;
packet->mark = flow->key.phy.skb_mark;
rcu_read_lock();
dp = get_dp_rcu(net, ovs_header->dp_ifindex);
err = -ENODEV;
if (!dp)
goto err_unlock;
input_vport = ovs_vport_rcu(dp, flow->key.phy.in_port);
if (!input_vport)
input_vport = ovs_vport_rcu(dp, OVSP_LOCAL);
if (!input_vport)
goto err_unlock;
packet->dev = input_vport->dev;
OVS_CB(packet)->input_vport = input_vport;
sf_acts = rcu_dereference(flow->sf_acts);
local_bh_disable();
err = ovs_execute_actions(dp, packet, sf_acts, &flow->key);
local_bh_enable();
rcu_read_unlock();
ovs_flow_free(flow, false);
return err;
err_unlock:
rcu_read_unlock();
err_flow_free:
ovs_flow_free(flow, false);
err_kfree_skb:
kfree_skb(packet);
err:
return err;
}
函数的第一个输入参数 struct sk_buff *skb 代表接收到的数据包(包含 Netlink 消息的 socket 缓冲区),第二个输入参数 struct genl_info *info 代表数据包相应的信息和属性(包含 Netlink 消息的元数据)。
函数首先通过 packet = __dev_alloc_skb(NET_IP_ALIGN + len, GFP_KERNEL) 分配一个新的 sk_buff 结构体,并通过 nla_memcpy(__skb_put(packet, len), a[OVS_PACKET_ATTR_PACKET], len) 将相应属性中的数据拷贝到新分配的 sk_buff 中。
然后函数通过 flow = ovs_flow_alloc() 分配一个新的 sw_flow 结构体,并使用 ovs_flow_key_extract_userspace(net, a[OVS_PACKET_ATTR_KEY], packet, &flow->key, log) 提取流表项的关键字段存储在 sw_flow 中。
函数使用 ovs_nla_copy_actions(net, a[OVS_PACKET_ATTR_ACTIONS], &flow->key, &acts, log) 提取动作列表,并存储在 sw_flow 的 sf_acts 字段中。接下来使用 dp = get_dp_rcu(net, ovs_header->dp_ifindex) 获取与 ovs_header->dp_ifindex 对应的 datapath 对象,并使用 input_vport = ovs_vport_rcu(dp, flow->key.phy.in_port) 从 datapath 中获取与数据包输入端口 flow->key.phy.in_port 对应的虚拟端口对象。注意此时如果找不到输入端口,则使用本地 OVSP_LOCAL 端口作为输入端口。
函数使用 packet->dev = input_vport->dev 和 OVS_CB(packet)->input_vport = input_vport 将 sk_buff 的 dev 字段设置为输入虚拟端口的设备,并将输入虚拟端口存储在 OVS_CB(packet)->input_vport 中。最后,函数调用 ovs_execute_actions(dp, packet, sf_acts, &flow->key) 函数来执行之前提取的行为列表,并返回执行结果。
总的来说,这个函数的主要作用是处理通过 Netlink 接口发送到 OVS 内核模块的数据包。它根据提供的属性信息构建一个 sw_flow 对象,并在 Datapath 模块中执行相应的行为,最终将数据包发送出去。
Tips:上述函数中提到的一些数据包相关属性都包含在 ovs_packet_attr 结构体中,定义在 ovs-main/datapath/linux/compat/include/linux/openvswitch.h 头文件中:
enum ovs_packet_attr {
OVS_PACKET_ATTR_UNSPEC,
OVS_PACKET_ATTR_PACKET, /* Packet data. */
OVS_PACKET_ATTR_KEY, /* Nested OVS_KEY_ATTR_* attributes. */
OVS_PACKET_ATTR_ACTIONS, /* Nested OVS_ACTION_ATTR_* attributes. */
OVS_PACKET_ATTR_USERDATA, /* OVS_ACTION_ATTR_USERSPACE arg. */
OVS_PACKET_ATTR_EGRESS_TUN_KEY, /* Nested OVS_TUNNEL_KEY_ATTR_attributes. */
OVS_PACKET_ATTR_UNUSED1,
OVS_PACKET_ATTR_UNUSED2,
OVS_PACKET_ATTR_PROBE, /* Packet operation is a feature probe, error logging should be suppressed. */
OVS_PACKET_ATTR_MRU, /* Maximum received IP fragment size. */
OVS_PACKET_ATTR_LEN, /* Packet size before truncation. */
OVS_PACKET_ATTR_HASH, /* Packet hash. */
__OVS_PACKET_ATTR_MAX
};
这些属性为 OVS 提供了丰富的数据包相关信息,可用于各种网络处理和分析任务。
Tips:上述函数中提到了 RCU 及其相关的函数,相关内容在 Linux 内核中实现。
RCU 是一种用于并发编程的技术,旨在提供高效且无锁(lock-free)的读操作,同时保证数据一致性和并发性(也就是说它并不需要锁的机制来保障数据一致性和并发性)。
RCU 的功能本质上是为了解决读写锁问题中读锁被写锁阻塞导致的性能开销。RCU 的具体实现原理和步骤如下所示:
- 初始阶段:所有读取线程可以同时访问共享数据,而不需要任何同步机制。因为在此阶段,尚未发生任何写操作,因此读取操作不会访问到不一致或无效的数据。
- 更新阶段:当需要修改共享数据时,RCU 采用写时复制(copy-on-write)的策略:它首先创建一个新的数据副本,然后将修改应用于该副本,而不是直接修改共享数据。这确保了在更新期间,读取线程仍然可以访问到旧版本的数据,而不会被更新操作所影响。
- 发布阶段:在更新完成后,RCU 使用一种特定的机制来通知读取线程有新的数据副本可用。这个机制类似于发布-订阅的模型,其中读取线程可以订阅通知,并在新的数据副本发布时接收到通知。通过这个发布机制,读取线程可以感知到新的数据副本,并在需要时切换到新版本的数据。
- 回收阶段: 在发布新的数据副本后,旧版本的数据并不立即被回收。RCU 采用延迟回收的策略,在确认所有读取线程都不再使用旧数据副本之后,才进行回收操作。这通常涉及使用计数器或其他形式的跟踪机制,以确保在旧版本的数据没有被使用时进行回收,从而避免破坏读取线程的正确性。
总结:
在 Open vSwitch 的 Datapath 模块中,主要通过 ovs_packet_cmd_execute() 函数在内核空间实现 reinject 数据包的接受,并执行相应的行为。
接下来讨论两个有趣的问题:
在 Open vSwitch 的数据包转发流程中,为什么需要 reinject 机制将发送到用户空间的数据包再发回来,而不是直接使用之前内核空间拷贝的数据包备份呢?
答:直接使用内核空间的数据包备份可能会限制未来对数据包处理流程的扩展和优化。也就是说,在数据包的处理过程中,可能会对数据包内容进行修改,变得与刚进入 OVS 时不同。此外 reinject 机制提供了一个通用的接口,使得未来可以轻松地扩展和优化数据包处理流程,而无需重大的架构变更。所以说尽管 reinject 机制会引入一些性能开销,但是它带来的灵活性和可扩展性通常能很好地弥补这一点(除非数据包的格式标准变得非常统一)。
上述函数在以下内容中使用了 RCU:
- rcu_assign_pointer(flow->sf_acts, acts) 用于将 acts 结构体赋值给 flow->sf_acts 指针
- rcu_read_lock() 和 rcu_read_unlock() 使用了 RCU 读锁定,保证获取 datapath 和 input_vport 对象的时候它们不会被删除或修改
- rcu_dereference(flow->sf_acts) 用于在执行 ovs_execute_actions() 函数时安全地读取 flow->sf_acts 指针。
为什么要使用 RCU 呢?
答:这里使用 RCU 的主要作用是通过简单高效的并发执行机制,确保在进行相关操作时相应的内容不会被删除或修改,提高并发性能的同时避免死锁。可以看到,虽然 ovs_packet_cmd_execute() 承担了 reinject 机制的接收行为,但是它也负责数据包的转发。所以这里使用 RCU 是 OVS 提高数据包转发性能和稳定性的重要措施。
由于本人水平有限,以上内容如有不足之处欢迎大家指正(评论区/私信均可)。