封装与解耦
每一个 plugin 封装了一个独立的功能模块,模块依赖的外部 so 接口也封装在每个 plugin 中。 vpp 提供统一的使能、禁能、初始化 plugin 的框架,同时每个 plugin 对外提供的接口也使用统一的方式,大多采用函数表注册的方式向外提供功能。
例如 dpdk plugin 中封装了对 dpdk 接口的调用, dpdk 的初始化、网卡接口的配置等等功能均通过 vpp 框架提供的统一入口进行调用,这里有类似接口的概念, vpp 框架面向接口调用,dpdk plugin 根据接口约束封装 dpdk 接口实现一类设备接口操作方法,dpdk 接口的变化只会影响到 dpdk plugin,不会影响 vpp 框架与其它的 plugin,实现了 dpdk 与其它 plugin 的解耦。
这里的解耦其实是将耦合的层次提高了, 从直接与 dpdk 接口耦合改进为与 vpp 框架封装的接口耦合,代码的复杂性提高了,框架的扩展性也提高了。
例如使用 cscope 检索到 vpp 实现的网络设备类部分列举如下:
*** src/plugins/dpdk/device/device.c:
<global>[727] VNET_DEVICE_CLASS (dpdk_device_class) = {
*** src/plugins/geneve/geneve.c:
<global>[118] VNET_DEVICE_CLASS (geneve_device_class, static ) = {
*** src/plugins/gtpu/gtpu.c:
<global>[132] VNET_DEVICE_CLASS (gtpu_device_class,static ) = {
*** src/plugins/l2tp/l2tp.c:
<global>[258] VNET_DEVICE_CLASS (l2tpv3_device_class,static ) = {
*** src/plugins/lisp/lisp-gpe/interface.c:
<global>[162] VNET_DEVICE_CLASS (lisp_gpe_device_class) = {
<global>[304] VNET_DEVICE_CLASS (l2_lisp_gpe_device_class,static ) = {
<global>[408] VNET_DEVICE_CLASS (nsh_lisp_gpe_device_class,static ) = {
*** src/plugins/marvell/pp2/pp2.c:
<global>[380] VNET_DEVICE_CLASS (mrvl_pp2_device_class,) =
*** src/plugins/memif/device.c:
<global>[479] VNET_DEVICE_CLASS (memif_device_class) = {
这让我想到了《深入设计模式》中设计原则中提到的如下内容:
- 面向接口进行开发, 而不是面向实现; 依赖于抽象类型, 而不是具体类。
plugin 之间的交叉调用
根据上文的描述,每个 plugin 封装了独立的功能模块,不同的 plugin 之间不应该相互依赖,但是在一些场景中可能存在需要在一个 plugin 中调用另外一个 plugin 中提供的函数的情况,这时候可能只是简单的使用某个、某几个接口,为这几个接口单独开发一套 interface 类显得有点小题大做,为此 vpp 提供了 vlib_get_plugin_symbol 函数,用于在一个 plugin 中获取其它 plugin 中的接口直接使用。
相关使用场景示例如下:
*** src/vlib/unix/plugin.h:
<global>[149] void *vlib_get_plugin_symbol (char *plugin_name, char *symbol_name);
*** extras/deprecated/plugins/gbp/gbp_recirc.c:
gbp_recirc_init[277] vlib_get_plugin_symbol ("l2e_plugin.so", "l2_emulation_enable");
gbp_recirc_init[279] vlib_get_plugin_symbol ("l2e_plugin.so", "l2_emulation_disable");
*** src/plugins/acl/public_inlines.h:
LOAD_SYMBOL_FROM_PLUGIN_TO[30] st = vlib_get_plugin_symbol(p, #s); \
*** src/plugins/builtinurl/builtinurl.c:
builtinurl_enable[47] fp = vlib_get_plugin_symbol
*** src/plugins/ikev2/ikev2.c:
ikev2_lazy_init[5448] vlib_get_plugin_symbol ("dns_plugin.so", "dns_resolve_name");
*** src/plugins/mactime/builtins.c:
mactime_url_init[160] fp = vlib_get_plugin_symbol ("http_static_plugin.so",
*** src/plugins/nsh/nsh-md2-ioam/nsh_md2_ioam_api.c:
nsh_md2_ioam_init[53] (u8 *) vlib_get_plugin_symbol ("ioam_plugin.so", "trace_main");
*** src/plugins/prom/prom.c:
prom_enable[385] pm->register_url = vlib_get_plugin_symbol ("http_static_plugin.so",
prom_enable[388] vlib_get_plugin_symbol ("http_static_plugin.so", "hss_session_send_data");
用户通过传入目标符号与此符号所在的 plugin 的名称就能够获取到符号的地址,然后直接调用即可。上面的示例由 cscope 搜索 vlib_get_plugin_symbol 符号引用获得,能够看到虽然有使用但是也只是在小范围使用而已。
vlib_get_plugin_symbol 函数实现
源码如下:
void *
vlib_get_plugin_symbol (char *plugin_name, char *symbol_name)
{
plugin_main_t *pm = &vlib_plugin_main;
uword *p;
plugin_info_t *pi;
if ((p = hash_get_mem (pm->plugin_by_name_hash, plugin_name)) == 0)
return 0;
pi = vec_elt_at_index (pm->plugin_info, p[0]);
return dlsym (pi->handle, symbol_name);
}
此函数获取 vlib_plugin_main 结构,然后以 plugin 作为 hash key 找到目标 plugin 所在的 plugin_info_t 结构,此结构的 handle 保存了 dlopen 打开的 plugin so 的句柄,使用此句柄与符号名称调用 dlsym 就能够获取到目标 plugin 中 api 的地址使用。
vpp plugin 框架对 plugin so 的约束
上文描述了 plugin 的封装,要实现它需要约束一个 plugin so 中的符号对其它 plugin so 不可见。如果这些 plugin so 都通过编译时链接的方式使用,不能够实现此约束且不能支持动态的使能、禁能特定的 plugin。
vpp 实现了一套独立的 plugin 管理框架,vpp 自身并不直接链接某个 plugin so,而是通过管理框架来调用 dlopen 打开使能的 plugin,同时也在此框架中引入了版本校验功能,当 plugin so 中的版本信息与 vpp 主程序版本信息不一致时,plugin 加载终止。
vpp plugin 管理框架在调用 dlopen 时并未设置 RTLD_GLOBAL 标志,这意味着打开的 so 内部的符号不能被其它 plugin so 直接访问,在技术层面实现了上文提到的约束条件。
当未指定 RTLD_GLOBAL 时,dlopen 默认使用 RTLD_LOCAL 标志打开 so,manual 中对此标志的描述内容如下:
This is the converse of RTLD_GLOBAL, and the default if neither flag is specified. Symbols defined in this shared
object are not made available to resolve references in subsequently loaded shared objects.
同时需要注意的是,如果 plugin so 在编译时链接了某个外部 so 时,使用 dlopen 打开此 plugin so 时这个外部 so 中的符号也仅仅对链接了它的 plugin so 可见。
如果 A plugin 链接了 test.so,B plugin 也链接了 test.so,使用 dlopen 且不设置 RTLD_GLOBAL 标志时,test.so 文件只会被打开一次,但是内部的符号在 A plugin 与 B plugin 中都是隔离的,如果交叉使用可能会造成奇怪的问题。
vpp 单个 plugin 的注册
通过 VLIB_PLUGIN_REGISTER 宏注册一个 plugin,示例如下:
VLIB_PLUGIN_REGISTER () = {
.version = VPP_BUILD_VER,
.description = "Data Plane Development Kit (DPDK)",
};
此宏会定义一个 vlib_plugin_registration_t 结构数据并填充必要的字段,此结构定义如下:
typedef struct
{
CLIB_CACHE_LINE_ALIGN_MARK (cacheline0);
u8 default_disabled : 1;
u8 deep_bind : 1;
const char version[64];
const char version_required[64];
const char overrides[256];
const char *early_init;
const char *description;
} vlib_plugin_registration_t;
VLIB_PLUGIN_REGISTER 宏中会声明此结构被保存到 .vlib_plugin_registration section 中,vpp plugin 框架在加载一个 plugin so 的时候会查找此 section 并获取到此结构,基于此结构,vpp plugin 框架实现了 plugin so 的动态使能、关闭、版本校验、早期初始化等功能。
vpp plugin 框架 load_one_plugin 函数的关键流程
- 读取 elf 文件内容,然后遍历每个 section,获取 .vlib_plugin_registration section
- 加载 .vlib_plugin_registration section,此 section 的内容指向一个 vlib_plugin_registration_t 结构
- 以当前 plugin 的名称为 key 查找 plugin 配置 hash,判断当前 plugin 的使能、禁能配置,如果设置不加载则打印警告信息后返回
- 校验 plugin so 版本与 vpp 版本是否一致,不一致则终止加载
- 使用 dlopen 函数打开 plugin.so,打开的 plugin.so 中的所有符号只能本地使用,其它的 so 无法直接调用
- 记录 dlopen 函数打开的句柄以及其它信息到 plugin_info_t 结构中,找到 dlopen 打开 so 后 vlib_plugin_registration 符号加载到的位置,保存此符号地址并根据条件调用 early_init 函数执行用户配置的初始化函数