简单实现使用API通信的vpp管理进程(C语言)

vpp api 介绍

vpp 提供了 CLI 命令行供大家进行配置或者查询,但是我们在实际使用当中不能期望通过解析命令行的输出结果来获取有用的信息,这可能需要增加一层脚本来实现,速度很慢,也很混乱。不利于达到管理程序的自动化运行的目的。

所幸,vpp 还提供了 API 供管理程序调用。这个 API 是基于共享内存实现的消息接口,管理程序可以异步非阻塞的处理相关消息。通过这些 API,我们就可以实现在不同的进程上去管理 vpp,而且可以实现非常高的吞吐量。

vpp API 支持三组消息类型:

1. Request/Reply,即 1 Request -> 1 Reply

大多数的 vpp API 都是这种消息类型,例如,只有唯一输出的 show 命令的 API, show_version -> show_version_reply,或者创建某些资源的 API,create_loopback -> create_loopback_reply。

这组消息的名称特征是 _reply,通过发送 1 条 到 vpp,立即就能收到 1 条 _reply 回复消息。

2. Dump/Details,即 1 Request -> N Replies

例如:sw_interface_dump -> sw_interface_details

这组消息的名称特征是 _dump 和 _details,通过发送 1 条 _dump 到 vpp,立即可以收到 1 条或多条 _details 回复消息。

3. Want/Event,即 1 Request -> 1 Reply + N events

例如:want_interface_events -> want_interfae_events_reply + sw_interface_event

这组消息的名称特征是 want_ 和 want__reply + _event,通过发送 1 条 want_ 到 vpp,可以立即收到 1 条 want__reply,后续可能会收到 0 条或者多条 _event。

这三组消息的命名格式是最常见的,但并不做强制要求,不按照这些规则也可以正确的实现 vpp API,但是为了使编码过程更加清晰,通常还是推荐按照这些规则去命名我们的 API 消息。

我们可以在 vpp 命令行里面通过 show api message-table 去查询当前 vpp 支持的所有消息。

vpp# show api message-table 
ID   Name
1    memclnt_create
2      [no handler]
3    memclnt_delete
4      [no handler]
5      [no handler]
6      [no handler]
7      [no handler]
8    rpc_call
9    rpc_call_reply
10   get_first_msg_id
11   get_first_msg_id_reply
12   api_versions
13     [no handler]
14   trace_plugin_msg_ids
15   sockclnt_create
16     [no handler]
17   sockclnt_delete
18     [no handler]
19   sock_init_shm
20     [no handler]
21   memclnt_keepalive
22   memclnt_keepalive_reply
23   bond_create
24   bond_create_reply
25   bond_create2
26   bond_create2_reply
27   bond_delete
28   bond_delete_reply
29   bond_enslave
30     [no handler]
31   bond_add_member
32   bond_add_member_reply
33   bond_detach_slave
34     [no handler]
35   bond_detach_member
36   bond_detach_member_reply
37   sw_interface_bond_dump
38     [no handler]
39   sw_bond_interface_dump
40   sw_bond_interface_details
41   sw_interface_slave_dump
42     [no handler]
43   sw_member_interface_dump
-- more -- (1-44/1513)

vpp 的原始消息结构都是写在 .api 文件当中的,在 vpp 2101 版本中包含了 125 个 .api 文件。

[root@wuhan vpp-2101]# find src -name *.api | wc -l 
125

通过编译,.api 文件可以自动生成 .api.json 文件,这些 json 文件可以用来自动化的生成 Java,Python 或其他语言的 API。

由于 vpp 本身就是用 C 语言写的,所以提供了直接通过 C 语言使用共享内存 API 的方法,其他的一些高级语言如 Java,Python,C++ 等,通常是通过构造一些 C 的包装函数来实现 API 的调用。

C 语言实现的 vpp 管理进程

下面介绍一下如何使用 C 语言编写一个简单的 vpp 管理进程。

首先需要准备好 vpp 的源码,我这里使用 vpp 2101 的版本进行测试,同一目录下的 api_examples 是我准备好的管理进程的代码,包括了一个 C 文件 agent.c 以及一个 Makefile。

[root@wuhan api]# git clone -b stable/2101 https://git.fd.io/vpp vpp-2101
Cloning into 'vpp-2101'...
remote: Enumerating objects: 128594, done.
remote: Counting objects: 100% (7104/7104), done.
remote: Compressing objects: 100% (2563/2563), done.
remote: Total 128594 (delta 6794), reused 4541 (delta 4541), pack-reused 121490
Receiving objects: 100% (128594/128594), 149.01 MiB | 9.22 MiB/s, done.
Resolving deltas: 100% (68067/68067), done.
[root@wuhan api]# ls
api_examples  vpp-2101
[root@wuhan api]# ls api_examples/
agent.c  Makefile

先看下管理程序的 Makefile 怎么写的,-lvppinfra -lvlibmemoryclient -lsvm -lvapiclient 这些是我们需要链接的 vpp 提供的库。

[root@wuhan api_examples]# cat Makefile 
LIBS = -L../vpp-2101/build-root/install-vpp-native/vpp/lib/
VPP_LIBS        += ${LIBS}
VPP_LIBS        += -lvppinfra -lvlibmemoryclient -lsvm -lvapiclient

LDFLAGS         += $(VPP_LIBS)
LDFLAGS         += -lpthread -lcheck -lrt -lm 

INCLUDES += -I../vpp-2101/build-root/install-vpp-native/vpp/include
CFLAGS   += $(INCLUDES)

SOFILE = vppagent

OBJS += agent.c

$(SOFILE): ${OBJS}
        $(QUIET_SLINK)$(CC) -o $(SOFILE) $(CFLAGS) $(LDFLAGS) ${OBJS} -g -std=gnu99

下面再看一下 agent.c 怎么写

[root@wuhan api_examples]# cat agent.c 
#include 

#include 
#include 
#include 
#include 

#define vl_typedefs
#include 
#undef vl_typedefs

#define vl_endianfun
#include 
#undef vl_endianfun

#define vl_print(handle, ...)
#define vl_printfun
#include 
#undef vl_printfun

#define DEFAULT_QUEUE_SIZE  1024

static void
vl_api_sw_interface_event_t_handler(vl_api_sw_interface_event_t * mp)
{
    printf("Received: sw_interface_event interface_index:%u interface_state:%u\n",
              ntohl(mp->sw_if_index), ntohl(mp->flags));
}

static void
vl_api_want_interface_events_reply_t_handler(vl_api_want_interface_events_reply_t * mp)
{
    printf("Received: want_interface_events_reply context:0x%x\n", ntohl(mp->context));
}

static void 
vl_api_sw_interface_details_t_handler(vl_api_sw_interface_details_t * mp)
{
    printf("Received: sw_interface_details interface_index:%u interface_name:%s context:0x%x\n",
              ntohl(mp->sw_if_index), mp->interface_name, ntohl(mp->context));
}

typedef struct
{
    svm_queue_t *vl_input_queue;  /* vpe input queue */
    u32 my_client_index;    /* API client handle */
} agent_main_t;

agent_main_t agent_main;

int connect_to_vpp (char *name)
{
    agent_main_t *agentm = &agent_main;
    api_main_t *am = vlibapi_get_main ();

    if (vl_client_connect_to_vlib ("/vpe-api", name, DEFAULT_QUEUE_SIZE) < 0)
    {
        printf("Shmem connect failed\n");
        return -1;
    }

    agentm->vl_input_queue = am->shmem_hdr->vl_input_queue;
    agentm->my_client_index = am->my_client_index;

    return 0;
}

#define foreach_vpe_api_reply_msg                           \
_(SW_INTERFACE_EVENT, sw_interface_event)                   \
_(WANT_INTERFACE_EVENTS_REPLY, want_interface_events_reply) \
_(SW_INTERFACE_DETAILS, sw_interface_details)

void agent_api_hookup (agent_main_t * agentm)
{
#define _(N,n)                                                  \
    vl_msg_api_set_handlers(VL_API_##N, #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 _
}

void send_want_interface_events (agent_main_t * agentm)
{
    vl_api_want_interface_events_t *mp;
    mp = vl_msg_api_alloc (sizeof (*mp));
    clib_memset (mp, 0, sizeof (*mp));

    mp->_vl_msg_id = ntohs (VL_API_WANT_INTERFACE_EVENTS);
    mp->client_index = agentm->my_client_index;
    mp->context = htonl (0xabcd);
    mp->enable_disable = -1;

    vl_msg_api_send_shmem (agentm->vl_input_queue, (u8 *) & mp);
    printf("Sent: want_interface_events context:0x%x\n", ntohl(mp->context));
}

void send_sw_interface_dump(agent_main_t * agentm)
{
    vl_api_sw_interface_dump_t * mp;

    mp = vl_msg_api_alloc(sizeof(*mp));
    memset(mp, 0, sizeof(*mp));
    mp->_vl_msg_id = ntohs(VL_API_SW_INTERFACE_DUMP);
    mp->client_index = agentm->my_client_index;
    mp->context = htonl (0x1234);

    vl_msg_api_send_shmem(agentm->vl_input_queue, (u8 *) &mp);
    printf("Sent: sw_interface_dump context:0x%x\n", ntohl(mp->context));
}

int main (int argc, char **argv)
{
    // 1. 内存初始化
    agent_main_t *agentm = &agent_main;
    clib_mem_init_thread_safe (0, 256 << 20);
    clib_memset (agentm, 0, sizeof (*agentm));

    // 2. 定义并实现回调函数
    agent_api_hookup(agentm);

    // 3. 与 vpp 建立共享内存连接
    if (connect_to_vpp ("vpp_agent"))
    {
        svm_region_exit ();
        printf("Couldn't connect to vpp!!!\n");
    }
    else
    {
        printf("Connected to vpp!!!\n");
    }

    // 4. API 调用及消息交互
    send_want_interface_events(agentm);
    sleep(1);
    send_sw_interface_dump(agentm);  

    while(1) {}

}

在 main 函数里面,代码分成 4 部分实现共享内存 API 的消息交互。

1. 内存初始化,在 vpp 的 heap 上分配管理进程所需要的内存,初始化管理进程的结构体。

    agent_main_t *agentm = &agent_main;
    clib_mem_init_thread_safe (0, 256 << 20);
    clib_memset (agentm, 0, sizeof (*agentm));

2. 定义并实现回调函数,reply,details,event 消息需要在管理进程上实现他们的回调,同样的,在 vpp 上也会实现 request,dump 的回调,这部分实现在 vpp 的源代码中已经包含了。如果想要实现一组新的 API,那么 request/reply 消息需要各自在 vpp 及管理进程代码当中去实现。

static void
vl_api_sw_interface_event_t_handler(vl_api_sw_interface_event_t * mp)
{
    printf("Received: sw_interface_event interface_index:%u interface_state:%u\n",
              ntohl(mp->sw_if_index), ntohl(mp->flags));
}

static void
vl_api_want_interface_events_reply_t_handler(vl_api_want_interface_events_reply_t * mp)
{
    printf("Received: want_interface_events_reply context:0x%x\n", ntohl(mp->context));
}

static void 
vl_api_sw_interface_details_t_handler(vl_api_sw_interface_details_t * mp)
{
    printf("Received: sw_interface_details interface_index:%u interface_name:%s context:0x%x\n",
              ntohl(mp->sw_if_index), mp->interface_name, ntohl(mp->context));
}

#define foreach_vpe_api_reply_msg                           \
_(SW_INTERFACE_EVENT, sw_interface_event)                   \
_(WANT_INTERFACE_EVENTS_REPLY, want_interface_events_reply) \
_(SW_INTERFACE_DETAILS, sw_interface_details)

void agent_api_hookup (agent_main_t * agentm)
{
#define _(N,n)                                                  \
    vl_msg_api_set_handlers(VL_API_##N, #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 _
}

3.  与 vpp 建立共享内存连接,和 vpp 的共享内存可以通过别名 "/vpe-api" 获取到,这个共享内存是在 vpp 上分配的,在 vl_client_connect_to_vlib 里面会完成队列的建立,同时还会开启一个新线程,收到消息后的回调函数调用就是在这个新线程上完成的。

int connect_to_vpp (char *name)
{
    agent_main_t *agentm = &agent_main;
    api_main_t *am = vlibapi_get_main ();

    if (vl_client_connect_to_vlib ("/vpe-api", name, DEFAULT_QUEUE_SIZE) < 0)
    {
        printf("Shmem connect failed\n");
        return -1;
    }

    agentm->vl_input_queue = am->shmem_hdr->vl_input_queue;
    agentm->my_client_index = am->my_client_index;

    return 0;
}

4. API 调用及消息交互,通过管理程序向 vpp 发送两条消息,1 条消息是用来订阅 interface 状态的,还有 1 条消息是用来获取所有 interface 状态。其中消息结构体里带的 context 字段,是用来识别收到的回复消息是否是由指定的 reqeust/dump 产生的。want_sw_interface_events 和 want_sw_interface_events_reply 中的 context 值应该是一样的,sw_interface_dump 和 sw_interface_details 中的 context 值也应该是一样的。

void send_want_interface_events (agent_main_t * agentm)
{
    vl_api_want_interface_events_t *mp;
    mp = vl_msg_api_alloc (sizeof (*mp));
    clib_memset (mp, 0, sizeof (*mp));

    mp->_vl_msg_id = ntohs (VL_API_WANT_INTERFACE_EVENTS);
    mp->client_index = agentm->my_client_index;
    mp->context = htonl (0xabcd);
    mp->enable_disable = -1;

    vl_msg_api_send_shmem (agentm->vl_input_queue, (u8 *) & mp);
    printf("Sent: want_interface_events context:0x%x\n", ntohl(mp->context));
}

void send_sw_interface_dump(agent_main_t * agentm)
{
    vl_api_sw_interface_dump_t * mp;

    mp = vl_msg_api_alloc(sizeof(*mp));
    memset(mp, 0, sizeof(*mp));
    mp->_vl_msg_id = ntohs(VL_API_SW_INTERFACE_DUMP);
    mp->client_index = agentm->my_client_index;
    mp->context = htonl (0x1234);

    vl_msg_api_send_shmem(agentm->vl_input_queue, (u8 *) &mp);
    printf("Sent: sw_interface_dump context:0x%x\n", ntohl(mp->context));
}

代码写完了,需要测试一下 API 接口是否好用。先把 vpp 运行起来,然后使用 create interface memif id 1 创建一个 memif 接口,有两个接口的话,方便分析由 dump 消息触发的 details 返回消息。

[root@wuhan vpp-2101]# make run-release
    _______    _        _   _____  ___ 
 __/ __/ _ \  (_)__    | | / / _ \/ _ \
 _/ _// // / / / _ \   | |/ / ___/ ___/
 /_/ /____(_)_/\___/   |___/_/  /_/    

vpp# show int 
              Name               Idx    State  MTU (L3/IP4/IP6/MPLS)     Counter          Count     
local0 
vpp# create interface memif id 1
vpp# show int 
              Name               Idx    State  MTU (L3/IP4/IP6/MPLS)     Counter          Count     
local0                            0     down          0/0/0/0       
memif0/1                          1     down         9000/0/0/0 

再进入 api_examples 目录,编译并运行我们的管理程序,在管理进程里,我们发送了 1 条 want_interface_events,然后立即就收到了 1 条 want_interface_events_reply,两个消息里面带的 context 值是一样的,标志着这是一对消息。发送了 1 条 sw_interface_dump, 由于配置了两个 interface,所以收到了两条 sw_interface_details,这组消息里面带的 context 值也是相同的。

[root@wuhan api_examples]# make 
cc -o vppagent -I../vpp-2101/build-root/install-vpp-native/vpp/include -L../vpp-2101/build-root/install-vpp-native/vpp/lib/ -lvppinfra -lvlibmemoryclient -lsvm -lvapiclient -lpthread -lcheck -lrt -lm  agent.c -g -std=gnu99
[root@vcpe8 api_examples]# ./vppagent 
Connected to vpp!!!
Sent: want_interface_events context:0xabcd
Received: want_interface_events_reply context:0xabcd
Sent: sw_interface_dump context:0x1234
Received: sw_interface_details interface_index:0 interface_name:local0 context:0x1234
Received: sw_interface_details interface_index:1 interface_name:memif0/1 context:0x1234

我们再到 vpp 上去操作一下 interface 的状态。

vpp# set interface state local0 up
vpp# set interface state memif0/1 up
vpp# set interface state local0 down
vpp# set interface state memif0/1 down

在管理进程上,立即就能收到通知消息,这个消息就是之前的 want_interface_events 订阅的通知事件。

Received: sw_interface_event interface_index:0 interface_state:1
Received: sw_interface_event interface_index:1 interface_state:1
Received: sw_interface_event interface_index:0 interface_state:0
Received: sw_interface_event interface_index:1 interface_state:0

Want/Event 这组消息其实是包含了 Request/Reply,那么 3 组不同的消息类型,已经都包含在这个管理程序的代码当中了。通过丰富这个管理程序,我们就可以实现快捷灵活的配置和查询 vpp。

参考文献:

VPP/How To Use The C API - fd.io

Vector Packet Processing 101: VPP Plugins & Binary API | PANTHEON.tech

https://www.marosmars.com/blog/managing-vpp-c-edition

你可能感兴趣的:(VPP,c语言,性能优化,多线程)