一、upcall 调用
Open vSwitch 中的 upcall 调用发生在数据包无法在内核中完全处理时,比如内核模块 Datapath 在数据包处理过程中匹配不到流表项,需要控制器下发流表的场景。在 upcall 调用的过程中,数据包的路径可以细分为内核态路径和用户态路径两个部分。内核态的路径通常从 Datapath 模块无法匹配流表后进行的 upcall 调用开始,直到将数据包发往用户态为止;用户态的路径通常从交换机维护的底层 upcall 接收函数开始,直到将数据包提取到 ovs-vswitchd 守护进程为止。整个过程借助 Netlink 实现在内核空间和用户空间之间的消息传递。
本文关注 upcall 调用过程中的内核态路径,以数据包到达 Datapath 模块进行流表匹配为起点,直到数据包从内核态发送至用户态为止。
二、数据包处理 ovs_dp_process_packet()
函数 ovs_dp_process_packet() 是 Datapath 模块进行数据包处理的核心函数,主要实现 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) {
......
/* 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;
}
......
}
函数的第一个输入参数 struct sk_buff *skb 代表接收到的数据包,第二个参数 struct sw_flow_key *key 代表数据包的 key 值信息。
函数使用 flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb), &n_mask_hit),以根据数据包的 hash 值和 key 结构体在数据平面的流表中查找匹配的流表项。如果找不到匹配的流表项,则会创建一个 upcall 信息结构体,包含 OVS_PACKET_CMD_MISS 命令和其他必要信息。
这里的 upcall 信息结构体 dp_upcall_info 定义在 ovs-main/datapath/datapath.h 头文件中:
struct dp_upcall_info {
struct ip_tunnel_info *egress_tun_info;
const struct nlattr *userdata;
const struct nlattr *actions;
int actions_len;
u32 portid;
u8 cmd;
u16 mru;
};
其中 struct ip_tunnel_info *egress_tun_info 是指向 ip_tunnel_info 结构体的指针,用于存储出口隧道信息,结构体 ip_tunnel_info 定义在 ovs-main/datapath/linux/compat/include/net/ip_tunnels.h 头文件中:
struct ip_tunnel_info {
struct ip_tunnel_key key;
struct dst_cache dst_cache;
u8 options_len;
u8 mode;
};
const struct nlattr *userdata 是指向 nlattr 结构体(Netlink 属性)的指针,用于存储用户定义的数据,结构体 nlattr 定义在 ovs-main/lib/netlink-protocol.h 头文件中:
struct nlattr {
uint16_t nla_len;
uint16_t nla_type;
};
BUILD_ASSERT_DECL(sizeof(struct nlattr) == 4);
而 const struct nlattr *actions 是另一个指向 nlattr 结构体的指针,用于存储与数据包处理相关的动作或规则。
此外,整数 actions_len 用于存储 actions 数组的长度,即有多少个动作或规则。整数 portid 用于存储端口 ID,整数 cmd 用于存储处理数据包的命令码,整数 mru 表示最大接收单元 MRU 的大小。
在构建完 upcall 信息结构体后,将会调用 ovs_dp_upcall(dp, skb, key, &upcall, 0) 函数,开启 upcall 调用的流程。
三、数据包转发 ovs_dp_upcall()
函数 ovs_dp_upcall() 主要将网络数据包转发到用户空间,同时也跟踪在这个过程中丢失的数据包数量,存储在 ovs-main/datapath/datapath.c 文件中:
int ovs_dp_upcall(struct datapath *dp, struct sk_buff *skb, const struct sw_flow_key *key, const struct dp_upcall_info *upcall_info, uint32_t cutlen) {
struct dp_stats_percpu *stats;
int err;
if (upcall_info->portid == 0) {
err = -ENOTCONN;
goto err;
}
if (!skb_is_gso(skb))
err = queue_userspace_packet(dp, skb, key, upcall_info, cutlen);
else
err = queue_gso_packets(dp, skb, key, upcall_info, cutlen);
if (err)
goto err;
return 0;
err:
stats = this_cpu_ptr(dp->stats_percpu);
u64_stats_update_begin(&stats->syncp);
stats->n_lost++;
u64_stats_update_end(&stats->syncp);
return err;
}
函数的第一个输入参数 struct datapath *dp 代表数据路径 Datapath 结构,第二个参数 struct sk_buff *skb 代表网络数据包,第三个参数 const struct sw_flow_key *key 代表数据包的 key 值信息,第四个参数 const struct dp_upcall_info *upcall_info 代表 ovs_dp_process_packet() 函数中生成的 upcall 信息结构体,第五个参数 uint32_t cutlen 代表截断长度(数据包长度阈值)。
函数首先检查 upcall 信息结构体中的 portid 是否为 0,即检查用户空间是否成功连接。然后检查是否为 GSO 数据包,若不是 GSO 数据包就调用 queue_userspace_packet(dp, skb, key, upcall_info, cutlen) 函数将其传输到用户空间;若是 GSO 数据包就调用 queue_gso_packets(dp, skb, key, upcall_info, cutlen) 函数先对 GSO 数据包进行处理。
在上面的过程中,如果发生错误,会通过 err 标签进入错误信息处理,并通过 u64_stats_update_begin() 和 u64_stats_update_end() 对统计信息进行原子更新。
Tips:GSO 全称 Generic Segmentation Offload,翻译为:通用分段延后处理。
在网卡在支持 GSO 功能时,对于超大数据包(这里指大于 MTU 的数据包),内核会将分段的工作延迟到交给驱动的前一刻,也就是说此时需要对 GSO 数据包进行分段。当然如果网卡不支持此功能,则内核会用软件的方式对数据包进行分段。
对于 GSO 数据包的处理函数存储在 ovs-main/datapath/datapath.c 文件中:
static int queue_gso_packets(struct datapath *dp, struct sk_buff *skb, const struct sw_flow_key *key, const struct dp_upcall_info *upcall_info, uint32_t cutlen) {
#ifdef HAVE_SKB_GSO_UDP
unsigned int gso_type = skb_shinfo(skb)->gso_type;
struct sw_flow_key later_key;
#endif
struct sk_buff *segs, *nskb;
struct ovs_skb_cb ovs_cb;
int err;
ovs_cb = *OVS_CB(skb);
segs = __skb_gso_segment(skb, NETIF_F_SG, false);
*OVS_CB(skb) = ovs_cb;
if (IS_ERR(segs))
return PTR_ERR(segs);
if (segs == NULL)
return -EINVAL;
#ifdef HAVE_SKB_GSO_UDP
if (gso_type & SKB_GSO_UDP) {
/* The initial flow key extracted by ovs_flow_key_extract() in this case is for a first fragment, so we need to properly mark later fragments. */
later_key = *key;
later_key.ip.frag = OVS_FRAG_TYPE_LATER;
}
#endif
/* Queue all of the segments. */
skb_list_walk_safe(segs, skb, nskb) {
*OVS_CB(skb) = ovs_cb;
#ifdef HAVE_SKB_GSO_UDP
if (gso_type & SKB_GSO_UDP && skb != segs)
key = &later_key;
#endif
err = queue_userspace_packet(dp, skb, key, upcall_info, cutlen);
if (err)
break;
}
/* Free all of the segments. */
skb_list_walk_safe(segs, skb, nskb) {
if (err)
kfree_skb(skb);
else
consume_skb(skb);
}
return err;
}
可以看到,在对 GSO 数据包进行处理后,仍然调用了 queue_userspace_packet(dp, skb, key, upcall_info, cutlen) 函数,将数据包传输到用户空间,又回到了 ovs_dp_upcall() 函数中相同的流程。
四、Netlink 发送 queue_userspace_packet()
函数 queue_userspace_packet() 实现了将一个数据包从内核空间传递到用户空间的功能,存储在 ovs-main/datapath/datapath.c 文件中:
static int queue_userspace_packet(struct datapath *dp, struct sk_buff *skb, const struct sw_flow_key *key, const struct dp_upcall_info *upcall_info, uint32_t cutlen)
{
struct ovs_header *upcall;
struct sk_buff *nskb = NULL;
struct sk_buff *user_skb = NULL; /* to be queued to userspace */
struct nlattr *nla;
size_t len;
unsigned int hlen;
int err, dp_ifindex;
u64 hash;
dp_ifindex = get_dpifindex(dp);
if (!dp_ifindex)
return -ENODEV;
if (skb_vlan_tag_present(skb)) {
nskb = skb_clone(skb, GFP_ATOMIC);
if (!nskb)
return -ENOMEM;
nskb = __vlan_hwaccel_push_inside(nskb);
if (!nskb)
return -ENOMEM;
skb = nskb;
}
if (nla_attr_size(skb->len) > USHRT_MAX) {
err = -EFBIG;
goto out;
}
/* Complete checksum if needed */
if (skb->ip_summed == CHECKSUM_PARTIAL && (err = skb_csum_hwoffload_help(skb, 0)))
goto out;
/* Older versions of OVS user space enforce alignment of the last Netlink attribute to NLA_ALIGNTO which would require extensive padding logic.
* Only perform zerocopy if padding is not required. */
if (dp->user_features & OVS_DP_F_UNALIGNED)
hlen = skb_zerocopy_headlen(skb);
else
hlen = skb->len;
len = upcall_msg_size(upcall_info, hlen - cutlen, OVS_CB(skb)->acts_origlen);
user_skb = genlmsg_new(len, GFP_ATOMIC);
if (!user_skb) {
err = -ENOMEM;
goto out;
}
upcall = genlmsg_put(user_skb, 0, 0, &dp_packet_genl_family, 0, upcall_info->cmd);
if (!upcall) {
err = -EINVAL;
goto out;
}
upcall->dp_ifindex = dp_ifindex;
err = ovs_nla_put_key(key, key, OVS_PACKET_ATTR_KEY, false, user_skb);
if (err)
goto out;
if (upcall_info->userdata)
__nla_put(user_skb, OVS_PACKET_ATTR_USERDATA, nla_len(upcall_info->userdata), nla_data(upcall_info->userdata));
if (upcall_info->egress_tun_info) {
nla = nla_nest_start_noflag(user_skb, OVS_PACKET_ATTR_EGRESS_TUN_KEY);
if (!nla) {
err = -EMSGSIZE;
goto out;
}
err = ovs_nla_put_tunnel_info(user_skb, upcall_info->egress_tun_info);
if (err)
goto out;
nla_nest_end(user_skb, nla);
}
if (upcall_info->actions_len) {
nla = nla_nest_start_noflag(user_skb, OVS_PACKET_ATTR_ACTIONS);
if (!nla) {
err = -EMSGSIZE;
goto out;
}
err = ovs_nla_put_actions(upcall_info->actions, upcall_info->actions_len, user_skb);
if (!err)
nla_nest_end(user_skb, nla);
else
nla_nest_cancel(user_skb, nla);
}
/* Add OVS_PACKET_ATTR_MRU */
if (upcall_info->mru &&
nla_put_u16(user_skb, OVS_PACKET_ATTR_MRU, upcall_info->mru)) {
err = -ENOBUFS;
goto out;
}
/* Add OVS_PACKET_ATTR_LEN when packet is truncated */
if (cutlen > 0 &&
nla_put_u32(user_skb, OVS_PACKET_ATTR_LEN, skb->len)) {
err = -ENOBUFS;
goto out;
}
/* Add OVS_PACKET_ATTR_HASH */
hash = skb_get_hash_raw(skb);
#ifdef HAVE_SW_HASH
if (skb->sw_hash)
hash |= OVS_PACKET_HASH_SW_BIT;
#endif
if (skb->l4_hash)
hash |= OVS_PACKET_HASH_L4_BIT;
if (nla_put(user_skb, OVS_PACKET_ATTR_HASH, sizeof (u64), &hash)) {
err = -ENOBUFS;
goto out;
}
/* Only reserve room for attribute header, packet data is added in skb_zerocopy() */
if (!(nla = nla_reserve(user_skb, OVS_PACKET_ATTR_PACKET, 0))) {
err = -ENOBUFS;
goto out;
}
nla->nla_len = nla_attr_size(skb->len - cutlen);
err = skb_zerocopy(user_skb, skb, skb->len - cutlen, hlen);
if (err)
goto out;
/* Pad OVS_PACKET_ATTR_PACKET if linear copy was performed */
pad_packet(dp, user_skb);
((struct nlmsghdr *) user_skb->data)->nlmsg_len = user_skb->len;
err = genlmsg_unicast(ovs_dp_get_net(dp), user_skb, upcall_info->portid);
user_skb = NULL;
out:
if (err)
skb_tx_error(skb);
kfree_skb(user_skb);
kfree_skb(nskb);
return err;
}
函数首先通过 dp_ifindex = get_dpifindex(dp) 获取数据包所属的数据路径 datapath 的接口索引,并使用 skb_vlan_tag_present(skb) 处理 VLAN 标签,如果数据包需要进行校验和计算,则完成该计算。然后函数基于 upcall 信息结构体构建一个新的内核消息 genlmsg,并将数据包有效载荷通过 skb_zerocopy(user_skb, skb, skb->len - cutlen, hlen) 函数以零拷贝的方式拷贝到新建的内核消息 genlmsg 中。接下来,将构建好的内核消息通过 genlmsg_unicast(ovs_dp_get_net(dp), user_skb, upcall_info->portid) 发送给用户空间。最后进行错误标签的处理。
Tips:Netlink 是 Linux 内核中用于内核空间与用户空间之间通信的一种套接字机制。Netlink 协议族有很多种,如 NETLINK_ROUTE、NETLINK_FIREWALL 等,每种协议族对应不同的内核子系统。在 Netlink 中,有两种主要的消息格式:
- Netlink 通用消息,即 Generic Netlink Message 简称 genlmsg
- Netlink 消息属性,即 Netlink Attributes 简称 nlattr
所以上面 genlmsg 消息的结构是在 Netlink 中的,也就是在 Linux 内核中的。相应的 genlmsg_unicast(ovs_dp_get_net(dp), user_skb, upcall_info->portid) 函数也是在内核中的。
这个函数的内容较多,不过大部分是对于各种复杂情况的判断以提升系统容错能力,另外的部分主要是 Netlink 消息的构造和信息的传输,偏向于 Linux 内核,并不是 Open vSwitch 的重点,所以这里只是简单介绍不做展开。
总结:
本文介绍了 Open vSwitch 进行 upcall 调用时数据包在内核空间的传输路径,即从 Datapath 模块无法匹配相应流表开始,调用 upcall 相关函数,直到数据包通过 Netlink 从用户态发送到内核态为止。
由于本人水平有限,以上内容如有不足之处欢迎大家指正(评论区/私信均可)。
参考资料:
怎么提高网络应用性能?让DPDK GRO和GSO来帮你!-腾讯云开发者社区
openVswitch(OVS)源代码分析 upcall调用(之linux中的NetLink通信机制) · openVswitch(OVS)源代码分析 · 看云