VPP源码与示例插件开发:graph node到feature arc实战

发布时间:2026/10/26 2:08:34
VPP源码与示例插件开发:graph node到feature arc实战 简介面向网络数据平面开发初学者与需要研究 VPPVector Packet Processing架构的工程师这套资料以源码阅读与插件开发实践为核心帮助读者理解 VPP 的工程组织、核心模块拆解以及在其上编写示例插件的基本路径并减少从零摸索的成本。压缩包共约 2000 个文件、22.26MB主体为 1329 个 C 源文件与 798 个头文件并配套 RST 文档、Python 脚本、API/HPP/CPP 代码、YAML 配置、补丁、Makefile/CMake 构建文件及少量 Shell 与 Docker 相关文件覆盖协议处理、CLI、NAT、ACL、IPsec 等模块目录层次较完整适合按模块逐层研读。已有 561 人学习说明该主题具备一定关注度。读者可从中获得较完整的 VPP 源码树、插件示例参考、接口定义与构建配置线索便于搭建本地实验环境、定位核心数据结构与调用关系并围绕二次开发、问题排查和源码笔记整理形成可复用的学习依据对入门与进阶读者均有参考价值。1. 从一次数据面改造说起VPP 源码加示例插件到底解决什么问题接一个转发面改造需求时最常卡在第一步包从哪进来、在哪个环节改、改完送到哪个节点。内核里挂 netfilter 或者写 eBPF 能覆盖一部分场景但当转发路径要按接口粒度开关、按 feature 顺序编排、还要能在几十 Gbps 下保持稳定图节点graph node这套模型更顺手。VPP 源码搭配一个能在它上面跑起来的示例插件解决的正是这个落地环节一棵能编译的源码树加一个最小但完整的插件实例从VLIB_REGISTER_NODE到vlib_get_next_frame再到挂进 feature arc整条链路走通一遍。适合写过 C、想往用户态转发面挪的工程师。2. 源码树与构建把 VPP 从仓库跑到能 ping 通的四步拿到源码第一件事不是急着写插件而是先确认这棵树在你机器上能编、能起、能查。很多人跳过这一步直接改代码结果后面所有异常都分不清是插件写错还是环境没搭好排查起来像开盲盒。这一章按「看目录 → 装依赖 → 编译 → 起服务验证」四步走每一步都给可对照的检查点。2.1 目录结构决定了插件该放哪VPP 的源码树布局是有强约定的插件写错位置或者头文件包含错层级编译期就会给你一堆找不到符号。先把几个关键目录认清楚目录职责写插件时用到的部分src/vppinfra基础容器vec、hash、pool、bitmap、heapvec_validate、format、clib_memsrc/vlibnode graph 调度、buffer 管理、主循环、CLI 解析vlib_get_next_frame、VLIB_REGISTER_NODE、VLIB_CLI_COMMANDsrc/vnet协议栈、接口抽象、FIB、feature arcvnet_feature_enable_disable、VNET_FEATURE_INITsrc/plugins插件目录一个插件一个子目录新插件默认落这里src/vppvpp主程序入口一般不需要改build-root构建工作区编译产物和安装前缀都在这运行时找插件路径靠它src/cmake公共 CMake 模块add_vpp_plugin宏定义在这add_vpp_plugin这个宏是整个插件构建的核心它帮你处理了.so命名后缀、安装路径、多架构源文件分组。所以插件目录里那份CMakeLists.txt通常只有几行不需要自己写add_library。2.2 依赖与构建make 封装和裸 CMake 两条路最省事的是走顶层 Makefile 封装。它会替你把 CMake 参数、Ninja 生成、安装前缀都串起来# 1. 安装编译依赖Debian/Ubuntu 系需要 root sudo make install-dep # 2. 可选拉取 DPDK、RDMA 这类外部依赖 # 只做插件开发、不碰硬件卸载的话可以跳过编译时用 -DVPP_USE_DPDKOFF make install-ext-deps # 3. 编译默认走 native 构建 make buildinstall-dep这个目标名在不同版本里可能是单数也可能是复数敲之前用 Tab 补一下最稳。install-ext-deps会把外部依赖下载到build-root/downloads网络受限的环境里这一步最容易卡住卡住就直接跳过后面显式关掉对应开关。想精细控制编译选项就绕过封装直接调 CMakemkdir -p build-root/build-vpp-native/vpp cd build-root/build-vpp-native/vpp cmake -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DVPP_USE_DPDKOFF \ -DVPP_USE_RDMAOFF \ /path/to/vpp-source ninja -j$(nproc)参数含义逐个说清CMAKE_BUILD_TYPE选Release还是Debug直接决定后面能不能用 gdb 看到有意义的栈排查崩溃时我一般单独建一个 Debug 目录编一份别在生产用的那份上反复切VPP_USE_DPDK关掉能显著缩短首次编译时间做插件逻辑验证阶段完全够用VPP_USE_RDMA同理CMAKE_INSTALL_PREFIX不指定就走默认前缀指定了要记住因为运行时找插件靠的就是它。编译耗时在普通开发机上通常十几到几十分钟取决于核数和是否开 LTO。中途失败先看最早那条 errorVPP 的编译错误经常是级联的第一条之后的几百行都是噪声。2.3 产物布局编译目录和安装前缀是两回事这里是新手第一个高频翻车点。build-root/build-vpp-native/vpp是编译工作目录里面是.o、.so、CMake 缓存build-root/install-vpp-native/vpp是安装前缀里面才是可以执行的bin/vpp和插件目录。你改了代码重新编译如果没触发安装步骤跑起来的可能还是旧二进制配新插件或者新二进制配旧插件。# 看可执行文件 ls -l build-root/install-vpp-native/vpp/bin/vpp # 看插件目录注意这里的多架构子路径 ls build-root/install-vpp-native/vpp/lib/x86_64-linux-gnu/vpp_plugins | head -20插件目录里的文件名有硬性约定必须以_plugin.so结尾否则加载器直接跳过日志里只留一行很不起眼的提示。后面第 5 章会专门讲这个坑。2.4 起服务与三条最小验证命令VPP 的配置就是命令行参数把配置块直接写在启动命令后面即可不用非要有配置文件sudo build-root/install-vpp-native/vpp/bin/vpp \ unix { nodaemon cli-listen /run/vpp/cli.sock log /tmp/vpp.log } \ plugins { path /home/dev/vpp/build-root/install-vpp-native/vpp/lib/x86_64-linux-gnu/vpp_plugins }nodaemon让进程留在前台调试阶段必须加cli-listen指定管理 socket 路径plugins { path ... }显式告诉它去哪找.so比依赖编译期前缀可靠。另一个终端连上去验证sudo vppctl -s /run/vpp/cli.sock show version sudo vppctl -s /run/vpp/cli.sock show plugins sudo vppctl -s /run/vpp/cli.sock show node | head -20show version确认进程活着show plugins确认插件目录被扫到了哪怕还没有你自己的插件也应该看到一批内置插件show node确认 graph 初始化完成、能看到dpdk-input、ip4-lookup、error-drop这些熟面孔。这三条都通过构建环境才算真正立住。3. 插件骨架拆解从 VLIB_REGISTER_NODE 到 vlib_get_next_frameVPP 的性能来源在于按 vector 批处理一次调度处理一整帧包而不是一个包一次函数调用。理解这一点插件代码的写法就没有第二种选择——你必须围绕 frame 和 buffer index 数组来组织循环任何「一个包一次 malloc」的写法都会把性能打回原形。3.1 一个包在 graph 里怎么走数据面的基本单位是vlib_buffer_t它由 buffer pool 统一分配本身不含数据数据在b-data指向的偏移处。节点之间传递的不是 buffer 指针而是u32类型的 buffer index。一帧vlib_frame_t就是一段连续的 buffer index 数组节点处理函数拿到的vlib_frame_vector_args (frame)就是这段数组的起始地址。节点注册时声明vector_size sizeof (u32)意思是「我的输入是一组 buffer index」。n_next_nodes和next_nodes声明出口——处理完的包要被送往哪个节点。graph 初始化时会把所有节点的出口连成 DAG运行时调度器按节点当前的 pending frame 决定下一次调度谁。处理函数的三个参数是固定搭配vlib_main_t *vm是当前线程的主结构注意 VPP 是多线程的每个 worker 有自己的vlib_main_tvlib_node_runtime_t *node是这个节点在本线程的运行时视图vlib_frame_t *frame是要处理的这批包。出口 frame 通过vlib_get_next_frame预取写满后vlib_put_next_frame归还。预取-填充-归还是 VPP 数据面最核心的三段式几乎所有节点代码都长这样。3.2 插件目录与 CMake 集成在src/plugins下建目录最小结构只需要两个文件# src/plugins/my_count/CMakeLists.txt add_vpp_plugin (my_count SOURCES my_count.c )add_vpp_plugin的第一个参数是插件名最终产物是my_count_plugin.so后缀由宏自动补。要多线程分发、把热点函数编成多架构版本时加MULTIARCH_SOURCES要暴露 C API 给别的插件用加INSTALL_HEADERS要生成二进制 API 加API_FILES。起步阶段只有SOURCES就够。然后在父级索引里登记否则这个目录根本不会进构建# src/plugins/CMakeLists.txt 末尾追加 add_subdirectory(my_count)这两处少任何一处症状都是「编译成功但没生成 .so」而且不会有明显报错。3.3 node 注册与处理函数骨架下面这份代码可以直接抄包含插件声明、节点注册、处理函数三块#include vlib/vlib.h #include vnet/vnet.h #include vnet/plugin/plugin.h #include vpp/app/version.h /* 插件元信息加载后在 show plugins 里能看到 */ VLIB_PLUGIN_REGISTER () { .version 1.0, .description my-count: 统计经过本节点的报文数与字节数, }; typedef struct { u64 packets; u64 bytes; } my_count_trace_t; typedef struct { /* 按线程分片的累加器下标是 thread index避免多 worker 抢同一缓存行 */ u64 * packets_by_thread; u64 * bytes_by_thread; } my_count_main_t; static my_count_main_t my_count_main; static u8 * format_my_count_trace (u8 * s, va_list * args) { vlib_main_t * vm va_arg (*args, vlib_main_t *); vlib_node_t * node va_arg (*args, vlib_node_t *); my_count_trace_t * t va_arg (*args, my_count_trace_t *); (void) vm; (void) node; /* format_get_indent 用来对齐 show trace 的缩进层级 */ s format (s, my-count: sw_if_index %u len %u, t-sw_if_index, t-current_length); return s; } VLIB_REGISTER_NODE (my_count_node) { .name my-count, .vector_size sizeof (u32), .format_trace format_my_count_trace, .type VLIB_NODE_TYPE_INTERNAL, .n_next_nodes 1, .next_nodes { [0] error-drop, }, }; static uword my_count_node_fn (vlib_main_t * vm, vlib_node_runtime_t * node, vlib_frame_t * frame) { u32 n_left_from frame-n_vectors; u32 * from vlib_frame_vector_args (frame); u32 thread_index vm-thread_index; my_count_main_t * mcm my_count_main; u32 next_index 0; /* 每个 frame 开头做一次长度校正比每包都做便宜得多 */ vec_validate (mcm-packets_by_thread, thread_index); vec_validate (mcm-bytes_by_thread, thread_index); while (n_left_from 0) { u32 n_left_to_next; u32 * to_next; vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next); while (n_left_from 0 n_left_to_next 0) { u32 bi0 from[0]; vlib_buffer_t * b0 vlib_get_buffer (vm, bi0); mcm-packets_by_thread[thread_index] 1; mcm-bytes_by_thread[thread_index] b0-current_length; /* 只有帧带 trace 标记时才写否则 vlib_add_trace 会白跑一趟 */ if (b0-flags VLIB_BUFFER_IS_TRACED) { my_count_trace_t * t vlib_add_trace (vm, node, b0, sizeof (*t)); t-sw_if_index vnet_buffer (b0)-sw_if_index[VLIB_RX]; t-current_length b0-current_length; } to_next[0] bi0; from 1; to_next 1; n_left_from - 1; n_left_to_next - 1; } vlib_put_next_frame (vm, node, next_index, n_left_to_next); } return frame-n_vectors; }几处关键逻辑值得展开。while (n_left_from 0)这个外层循环不能省内层循环的条件是「输入还有包」且「输出还有空位」一旦输出 frame 满了就得归还再预取如果只写内层循环超出一帧容量的包会被静默丢掉而且不会有任何报错这是最难查的一类问题。vec_validate放在外层循环之前一个 frame 只执行一次避免每包都做边界检查。format_trace的va_list参数顺序是固定契约vlib_main_t *、vlib_node_t *、然后才是你自己的 trace 结构。顺序写错在编译期通常不会报错运行到打印 trace 时直接崩这是典型的「编译通过、跑起来就没了」。next_nodes里挂error-drop只是为了有个合法出口实际业务里这里会指向下一个处理节点或者interface-output。3.4 自定义计数器与 CLI 命令数据面统计出来了得能查。VPP 的 CLI 命令注册同样是一个宏static uword my_count_show_fn (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd) { my_count_main_t * mcm my_count_main; u64 total_packets 0, total_bytes 0; int i; for (i 0; i vec_len (mcm-packets_by_thread); i) { total_packets mcm-packets_by_thread[i]; total_bytes mcm-bytes_by_thread[i]; } vlib_cli_output (vm, my-count: %llu packets, %llu bytes, (unsigned long long) total_packets, (unsigned long long) total_bytes); return 0; } VLIB_CLI_COMMAND (my_count_show_command, static) { .path show my-count, .short_help show my-count, .function my_count_show_fn, };path支持空格分层show my-count敲出来时 VPP 的 CLI 会做前缀匹配所以命令名别和内置命令撞车。返回值统一0。vlib_cli_output是线程安全的输出入口别用printf——在 worker 线程里直接往 stdout 写会把管理通道的输出搅乱。计数器按线程分片是必须的。如果所有 worker 往同一个u64上累加缓存行会在核间来回弹包量一大性能掉得莫名其妙这种问题看代码完全看不出来只能从show runtime里发现某几个核的时钟数异常。4. 接进转发路径feature arc 挂载与 startup.conf 加载节点注册成功、show node里能看到名字只说明它进了 graph 的节点表不代表有包会流经它。一个VLIB_NODE_TYPE_INTERNAL节点如果没有任何其它节点在next_nodes里指向它它就是孤立的计数永远为 0。让包真正流过来有三条路可选。4.1 三种接入方式的取舍第一种是挂 feature arc。这是最正规的做法好处是能按接口开关、能通过runs_before/runs_after控制在同一条 arc 上的相对顺序和其它 feature 不打架。缺点是要理解 arc 的语义ip4-unicast、ip6-unicast、ip4-output各自的挂载点不一样。第二种是硬改别人的next_nodes把某个内置节点的出口直接接到你的节点上。适合做验证不适合交付——你改的是别人的代码升级版本时合并冲突会让人怀疑人生。第三种是注册成VLIB_NODE_TYPE_INPUT节点。这类节点由主循环主动轮询state设成VLIB_NODE_STATE_POLLING适合自己从某个 fd 或者共享内存环形队列拉包比如对接自定义采集卡或者外部转发进程。它不需要别人指向它自己就是图的入口。做转发面逻辑增强选第一种做流量镜像或者旁路采集选第三种。4.2 feature arc 注册与按接口使能挂到 IPv4 单播路径上在 FIB 查找之前执行#include vnet/feature/feature.h VNET_FEATURE_INIT (my_count_feature, static) { .arc_name ip4-unicast, .node_name my-count, .runs_before VNET_FEATURES (ip4-lookup), };runs_before是相对约束不是绝对位置。graph 初始化时会把所有 feature 的约束收集起来做一次拓扑排序如果你的约束和别人的冲突比如 A 要求在你之前、B 要求在你之后而 A、B 之间又矛盾启动阶段会直接报 graph 初始化失败日志里能看到具体是哪两个节点。这算是 VPP 比较友好的地方——顺序问题在启动期暴露而不是运行期表现成玄学丢包。注册完只是「可以挂」还得显式使能才会生效。用一个自定义 CLI 命令包一层方便在验证阶段随时开关static uword my_count_enable_fn (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd) { u32 sw_if_index ~0; int is_enable 1; while (unformat_check_input (input) ! UNFORMAT_END_OF_INPUT) { if (unformat (input, %U, unformat_vnet_sw_interface, vnet_get_main (), sw_if_index)) ; else if (unformat (input, disable)) is_enable 0; else return clib_error_return (0, unknown input); } if (sw_if_index ~0) return clib_error_return (0, please specify an interface); vnet p a hrefhttps://download.csdn.net/download/hechuansen123/87886734 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询