今天我们介绍一下VPP的启动流程。如果把vpp看成一个应用程序,那么他是怎么工作的呢:
首先,vpp有许多插件so,每个插件下面有许多node,比如我们如果要新加一个功能为ip地址过滤,那么我们其实就是加一个node,当数据经过node时,调用我们提前注册的function函数就实现了我们的功能。接下来就理一下vpp从启动后到node生效的过程是怎样的。
开始之前先说下DPDK是怎么和vpp交互的。从vpp看来,dpdk就是一个插件,只不过这个插件运行得比较靠前,当数据包到来之后,dpdk将其收到rte_mbuf中,后面再将rte_mbuf转化为vlib_buf,后面的node就是从vlib_buf中取出数据再来处理,简单讲下rte_mbuf和vlib_buf的关系,以及他们是如何转化的:

#define rte_mbuf_from_vlib_buffer(x) (((struct rte_mbuf *)x) - 1)
#define vlib_buffer_from_rte_mbuf(x) ((vlib_buffer_t *)(x+1))
这两个宏定义用于在vlib buffer和DPDK的rte_mbuf之间进行转换。rte_mbuf 是DPDK中用于表示数据包的结构体,而 vlib_buffer_t 是VPP中用于表示数据包的结构体。以下是对这两个宏的解释:
rte_mbuf_from_vlib_buffer(x): 这个宏将一个 vlib_buffer_t 转换成对应的 rte_mbuf。vlib_buffer_t 中的数据的指针指向 rte_mbuf 的后一个字节,所以通过将 x 强制转换为 struct rte_mbuf *,然后减去1,就可以得到对应的 rte_mbuf。
vlib_buffer_from_rte_mbuf(x): 这个宏将一个 rte_mbuf 转换成对应的 vlib_buffer_t。由于上面的宏中的指针偏移关系,将 x 强制转换为 vlib_buffer_t * 之后,可以得到指向 vlib_buffer_t 数据部分的指针
VPP 初始化
初始化里面主要做了四件事
1、load so //加载静态库
2、register node //注册处理节点
3、function init (cmd init,feture init) //注册处理函数
先上个伪代码简单看下流程
int main(){
//VPP 程序的入口函数,它初始化全局状态、配置、插件以及其他运行时环境,并最终进入主循环以运行网络处理和其他功能
vlib_unix_main();
//执行了插件的早期初始化,设置了插件路径、日志等,并准备加载新的插件
vlib_plugin_early_init();
//加载新插件,并进行相应的管理和处理,确保只加载有效的插件,并进行了排序、覆盖等操作
vlib_load_new_plugins();
//加载插件并进行一系列的检查,确保插件可以正确加载和初始化
load_one_plugin();
//通过协程跳转到thread0执行
clib_calljmp();
//线程函数,作为独立线程的入口点
thread0();
//各种初始化操作,调用初始化函数、配置函数、主循环函数、以及一系列的清理操作
vlib_main();
//调用所有初始化函数
vlib_call_all_init_functions();
vlib_call_init_exit_functions
//遍历所有的节点注册(node_registrations 链表),并将这些注册的节点全部注册到 VLIB 中
vlib_register_all_static_nodes();
//
vlib_main_loop (vm);
//VLIB 主循环或工作线程循环的核心部分。它处理节点的调度和执行,以及一些循环计数和时间间隔的计算
vlib_main_or_worker_loop (vm, /* is_main */ 1);
//负责管理进程的调度、执行和暂停等。根据进程的状态和标志,它会判断是否继续执行进程,
//启动定时器等。执行完毕后,会更新性能计数器、事件日志以及进程的统计信息
dispatch_process();
//责将进程切换到新的堆栈并执行其启动函数
vlib_process_startup
//进程启动的具体逻辑,包括在进程的堆栈上调用进程函数并处理返回值
vlib_process_bootstrap
// 这里就调用到了我们注册一个node之后,真正的处理函数
n = node->function (vm, node, f);
}
接下来对上面伪代码中的函数进行详细分析下:
从main函数开始,main函数中其实都是配置和初始化VPP环境的相关东西,大概就是下面这样:
int main(){
//创建主堆,使用了之前设置的 main_heap_size 和 main_heap_log2_page_sz
main_heap = clib_mem_init_with_page_size(main_heap_size, main_heap_log2_page_sz);
//初始化VLIB库,初始化VPE(VPP Environment)库
//将一个VLIB主要实例传递给了 vpe_main_init() 函数,以便VPE库能够与VLIB库进行集成
vlib_main_init();
vpe_main_init(vlib_get_first_main());
//进入VLIB的主循环并开始执行网络处理程序。这是整个程序的核心部分,处理数据包、事件和各种网络功能
return vlib_unix_main(argc, argv);
}
vlib_unix_main函数解析:
int
vlib_unix_main (int argc, char *argv[])
{
//初始化时间库
clib_time_init (&vm->clib_time);
//初始化并启用事件日志,用于记录系统中的事件
elog_init (vlib_get_elog_main (), vgm->configured_elog_ring_size);
elog_enable_disable (vlib_get_elog_main (), 1);
//初始化插件的配置,通过调用插件的配置函数来加载和处理插件特定的配置信息
if ((e = vlib_plugin_config (vm, &input)))
{
clib_error_report (e);
return 1;
}
unformat_free (&input);
//这里是各种so插件初始化的地方
i = vlib_plugin_early_init (vm);
if (i)
return i;
//调用注册的配置函数,这些函数在插件中注册,用于配置程序的各种功能
e = vlib_call_all_config_functions (vm, &input, 1 /* early */ );
if (e != 0)
{
clib_error_report (e);
return 1;
}
unformat_free (&input);
//加载 ELF 格式文件的符号,用于处理信号处理程序和内存分配追踪
clib_elf_main_init (vgm->exec_path);
vec_validate (vlib_thread_stacks, 0);
vlib_thread_stack_init (0);
__os_thread_index = 0;
vm->thread_index = 0;
//启动主循环,将堆栈切换到主线程的堆栈,然后使用 clib_calljmp 调用主循环
vlib_process_start_switch_stack (vm, 0);
i = clib_calljmp (thread0, (uword) vm,
(void *) (vlib_thread_stacks[0] +
VLIB_THREAD_STACK_SIZE));
return i;
}
这里我们再看看插件是如何被加载的,通过vlib_plugin_early_init
int
vlib_plugin_early_init (vlib_main_t * vm)
{
plugin_main_t *pm = &vlib_plugin_main;
pm->logger =
vlib_log_register_class_rate_limit ("plugin", "load",
0x7FFFFFFF /* aka no rate limit */ );
//如果插件路径未设置,则使用默认的插件路径 vlib_plugin_path
if (pm->plugin_path == 0)
pm->plugin_path = format (0, "%s", vlib_plugin_path);
//如果有额外的插件路径附加,将其添加到插件路径中
if (pm->plugin_path_add)
pm->plugin_path = format (pm->plugin_path, ":%s", pm->plugin_path_add);
//在插件路径的末尾添加一个 null 字节,以便可以在字符串中找到插件路径的终止
pm->plugin_path = format (pm->plugin_path, "%c", 0);
PLUGIN_LOG_DBG ("plugin path %s", pm->plugin_path);
//创建两个哈希表,用于存储插件的信息和覆盖信息
pm->plugin_by_name_hash = hash_create_string (0, sizeof (uword));
pm->plugin_overrides_by_name_hash = hash_create_string (0, sizeof (uword));
//设置插件主结构体的 vlib_main 字段,以指向 VLIB 主结构体
pm->vlib_main = vm;
//加载新的插件
return vlib_load_new_plugins (pm, 1 /* from_early_init */ );
}
插件的加载就是vlib_load_new_plugins 函数,它做了哪些事呢:加载新插件,并进行相应的管理和处理,确保只加载有效的插件,并进行了排序、覆盖等操作。我们如果自己新定制了plugin插件,也是在这里处理
int
vlib_load_new_plugins (plugin_main_t * pm, int from_early_init)
{
DIR *dp;
struct dirent *entry;
struct stat statb;
uword *p;
plugin_info_t *pi;
u8 **plugin_path;
uword *not_loaded_indices = 0;
int i;
//根据插件路径字符串生成一个字符串数组,每个元素表示一个路径
plugin_path = split_plugin_path (pm);
for (i = 0; i < vec_len (plugin_path); i++)
{
dp = opendir ((char *) plugin_path[i]);
if (dp == 0)
continue;
//遍历插件路径数组,打开每个路径对应的目录,并遍历目录中的每个文件项
while ((entry = readdir (dp)))
{
u8 *plugin_name;
u8 *filename;
//如果设置了插件名过滤,将会检查文件名是否满足过滤条件。如果不满足,则跳过当前文件项
if (pm->plugin_name_filter)
{
int j;
for (j = 0; j < vec_len (pm->plugin_name_filter); j++)
if (entry->d_name[j] != pm->plugin_name_filter[j])
goto next;
}
//检查文件的扩展名是否是 .so,并且通过 stat 函数检查文件是否可读
filename = format (0, "%s/%s%c", plugin_path[i], entry->d_name, 0);
char *ext = strrchr ((const char *) filename, '.');
/* unreadable */
if (!ext || (strcmp (ext, ".so") != 0) ||
stat ((char *) filename, &statb) < 0)
{
ignore:
vec_free (filename);
continue;
}
/* 检查文件是否是一个常规文件,即普通文件,而不是目录或其他类型的文件 */
if (!S_ISREG (statb.st_mode))
goto ignore;
//如果文件是有效的插件文件,将其信息添加到插件信息数组中,并在哈希表中建立插件名称到插件信息的映射
plugin_name = format (0, "%s%c", entry->d_name, 0);
p = hash_get_mem (pm->plugin_by_name_hash, plugin_name);
if (p == 0)
{
/* No, add it to the plugin vector */
vec_add2 (pm->plugin_info, pi, 1);
pi->name = plugin_name;
pi->filename = filename;
pi->file_info = statb;
pi->handle = 0;
hash_set_mem (pm->plugin_by_name_hash, plugin_name,
pi - pm->plugin_info);
}
next:
;
}
closedir (dp);
vec_free (plugin_path[i]);
}
vec_free (plugin_path);
/*
根据插件名称对插件信息数组进行排序,确保插件按照名称的字典顺序排列
*/
vec_sort_with_function (pm->plugin_info, plugin_name_sort_cmp);
/*
遍历插件信息数组,并尝试加载每个插件,如果加载失败,则记录下来
*/
for (i = 0; i < vec_len (pm->plugin_info); i++)
{
pi = vec_elt_at_index (pm->plugin_info, i);
if (load_one_plugin (pm, pi, from_early_init))
{
/* Make a note of any which fail to load */
vec_add1 (not_loaded_indices, i);
}
}
/*
处理未能成功加载的插件,以及通过覆盖列表进行覆盖的插件
*/
for (i = 0; i < vec_len (pm->plugin_info); i++)
{
uword *p;
pi = vec_elt_at_index (pm->plugin_info, i);
p = hash_get_mem (pm->plugin_overrides_by_name_hash, pi->name);
/* Plugin overridden? */
if (p)
{
PLUGIN_LOG_NOTICE ("Plugin '%s' overridden by '%s'", pi->name,
p[0])

本文介绍了VPP的启动流程,包括VPP与DPDK的交互,DPDK将数据包收到rte_mbuf后转化为vlib_buf供后续node处理。还阐述了VPP初始化的四件事,如加载静态库、注册处理节点等,并详细分析了相关函数,展示了注册node后处理函数生效的过程。
最低0.47元/天 解锁文章
1278

被折叠的 条评论
为什么被折叠?



