VPP作为一个开源的、高性能的用户态网络协议栈,以进程的形式运行于Linux或(类unix)系统下,即VPP实际是一个用户进程,VPP启动后可通过"ps -ef | grep vpp"命令查看。
用户态进程启动都有一个main函数即程序入口函数,VPP也不例外,VPP main函数位于/src/vpp/net/main.c中,主要做了如下工作:
1)加载并解析VPP配置文件startup.conf,获取诸如heapsize、plugin_path等配置参数值。
2)利用配置参数值完成内存堆初始化
3)vlib_main_init
4)vpe_main_init
5)vlib_unix_main
针对上述过程,分别了解学习
VPP配置文件在启动时通过 如下形式加载:
vpp -c /etc/vpp/startup.conf
关于配置文件形式、内容及参数含义可参考此前博客:VPP配置文件介绍
配置文件读取、解析获取配置参数信息。
配置文件解析后,完成相关初始化工作,具体如下:
1)设置堆大小,堆页大小参数,堆大小默认为1G,在main函数入口时设定uword main_heap_size = (1ULL << 30),
2)设置主线程CPU亲和性
3)根据此前设置好的堆相关参数,分配并完成堆的初始化
a)main_heap = clib_mem_init_with_page_size (main_heap_size,
main_heap_log2_page_sz)
i) clib_mem_init_internal
1) clib_mem_main_init初始化VPP巨页大小及numa节点信息
2)clib_mem_create_heap_internal (base, size, log2_page_sz,
1 /*is_locked */ , "main heap") 分配创建堆,
a)获取有效的页大小信息
b)使得分配内存的大小对于页大小
c)clib_mem_vm_map_internal分配heap内存
d)初始化并返回结构体变量指针clib_mem_heap_t *h.
b)获取当前numa 节点索引
c)设置巨页大小
d)根据此前分配的主堆设置每一个numa节点的main_heap: clib_mem_set_per_numa_heap (main_heap);
1)vgm->init_functions_called = hash_create (0, /* value bytes */ 0);创建hash表用于记录已被调用的初始化函数;当vpp启动后,可以通过命令行查看有哪些初始化函数,如下所示:
2)vm = clib_mem_alloc_aligned (sizeof (*vm), CLIB_CACHE_LINE_BYTES); 分配vlib_main_t 结构体变量,用于指示每个线程的mains。
3)将vm加入一个向量(vector)vgm->vlib_mains
vpe_main_init函数完成插件路径的设置,具体实现如下:
vpe_main_init (vlib_main_t * vm)
{
#if VPP_API_TEST_BUILTIN > 0
void vat_plugin_hash_create (void);
#endif
/* 设置CLI客户端命令行提示符 */
if (CLIB_DEBUG > 0)
vlib_unix_cli_set_prompt ("DBGvpp# ");
else
vlib_unix_cli_set_prompt ("vpp# ");
/* Turn off network stack components which we don't want */
vlib_mark_init_function_complete (vm, srp_init);
/*
* Create the binary api plugin hashes before loading plugins
*/
#if VPP_API_TEST_BUILTIN > 0
vat_plugin_hash_create ();
#endif
/* 设置插件路径 */
if (!vlib_plugin_path)
vpp_find_plugin_path ();
}
在vlib_unix_main函数中
1)首先通过vlib_plugin_config函数从命令行加载和配置插件。
2)调用vlib_plugin_early_init函数实现在main函数之前初始化插件。需要注意的是,vlib_plugin_early_init函数是通过调用vlib_load_new_plugins函数来加载插件目录,这个目录可以通过命令行指定,默认的插件路径是 /usr/lib/vpp_plugins;
vlib_load_new_plugins函数将会调用load_one_plugin函数,load_one_plugin函数会通过dlopen函数加载插件目录下的所有插件(插件以动态库(.so)的形式存在的);
dlopen每个插件后,VPP会获取函数vlib_plugin_registration的符号地址,每个插件都要求实现该函数,这个函数会传递非常重要的数据,包括插件的版本信息、early_init初始化函数指针、描述信息等
3)vlib_unix_main函数调用vlib_call_all_config_functions来对命令行的选项进行解析,完成相应的配置。
4)vlib_thread_stack_init函数初始化线程栈,并将os线程指定为索引0线程。vlib_unix_main最后调用clib_calljmp函数,clib_calljmp函数使用thread0作为回调函数,thread0也是初始化得到的线程;在thread0函数中,函数跳转到src/vlib/main.c中的vlib_main函数。具体实现如下:
static uword
thread0 (uword arg)
{
vlib_main_t *vm = (vlib_main_t *) arg;
vlib_global_main_t *vgm = vlib_get_global_main ();
unformat_input_t input;
int i;
vlib_process_finish_switch_stack (vm);
/* 初始化命令行参数 */
unformat_init_command_line (&input, (char **) vgm->argv);
/* 跳转到vlib_main函数处理 */
i = vlib_main (vm, &input);
unformat_free (&input);
return i;
}
在vlib_main函数中:
1)通过vlib_register_all_static_nodes注册所有的静态节点
2)通过vlib_node_main_init函数初始化节点图,
3)通过vlib_call_all_config_functions配置子系统,
4) 然后进入vlib_call_all_init_functions,完成所有函数的初始化。
5)vlib_main函数最后调用vlib_main_loop函数,此函数是thread0的主循环。
vlib_main具体实现如下:
int vlib_main (vlib_main_t * volatile vm, unformat_input_t * input)
{
vlib_global_main_t *vgm = vlib_get_global_main ();
clib_error_t *volatile error;
vlib_node_main_t *nm = &vm->node_main;
vm->queue_signal_callback = placeholder_queue_signal_callback;
/* Reconfigure event log which is enabled very early */
if (vgm->configured_elog_ring_size &&
vgm->configured_elog_ring_size != vgm->elog_main.event_ring_size)
elog_resize (&vgm->elog_main, vgm->configured_elog_ring_size);
vl_api_set_elog_main (vlib_get_elog_main ());
(void) vl_api_set_elog_trace_api_messages (1);
/* Default name. */
if (!vgm->name)
vgm->name = "VLIB";
if ((error = vlib_physmem_init (vm)))
{
clib_error_report (error);
goto done;
}
if ((error = vlib_log_init (vm)))
{
clib_error_report (error);
goto done;
}
if ((error = vlib_stats_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;
}
/* 注册所有节点的节点函数变量 */
vlib_register_all_node_march_variants (vm);
/* 注册所有静态节点 */
vlib_register_all_static_nodes (vm);
/* 设置随机生成器的种子 */
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 (vgm->init_functions_called == 0)
vgm->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_set_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,
process_expired_timer_cb /* callback */, 10e-6 /* timer period 10us */,
~0 /* max expirations per call */);
vec_validate (vm->pending_rpc_requests, 0);
vec_set_len (vm->pending_rpc_requests, 0);
vec_validate (vm->processing_rpc_requests, 0);
vec_set_len (vm->processing_rpc_requests, 0);
/* Default params for the buffer allocator fault injector, if configured */
if (VLIB_BUFFER_ALLOC_FAULT_INJECTOR > 0)
{
vm->buffer_alloc_success_seed = 0xdeaddabe;
vm->buffer_alloc_success_rate = 0.80;
}
if ((error = vlib_call_all_config_functions (vm, input, 0 /* is_early */ )))
goto done;
/*
* Use exponential smoothing, with a half-life of 1 second
* reported_rate(t) = reported_rate(t-1) * K + rate(t)*(1-K)
*
* Sample every 20ms, aka 50 samples per second
* K = exp (-1.0/20.0);
* K = 0.95
*/
vm->damping_constant = exp (-1.0 / 20.0);
/* Sort per-thread init functions before we start threads */
vlib_sort_init_exit_functions (&vgm->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:
/* Stop worker threads, barrier will not be released */
vlib_worker_thread_barrier_sync (vm);
/* 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 vm->main_loop_exit_status;
}
在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的循环,如下所示:
static void
vlib_main_loop (vlib_main_t * vm)
{
/* is_main 为1 表示main_loop */
vlib_main_or_worker_loop (vm, /* is_main */ 1);
}
主要涉及以下内容:
1) 启动所有线程:dispatch_process (vm, nm->processes[i], /* frame */ 0, cpu_time_now);创建相互协作的多线程,VPP主要有三种类型的线程:
普通线程:比如统计采集。
EAL线程:处理包的工作。
Processes:这些都是定期执行、相互协作的多线程,VPP主线程的超时到期之后会执行这些。
2)在while循环中处理不同类型的节点;
while(1) {
//PRE_INPUT是在调用INPUT之前需要处理的node,例如清除网卡发送队列等
dispatch_node (..., VLIB_NODE_TYPE_PRE_INPUT, ....);
//处理所有INPUT类型的NODE,例如DPDK的node
dispatch_node (..., VLIB_NODE_TYPE_INPUT, ....);
//处理所有INTERNAL类型的NODE,所有业务相关的都是该类型的NODE,例如ACL、ROUTE等的node
dispatch_pending_node (VLIB_NODE_TYPE_INTERNAL);
//处理所有PROCESS类型的NODE,对应产生事件的node,恢复到该node的睡眠点执行。例如定时器等
//仅在主线程中处理该类型的node
dispatch_suspended_process (VLIB_NODE_TYPE_PROCESS);
}
阐述一下VPP初始化流程