Jieqiang · 2019年12月25日

VPP初始化流程和图节点调度机制

关键词: VPP dpdk VPP初始化 图节点调度

0. 前言

VPP(Vector Packet Processing)是Cisco的FD.io项目组的核心开源项目,它实现了在用户空间的快速转发平面,解决了传统标量报文处理过程中遇到的I-cache 不命中和I-cache抖动等问题。VPP具有高性能、模块化、可扩展等优势,且包含了二层交换、三层路由、ACL、IPsec等丰富的功能特性。

VPP将各个网络功能模块划分成图节点(Graph Node),矢量数据包在由图节点构成的有向图间流动。VPP采用dpdk作为收发包框架,并且采用dpdk_input节点作为其报文输入节点。本文将结合VPP源码,着重阐述VPP初始化流程以及VPP图节点调度机制。

1. VPP初始化流程

1.1 理解宏函数VLIB_INIT_FUNCTION

当在VPP的命令行中输入show init-function命令时,可以看到以下输出结果:

vpp# show init-function
[0]: vlib_cli_init
[1]: unix_main_init
[2]: linux_vmbus_init
[3]: unix_input_init
[4]: linux_pci_init
[5]: vlib_log_init
[6]: pci_bus_init
[7]: punt_init
[8]: punt_node_init
[9]: threads_init
[10]: cj_init
......

显示的结果都是VPP在进入main函数loop循环之前的初始化函数。VPP初始化的实现代码是src/vlib/init.h,通过宏VLIB_INIT_FUNCTION来定义构造函数和析构函数。以初始化函数avf_init为例,上述宏定义的代码展开如下所示。

vlib_init_function_t * _vlib_init_function_avf_init = avf_init;
static void __vlib_add_init_function_avf_init (void) __attribute__((__constructor__));
static _vlib_init_function_list_elt_t _vlib_init_function_init_avf_init;
static void __vlib_add_init_function_avf_init (void)
{
    vlib_main_t * vm = vlib_get_main();
    _vlib_init_function_init_avf_init.next_init_function 
      = vm->init_function_registrations;
    vm->init_function_registrations = &_vlib_init_function_init_avf_init;
    _vlib_init_function_init_avf_init.f = &avf_init;
    _vlib_init_function_init_avf_init.name = "avf_init";
}
static void __vlib_rm_init_function_avf_init (void) __attribute__((__destructor__));
static void __vlib_rm_init_function_avf_init (void)
{
    vlib_main_t * vm = vlib_get_main();
    _vlib_init_function_list_elt_t *this, *prev;
    this = vm->init_function_registrations;
    if (this == 0)
        return;
    if (this->f == &avf_init)
    {
        vm->init_function_registrations = this->next_init_function;
        return;
    }
    prev = this;
    this = this->next_init_function;
    while (this)
    {
        if (this->f == &avf_init)
        {
            prev->next_init_function = this->next_init_function;
            return;
        }
        prev = this;
        this = this->next_init_function;
    }
}

avf_init的构造函数中,先通过vlib_get_main()获得指向vlib_main_t结构体的指针vm,再通过vm->init_function_registrations注册avf_init函数,构成由_vlib_init_function_list_elt_t结构体组成的初始化函数的单链表。同样在avf_init的析构函数中,我们可以看到while代码实现的功能就是遍历整个单链表寻找avf_init函数节点,找到节点后将avf_init节点从链表中删除。

1.2 main函数入口

src/vpp/vnet/main.c中的main函数是程序的入口,函数首先完成对VPP配置文件startup.conf的加载和解析,获取诸如heapsizeplugin_path等配置参数的值。再在defaulted代码中,利用这些参数完成相应的初始化操作。通过clib_mem_init_thread_safe函数完成主函数main堆的分配,堆的大小的默认值是1G,在main函数首部uword main_heap_size = (1ULL << 30);定义,用户也可以在startup.conf配置文件中指定heapsize的值。vpe_main_init函数完成插件的加载。main函数最后调用vlib_unix_main函数,函数跳转到vlib_unix_main函数。defaulted的源代码如下所示。

defaulted:

  /* set process affinity for main thread */
  CPU_ZERO (&cpuset);
  CPU_SET (main_core, &cpuset);
  pthread_setaffinity_np (pthread_self (), sizeof (cpu_set_t), &cpuset);

  /* Set up the plugin message ID allocator right now... */
  vl_msg_api_set_first_available_msg_id (VL_MSG_FIRST_AVAILABLE);

  /* Allocate main heap */
  if (clib_mem_init_thread_safe (0, main_heap_size))
    {
      vm->init_functions_called = hash_create (0, /* value bytes */ 0);
      vpe_main_init (vm);
      return vlib_unix_main (argc, argv);  ## 最后一步调用函数vlib_unix_main
    }
  else
    {
      {
    int rv __attribute__ ((unused)) =
      write (2, "Main heap allocation failure!\r\n", 31);
      }
      return 1;
    }
}

1.3 vlib_unix_main函数

vlib_unix_main函数在src/vlib/unix/main.c中。其源代码如下所示。

int
vlib_unix_main (int argc, char *argv[])
{
  vlib_main_t *vm = &vlib_global_main;    /* one and only time for this! */
  unformat_input_t input;
  clib_error_t *e;
  int i;
  // 相关参数的初始化
  vm->argv = (u8 **) argv;
  vm->name = argv[0];
  vm->heap_base = clib_mem_get_heap ();
  vm->heap_aligned_base = (void *)
    (((uword) vm->heap_base) & ~(VLIB_FRAME_ALIGN - 1));
  ASSERT (vm->heap_base);
  // 获取命令行参数
  unformat_init_command_line (&input, (char **) vm->argv);
  if ((e = vlib_plugin_config (vm, &input)))
    {
      clib_error_report (e);
      return 1;
    }
  unformat_free (&input);
  // vlib_plugin_early_init函数会调用vlib_load_new_plugins函数
  // 完成对插件目录下所有插件的加载
  i = vlib_plugin_early_init (vm);
  if (i)
    return i;
  // vlib_call_all_config_functions函数解析命令行参数
  unformat_init_command_line (&input, (char **) vm->argv);
  if (vm->init_functions_called == 0)
    vm->init_functions_called = hash_create (0, /* value bytes */ 0);
  e = vlib_call_all_config_functions (vm, &input, 1 /* early */ );
  if (e != 0)
    {
      clib_error_report (e);
      return 1;
    }
  unformat_free (&input);

  /* always load symbols, for signal handler and mheap memory get/put backtrace */
  clib_elf_main_init (vm->name);
  
  // vlib_thread_stack_init初始化线程栈
  vec_validate (vlib_thread_stacks, 0);
  vlib_thread_stack_init (0);

  __os_thread_index = 0;
  vm->thread_index = 0;

  i = clib_calljmp (thread0, (uword) vm,
            (void *) (vlib_thread_stacks[0] +
                  VLIB_THREAD_STACK_SIZE));
  return i;
}

vlib_unix_main函数中,首先通过vlib_plugin_config函数从命令行加载和配置插件。再调用vlib_plugin_early_init函数实现在main函数之前初始化插件。需要注意的是,vlib_plugin_early_init函数是通过调用vlib_load_new_plugins函数来加载插件目录,vlib_load_new_plugins函数将会调用load_one_plugin函数,load_one_plugin函数会通过dlopen函数加载插件目录下的所有插件。vlib_unix_main函数调用vlib_call_all_config_functions来对命令行的选项进行解析,完成相应的配置。

vlib_thread_stack_init函数初始化线程栈,并将os线程指定为索引0线程。vlib_unix_main最后调用clib_calljmp函数,clib_calljmp函数使用thread0作为回调函数,thread0也是初始化得到的线程。在thread0函数中,函数跳转到src/vlib/main.c中的vlib_main函数。

1.4 vlib_main函数

vlib_main函数的源代码如下所示。

/* Main function. */
int
vlib_main (vlib_main_t * volatile vm, unformat_input_t * input)
{
  clib_error_t *volatile error;
  vlib_node_main_t *nm = &vm->node_main;

  vm->queue_signal_callback = dummy_queue_signal_callback;

  clib_time_init (&vm->clib_time);

  /* Turn on event log. */
  if (!vm->elog_main.event_ring_size)
    vm->elog_main.event_ring_size = 128 << 10;
  elog_init (&vm->elog_main, vm->elog_main.event_ring_size);
  elog_enable_disable (&vm->elog_main, 1);
  vl_api_set_elog_main (&vm->elog_main);
  (void) vl_api_set_elog_trace_api_messages (1);

  /* Default name. */
  if (!vm->name)
    vm->name = "VLIB";

  if ((error = vlib_physmem_init (vm)))
    {
      clib_error_report (error);
      goto done;
    }

  if ((error = vlib_map_stat_segment_init (vm)))
    {
      clib_error_report (error);
      goto done;
    }

  if ((error = vlib_buffer_main_init (vm)))
    {
      clib_error_report (error);
      goto done;
    }

  if ((error = vlib_thread_init (vm)))
    {
      clib_error_report (error);
      goto done;
    }

  /* Register static nodes so that init functions may use them. */
  // 注册所有静态节点
  vlib_register_all_static_nodes (vm);

  /* Set seed for random number generator.
     Allow user to specify seed to make random sequence deterministic. */
  if (!unformat (input, "seed %wd", &vm->random_seed))
    vm->random_seed = clib_cpu_time_now ();
  clib_random_buffer_init (&vm->random_buffer, vm->random_seed);

  /* Initialize node graph. */
  // 完成节点图的初始化
  if ((error = vlib_node_main_init (vm)))
    {
      /* Arrange for graph hook up error to not be fatal when debugging. */
      if (CLIB_DEBUG > 0)
    clib_error_report (error);
      else
    goto done;
    }

  /* Direct call / weak reference, for vlib standalone use-cases */
  if ((error = vpe_api_init (vm)))
    {
      clib_error_report (error);
      goto done;
    }

  if ((error = vlibmemory_init (vm)))
    {
      clib_error_report (error);
      goto done;
    }

  if ((error = map_api_segment_init (vm)))
    {
      clib_error_report (error);
      goto done;
    }

  /* See unix/main.c; most likely already set up */
  if (vm->init_functions_called == 0)
    vm->init_functions_called = hash_create (0, /* value bytes */ 0);
  if ((error = vlib_call_all_init_functions (vm)))
    goto done;

  nm->timing_wheel = clib_mem_alloc_aligned (sizeof (TWT (tw_timer_wheel)),
                         CLIB_CACHE_LINE_BYTES);

  vec_validate (nm->data_from_advancing_timing_wheel, 10);
  _vec_len (nm->data_from_advancing_timing_wheel) = 0;

  /* Create the process timing wheel */
  TW (tw_timer_wheel_init) ((TWT (tw_timer_wheel) *) nm->timing_wheel,
                0 /* no callback */ ,
                10e-6 /* timer period 10us */ ,
                ~0 /* max expirations per call */ );

  vec_validate (vm->pending_rpc_requests, 0);
  _vec_len (vm->pending_rpc_requests) = 0;
  vec_validate (vm->processing_rpc_requests, 0);
  _vec_len (vm->processing_rpc_requests) = 0;

  if ((error = vlib_call_all_config_functions (vm, input, 0 /* is_early */ )))
    goto done;

  /* Sort per-thread init functions before we start threads */
  vlib_sort_init_exit_functions (&vm->worker_init_function_registrations);

  /* Call all main loop enter functions. */
  {
    clib_error_t *sub_error;
    sub_error = vlib_call_all_main_loop_enter_functions (vm);
    if (sub_error)
      clib_error_report (sub_error);
  }

  switch (clib_setjmp (&vm->main_loop_exit, VLIB_MAIN_LOOP_EXIT_NONE))
    {
    case VLIB_MAIN_LOOP_EXIT_NONE:
      vm->main_loop_exit_set = 1;
      break;

    case VLIB_MAIN_LOOP_EXIT_CLI:
      goto done;

    default:
      error = vm->main_loop_error;
      goto done;
    }

  vlib_main_loop (vm);

done:
  /* Call all exit functions. */
  {
    clib_error_t *sub_error;
    sub_error = vlib_call_all_main_loop_exit_functions (vm);
    if (sub_error)
      clib_error_report (sub_error);
  }

  if (error)
    clib_error_report (error);

  return 0;
}

vlib_main函数中,通过vlib_register_all_static_nodes注册所有的静态节点,通过vlib_node_main_init函数初始化节点图,通过vlib_call_all_config_functions配置子系统,然后进入vlib_call_all_main_loop_enter_functions的主循环,完成所有函数的初始化。vlib_main函数最后调用vlib_main_loop函数,此函数是thread0的主循环。

1.5 vlib_main_loop函数

src/vlib/main.c函数中定义了两个循环函数,vlib_main_loopvlib_worker_loopvlib_main_loop对应的就是thread0线程,即控制平面;而vlib_worker_loop对应的是worker线程,即数据平面。两个函数都调用vlib_main_or_worker_loop函数进入循环。我们仅关注vlib_main_loop的循环,可以看到在while(1)循环中,涉及到以下内容:

  • pre-input节点,epoll节点,对socket相关逻辑提供服务,用在控制平面上。
  • input节点,收发包逻辑节点
  • 处理中断。
  • timer_wheel和process节点,判断process节点是否在定时轮中过期。
  • pending frames

    • 循环的矢量包处理基本都在这里完成。
    • 矢量包 + 节点派遣函数(dispatching function)。

2. VPP图节点调度

2.1 节点类型

VPP中的图节点分为四种类型:

  • VLIB_NODE_TYPE_INTERNAL,被调用图的内部节点,负责处理数据包。
  • VLIB_NODE_TYPE_INPUT,收包逻辑节点,每次main loop迭代之前都会被调用。
  • VLIB_NODE_TYPE_PRE_INPUT,调用输入节点之前的图节点,用作诸如在处理输入数据包前清空网卡TX rings
  • VLIB_NODE_TYPE_PROCESS,节点可挂起也可恢复,类似实现了单个线程中的多任务调度机制。

2.2 主要结构体

VPP图节点调度涉及如下结构体:

  • vlib_node_main_t,图节点柱结构,记录图节点的全局信息。
  • vlib_node_t,记录图节点的相关静态信息。
  • vlib_node_runtime_t,图节点调度实际使用的结构体,由vlib_node_t结构体中的信息和私有信息组成。
  • vlib_frame_t,保存图节点要处理的数据的内存地址信息。
  • vlib_pending_frame_t,记录运行节点的索引、数据包索引和下一个数据包的索引。
  • vlib_next_frame_t,记录图节点要处理的下一条的数据。

2.3 图节点调度流程

vlib_main_loop函数负责调度图节点。基本的矢量数据包的处理流程很简单,首先由一组输入图节点产生矢量数据包,再由图节点调度函数将矢量数据包推向有向图,并根据需要将数据包细分,直到原始的矢量数据包已被完全处理,然后继续重复这个过程。

vpp的函数调用更像是一种各个节点之间相互连接,通过决定下一跳节点的路径在确定整个代码的执行路径。同样这种方式的函数调用提供了很低的耦合性,所以基于这种方式的二次开发不用太多考虑各个模块之间的相互影响,甚至可以完全不用考虑,自己定义的节点根据格式给出相应的回调函数来插入自定义的功能,下面是一个简单转发的node调用图。

graph node dispatch.png

参考链接

  1. https://zyao.org/linux/vpp/20...
  2. https://www.youtube.com/watch...
  3. https://blog.csdn.net/jqh9804...
推荐阅读
关注数
2444
内容数
14
介绍Arm相关的开源软件。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息