VPP-API机制分析上

28次阅读

共计 8748 个字符,预计需要花费 22 分钟才能阅读完成。

VPP 除了使用命令行进行配置外,还可以使用 API 进行配置。VPP 不仅支持 c 语言的 API,还支持 python,java,lua 等高级语言的 API,非常适合自动化部署。

API 简单使用案例

VPP 提供了一个 VAT 客户端程序,用于进行简单的 API 测试。可执行文件位于:

  • debug 版本:vpp 源码路径 /build-root/build-vpp_debug-native/vpp/bin/vpp_api_test
  • release 版本:vpp 源码路径 /build-root/build-vpp-native/vpp/bin/vpp_api_test

启动 vpp 与 vat 程序

sudo systemctl start vpp
cd vpp 源码路径 /build-root/build-vpp-native/vpp/bin/
sudo vpp_api_test
load_one_plugin:68: Loaded plugin: /usr/lib/x86_64-linux-gnu/vpp_api_test_plugins/memif_test_plugin.so
......
load_one_plugin:68: Loaded plugin: /usr/lib/x86_64-linux-gnu/vpp_api_test_plugins/mactime_test_plugin.so
vat# 
vat# 

简单使用

  • [] help命令列出所有的命令
vat# 
vat# help
Help is available for the following:
acl_add_replace
......
  • [] help + 具体命令 列出指定命令的帮助信息
vat# help acl_del
usage: acl_del <acl-idx>
vat# 
  • [] quit 或者 q 命令退出 vat
vat# quit
admin@ubuntu:~/vpp/build-root/build-vpp-native/vpp/bin$ 
  • [] show_version 显示 VPP 版本
vat# show_version
        program: vpe
        version: 19.08-rc0~65-g3b62e29c3
     build date: Sat Apr 20 13:38:27 CST 2019
build directory: /home/admin/vpp
vat# 

VPP API 交互过程

vpp 的 api 是 CS 模型。VPP 是服务器端,监听客户端连接请求,处理客户端发送的 api 请求,返回应答。如下图所示:

如上图所示,客户端 VAT 向 VPP 请求连接,VPP 返回新连接。连接建立之后,VAT 发送 API 请求,VPP 处理请求返回应答。VPP 支持 unix 套接字或者 inet 套接字。VPP 支持多个客户端同时进行请求。VPP 支持使用共享内存进行数据交换,当客户端和服务器端都在同一个节点上的时候,可以选择使用共享内存进行 message 交换(3, 和 4 交互过程可以选择共享内存交换也可以选择套接字交换),提高通信速率。

VPP API 编写说明

VPP API 命名规则

vpp 一共支持三种类型的 API:

  • [] request/reply

    这种类型的规则,客户端发送一个请求,VPP 发送一个应答。应答消息命名为:method_name + _reply。

  • [] Dump/Detail

    客户端发送一个 bulk 请求,VPP 发送多个 details 消息。一个 dump/detail 请求最终会以一个 ”control ping block “ 结束。请求方法命名规则为:method + _dump。应答方法命名规则为 method + _details。该类型消息主要用来请求一个表消息,比如 FIB 表包含多个表项,可以使用一个该消息获取所有的 FIB 表项信息。

  • [] Events

    客户端可以通过该类型 API 向服务器注册一些获取异步消息通知。比如获取 interface 的状态变化消息,周期性统计消息等。这类 API 通常以 ”want_” 字段作为前缀,比如:”want_interface_events”。

客户端发送的消息会包含一个 ’client_index’ 字段,该字段对服务器端是透明的,相当于一个‘cookie’,用于区分不同的客户端。

  • [] 命名

    • Reply/Request 方法. 请求: 名字 应答: 名字+_reply
    • Dump/Detail 方法. 请求: 名字 +_dump Reply: 名字+_details
    • Event 注册方法: 请求: want_+名字 Reply: want_+ 名字+_reply

VPP API 编写步骤(这部分内容来自于网络)

注:这部分内容来自于网络 [](https://blog.51cto.com/zhangc…

添加一个新的二进制控制层的 API,涉及两部分内容,一部分是客户端,另一个就是服务器端。我们以 acl 插件的 api 为例进行说明。

添加一个 api 需要修改三个文件。代码路径是 vpp/src/plugins/acl 下
acl.api — vat 与 vpp 通信的结构体定义
acl_test.c — vat 使用(客户端)
acl.c — vpp 使用(服务器端)

1.acl.api

acl.api 中定义 vat 与 vpp 通信的结构体,然后由 vppapigen 文件处理,最终生成 acl.api.h 的头文件。两边都包含这个头文件,这样 vat 与 vpp 就使用了相同的结构体通信了。我们看一下 acl.api 中的定义:

/** \brief Delete an ACL
    @param client_index - opaque cookie to identify the sender
    @param context - sender context, to match reply w/ request
    @param acl_index - ACL index to delete
*/

autoreply manual_print define acl_del
{
  u32 client_index;  // 系统使用
  u32 context;       // 系统使用
  u32 acl_index;     // 通过命令 acl_del <acl-idx> 输入的 acl-idx
};

这个结构体的定义由 3 个关键字(autoreply、manual_print、define)加上名称再加成员构成,最终会被转化为:

typedef VL_API_PACKED(struct _vl_api_acl_del {
    u16 _vl_msg_id;      
    u32 client_index;
    u32 context;
    u32 acl_index;
}) vl_api_acl_del_t;

typedef VL_API_PACKED(struct _vl_api_acl_del_reply {
    u16 _vl_msg_id;
    u32 context;
    i32 retval;
}) vl_api_acl_del_reply_t;

这样就可以用使用 vl_api_acl_del_t 与 vl_api_acl_del_reply 这个结构体通信了。
具体说一下每个部分:

关键字分析

  • 关键字 autoreply
    在这里需要先提一下 reply
    正常情况下,vat 发送一个请求消息,然后等待一个 reply 消息。所以 xxx_reply_t 结构是不可少的,可以自己写,也可自动生成。
    而这个关键字表示了自动生成 xxx_reply_t 结构体,但是自动生成的结构体只有默认的参数_vl_msg_id,context,retval。如上所示 vl_api_acl_del_reply_t。
    这个转换的函数实现如下。
void autoreply (void *np_arg)
{
static u8 *s;
node_t *np = (node_t *)np_arg;
int i;

vec_reset_length (s);

s = format (0, "define %s_reply\n", (char *)(np->data[0]));
s = format (s, "{\n");
s = format (s, "u32 context;\n");
s = format (s, "i32 retval;\n");
s = format (s, "};\n");

for (i = 0; i < vec_len (s); i++)
    clib_fifo_add1 (push_input_fifo, s[i]);
}
  • 关键字 manual_print
    xxx_print 函数是用来打印消息结构体内容的。默认情况下会自动生成。如果你想自己来实现,就需要加入这个关键字,然后在模块路径下的 manual_fns.h 中实现。
static inline void *
vl_api_acl_del_t_print (vl_api_macip_acl_del_t * a, void *handle)
{
  u8 *s;

  s = format (0, "SCRIPT: acl_del %d",
              clib_host_to_net_u32 (a->acl_index));

  PRINT_S;
  return handle;
}
  • 关键字 define
    define 关键字是转化的关键。每个定义都要加上。
  • 其他关键字
    还有一些其他的关键字,大家对比一下定义与生成的结果也基本都能看出来,在这里就不赘述了。

2.acl_test.c

这个文件是 vat 使用。
有三件事要做,1. 写 cli 的 help 2. 写函数 3. 函数加载

2.1 写 cli 的 help

#define foreach_vpe_api_msg \
_(acl_del, "<acl-idx>")

2.2 写函数

我们需要写两个函数
api_acl_del 与 vl_api_acl_del_reply_t_handler
这两个函数是配合使用的,来一个一个看

  • api_acl_del
    这个函数是与 cli 直接关联,命令输入后就调用的就是这个函数.
static int api_acl_del (vat_main_t * vam)
{
    unformat_input_t * i = vam->input;
    // 这个结构体就是在 acl.api 中定义的消息传递结构体
    vl_api_acl_del_t * mp; 
    u32 acl_index = ~0;
    int ret;
    
    // 解析字符串,跟 vpp 的命令行解析一样
    if (!unformat (i, "%d", &acl_index)) {errmsg ("missing acl index\n");
      return -99;
    }

    // 给 mp 分配内存,然后填写要传递的值
    /* Construct the API message */
    M(ACL_DEL, mp);
    mp->acl_index = ntohl(acl_index);

    /* send it... */
    S(mp);

    /* Wait for a reply... */
    W (ret);
    return ret;
}

在这里把这几个宏的实现也贴一下。对应一下,就能看明白了。

/* M: construct, but don't yet send a message */
#define M(T, mp)                                                \
do {                                                            \
    vam->result_ready = 0;                                      \
    mp = vl_msg_api_alloc_as_if_client(sizeof(*mp));            \
    memset (mp, 0, sizeof (*mp));                               \
    mp->_vl_msg_id = ntohs (VL_API_##T+__plugin_msg_base);      \
    mp->client_index = vam->my_client_index;                    \
} while(0);

/* S: send a message */
#define S(mp) (vl_msg_api_send_shmem (vam->vl_input_queue, (u8 *)&mp))

/* W: wait for results, with timeout */
#define W(ret)                  \
do {                                            \
    f64 timeout = vat_time_now (vam) + 1.0;     \
    ret = -99;                                  \
                                                \
    while (vat_time_now (vam) < timeout) {      \
        if (vam->result_ready == 1) {           \
            ret = vam->retval;                  \
            break;                              \
        }                                       \
        vat_suspend (vam->vlib_main, 1e-5);     \
    }                                           \
} while(0);
  • vl_api_acl_del_reply_t_handler
    这个函数是在 vpp 回复消息后,clinet 接收回应的函数。
    由于大多数都一样,就直接用宏来实现了。
#define foreach_standard_reply_retval_handler   \
_(acl_del_reply) 

#define _(n)                                            \
    static void vl_api_##n##_t_handler                  \
    (vl_api_##n##_t * mp)                               \
    {                                                   \
        vat_main_t * vam = acl_test_main.vat_main;   \
        i32 retval = ntohl(mp->retval);                 \
        if (vam->async_mode) {                          \
            vam->async_errors += (retval < 0);          \
        } else {                                        \
            vam->retval = retval;                       \
            vam->result_ready = 1;                      \
        }                                               \
    }
foreach_standard_reply_retval_handler;
#undef _

注上面这个宏只是定义多个 xxx_reply_retval_handler 函数

  • api_acl_del 和 acl_del_reply 函数的关系
    这两个函数是在不同的线程,在宏 W 中,是在等待 result_ready 被置位;而 result_ready 就是在 acl_del_reply 中被置位的。

    从下面信息可以看出 VAT 有两个线程:

admin@ubuntu:~/vpp$ pstree -p `pidof vpp_api_test`
vpp_api_test(25810)───{vpp_api_test}(25811)
admin@ubuntu:~/vpp$ 

也可以从如下的 gdb 信息可以看出 vat 有一个线程进行应答消息的处理。

(gdb) info thread
  Id   Target Id         Frame 
  1    Thread 0x7f3bb05d2b80 (LWP 25810) "vpp_api_test" vat_time_now (vam=vam@entry=0x5575ebcdc800 <vat_main>)
    at /home/jd/vpp/src/vat/api_format.c:141
* 2    Thread 0x7f3ba6d09700 (LWP 25811) "vpp_api_test" 0x00007f3ba5cec470 in vl_api_acl_del_reply_t_handler (mp=0x13004dee8)
    at /home/jd/vpp/src/plugins/acl/acl_test.c:91
(gdb) bt
#0  0x00007f3ba5cec470 in vl_api_acl_del_reply_t_handler (mp=0x13004dee8)
    at /home/jd/vpp/src/plugins/acl/acl_test.c:91
#1  0x00007f3bb01c2251 in msg_handler_internal (free_it=1, do_it=1, 
    trace_it=<optimized out>, the_msg=0x13004dee8, 
    am=0x7f3bb03ca420 <api_main>)
    at /home/jd/vpp/src/vlibapi/api_shared.c:425
#2  vl_msg_api_handler (the_msg=0x13004dee8)
    at /home/jd/vpp/src/vlibapi/api_shared.c:559
#3  0x00007f3bb01c321a in vl_msg_api_queue_handler (q=q@entry=0x1301c69c0) at /home/jd/vpp/src/vlibapi/api_shared.c:770
#4  0x00007f3bb01bc07e in rx_thread_fn (arg=<optimized out>)
    at /home/jd/vpp/src/vlibmemory/memory_client.c:94
#5  0x00007f3baf6b86db in start_thread (arg=0x7f3ba6d09700)
    at pthread_create.c:463
#6  0x00007f3baf3e188f in clone ()
    at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
(gdb) 

2.3 加载函数

需要把写的两个函数挂载上。只需要在对应的宏下按格式写就好了。

2.3.1 在宏中添加定义

  • api_acl_del

    在 foreach_vpe_api_msg 宏下定义
    其实这个在“2.1 写 cli 的 help”中已经写过,就不用再写了

/*
 * List of messages that the api test plugin sends,
 * and that the data plane plugin processes
 */
#define foreach_vpe_api_msg 
_(acl_del, "<acl-idx>") \
  • vl_api_acl_del_reply_t_handler

    在 foreach_vpe_api_reply_msg 宏下定义

  /*
  * Table of message reply handlers, must include boilerplate handlers
  * we just generated
  */
  #define foreach_vpe_api_reply_msg                                       \
  _(ACL_DEL_REPLY, acl_del_reply)

2.3.2 函数挂载

上一节提到的宏,都是在 acl_vat_api_hookup 这个函数中使用的,我们不需要做任何修改。

static
void acl_vat_api_hookup (vat_main_t *vam)
{
    acl_test_main_t * sm = &acl_test_main;
    /* Hook up handlers for replies from the data plane plug-in */
#define _(N,n)                                                  \
    vl_msg_api_set_handlers((VL_API_##N + sm->msg_id_base),     \
                           #n,                                  \
                           vl_api_##n##_t_handler,              \
                           vl_noop_handler,                     \
                           vl_api_##n##_t_endian,               \
                           vl_api_##n##_t_print,                \
                           sizeof(vl_api_##n##_t), 1);
    foreach_vpe_api_reply_msg;
#undef _

    /* API messages we can send */
#define _(n,h) hash_set_mem (vam->function_by_name, #n, api_##n);
    foreach_vpe_api_msg;
#undef _

    /* Help strings */
#define _(n,h) hash_set_mem (vam->help_by_name, #n, h);
    foreach_vpe_api_msg;
#undef _
}

3.acl.c

这个文件是 vpp 使用,它用来接收 vat(vpp-api-test)发送的消息,然后处理,最后回应给 vat。
我们需要写对应的函数,然后挂上就可以了

  • 写宏
    只需要在这个宏里,把函数添加进去即可
/* List of message types that this plugin understands */

#define foreach_acl_plugin_api_msg      \
_(ACL_DEL, acl_del) 
  • 写函数
    这里的函数也得遵循格式,vl_api_xxx_t_handler,函数如下所示
static void
vl_api_acl_del_t_handler (vl_api_acl_del_t * mp)
{
  acl_main_t *am = &acl_main;
  // 这个结构体就是在 acl.api 中定义的消息应答传递结构体,用于给 VAT 发送应答消息
  vl_api_acl_del_reply_t *rmp;
  int rv;

  //mp 中就是 VAT 发送来的结构体,我们可以从中取得配置的 acl_index 使用。然后调用相应的处理函数。rv = acl_del_list (ntohl (mp->acl_index));

  // 这里是消息处理完毕后的应答消息,VAT 会在那里等待回应。也是通过共享内存的方式来通信。// 如果需要在回应消息里传递参数,可以使用另一个宏 ---  REPLY_MACRO2
  REPLY_MACRO (VL_API_ACL_DEL_REPLY);
}
  • 挂钩子
    使用定义的宏来挂载函数。这里也不用做任何的改变。
/* Set up the API message handling tables */
static clib_error_t *
acl_plugin_api_hookup (vlib_main_t * vm)
{
acl_main_t *am = &acl_main;
#define _(N,n)                                                  \
vl_msg_api_set_handlers((VL_API_##N + am->msg_id_base),     \
                       #n,                  \
                       vl_api_##n##_t_handler,              \
                       vl_noop_handler,                     \
                       vl_api_##n##_t_endian,               \
                       vl_api_##n##_t_print,                \
                       sizeof(vl_api_##n##_t), 1);
foreach_acl_plugin_api_msg;
#undef _

return 0;
}

参考链接

[](https://wiki.fd.io/view/VPP/A…

[](https://wiki.fd.io/view/VPP/H…

正文完
 0