一、upcall 调用
Open vSwitch 中的 upcall 调用发生在数据包无法在内核中完全处理时,比如内核模块 Datapath 在数据包处理过程中匹配不到流表项,需要控制器下发流表的场景。在 upcall 调用的过程中,数据包的路径可以细分为内核态路径和用户态路径两个部分。内核态的路径通常从 Datapath 模块无法匹配流表后进行的 upcall 调用开始,直到将数据包发往用户态为止;用户态的路径通常从交换机维护的底层 upcall 接收函数开始,直到将数据包提取到 ovs-vswitchd 守护进程为止。整个过程借助 Netlink 实现在内核空间和用户空间之间的消息传递。
本文关注 upcall 调用过程中的用户态路径,以数据包进入用户态为起点,直到数据包进入 ovs-vswitchd 守护进程为止。
二、dpif 接收接口
在 ovs-main/lib/dpif-provider.h 头文件中定义了一个包含多个函数指针和其他数据成员的结构体 dpif_class,其中 recv 成员是一个函数指针,它指向一个具有特定输入参数的函数:
struct dpif_class {
......
int (*recv)(struct dpif *dpif, uint32_t handler_id, struct dpif_upcall *upcall, struct ofpbuf *buf);
......
};
这时基础非常扎实的你一眼就看出来了,这是设计模式中策略模式的实现。这里的 recv 函数指针可以理解为一个通用接口,这就意味着 recv 函数的具体实现可能因 dpif_class 的不同实例而有所不同。
所以我们暂时先忽略从 Netlink 接收 upcall 消息的实现细节,而是以此作为起点,讨论 upcall 消息在用户空间(更准确的说是在 vswitchd 守护进程的底层调用)中的传输路径。
三、upcall 接收 dpif_recv()
函数 dpif_recv() 负责从 dpif 模块接收 upcall 消息,存储在 ovs-main/lib/dpif.c 文件中:
int dpif_recv(struct dpif *dpif, uint32_t handler_id, struct dpif_upcall *upcall, struct ofpbuf *buf) {
int error = EAGAIN;
if (dpif->dpif_class->recv) {
error = dpif->dpif_class->recv(dpif, handler_id, upcall, buf);
if (!error) {
OVS_USDT_PROBE(dpif_recv, recv_upcall, dpif->full_name, upcall->type, dp_packet_data(&upcall->packet), dp_packet_size(&upcall->packet), upcall->key, upcall->key_len);
dpif_print_packet(dpif, upcall);
} else if (error != EAGAIN) {
log_operation(dpif, "recv", error);
}
}
return error;
}
函数的第一个输入参数 struct dpif *dpif 是指向 dpif 模块的指针,第二个输入参数 uint32_t handler_id 代表 upcall 的处理程序 ID,第三个输入参数 struct dpif_upcall *upcall 是指向 upcall 数据结构的指针,第四个输入参数 struct ofpbuf *buf 指向用于存储 upcall 消息的数据缓冲区。
函数检查 dpif 对象的 dpif_class 成员是否定义了 recv 函数,如果 recv 函数存在则调用它;如果 recv 函数不存在或者调用失败,则输出一些错误提示信息。
四、upcall 批量处理 recv_upcalls()
函数 recv_upcalls() 用于批量处理数据平面向控制平面发送的 upcall 消息,存储在 ovs-main/ofproto/ofproto-dpif-upcall.c 文件中:
static size_t recv_upcalls(struct handler *handler) {
struct udpif *udpif = handler->udpif;
uint64_t recv_stubs[UPCALL_MAX_BATCH][512 / 8];
struct ofpbuf recv_bufs[UPCALL_MAX_BATCH];
struct dpif_upcall dupcalls[UPCALL_MAX_BATCH];
struct upcall upcalls[UPCALL_MAX_BATCH];
struct flow flows[UPCALL_MAX_BATCH];
size_t n_upcalls, i;
n_upcalls = 0;
while (n_upcalls < UPCALL_MAX_BATCH) {
struct ofpbuf *recv_buf = &recv_bufs[n_upcalls];
struct dpif_upcall *dupcall = &dupcalls[n_upcalls];
struct upcall *upcall = &upcalls[n_upcalls];
struct flow *flow = &flows[n_upcalls];
unsigned int mru = 0;
uint64_t hash = 0;
int error;
ofpbuf_use_stub(recv_buf, recv_stubs[n_upcalls], sizeof recv_stubs[n_upcalls]);
if (dpif_recv(udpif->dpif, handler->handler_id, dupcall, recv_buf)) {
ofpbuf_uninit(recv_buf);
break;
}
upcall->fitness = odp_flow_key_to_flow(dupcall->key, dupcall->key_len, flow, NULL);
if (upcall->fitness == ODP_FIT_ERROR) {
goto free_dupcall;
}
if (dupcall->mru) {
mru = nl_attr_get_u16(dupcall->mru);
}
if (dupcall->hash) {
hash = nl_attr_get_u64(dupcall->hash);
}
error = upcall_receive(upcall, udpif->backer, &dupcall->packet, dupcall->type, dupcall->userdata, flow, mru, &dupcall->ufid, PMD_ID_NULL);
if (error) {
if (error == ENODEV) {
/* Received packet on datapath port for which we couldn't associate an ofproto.
* This can happen if a port is removed while traffic is being received.
* Print a rate-limited message in case it happens frequently. */
dpif_flow_put(udpif->dpif, DPIF_FP_CREATE, dupcall->key, dupcall->key_len, NULL, 0, NULL, 0, &dupcall->ufid, PMD_ID_NULL, NULL);
VLOG_INFO_RL(&rl, "received packet on unassociated datapath " "port %"PRIu32, flow->in_port.odp_port);
}
goto free_dupcall;
}
upcall->key = dupcall->key;
upcall->key_len = dupcall->key_len;
upcall->ufid = &dupcall->ufid;
upcall->hash = hash;
upcall->out_tun_key = dupcall->out_tun_key;
upcall->actions = dupcall->actions;
pkt_metadata_from_flow(&dupcall->packet.md, flow);
flow_extract(&dupcall->packet, flow);
error = process_upcall(udpif, upcall, &upcall->odp_actions, &upcall->wc);
if (error) {
goto cleanup;
}
n_upcalls++;
continue;
cleanup:
upcall_uninit(upcall);
free_dupcall:
dp_packet_uninit(&dupcall->packet);
ofpbuf_uninit(recv_buf);
}
if (n_upcalls) {
handle_upcalls(handler->udpif, upcalls, n_upcalls);
for (i = 0; i < n_upcalls; i++) {
dp_packet_uninit(&dupcalls[i].packet);
ofpbuf_uninit(&recv_bufs[i]);
upcall_uninit(&upcalls[i]);
}
}
return n_upcalls;
}
函数的输入参数 struct handler *handler 代表一个 upcall 处理器。函数在开始时使用 struct udpif *udpif 定义了一个指向 Open vSwitch 数据平面上下文的指针,并定义了数据缓冲区的大小来规定最大处理数据量。这里的 size_t n_upcalls 表示实际处理的 upcall 数量。
函数维护一个 while 循环,并通过 UPCALL_MAX_BATCH 限制实际处理 upcall 的数目。函数在循环中调用 dpif_recv(udpif->dpif, handler->handler_id, dupcall, recv_buf) 从数据平面接收一个 upcall 消息,如果接收成功将会进行下一步处理;如果接收失败则会直接退出循环。
如果 upcall 消息接收成功,则调用 odp_flow_key_to_flow(dupcall->key, dupcall->key_len, flow, NULL) 函数将 upcall 的 flow key 解析为 struct flow,然后从 upcall 元数据中获取相关信息。
接下来调用 upcall_receive(upcall, udpif->backer, &dupcall->packet, dupcall->type, dupcall->userdata, flow, mru, &dupcall->ufid, PMD_ID_NULL) 函数初始化和设置 struct upcall 对象,并将 upcall 的关键信息也保存到 struct upcall 中。之后调用 process_upcall(udpif, upcall, &upcall->odp_actions, &upcall->wc) 函数进一步处理这个 upcall 消息。
最后,进行资源回收和错误处理,并返回实际处理的 upcall 数量。
Tips:函数 recv_upcalls() 最终得到的是一个 struct upcall,也就是说它的工作主要是通过一些转换机制,将内核态传输过来的 upcall 消息进行重新整合,以适配 vswitchd 守护进程。所以这里的内容很多,但是对于一些处理机制的细节不做展开。
这里的 upcall 结构体也存储在 ovs-main/ofproto/ofproto-dpif-upcall.c 文件中:
struct upcall {
struct ofproto_dpif *ofproto; /* Parent ofproto. */
const struct recirc_id_node *recirc; /* Recirculation context. */
bool have_recirc_ref; /* Reference held on recirc ctx? */
/* The flow and packet are only required to be constant when using dpif-netdev.
* If a modification is absolutely necessary, a const cast may be used with other datapaths. */
const struct flow *flow; /* Parsed representation of the packet. */
enum odp_key_fitness fitness; /* Fitness of 'flow' relative to ODP key. */
const ovs_u128 *ufid; /* Unique identifier for 'flow'. */
unsigned pmd_id; /* Datapath poll mode driver id. */
const struct dp_packet *packet; /* Packet associated with this upcall. */
ofp_port_t ofp_in_port; /* OpenFlow in port, or OFPP_NONE. */
uint16_t mru; /* If !0, Maximum receive unit of
fragmented IP packet */
uint64_t hash;
enum upcall_type type; /* Type of the upcall. */
const struct nlattr *actions; /* Flow actions in DPIF_UC_ACTION Upcalls. */
bool xout_initialized; /* True if 'xout' must be uninitialized. */
struct xlate_out xout; /* Result of xlate_actions(). */
struct ofpbuf odp_actions; /* Datapath actions from xlate_actions(). */
struct flow_wildcards wc; /* Dependencies that megaflow must match. */
struct ofpbuf put_actions; /* Actions 'put' in the fastpath. */
struct dpif_ipfix *ipfix; /* IPFIX pointer or NULL. */
struct dpif_sflow *sflow; /* SFlow pointer or NULL. */
struct udpif_key *ukey; /* Revalidator flow cache. */
bool ukey_persists; /* Set true to keep 'ukey' beyond the
lifetime of this upcall. */
uint64_t reval_seq; /* udpif->reval_seq at translation time. */
/* Not used by the upcall callback interface. */
const struct nlattr *key; /* Datapath flow key. */
size_t key_len; /* Datapath flow key length. */
const struct nlattr *out_tun_key; /* Datapath output tunnel key. */
struct user_action_cookie cookie;
uint64_t odp_actions_stub[1024 / 8]; /* Stub for odp_actions. */
};
五、upcall 接收线程 udpif_upcall_handler()
函数 ovs_dp_process_packet() 是 ovs-vswitchd 守护进程模块接收 upcall 的线程函数,存储在 ovs-main/ofproto/ofproto-dpif-upcall.c 文件中:
/* The upcall handler thread tries to read a batch of UPCALL_MAX_BATCH upcalls from dpif, processes the batch and installs corresponding flows in dpif. */
static void *udpif_upcall_handler(void *arg) {
struct handler *handler = arg;
struct udpif *udpif = handler->udpif;
while (!latch_is_set(&handler->udpif->exit_latch)) {
if (recv_upcalls(handler)) {
poll_immediate_wake();
} else {
dpif_recv_wait(udpif->dpif, handler->handler_id);
latch_wait(&udpif->exit_latch);
}
poll_block();
}
return NULL;
}
首先通过 udpif_upcall_handler() 函数的输入参数 void *arg 和返回值类型 void * 可以推断出这个函数将作为一个独立的线程运行。事实上,线程启动的函数也存储在 ovs-main/ofproto/ofproto-dpif-upcall.c 文件中:
/* Starts the handler and revalidator threads. */
static void udpif_start_threads(struct udpif *udpif, uint32_t n_handlers_, uint32_t n_revalidators_) {
if (udpif && n_handlers_ && n_revalidators_) {
/* Creating a thread can take a significant amount of time on some systems, even hundred of milliseconds, so quiesce around it. */
ovsrcu_quiesce_start();
udpif->n_handlers = n_handlers_;
udpif->n_revalidators = n_revalidators_;
udpif->handlers = xzalloc(udpif->n_handlers * sizeof *udpif->handlers);
for (size_t i = 0; i < udpif->n_handlers; i++) {
struct handler *handler = &udpif->handlers[i];
handler->udpif = udpif;
handler->handler_id = i;
handler->thread = ovs_thread_create(
"handler", udpif_upcall_handler, handler);
}
atomic_init(&udpif->enable_ufid, udpif->backer->rt_support.ufid);
dpif_enable_upcall(udpif->dpif);
ovs_barrier_init(&udpif->reval_barrier, udpif->n_revalidators);
ovs_barrier_init(&udpif->pause_barrier, udpif->n_revalidators + 1);
udpif->reval_exit = false;
udpif->pause = false;
udpif->offload_rebalance_time = time_msec();
udpif->revalidators = xzalloc(udpif->n_revalidators
* sizeof *udpif->revalidators);
for (size_t i = 0; i < udpif->n_revalidators; i++) {
struct revalidator *revalidator = &udpif->revalidators[i];
revalidator->udpif = udpif;
revalidator->thread = ovs_thread_create(
"revalidator", udpif_revalidator, revalidator);
}
ovsrcu_quiesce_end();
}
}
在 udpif_upcall_handler() 函数中,它接收一个 handler 结构体作为参数,该结构体包含了一个 udpif 指针,指向了一个 udpif 结构体。接下来这个线程函数会进入一个 while 循环,直到 latch_is_set(&handler->udpif->exit_latch) 的结果为 true 才会退出线程。在循环中,线程调用 recv_upcalls(handler) 函数试图从 dpif 中读取 upcall 消息,并对它们进行处理。如果 recv_upcalls(handler) 函数成功读取到了 upcall 消息,则调用 poll_immediate_wake() 函数来唤醒其他等待的线程;如果 recv_upcalls(handler) 函数没有读取到任何 upcall 消息,线程会调用 dpif_recv_wait(udpif->dpif, handler->handler_id) 函数等待新的 upcall 消息。最后,线程会调用 poll_block() 函数,等待下一个事件的到来。
总之,这个线程的作用就是通过 recv_upcalls(handler) 函数不断地从 dpif 中读取 upcall 消息并对它们进行处理。当没有 upcall 消息可处理时,它会等待 dpif 产生新的 upcall 消息或者等待退出信号。
Tips:至此我们完成了 upcall 消息在用户空间的传输过程,主要的工作内容是将内核空间发送上来的 upcall 信息重新整合,然后交给 vswitchd 守护进程。守护进程会调用 process_upcall() 函数对收到的 upcall 消息进行处理,不过就不在这里讨论了。
总结:
本文介绍了 Open vSwitch 进行 upcall 调用时数据包在用户空间的传输路径,即从数据包通过 Netlink 进入用户空间开始,对 upcall 消息进行接收、重组和上传,直到数据包到达 vswitchd 守护进程为止。
由于本人水平有限,以上内容如有不足之处欢迎大家指正(评论区/私信均可)。