一、upcall 调用的流程
在 Open vSwitch 的数据包转发流程中,如果数据包在内核空间无法完全处理(比如匹配不到流表项),就会发生 upcall 调用,将数据包从内核空间的 Datapath 模块传输至用户空间的 ovs-vswitchd 守护进程进行处理。由于 upcall 调用经过了内核态到用户态的转发和 Open vSwitch 在用户空间的分层设计,所以 upcall 消息需要经过层层封装、解析和处理才能最终到达 vswitchd 守护进程。这里将详细讨论在 upcall 调用过程的不同阶段中,消息类型的变化。
二、内核空间中的 upcall 消息类型
内核空间对应的 upcall 结构体主要用于 Netlink 消息的封装,定义在 ovs-main/datapath/linux/compat/include/linux/openvswitch.h 头文件中:
enum ovs_packet_cmd {OVS_PACKET_CMD_UNSPEC,/* Kernel-to-user notifications. */OVS_PACKET_CMD_MISS, /* Flow table miss. */OVS_PACKET_CMD_ACTION, /* OVS_ACTION_ATTR_USERSPACE action. *//* Userspace commands. */OVS_PACKET_CMD_EXECUTE /* Apply actions to a packet. */
};
这时的 ovs_packet_cmd 对于 upcall 调用而言,只有 OVS_PACKET_CMD_MISS 和 OVS_PACKET_CMD_ACTION 这两种主要类型,分别对应内核模块 Datapath 匹配不到流表项和需要执行一些特定动作的情况。
也就是说,当内核模块 Datapath 匹配不到流表项时,会产生 MISS 类型的 upcall 消息,相应代码存储在 ovs-main/datapath/datapath.c 文件中:
void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key) {......if (unlikely(!flow)) {......upcall.cmd = OVS_PACKET_CMD_MISS;......error = ovs_dp_upcall(dp, skb, key, &upcall, 0);......}......
}
而相应的,ACTION 类型的处理函数存储在 ovs-main/datapath/actions.c 文件中:
static int output_userspace(struct datapath *dp, struct sk_buff *skb, struct sw_flow_key *key, const struct nlattr *attr, const struct nlattr *actions, int actions_len, uint32_t cutlen) {......upcall.cmd = OVS_PACKET_CMD_ACTION;......err = ovs_dp_upcall(dp, skb, key, &upcall, cutlen);......
}
二、用户空间中的 upcall 消息类型
由于 Open vSwitch 在用户空间的分层设计,用户空间需要对 upcall 消息结构进行重新整合。
(1)dpif 中的 upcall 消息类型
这里 dpif 对应的 upcall 结构体主要用于接收来自内核的消息,定义在 ovs-main/lib/dpif.h 头文件中:
enum dpif_upcall_type {DPIF_UC_MISS, /* Miss in flow table. */DPIF_UC_ACTION, /* OVS_ACTION_ATTR_USERSPACE action. */DPIF_N_UC_TYPES
};
可以看到这里的 DPIF_UC_MISS 和 DPIF_UC_ACTION 和内核空间中 upcall 消息类型是一一对应的关系。由此可见,此时是接收后直接存储,不需要进行额外关于类型的处理。
(2)ofproto 中的 upcall 消息类型
这里 ofproto 对应的 upcall 结构体主要为了便于 upcall 消息处理函数 process_upcall() 对不同类型 upcall 消息的处理,存储在 ovs-main/ofproto/ofproto-dpif-upcall.c 文件中:
enum upcall_type {BAD_UPCALL, /* Some kind of bug somewhere. */MISS_UPCALL, /* A flow miss. */SLOW_PATH_UPCALL, /* Slow path upcall. */SFLOW_UPCALL, /* sFlow sample. */FLOW_SAMPLE_UPCALL, /* Per-flow sampling. */IPFIX_UPCALL, /* Per-bridge sampling. */CONTROLLER_UPCALL /* Destined for the controller. */
};
此时相应的 process_upcall() 处理流程如下图所示:
(3)用户空间的 upcall 消息类型转换
用户空间的 upcall 消息类型转换主要发生在 dpif 和 ofproto 之间,将 dpif 中简单粗略的 upcall 消息类型进行细化,存储在 ovs-main/ofproto/ofproto-dpif-upcall.c 文件中:
static enum upcall_type classify_upcall(enum dpif_upcall_type type, const struct nlattr *userdata, struct user_action_cookie *cookie) {/* First look at the upcall type. */switch (type) {case DPIF_UC_ACTION:break;case DPIF_UC_MISS:return MISS_UPCALL;case DPIF_N_UC_TYPES:default:VLOG_WARN_RL(&rl, "upcall has unexpected type %"PRIu32, type);return BAD_UPCALL;}/* "action" upcalls need a closer look. */if (!userdata) {VLOG_WARN_RL(&rl, "action upcall missing cookie");return BAD_UPCALL;}size_t userdata_len = nl_attr_get_size(userdata);if (userdata_len != sizeof *cookie) {VLOG_WARN_RL(&rl, "action upcall cookie has unexpected size %"PRIuSIZE,userdata_len);return BAD_UPCALL;}memcpy(cookie, nl_attr_get(userdata), sizeof *cookie);if (cookie->type == USER_ACTION_COOKIE_SFLOW) {return SFLOW_UPCALL;} else if (cookie->type == USER_ACTION_COOKIE_SLOW_PATH) {return SLOW_PATH_UPCALL;} else if (cookie->type == USER_ACTION_COOKIE_FLOW_SAMPLE) {return FLOW_SAMPLE_UPCALL;} else if (cookie->type == USER_ACTION_COOKIE_IPFIX) {return IPFIX_UPCALL;} else if (cookie->type == USER_ACTION_COOKIE_CONTROLLER) {return CONTROLLER_UPCALL;} else {VLOG_WARN_RL(&rl, "invalid user cookie of type %"PRIu16" and size %"PRIuSIZE, cookie->type, userdata_len);return BAD_UPCALL;}
}
函数通过 case 检查 dpif_upcall_type 类型:如果是 DPIF_UC_MISS,则返回 MISS_UPCALL;如果是 DPIF_N_UC_TYPES 或其他未知类型,则返回 BAD_UPCALL;如果是 DPIF_UC_ACTION 则根据 userdata 和 user_action_cookie 进行更详细的和分类。
这里的 user_action_cookie 定义在 ovs-main/lib/odp-util.h 头文件中:
/* user_action_cookie is passed as argument to OVS_ACTION_ATTR_USERSPACE. */
struct user_action_cookie {uint16_t type; /* enum user_action_cookie_type. */ofp_port_t ofp_in_port; /* OpenFlow in port, or OFPP_NONE. */struct uuid ofproto_uuid; /* UUID of ofproto-dpif. */union {struct {/* USER_ACTION_COOKIE_SFLOW. */ovs_be16 vlan_tci; /* Destination VLAN TCI. */uint32_t output; /* SFL_FLOW_SAMPLE_TYPE 'output' value. */} sflow;struct {/* USER_ACTION_COOKIE_SLOW_PATH. */uint16_t unused;uint32_t reason; /* enum slow_path_reason. */} slow_path;struct {/* USER_ACTION_COOKIE_FLOW_SAMPLE. */uint16_t probability; /* Sampling probability. */uint32_t collector_set_id; /* ID of IPFIX collector set. */uint32_t obs_domain_id; /* Observation Domain ID. */uint32_t obs_point_id; /* Observation Point ID. */odp_port_t output_odp_port; /* The output odp port. */enum nx_action_sample_direction direction;} flow_sample;struct {/* USER_ACTION_COOKIE_IPFIX. */odp_port_t output_odp_port; /* The output odp port. */} ipfix;struct {/* USER_ACTION_COOKIE_CONTROLLER. */uint8_t dont_send; /* Don't send the packet to controller. */uint8_t continuation; /* Send packet-in as a continuation. */uint16_t reason;uint32_t recirc_id;ovs_32aligned_be64 rule_cookie;uint16_t controller_id;uint16_t max_len;} controller;};
};
BUILD_ASSERT_DECL(sizeof(struct user_action_cookie) == 48);
总结:
通过上面的分析可以得到以下结论:
(1)对于 upcall 调用中的消息类型而言,只有 MISS 和 ACTION 这两种主要类型。
(2)当内核空间的 Datapath 模块匹配不到流表项时,产生的一定是 MISS 类型的 upcall 消息,并且在后续的过程中如果没有出现错误,则一直将会是 MISS 类型的消息,只是名称不同。
(3)当内核空间的 Datapath 模块判断出数据包需要传输至 vswitchd 守护进程进行进一步处理时,会产生 ACTION 类型的 upcall 消息,并将配套信息通过 OVS_ACTION_ATTR_USERSPACE 也传输到用户空间。用户空间会根据这些信息将 ACTION 类型的 upcall 消息进行更细致的划分后,再进行处理。
此外,还有一点需要注意:我们在数据包的处理流程中提到过快速路径和慢速路径的概念,但是这和 upcall 调用过程在名称上并不是完全匹配。
这里的慢速路径对应的 upcall 类型其实是 MISS 类型,而 upcall 消息在用户空间也存在 SLOW_PATH 的 upcall 类型。虽然在进行最终处理的时候二者被归为一类,并且处理的流程区别不大,但认真分辨下来还是有细微差别的,在特定场景下需要注意区分。
由于本人水平有限,以上内容如有不足之处欢迎大家指正(评论区/私信均可)。
参考资料:
Open vSwitch 官网
Open vSwitch 源代码 GitHub
Open vSwitch 数据包处理流程-CSDN博客
Open vSwitch 的 upcall 调用(内核空间部分)-CSDN博客
Open vSwitch 的 upcall 调用(用户空间部分)-CSDN博客
Open vSwitch 守护进程的 upcall 处理-CSDN博客
Open vSwitch v2.17.10 LTS 源代码