Featured image of post VPP插件开发标准模板

VPP插件开发标准模板

本文解析了一个名为myplugin的VPP插件。作为框架示例,它遵循VPP插件开发的标准规范,提供了一个最小可运行、具备CLI命令和API接口、可注册特性并通过VPP Feature Arc挂载到接口上的插件骨架。

创建插件

可以使用VPP提供的插件生成工具make-plugin.sh来快速创建插件模板。该脚本位于./extras/emacs目录中,本质上是一个简单的封装器,调用一系列Emacs Lisp脚本自动生成插件的基础代码结构。

1
sudo apt install emacs
1
2
cd src/plugins
emacs --script ../../extras/emacs/make-plugin.sh

根据引导创建,需要输入相关信息:

  1. 插件名称:此处命名为myplugin;
  2. 插件类型:dual使用传统的单包处理循环,适合逻辑复杂、精细控制型插件;qs则采用批处理,配合 SIMD 优化,适合高性能场景如调度器或整形器。两者结构可互转,qs更简洁明了,dual则更性能高效。本文以dual为例说明介绍。

该插件把每个数据包的以太网源MAC和目的MAC地址交换,然后把包发回原接收接口,并支持Trace功能记录调试信息,可以观察到插件目录包含以下文件内容:

  1. myplugin.h:定义了插件的核心数据结构;
  2. myplugin.c:负责初始化插件环境、注册插件、注册feature、处理CLI消息、处理API消息;
  3. node.c:数据包调度与处理函数;
  4. myplugin_periodic.c:定义周期性后台进程,处理插件周期任务;
  5. myplugin.api:定义插件的API消息格式与结构;
  6. myplugin_test.c:实现插件API的单元测试,通过vpp_api_test工具载入,快速调试API接口。
1
2
cd myplugin
ls

添加或修改完插件代码后,需要返回顶层目录、重新编译整个VPP目录。

1
2
3
4
5
6
7
cd ~
cd vpp

# Build VPP (Debug)
make rebuild
# Build VPP (Release)
make rebuild-release

编译完成之后即可运行使用,可查看插件是否被启用。

1
2
3
4
5
6
# For Debug Building
make run STARTUP_CONF=./startup.conf
# For Release Building
make run-release STARTUP_CONF=./startup.conf

vpp > show plugins

注册插件

VLIB_PLUGIN_REGISTER是VPP提供的一个标准宏,用于将插件的信息注册到VPP的插件管理系统中,供运行时加载和识别,包含以下字段:

  1. .version = VPP_BUILD_VER:表示该插件的版本,通常建议直接从构建系统中定义(即VPP_BUILD_VER宏,代表当前VPP的版本号),当然也可以自定义为任意版本标识;
  2. .description = “…":插件的简要描述;会在VPP的CLI输出中查看(即vppctl show plugins命令);
  3. .default_disabled = 1:可选字段,表示插件的feature默认是禁用状态(在feature arc上不启用,随后会进行介绍),需要用户通过CLI或API手动启用它。
1
2
3
4
5
6
VLIB_PLUGIN_REGISTER() =
{
  .version = VPP_BUILD_VER,
  // .default_disabled = 1,
  .description = "myplugin plugin description goes here",
};

插件初始化

myplugin_main_t是插件的全局状态结构体,通常定义为全局变量,用于保存插件运行时的各种状态和上下文,方便插件各部分功能共享访问。结构体内的字段根据需求自行设计,插件模板设计了以下字段:

  1. msg_id_base:用于实现API功能,VPP中所有API消息都有一个唯一的ID,插件一般会注册一组相关的消息,这些消息的ID是连续的,msg_id_base代表插件消息ID的起始号,插件内所有消息的ID都是基于这个值偏移,如果msg_id_base是1000,则插件第一个消息ID是1000,第二个是1001,依此类推;
  2. periodic_timer_enabled:标识插件的周期性任务(定时器)是否启用,值为0表示定时任务关闭、1表示定时任务开启;
  3. periodic_node_index:VPP中的定时任务往往实现为节点,该索引用来标识该周期任务节点在VPP的节点表中的位置,0通常表示该周期节点还没创建或注册;
  4. vlib_main:指向VPP主调度器的上下文结构体,管理调度、内存、节点调度、线程等;
  5. vnet_main:指向VPP的网络栈子系统核心结构体,管理接口、协议栈、流量转发等网络层功能;
  6. ethernet_main:指向以太网子系统的核心结构体,管理 MAC 地址表、协议解析等以太网相关功能。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
typedef struct {
    /* API message ID base */
    u16 msg_id_base;

    /* on/off switch for the periodic function */
    u8 periodic_timer_enabled;
    /* Node index, non-zero if the periodic process has been created */
    u32 periodic_node_index;

    /* convenience */
    vlib_main_t * vlib_main;
    vnet_main_t * vnet_main;
    ethernet_main_t * ethernet_main;
} myplugin_main_t;

VLIB_INIT_FUNCTION宏使得VPP在系统启动或插件加载时自动调用函数myplugin_init执行初始化工作,函数的参数vm是由VPP调用时传入,函数依次执行以下工作:

  1. 定义一个局部变量指针mmp,指向myplugin_main,以方便后续访问全局插件状态结构体;
  2. 初始化一个错误指针变量error,默认值0表示当前没有错误,如果初始化过程中遇到错误,可以用这个变量返回错误信息;
  3. 将传入的VPP主调度器上下文指针vm保存到插件主结构体mmp的vlib_main字段,这样插件其他功能函数就能通过mmp->vlib_main访问VPP主调度器上下文;
  4. 调用函数vnet_get_main以获取VPP网络栈子系统的上下文指针,保存到插件主结构体mmp的vnet_main字段,这样插件就能访问网络栈的各种接口、路由等数据;
  5. 调用函数setup_message_id_table()完成插件API消息的注册,VPP通过这套机制知道插件支持哪些API消息、对应的消息ID,函数返回插件API消息的起始 ID,进而保存到mmp->msg_id_base中;
  6. 最后返回初始化过程的错误状态,返回值为0表示初始化成功,没有错误发生。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
myplugin_main_t myplugin_main;

static clib_error_t * myplugin_init(vlib_main_t * vm)
{
  myplugin_main_t * mmp = &myplugin_main;
  clib_error_t * error = 0;

  mmp->vlib_main = vm;
  mmp->vnet_main = vnet_get_main();

  /* Add our API messages to the global name_crc hash table */
  mmp->msg_id_base = setup_message_id_table();

  return error;
}

VLIB_INIT_FUNCTION(myplugin_init);

节点注册与Feature机制

注册节点

VPP内部的业务逻辑是通过一系列节点(node)连接实现的,如二层以太网处理节点ethernet-input、三层IPv4处理节点ip4-input等。旧版本的VPP节点框架比较固定,各节点之间的逻辑连接在编译完成后就已经固定,形成一个有序的图结构,以实现完整的数据包处理功能。

使用以下命令可以输出VPP中myplugin节点的前一个节点和后一个节点的信息。

1
vpp > show node myplugin

以下代码是VPP插件中注册一个节点的典型方式,其在VPP的数据图中注册一个名为myplugin的自定义处理节点,并指定它的行为特征和下一跳关系:

  1. VLIB_REGISTER_NODE:是VPP框架提供的宏,用于将一个处理节点注册到VPP的节点调度系统中;
  2. myplugin_node:是定义为vlib_node_registration_t类型的静态变量,用于存储节点的信息,VLIB_REGISTER_NODE宏将以下信息(包括name、vector_size等)填充进该变量的相应字段,以供VPP启动时自动加载读取该节点的注册信息;
  3. name:表示该节点的名字,可以在vppctl中通过show node命令查看,也是其他节点指定next_nodes时引用它的方式;
  4. vector_size:VPP是批处理模型,u32的设置表示每个数据包在VPP内部由一个u32编号表示;
  5. format_trace:指定用于执行trace输出的函数为format_myplugin_trace函数,在开启trace调试时用于打印调试信息;
  6. type:表明这是一个内部节点,负责处理数据包,常见的节点类型还包括VLIB_NODE_TYPE_INPUT(收包节点,负责处理来自物理接口或虚拟接口的数据)、VLIB_NODE_TYPE_PRE_INPUT(在输入节点前执行,用于预处理或特殊过滤)、LIB_NODE_TYPE_PROCESS;
  7. n_errors:可能产生的错误数量,即error_strings字段的数组长度;
  8. error_strings:错误字符串数组,即该节点可能产生的所有错误的错误描述集合;
  9. n_next_nodes:表示下一跳节点的数量,即可能转发到的下一个处理节点的数量,和next_nodes字段中的数量保持一致;
  10. next_nodes:明确指定在当前节点处理完数据包后,下一跳节点的集合,每一个下一跳节点名称使用一个编号索引,例如interface-output节点使用编号0(即MYPLUGIN_NEXT_INTERFACE_OUTPUT枚举值)索引。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typedef enum {
    MYPLUGIN_ERROR_SWAPPED,
    MYPLUGIN_N_ERROR,
} myplugin_error_t;

static char *myplugin_error_strings[] = {
    "Mac swap packets processed",
};

typedef enum 
{
  MYPLUGIN_NEXT_INTERFACE_OUTPUT,
  MYPLUGIN_N_NEXT,
} myplugin_next_t;

vlib_node_registration_t myplugin_node;
VLIB_REGISTER_NODE(myplugin_node) = 
{
  .name = "myplugin",
  .vector_size = sizeof (u32),
  .format_trace = format_myplugin_trace,
  .type = VLIB_NODE_TYPE_INTERNAL,
  
  .n_errors = ARRAY_LEN(myplugin_error_strings),
  .error_strings = myplugin_error_strings,

  .n_next_nodes = MYPLUGIN_N_NEXT,

  /* edit / add dispositions here */
  .next_nodes = {
        [MYPLUGIN_NEXT_INTERFACE_OUTPUT] = "interface-output",
  },
};

Feature机制

为了提高灵活性和可扩展性,新版本VPP引入了feature机制。每个feature本质上也是一个节点,用户可以根据需要启用或禁用特定feature。同时,用户还可以通过插件将自定义节点插入到指定位置,扩展 VPP 功能。

不同的feature被按类型分组,每组称为一个feature arc。每个feature arc由一系列按特定顺序排列的feature组成,VPP在初始化时使用拓扑排序算法为整个feature arc的节点安排一个不会冲突、符合所有依赖关系的执行顺序。具体来说,每个节点可以通过runs_before字段指定它要在谁之前运行,所有这些依赖关系会构成一张有向无环图,然后VPP用拓扑排序生成一个线性的、不会冲突的执行顺序。

因此节点可以在运行的时候通过命令进行配置是否打开或关闭,从而影响数据流的走向,无需修改VPP源码即可灵活调整数据流。

可以使用以下命令输出显示VPP中的feature arc及其包含的节点。比如arp这个 feature arc,里面包含vrrp4-arp-input、arping-input、arp-reply、arp-proxy、 arp-disabled、error-drop一系列节点,它们按索引顺序依次处理包。

1
vpp > show feature verbose

以下代码是使用宏VNET_FEATURE_INIT向VPP注册插件节点myplugin为device-input这个feature arc中的一个处理节点,具体如下:

  1. arc_name:说明要把当前节点挂载到哪个feature arc上,例如是device-input,这是处理刚从物理接口进入VPP的feature arc;
  2. node_name:要挂载的节点名,也就是上一步注册节点时,VLIB_REGISTER_NODE中填写的节点名;
  3. runs_before:指定在哪个节点之前运行,也就是说当前节点的执行优先级比ethernet-input节点更高。
  4. runs_after:指定在哪个节点之后运行,也就是说当前节点的执行优先级比设定的节点更低。
1
2
3
4
5
6
VNET_FEATURE_INIT(myplugin, static) =
{
  .arc_name = "device-input",
  .node_name = "myplugin",
  .runs_before = VNET_FEATURES("ethernet-input"),
};

在注册插件时,default_disabled参数默认置为1,因此myplugin节点的feature没有被默认启用,可以查看ethernet-input节点的前一个节点,图中可见没有myplugin节点。同时也可以查看vpp-eth0接口下所有feature arc的使能情况。

1
2
vpp > show node ethernet-input
vpp > show interface features vpp-eth0

可以使用以下两种方式为接口vpp-eth0开启feature:

  1. 使用VPP标准CLI命令,在特定接口与feature arc上启用特定节点;
  2. 使用插件自定义的CLI命令,执行插件自定义逻辑,由插件内部逻辑完成feature的开启。
1
2
3
4
# Mehtod A
vpp > set interface feature vpp-eth0 myplugin arc device-input
# Mehtod B
vpp > myplugin enable-disable vpp-eth0

思考:节点的跳转方式

至此可以观察到VPP有两种节点的跳转机制,一种方式是直接告诉VPP下一跳节点,即在VLIB_REGISTER_NODE注册节点时在next_nodes字段声明全部可能的下一跳节点名称,然后在数据处理函数中,在vlib_get_next_frame函数中通过next0参数(next0为索引值,在next_nodes字段中索引到对应的节点名称interface-output)选择下一跳节点。

1
2
3
4
5
6
VLIB_REGISTER_NODE(myplugin_node) = {
  // ……
  .next_nodes = {
    [MYPLUGIN_NEXT_INTERFACE_OUTPUT] = "interface-output",
  },
};
1
2
u32 next0 = MYPLUGIN_NEXT_INTERFACE_OUTPUT;
vlib_get_next_frame(vm, node, next0, to_next, n_left_to_next);

第二种方式是VPP的featur机制,初始化使用runs_before和runs_after字段来设置优先级,随后使用CLI或API启动featur,在数据处理函数中使用vnet_feature_next_u16函数获取数据包下一个feature的index。

1
2
3
4
5
VNET_FEATURE_INIT(myplugin, static) =
{
  // ……
  .runs_before = VNET_FEATURES("ethernet-input"),
};

注册与响应CLI命令

在“Feature机制”小节中提到,可以使用插件自定义的CLI命令,由插件内部逻辑完成feature的开启。

1
vpp > myplugin enable-disable vpp-eth0

注册CLI命令

注册CLI命令则由VLIB_CLI_COMMAND宏完成,可用于向VPP的命令行vppctl添加自定义的命令:

  1. path:CLI命令路径,在vppctl里键入该字符串来调用它,在上面的例子中"myplugin enable-disable"就是路径、“vpp-eth0”就是参数;
  2. short_help:用户在vppctl输入?或错误时,显示的帮助字符串;
  3. function:实际处理输入命令参数的函数。
1
2
3
4
5
6
VLIB_CLI_COMMAND(myplugin_enable_disable_command, static) = 
{
    .path = "myplugin enable-disable",
    .short_help = "myplugin enable-disable <interface-name> [disable]",
    .function = myplugin_enable_disable_command_fn,
};

响应CLI命令

实际处理输入命令的函数myplugin_enable_disable_command_fn输入参数与返回值如下:

  1. vlib_main_t:VPP的主控制结构体,代表整个VPP引擎运行时上下文,包含线程、节点、调度器、内存等全局资源、全局信息、统计数据;
  2. unformat_input_t:VPP中用于命令行/配置/字符串解析的结构体,它封装了一个输入源(如字符串缓冲区)和当前解析状态(当前读取到的位置);
  3. vlib_cli_command_t:是VLIB_CLI_COMMAND在注册CLI命令时的结构体指针myplugin_enable_disable_command,它包含了命令路径字符串path、帮助信息short_help、处理函数function;
  4. 返回值是一个指向clib_error_t类型的指针:它是VPP中用于描述错误信息的结构体,返回值为0(即NULL指针)表示无错误,函数执行成功。
1
2
3
4
static clib_error_t *
myplugin_enable_disable_command_fn(vlib_main_t * vm, 
                                   unformat_input_t * input,
                                   vlib_cli_command_t * cmd)

myplugin_enable_disable_command_fn函数首先对输入的命令参数进行解析,依次执行以下过程:

  1. 获取myplugin_main插件全局状态结构体指针mmp,后面用于传参与操作;
  2. 准备参数以存储解析出的命令参数,sw_if_index存储用户指定的接口编号,enable_disable标记用户对接口的操作是禁用还是启用,其中的sw_if_index初始为无效值~0(即即0xFFFFFFFF);
  3. 随后的while语句不断循环解析输入,直到所有命令行参数都处理完,unformat_input_t函数的作用是检查input中是否还有未被读取的内容,若全部读取完成,则返回UNFORMAT_END_OF_INPUT;
  4. 若input中还有未被读取的内容,使用unformat(input, “disable”)查看当前命令行参数是不是字符串"disable”,如果是,就匹配成功并将游标向前推进,并返回1表示匹配成功,此时即可将enable_disable置为0,以表示对接口的操作是禁用;
  5. 若使用unformat(input, “disable”)匹配失败,则当前参数可能是接口名,需使用unformat的高级用法进行匹配,并根据接口名找到接口的索引编号:
    1. input:当前的命令行输入;
    2. “%U”:表示使用自定义解析函数,注意不是printf系列的标准格式;
    3. unformat_vnet_sw_interface:是"%U"所调用的解析函数,用于解析接口名,该函数在VNET接口表中查找接口名,返回对应的接口编号,并放到sw_if_index里;
    4. mmp->vnet_main:vnet_main是VPP中网络子系统的全局上下文结构,包含了所有接口信息、软件接口数组、链路类型、MTU 等网络数据,即负责网络抽象和接口管理;
    5. sw_if_index:输出参数,保存解析后的接口索引;
  6. 在输入参数既不是“disable”,也不是接口名时,退出解析循环,防止意外输入导致死循环;
  7. 随后检查sw_if_index是否为非法值(即是否为初始值~0),若是则使用clib_error_return返回一个错误结构体,其第一个参数为错误码(填0表示只返回错误消息字符串,没用具体错误码)、第二个参数是错误信息(CLI会显示错误信息,提示用户需要指定接口)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
myplugin_main_t *mmp = &myplugin_main;
u32 sw_if_index = ~0;
int enable_disable = 1;

while (unformat_check_input(input) != UNFORMAT_END_OF_INPUT)
{
    if (unformat(input, "disable"))
        enable_disable = 0;
    else if (unformat(input, "%U", unformat_vnet_sw_interface, mmp->vnet_main, &sw_if_index))
        ;
    else
        break;
}

if (sw_if_index == ~0)
    return clib_error_return(0, "Please specify an interface...");

在完成命令行参数解析、获得sw_if_index与enable_disable两个关键参数后,即可调用myplugin_enable_disable函数进行配置,并根据函数的处理结果执行对应的处理,即在配置失败后返回错误信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int rv;
rv = myplugin_enable_disable(mmp, sw_if_index, enable_disable);
switch (rv)
{
case 0:
    break;
case VNET_API_ERROR_INVALID_SW_IF_INDEX:
    return clib_error_return(0, "Invalid interface, only works on physical ports");
    break;
case VNET_API_ERROR_UNIMPLEMENTED:
    return clib_error_return(0, "Device driver doesn't support redirection");
    break;
default:
    return clib_error_return(0, "myplugin_enable_disable returned %d", rv);
}
return 0;

myplugin_enable_disable函数依次执行以下操作:

  1. 使用pool_is_free_index函数判断接口索引是否合法有效,如果无效,立即返回错误码VNET_API_ERROR_INVALID_SW_IF_INDEX,其中传入的参数vnet_main是全局网络系统上下文、interface_main是接口管理模块、sw_interfaces保存所有的接口;
  2. 随后使用vnet_get_sw_interface函数通过接口索引sw_if_index获取对应的接口结构体;
  3. 根据接口结构体sw的type字段判断接口是否为物理接口(VNET_SW_INTERFACE_TYPE_HARDWARE),对于非物理接口(比如虚拟接口)返回错误码VNET_API_ERROR_INVALID_SW_IF_INDEX;
  4. 使用vnet_feature_enable_disable函数在接口sw_if_index的device-input这个feature arc上启用或禁用名为myplugin的feature;
  5. 使用vlib_process_signal_event函数发送事件给后台进程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int myplugin_enable_disable(myplugin_main_t *mmp, u32 sw_if_index, int enable_disable)
{
    vnet_sw_interface_t *sw;
    int rv = 0;

    /* Utterly wrong? */
    if (pool_is_free_index(mmp->vnet_main->interface_main.sw_interfaces, sw_if_index))
        return VNET_API_ERROR_INVALID_SW_IF_INDEX;

    /* Not a physical port? */
    sw = vnet_get_sw_interface(mmp->vnet_main, sw_if_index);
    if (sw->type != VNET_SW_INTERFACE_TYPE_HARDWARE)
        return VNET_API_ERROR_INVALID_SW_IF_INDEX;

    myplugin_create_periodic_process(mmp);

    vnet_feature_enable_disable("device-input", "myplugin", sw_if_index, enable_disable, 0, 0);

    /* Send an event to enable/disable the periodic scanner process */
    vlib_process_signal_event(mmp->vlib_main, mmp->periodic_node_index, MYPLUGIN_EVENT_PERIODIC_ENABLE_DISABLE, (uword)enable_disable);
    return rv;
}

注册与响应API指令

定义API消息

下面所示为myplugin.api文件中的内容,是为VPP插件myplugin定义的API接口消息格式,让外部客户端可以发送命令来启用或禁用插件功能。

  1. option version:指定该 API 文件的版本号,由VPP的API编译工具在生成API文件(.c与.h文件)时自动识别和使用;
  2. autoreply define myplugin_enable_disable:define表示声明一个API请求消息;myplugin_enable_disable是消息的名字,VPP将自动生成相关handler;autoreply表示VPP会自动为这个请求消息生成一个配套的reply消息;
  3. client_index:客户端连接标识,用于在VPP内部管理多个客户端连接,通常由VPP自己填写;
  4. context:客户端自己设定,用于请求-响应对应,当回复该请求时会原样返回这个值,让接收者知道这是对哪个请求的响应;
  5. enable_disable:表示是否启用feature;
  6. sw_if_index:接口索引,表示要在哪个接口上启用/禁用feature,实际类型是一个u32(在import的api中,typedef u32 vl_api_interface_index_t)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
option version = "0.1.0";
import "vnet/interface_types.api";

autoreply define myplugin_enable_disable {
    /* Client identifier, set from api_main.my_client_index */
    u32 client_index;

    /* Arbitrary context, so client can match reply to request */
    u32 context;

    /* Enable / disable the feature */
    bool enable_disable;

    /* Interface handle */
    vl_api_interface_index_t sw_if_index;
};

处理API消息

在myplugin.c文件中需要对应的实现名为vl_api_msgname_t_handler的handler函数,VPP的API系统能自动找到并调用该函数:

  1. 首先获取插件的全局状态结构mmp;
  2. 从客户端请求中取出:接口编号sw_if_index(注意使用ntohl转换字节序)、是否启用插件enable_disable,随后调用函数myplugin_enable_disable处理执行;
  3. REPLY_MACRO是VPP提供的宏,自动构造并将消息发送回原始客户端。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <myplugin/myplugin.api.c>

static void vl_api_myplugin_enable_disable_t_handler
(vl_api_myplugin_enable_disable_t * mp)
{
  vl_api_myplugin_enable_disable_reply_t * rmp;
  myplugin_main_t * mmp = &myplugin_main;
  int rv;

  rv = myplugin_enable_disable (mmp, ntohl(mp->sw_if_index), (int) (mp->enable_disable));

  REPLY_MACRO(VL_API_MYPLUGIN_ENABLE_DISABLE_REPLY);
}

REPLY_MACRO内部包含以下步骤:

  1. 填写msg_id:其中的VL_API_MYPLUGIN_ENABLE_DISABLE_REPLY表示的是在 api文件中定义的响应消息(响应消息是自动生成)的编号,该编号是VPP在编译时自动生成的;mmp->msg_id_base是插件API消息的起始编号;
  2. 填写context:自动填写与请求消息一致的context;
  3. 填写rv:自动填写结果;
  4. 执行响应消息的发送。
1
2
3
4
rmp->_vl_msg_id = htons(VL_API_MYPLUGIN_ENABLE_DISABLE_REPLY + mmp->msg_id_base);
rmp->context = mp->context;
rmp->retval = htonl(rv);
vl_api_send_msg(mp->client_index, rmp);

数据包处理

使用VLIB_NODE_FN宏定义声明的函数是VPP数据包处理的入口,入口函数的参数是确定且规范的,包含:

  1. vm:VPP的主控制结构体,代表整个VPP引擎运行时上下文;
  2. node:当前节点的运行时信息,包含节点状态、缓存的next_index等;
  3. frame:当前传入的数据包帧,里面包含待处理的数据包索引列表。
1
2
3
4
vlib_node_registration_t myplugin_node;
VLIB_NODE_FN(myplugin_node)(vlib_main_t *vm,
                            vlib_node_runtime_t *node,
                            vlib_frame_t *frame)

数据包处理主框架

数据包处理的框架步骤如下:

  1. frame结构中包含一个索引数组,存储了多个数据包的buffer索引,函数vlib_frame_vector_args返回指向这个索引数组的指针、并存至from。而待处理的数据包数量n_left_from通过frame结构中的n_vectors字段获得;
  2. 在VPP中每个节点处理完后,把数据包交给下一个节点继续处理,因此当前节点需向下一跳节点申请获取缓存空间,以准备把当前处理的数据包发过去。由于本插件的下一跳节点固定且单一,为提高效率,下一跳节点的索引next_index可以直接从node结构体的cached_next_index字段获取,;
  3. 只要n_left_from指示还有未处理的数据包,就继续处理;
  4. 在每一轮处理中,将通过vlib_get_next_frame函数向一跳节点请求一段缓存空间,返回的to_next指针将指向授权的缓存空间,返回的n_left_to_next指示空间里还可以写入多少个数据包;
  5. 根据剩余数据包数量与缓存空间大小,决定采用快速路径还是慢速路径进行数据包的处理;
  6. 在每一轮处理的最后,将剩余未使用的缓存空间使用函数vlib_put_next_frame归还给下一跳节点。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
u32 n_left_from, *from, *to_next;
myplugin_next_t next_index;
u32 pkts_swapped = 0;

from = vlib_frame_vector_args(frame);
n_left_from = frame->n_vectors;
next_index = node->cached_next_index;

while (n_left_from > 0)
{
    u32 n_left_to_next;
    vlib_get_next_frame(vm, node, next_index, to_next, n_left_to_next);
    while (n_left_from >= 4 && n_left_to_next >= 2)
    {
        // ……
    }
    while (n_left_from > 0 && n_left_to_next > 0)
    {
        // ……
    }
    // ……
    vlib_put_next_frame(vm, node, next_index, n_left_to_next);
}

vlib_node_increment_counter(vm, myplugin_node.index, MYPLUGIN_ERROR_SWAPPED, pkts_swapped);
return frame->n_vectors;

在最后处理完成后,调用vlib_node_increment_counter函数统计完成数据包处理的数目,参数如下:

  1. vm:VPP运行时上下文对象;
  2. myplugin_node.index:当前节点的索引;
  3. MYPLUGIN_ERROR_SWAPPED:要统计的错误(或事件)类型的编号;
  4. pkts_swapped:要累加的事件数量。

插件可能产生的错误类型在节点注册时被声明注册,因此传入的MYPLUGIN_ERROR_SWAPPED其实值为0,可以索引到字符串"Mac swap packets processed"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Type A
#define foreach_myplugin_error \
  _(SWAPPED, "Mac swap packets processed")

typedef enum {
#define _(sym,str) MYPLUGIN_ERROR_##sym,
  foreach_myplugin_error
#undef _
  MYPLUGIN_N_ERROR,
} myplugin_error_t;

#ifndef CLIB_MARCH_VARIANT
static char * myplugin_error_strings[] = 
{
#define _(sym,string) string,
  foreach_myplugin_error
#undef _
};
#endif /* CLIB_MARCH_VARIANT */

// Type B
typedef enum {
  MYPLUGIN_ERROR_SWAPPED,
  MYPLUGIN_N_ERROR
} myplugin_error_t;

static char * myplugin_error_strings[] = {
  "Mac swap packets processed"
};

可以使用以下命令查看事件的统计结果。

1
2
vppctl > show error
myplugin: Mac swap packets processed          12345

早期VPP中,error计数器最初只用来统计错误的,但随着插件和节点功能变复杂,开发者发现成功处理的包、丢弃的包、缓存命中等也希望能被计数,为了避免增加新的统计系统,VPP沿用了原有的机制与名称来记录一切类型的事件。

快速路径

快速路径一次处理两个数据包,并提前对下一个数据包执行预取以优化缓存命中率。

初始化变量:

  1. next0与next1:为即将处理的两个数据包指定下一跳节点编号,默认填写为interface-output节点;
  2. sw_if_index0与sw_if_index1:记录每个数据包从哪个接口接收的接口索引;
  3. tmp0与tmp1:暂存两个数据包的源MAC地址,用于MAC地址交换;
  4. en0与en1:两个数据包的以太网头指针;
  5. bi0与bi1:两个数据包的buffer索引;
  6. b0与b1:两个数据包的buffer对象(用buffer索引从池中取出对应的buffer对象指针)。
1
2
3
4
5
6
7
u32 next0 = MYPLUGIN_NEXT_INTERFACE_OUTPUT;
u32 next1 = MYPLUGIN_NEXT_INTERFACE_OUTPUT;
u32 sw_if_index0, sw_if_index1;
u8 tmp0[6], tmp1[6];
ethernet_header_t *en0, *en1;
u32 bi0, bi1;
vlib_buffer_t * b0, * b1;

为提升数据包的处理效率,尽可能减少每个包的处理延迟,预取下一批包,让当前批次处理和下一批次的内存访问重叠,步骤如下:

  1. 定义两个指针,准备指向数据包的buffer对象,p2和p3分别对应第3和第4个数据包;
  2. from是当前处理批次数据包的索引数组,存储的是数据包的buffer索引,使用vlib_get_buffer函数可以获取第3个和第4个数据包的buffer对象;
  3. 使用vlib_prefetch_buffer_header函数预取buffer结构头部(即metadata,包含如接口信息、包长、标志、数据偏移等信息)、并提前加载到CPU缓存,LOAD表示预取动作是读取数据;
  4. 使用CLIB_PREFETCH宏预取buffer数据区(即payload,为实际数据),CLIB_CACHE_LINE_BYTES是CPU缓存行大小(通常64字节),指示CPU预取多少字节。STORE表示预取是为后续写操作做准备。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* Prefetch next iteration. */
{
    vlib_buffer_t *p2, *p3;

    p2 = vlib_get_buffer(vm, from[2]);
    p3 = vlib_get_buffer(vm, from[3]);

    vlib_prefetch_buffer_header(p2, LOAD);
    vlib_prefetch_buffer_header(p3, LOAD);

    CLIB_PREFETCH(p2->data, CLIB_CACHE_LINE_BYTES, STORE);
    CLIB_PREFETCH(p3->data, CLIB_CACHE_LINE_BYTES, STORE);
}

提取两个待处理的数据包,假设性地将它们入队到下一跳节点:

  1. 猜测性入队:to_next是指向下一跳节点的buffer队列指针,from是当前节点接收到的待处理buffer索引数组,将from中前2个数据包索引直接加入到to_next中,表示假设这两个数据包是可以直接交给下一跳处理的。注意最后可以使用vlib_validate_buffer_enqueue_x1函数确定猜测性入队是否正确,如果猜测错误,则函数会将数据包从to_next中取出,再放入正确的下一跳节点队列中,确保数据包走正确的处理路径;
  2. 然后移动from指针和to_next指针,更新剩余待处理数据包数量n_left_from和剩余buffer可用的数量n_left_to_next;
  3. 使用函数vlib_get_buffer把buffer索引bi0、bi1转换为实际的buffer指针b0、b1;
  4. current_data是当前要处理的数据相对于数据包起始位置的偏移,本插件从接收上来的数据包应当是未处理的,即current_data为0,所以使用断言检测接收的数据包是从buffer数据区域的起始位置开始;
  5. 使用函数vlib_buffer_get_current取出数据包的以太网头部地址,为后续操作做准备。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* speculatively enqueue b0 and b1 to the current next frame */
to_next[0] = bi0 = from[0];
to_next[1] = bi1 = from[1];
from += 2;
to_next += 2;
n_left_from -= 2;
n_left_to_next -= 2;

b0 = vlib_get_buffer (vm, bi0);
b1 = vlib_get_buffer (vm, bi1);

ASSERT (b0->current_data == 0);
ASSERT (b1->current_data == 0);

en0 = vlib_buffer_get_current (b0);
en1 = vlib_buffer_get_current (b1);

随后交换以太网帧中源MAC地址和目的MAC地址,以第一个数据包en0为例,对于第二个数据包en1同理:

  1. 拷贝src_address到临时变量tmp;
  2. 把dst_address赋值给src_address;
  3. 把临时变量tmp赋值给dst_address。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define foreach_mac_address_offset              \
_(0)                                            \
_(1)                                            \
_(2)                                            \
_(3)                                            \
_(4)                                            \
_(5)

/* This is not the fastest way to swap src + dst mac addresses */
#define _(a) tmp0[a] = en0->src_address[a];
	foreach_mac_address_offset;
#undef _
#define _(a) en0->src_address[a] = en0->dst_address[a];
    foreach_mac_address_offset;
#undef _
#define _(a) en0->dst_address[a] = tmp0[a];
    foreach_mac_address_offset;
#undef _

#define _(a) tmp1[a] = en1->src_address[a];
    foreach_mac_address_offset;
#undef _
#define _(a) en1->src_address[a] = en1->dst_address[a];
    foreach_mac_address_offset;
#undef _
#define _(a) en1->dst_address[a] = tmp1[a];
    foreach_mac_address_offset;
#undef _

注意模板代码使用了宏技巧,下面所示代码的Type A:定义了一个宏 _,它接受参数 a;使用foreach_mac_address_offset来为参数 a赋值;最后使用undef删除宏 _,避免污染。将foreach_mac_address_offset替换,即可等价于Type B的形式,进一步可等价于Type C的形式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Type A
#define _(a) tmp0[a] = en0->src_address[a];
foreach_mac_address_offset;
#undef _

// Type B
#define _(a) tmp0[a] = en0->src_address[a];
_(0)
_(1)
_(2)
_(3)
_(4)
_(5)
#undef _

// Type C
tmp0[0] = en0->src_address[0];
tmp0[1] = en0->src_address[1];
tmp0[2] = en0->src_address[2];
tmp0[3] = en0->src_address[3];
tmp0[4] = en0->src_address[4];
tmp0[5] = en0->src_address[5];

完成数据包的处理操作后,即可将数据包递交给下一跳节点:

  1. 读取接收接口的索引:buffer结构体中预留了一块空间,能够存放VNET层扩展数据,但需要通过vnet_buffer宏来转换类型为vnet_buffer结构,才能访问。因此使用vnet_buffer宏获取b0、b1对应的vnet_buffer结构指针,进而通过其中的sw_if_index字段查询该数据包从哪个物理或逻辑接口接收的,并分别将接口索引存至sw_if_index0和sw_if_index1。注意sw_if_index是一个数组,第一个元素(VLIB_RX=0)表示接收方向、第二个元素(VLIB_TX)表示发送方向;
  2. 设置发送接口的索引:将数据包的发送接口设置为接收接口的索引,即数据包从接收接口进入后,处理后发送回同一个接口;
  3. 统计处理的包数:pkts_swapped增加2,表示完成2个数据包的处理;
  4. Trace:后续单独展开介绍与分析;
  5. 验证并完成数据包入队操作:函数vlib_validate_buffer_enqueue_x2根据数据实际的下一跳节点索引next0、next1,验证之前猜测的下一跳节点是否正确, 若猜测错误,则把这两个数据包分派到正确的下一跳节点队列。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sw_if_index0 = vnet_buffer(b0)->sw_if_index[VLIB_RX];
sw_if_index1 = vnet_buffer(b1)->sw_if_index[VLIB_RX];

/* Send pkt back out the RX interface */
vnet_buffer(b0)->sw_if_index[VLIB_TX] = sw_if_index0;
vnet_buffer(b1)->sw_if_index[VLIB_TX] = sw_if_index1;

pkts_swapped += 2;

{
    // Trace ...
}

/* verify speculative enqueues, maybe switch current next frame */
vlib_validate_buffer_enqueue_x2(vm, node, next_index, to_next, n_left_to_next, bi0, bi1, next0, next1);

慢速路径

慢速路径与快速路径基本一致,区别在于一次只对一个数据包进行处理、不使用预取,用于收尾处理数据包。

Trace机制

VPP中的Trace并不会在数据路径中自动执行,而是需要在vppctl的CLI界面执行以下操作:

  1. 为经过myplugin插件的数据包开启trace,其中的参数10表示最多记录10条trace信息;
  2. 使用show trace命令即可查看格式化输出的trace信息。
1
2
vpp > trace add myplugin 10
vpp > show trace

以下代码用于在数据包处理后记录trace信息,当通过vppctl开启了trace功能时,以下代码会被执行:

  1. 外层判断:通过node的flags标志判断节点是否开启了trace功能,PREDICT_FALSE是一个性能优化的宏,表示其中的条件很少成立(trace功能通常是关闭的),进而优化CPU的分支预测;
  2. 判断b0这个数据包buffe是否需要被trace,因为不是所有的数据包都会被trace;
  3. 添加trace:使用函数vlib_add_trace为b0这个数据包buffer分配一个trace数据结构,随后填写数据包的接口索引、下一条节点索引、源MAC地址、目的MAC地址。
1
2
3
4
5
6
7
typedef struct 
{
  u32 next_index;
  u32 sw_if_index;
  u8 new_src_mac[6];
  u8 new_dst_mac[6];
} myplugin_trace_t;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if (PREDICT_FALSE((node->flags & VLIB_NODE_FLAG_TRACE)))
{
    if (b0->flags & VLIB_BUFFER_IS_TRACED)
    {
        myplugin_trace_t *t = vlib_add_trace(vm, node, b0, sizeof(*t));
        t->sw_if_index = sw_if_index0;
        t->next_index = next0;
        clib_memcpy(t->new_src_mac, en0->src_address, sizeof(t->new_src_mac));
        clib_memcpy(t->new_dst_mac, en0->dst_address, sizeof(t->new_dst_mac));
    }
    if (b1->flags & VLIB_BUFFER_IS_TRACED)
    {
        myplugin_trace_t *t = vlib_add_trace(vm, node, b1, sizeof(*t));
        t->sw_if_index = sw_if_index1;
        t->next_index = next1;
        clib_memcpy(t->new_src_mac, en1->src_address, sizeof(t->new_src_mac));
        clib_memcpy(t->new_dst_mac, en1->dst_address, sizeof(t->new_dst_mac));
    }
}

Trace处理函数在注册节点时被注册,在show trace时会使用该函数将记录的trace进行格式化输出:

  1. s:字符串内容输出缓冲区;
  2. args:可变参数列表,VPP会传进来vlib_main_t * vm、vlib_node_t * node、myplugin_trace_t * t三个参数,因此使用va_arg函数依次取出vm、node、t三个参数,但由于vm、node在后续没有使用上,所以标记为CLIB_UNUSED,以避免编译器警告;
  3. 随后打印trace信息,其中的%U是VPP自定义的解析函数,用my_format_mac_address来格式化MAC地址。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static u8 *
my_format_mac_address(u8 *s, va_list *args)
{
    u8 *a = va_arg(*args, u8 *);
    return format(s, "%02x:%02x:%02x:%02x:%02x:%02x",
                  a[0], a[1], a[2], a[3], a[4], a[5]);
}

static u8 *format_myplugin_trace(u8 *s, va_list *args)
{
    CLIB_UNUSED(vlib_main_t * vm) = va_arg(*args, vlib_main_t *);
    CLIB_UNUSED(vlib_node_t * node) = va_arg(*args, vlib_node_t *);
    myplugin_trace_t *t = va_arg(*args, myplugin_trace_t *);

    s = format(s, "MYPLUGIN: sw_if_index %d, next index %d\n", t->sw_if_index, t->next_index);
    s = format(s, "  new src %U -> new dst %U", my_format_mac_address, t->new_src_mac, my_format_mac_address, t->new_dst_mac);
    return s;
}

周期性事件

VPP插件还支持实现负责周期性事件处理的VPP后台进程,myplugin模板插件提供了一个模板,未具体实现实际的功能,但可以作为框架。在模板中,后台进程在feature开启(即vl_api_myplugin_enable_disable_t_handler函数中)时被调用启动,当然也可以在插件初始化(即myplugin_init函数)时就被调用启动。

以下代码是创建一个VPP后台进程的函数:

  1. 判断是否创建过:插件的myplugin_main_t结构体里有periodic_node_index字段,该字段用来记录创建的周期进程的节点索引,默认0表示未创建,这样可以避免重复创建后台进程;
  2. 如果还没有创建,则调用vlib_process_create函数创建一个新的后台进程,该函数向VPP注册一个协程,该进程会一直运行, 并且可以使用vlib_process_wait_for_event或者计时器等机制实现定期唤醒, 函数返回值为创建的后台进程节点的索引值,函数的参数包括:VPP的主上下文vlib_main、进程名称"myplugin-periodic-process"、进程的执行函数指针myplugin_periodic_process、栈大小(以2的幂次方计,16表示64KB,用于为进程分配合适的运行栈空间)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void myplugin_create_periodic_process(myplugin_main_t *mmp)
{
    /* Already created the process node? */
    if (mmp->periodic_node_index > 0)
        return;

    /* No, create it now and make a note of the node index */
    mmp->periodic_node_index = vlib_process_create(
        mmp->vlib_main,
        "myplugin-periodic-process",
        myplugin_periodic_process,
        16 /* log2_n_stack_bytes */);
}

以下代码后台进程的运行的函数:

  1. 通过while循环使得进程永远在后台运行,循环等待事件或者超时发生;
  2. 等待事件或者定时器超时:vlib_process_wait_for_event_or_clock可以阻塞进程,直到发生某个事件、或者超时(超过设定的timeout秒),而vlib_process_wait_for_event只等待事件,而采用何种等待类型可根据periodic_timer_enabled变量确定;
  3. 超时或发生事件后,解析事件:使用vlib_time_now函数获取当前时间(读取当前CPU周期数,再通过频率换算成以秒为单位的时间戳),使用vlib_process_get_events函数获取事件的类型(存在event_type)、事件的附带数据(存在event_data);
  4. 处理事件:根据事件类型event_type调用不同的处理函数:
    1. MYPLUGIN_EVENT1:执行handle_event1,对event_data中的每一条事件附带数据进行处理;
    2. MYPLUGIN_EVENT2:执行handle_event2,对event_data中的每一条事件附带数据进行处理;
    3. MYPLUGIN_EVENT_PERIODIC_ENABLE_DISABLE:同理;
    4. ~0(即二进制全1),表示超时事件,调用handle_timeout处理;
  5. 处理事件完成后,调用vec_reset_length函数清空event_data中的事件数据。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#define MYPLUGIN_EVENT1 1
#define MYPLUGIN_EVENT2 2
#define MYPLUGIN_EVENT_PERIODIC_ENABLE_DISABLE 3

static uword
myplugin_periodic_process(vlib_main_t *vm,
                          vlib_node_runtime_t *rt,
                          vlib_frame_t *f)
{
    myplugin_main_t *pm = &myplugin_main;
    f64 now;
    f64 timeout = 10.0;
    uword *event_data = 0;
    uword event_type;
    int i;

    while (1)
    {
        if (pm->periodic_timer_enabled)
            vlib_process_wait_for_event_or_clock(vm, timeout);
        else
            vlib_process_wait_for_event(vm);

        now = vlib_time_now(vm);

        event_type = vlib_process_get_events(vm, (uword **)&event_data);

        switch (event_type)
        {
        /* Handle MYPLUGIN_EVENT1 */
        case MYPLUGIN_EVENT1:
            for (i = 0; i < vec_len(event_data); i++)
                handle_event1(pm, now, event_data[i]);
            break;
        /* Handle MYPLUGIN_EVENT2 */
        case MYPLUGIN_EVENT2:
            for (i = 0; i < vec_len(event_data); i++)
                handle_event2(pm, now, event_data[i]);
            break;
        /* Handle the periodic timer on/off event */
        case MYPLUGIN_EVENT_PERIODIC_ENABLE_DISABLE:
            for (i = 0; i < vec_len(event_data); i++)
                handle_periodic_enable_disable(pm, now, event_data[i]);
            break;
        /* Handle periodic timeouts */
        case ~0:
            handle_timeout(pm, now);
            break;
        }
        vec_reset_length(event_data);
    }
    return 0; /* or not */
}

各事件的处理函数如下,其中的clib_warning是VPP的日志打印函数,类似于printf,但带有颜色标记等,日志输出在日志文件中(默认文件路径可通过startup.conf配置)与vppctl终端。此外handle_periodic_enable_disable用来设置、切换超时事件类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void
handle_event1(myplugin_main_t *pm, f64 now, uword event_data)
{
    clib_warning("received MYPLUGIN_EVENT1");
}

static void
handle_event2(myplugin_main_t *pm, f64 now, uword event_data)
{
    clib_warning("received MYPLUGIN_EVENT2");
}

static void
handle_periodic_enable_disable(myplugin_main_t *pm, f64 now, uword event_data)
{
    clib_warning("Periodic timeouts now %s", event_data ? "enabled" : "disabled");
    pm->periodic_timer_enabled = event_data;
}

static void
handle_timeout(myplugin_main_t *pm, f64 now)
{
    clib_warning("timeout at %.2f", now);
}

可以通过vlib_process_signal_event函数来发送事件,参数如下:

  1. vm:VPP主实例指针;
  2. node_index:目标后台进程节点的索引,如mmp->periodic_node_index;
  3. event_type:事件类型,根据自定义的宏传相应的值;
  4. event_data:附带的参数,可以是整数或指针。
1
2
3
4
vlib_process_signal_event(vlib_main_t *vm,
                          uword node_index,
                          uword event_type,
                          uword event_data);
Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计