关键词: 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
的加载和解析,获取诸如heapsize
、plugin_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_loop
与vlib_worker_loop
。vlib_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调用图。