Open vSwitch 的 upcall 调用(内核空间部分)

 一、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) 发送给用户空间。最后进行错误标签的处理。

TipsNetlink 是 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 从用户态发送到内核态为止。

        由于本人水平有限,以上内容如有不足之处欢迎大家指正(评论区/私信均可)。

参考资料:

Open vSwitch 官网

Open vSwitch 源代码 GitHub

Open vSwitch v2.17.10 LTS 源代码

怎么提高网络应用性能?让DPDK GRO和GSO来帮你!-腾讯云开发者社区

netlink 原理及应用-优快云博客

openVswitch(OVS)源代码分析 upcall调用(之linux中的NetLink通信机制) · openVswitch(OVS)源代码分析 · 看云

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值