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

一、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 守护进程为止。

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

参考资料:

Open vSwitch 官网

Open vSwitch 源代码 GitHub

Open vSwitch v2.17.10 LTS 源代码

设计模式中的多态——策略模式详解

openvswitch 处理 upcall 过程分析-优快云博客

ovs upcall 处理流程-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值