注:本文是转载,但不是100%的转载,可能稍微有些出入,原文地址点击这里

vpp的功能逻辑被划分为一个个node,node之间通过下一跳传递处理完的数据包集合,从而组成整个业务图谱。本文将分析node调度框架源码。

基本概念


vlib_node_type_t

node分为四种类型:

  • VLIB_NODE_TYPE_INTERNAL对数据包真正处理的业务node。
  • VLIB_NODE_TYPE_INPUT收包逻辑node,比如:dpdk,pcap等。
  • VLIB_NODE_TYPE_PRE_INPUT目前只有一个epoll node,对socket相关逻辑提供服务,主要使用在控制业务上。
  • VLIB_NODE_TYPE_PROCESS该类型node可以被挂起也可以被恢复,有独立的分配在heap上的运行时栈。类似于在一个线程中实现了多任务调度机制。主要用来修改vpp node内部参数。

vlib_node_main_t

记录各种全局信息,比如各种数据结构集合,数据结构内存池之类。

vlib_node_t

注册node时将业务逻辑,几乎不怎么修改的参数,状态信息保存在这里。

vlib_node_runtime_t

这是调度框架实际频繁使用的结构,从vlib_node_t拷贝了部分信息,以及私有的频繁变动的信息。

vlib_process_t

VLIB_NODE_TYPE_PROCESS类型node专用结构,记录用于模拟task的基础结构:heap上的运行时栈,2种返回时寄存器备份,等。

vlib_frame_t

每个node都有一个对应的vlib_frame_t,用来保存供node使用的数据包集合。这是每个node最终处理数据的内存所在地。

vlib_pending_frame_t

当一个node处理完数据包,则填充该数据结构,并加入到全局链表,调度框架便能在下一次调度时找到需要接收该数据包的下一个node。

vlib_next_frame_t

主要是node内部逻辑使用,定位该node的下一条信息。

关键结构


vlib_node_main_t

typedef struct
{
    //一块连续内存,头部是vec_header_t,数据部分是node指针数组
    vlib_node_t **nodes;

    //node按名字组成hash表
    uword *node_by_name;

    //目前只有VLIB_NODE_MAIN_RUNTIME_STARTED一个状态,暂时没啥用,忽略之。
    u32 flags;
#define VLIB_NODE_MAIN_RUNTIME_STARTED (1 << 0)

    /*
     * node有三种类型:VLIB_NODE_TYPE_INTERNAL,VLIB_NODE_TYPE_INPUT,
     * VLIB_NODE_TYPE_PRE_INPUT,按类型分类索引
     */
    vlib_node_runtime_t *nodes_by_type[VLIB_N_NODE_TYPE];

    //以下三个用于类似于网卡napi收包模式,dpdk没用到,忽略之
    u32 *pending_interrupt_node_runtime_indices;

    //假设node1有n1个下一跳,node2有n2个下一跳....共n1 + n2 +....+ni个下一跳
    //连续保存在next_frames指向的vec中。
    vlib_next_frame_t *next_frames;

    /*
     * 数据包从node输出到下一跳,那么下一跳node即是pending frame,会加入pending_frames指向的vec。
     * 主循环会遍历该vec,对每个node来调用处理逻辑
     */
    vlib_pending_frame_t *pending_frames;

    //定时器,信号相关,源码及其恶心,博主暂时放弃阅读
    timing_wheel_t timing_wheel;
    vlib_signal_timed_event_data_t *signal_timed_event_data_pool;

    /*
     * VLIB_NODE_TYPE_PROCESS类型的node,每个node有分配在heap上的运行栈,
     * 没错就是通常说的在顶部那种栈,但是这里分配在了heap上。
     * 这里在单线程中模拟了类似多线程的效果,以后会详细分析。
     * processes保存了所有VLIB_NODE_TYPE_PROCESS类型node的描述结构指针。
     */
    vlib_process_t **processes;

    //VLIB_NODE_TYPE_PROCESS类型的node,执行时都会把
    //current_process_index赋值为本node的runtime_index
    u32 current_process_index;

    //VLIB_NODE_TYPE_PROCESS类型node专用,挂起时在其中保存信息。
    vlib_pending_frame_t *suspended_process_frames;

    //hash表,把node的scalar_size,vector_size值组合成key,查找对应的vlib_frame_size_t结构。
    uword *frame_size_hash;

    //通过hash查找到node对应的vlib_frame_size_t值,从中的内存池来分配vlib_frame_t。
    vlib_frame_size_t *frame_sizes;

    //注册node函数提交的注册信息链接在这里,仅仅初始化时使用
    vlib_node_registration_t *node_registrations;
} vlib_node_main_t;

vlib_node_t

typedef struct vlib_node_t
{
    //业务逻辑
    vlib_node_function_t *function;

    //node类型,之前提到的那四种之一
    vlib_node_type_t type;

    //vlib_node_t和vlib_node_runtime_t是一一对应的好基友
    u32 runtime_index;

    //这两个成员组合成key,在vlib_node_main_t->frame_size_hash中查找,
    //确定本node相关的frame的内存池
    u16 scalar_size, vector_size;

    //初始化时用用
    char **next_node_names;

    //根据next_node_names来生成next_nodes,vec结构,记录了每个可选下一条的index。
    u32 *next_nodes;

    //统计发给下一条node的数据包总数
    u64 *n_vectors_by_next_node;
} vlib_node_t;

vlib_node_runtime_t

typedef struct vlib_node_runtime_t
{
    //本node的多个下一条中,第一个下一条在vlib_node_main_t->next_frames中的索引
    u32 next_frame_index;

    //vlib_node_t和vlib_node_runtime_t是一一对应的好基友
    u32 node_index;
}

vlib_process_t

typedef struct
{
    //对应的PROSESS类型node索引号
    vlib_node_runtime_t node_runtime;

    //如下return_longjmp,resume_longjmp用于保存当前寄存器内容,
    //可以参考C库的setjump,longjump原理
    clib_longjmp_t return_longjmp;

    //PROSESS类型node的业务逻辑运行时栈地址,在heap上
#define VLIB_PROCESS_STACK_MAGIC (0xdead7ead)
    u32 stack[0] ALIGN_ON_MULTIPLE_PAGE_BOUNDARY_FOR_MPROTECT;
} vlib_process_t __attribute__ ((aligned (CLIB_CACHE_LINE_BYTES)));

调度逻辑


VPP支持多工作线程模型,这里不考虑多线程,以单线程模型来分析源码。
核心调度函数:

vlib_main_loop

static void vlib_main_loop (vlib_main_t * vm)
{
    ...

    //dpdk收包时下面用不到。它们是用来模拟标准网卡NAPI机制的
    if (!nm->polling_threshold_vector_length)
        nm->polling_threshold_vector_length = 10;
    if (!nm->interrupt_threshold_vector_length)
        nm->interrupt_threshold_vector_length = 5;

    nm->current_process_index = ~0;

    /*
     * 执行所有VLIB_NODE_TYPE_PROCESS类型node,利用setjump,longjump机制,
     * 把node挂起来,等待之后唤醒。
     * 可以理解为一种多任务模型。该类型node主要时用在作运行时配置相关。
     * 之后会详细论述该类型node。
     */
    uword i;
    for (i = 0; i < vec_len (nm->processes); i++)
        cpu_time_now =
                dispatch_process (vm, nm->processes[i], /* frame */ 0, cpu_time_now);

    while (1)
    {
        vlib_node_runtime_t *n;

        //目前只有一个epoll相关的node,监听socket,辅助功能
        vec_foreach (n, nm->nodes_by_type[VLIB_NODE_TYPE_PRE_INPUT])
            cpu_time_now = dispatch_node (vm, n,
                    VLIB_NODE_TYPE_PRE_INPUT,
                    VLIB_NODE_STATE_POLLING,
                    /* frame */ 0,
                    cpu_time_now);

        //收包node,假设使用dpdk的node
        vec_foreach (n, nm->nodes_by_type[VLIB_NODE_TYPE_INPUT])
            cpu_time_now = dispatch_node (vm, n,
                    VLIB_NODE_TYPE_INPUT,
                    VLIB_NODE_STATE_POLLING,
                    /* frame */ 0,
                    cpu_time_now);

        //memclnt_node会用到,但是博主发现该node没有使用。无视之。
        if (PREDICT_TRUE (vm->queue_signal_pending == 0))
            vm->queue_signal_callback (vm);

        //dpdk没有中断机制,这里不会执行
        {
            ...
        }

        /*
         * 处理超时事件,通过api发生的唤醒node事件。
         * 只对VLIB_NODE_TYPE_PROCESS类型node有效。
         * VLIB_NODE_TYPE_PROCESS类型node处理需要单独描述,
         * 但不是开发人员关注的重点,只是配置相关。
         */
        nm->data_from_advancing_timing_wheel
                = timing_wheel_advance (&nm->timing_wheel, cpu_time_now,
                nm->data_from_advancing_timing_wheel,
                &nm->cpu_time_next_process_ready);

        ...

        /*
         * 真正开发人员关注的重点,数据包处理的核心逻辑都在VLIB_NODE_TYPE_INTERNAL类型node中。
         * nm->pending_frames记录了上一个node转给下一个node信息,
         * 最后可以找到传递给下一个node使用的数据包
         */
        for (i = 0; i < _vec_len (nm->pending_frames); i++)
            cpu_time_now = dispatch_pending_node (vm, nm->pending_frames + i,
                    cpu_time_now);
    }
}
u64 dispatch_pending_node (vlib_main_t * vm,
               vlib_pending_frame_t * p, u64 last_time_stamp)
{
    vlib_node_main_t *nm = &vm->node_main;
    vlib_frame_t *f;
    vlib_next_frame_t *nf, nf_dummy;
    vlib_node_runtime_t *n;
    u32 restore_frame_index;

    //之后将执行n的业务逻辑
    n = vec_elt_at_index (nm->nodes_by_type[VLIB_NODE_TYPE_INTERNAL],
            p->node_runtime_index);

    //node n的对应frame,里面包含了node需要处理的数据包
    f = vlib_get_frame (vm, p->frame_index);
    /*
     * 不是下一跳传过来的数据包,比如自己生成的包注入某个node,此时if判断为真。
     * vlib_put_frame_to_node()完成注入逻辑。
     */
    if (p->next_frame_index == VLIB_PENDING_FRAME_NO_NEXT_FRAME)
    {
        nf = &nf_dummy;
        nf->flags = f->flags & VLIB_NODE_FLAG_TRACE;
        nf->frame_index = ~p->frame_index;
    }
    else
        //通过node下一跳传递来的数据包
        nf = vec_elt_at_index (nm->next_frames, p->next_frame_index);

    /*
     * node传递数据包到下一个node是通过调用vlib_put_next_frame()接口:
     * p->frame_index = nf->frame_index;
     * p->node_runtime_index = nf->node_runtime_index;
     * p->next_frame_index = nf - nm->next_frames;
     */
    if (nf->frame_index == p->frame_index)
    {
        //有两个位置同时引用了frame,把nf中的清掉,以免干扰本线程对frame的使用。
        nf->frame_index = ~0;
        nf->flags &= ~VLIB_FRAME_IS_ALLOCATED;
        if (!(n->flags & VLIB_NODE_FLAG_FRAME_NO_FREE_AFTER_DISPATCH))
            restore_frame_index = p->frame_index;
    }

    ...

    //调度node业务逻辑
    last_time_stamp = dispatch_node (vm, n,
            VLIB_NODE_TYPE_INTERNAL,
            VLIB_NODE_STATE_POLLING,
            f, last_time_stamp);

    f->flags &= ~VLIB_FRAME_PENDING;

    //frame已经在业务逻辑中使用完了,重新保存到nf中
    if (restore_frame_index != ~0)
    {
        //vlib_next_frame_change_ownership()可能会修改p->next_frame_index
        nf = vec_elt_at_index (nm->next_frames, p->next_frame_index);
        nf->frame_index = restore_frame_index;
        nf->flags |= VLIB_FRAME_IS_ALLOCATED;
    }

    return last_time_stamp;
}
//对DPDK驱动,该函数基本没有什么复杂处理。
u64 dispatch_node (vlib_main_t * vm,
           vlib_node_runtime_t * node,
           vlib_node_type_t type,
           vlib_node_state_t dispatch_state,
           vlib_frame_t * frame, u64 last_time_stamp)
{
    ...

    if (node->n_next_nodes > 0)
    {
        //node的业务逻辑肯定会用到nf
        nf = vec_elt_at_index (nm->next_frames, node->next_frame_index);
        CLIB_PREFETCH (nf, 4 * sizeof (nf[0]), WRITE);
    }

    ...

    //业务逻辑
    n = node->function (vm, node, frame);

    ...

    //跟dpdk没关系了
    if ((DPDK == 0 && dispatch_state == VLIB_NODE_STATE_INTERRUPT)
            || (DPDK == 0 && dispatch_state == VLIB_NODE_STATE_POLLING
            && (node->flags
                & VLIB_NODE_FLAG_SWITCH_FROM_INTERRUPT_TO_POLLING_MODE)))
    {
        ...
    }

    return t;
}

有时间再详细分析,该函数背后机制比较复杂,但是跟vpp框架使用者关系不大。

static u64
dispatch_process (vlib_main_t * vm,
          vlib_process_t * p, vlib_frame_t * f, u64 last_time_stamp)
{
    ...
}
最后修改:2021 年 08 月 18 日
如果觉得我的文章对你有用,请随意赞赏