赵占旭的博客

[转]Cisco VPP Node调度框架

注:本文是转载,但不是100%的转载,可能稍微有些出入,原文地址点击这里

vpp的功能逻辑被划分为一个个node,node之间通过下一跳传递处理完的数据包集合,从而组成整个业务图谱。本文将分析node调度框架源码。

基本概念


vlib_node_type_t

node分为四种类型:

  • VLIB_NODE_TYPE_INTERNAL对数据包真正处理的业务node。
  • VLIB_NODE_TYPE_INPUT收包逻辑node,比如:dpdk,pcap等。
  • VLIB_NODE_TYPE_PRE_INPUT目前只有一个epoll node,对socket相关逻辑提供服务,主要使用在控制业务上。
  • VLIB_NODE_TYPE_PROCESS该类型node可以被挂起也可以被恢复,有独立的分配在heap上的运行时栈。类似于在一个线程中实现了多任务调度机制。主要用来修改vpp node内部参数。

vlib_node_main_t

记录各种全局信息,比如各种数据结构集合,数据结构内存池之类。

vlib_node_t

注册node时将业务逻辑,几乎不怎么修改的参数,状态信息保存在这里。

vlib_node_runtime_t

这是调度框架实际频繁使用的结构,从vlib_node_t拷贝了部分信息,以及私有的频繁变动的信息。

vlib_process_t

VLIB_NODE_TYPE_PROCESS类型node专用结构,记录用于模拟task的基础结构:heap上的运行时栈,2种返回时寄存器备份,等。

vlib_frame_t

每个node都有一个对应的vlib_frame_t,用来保存供node使用的数据包集合。这是每个node最终处理数据的内存所在地。

vlib_pending_frame_t

当一个node处理完数据包,则填充该数据结构,并加入到全局链表,调度框架便能在下一次调度时找到需要接收该数据包的下一个node。

vlib_next_frame_t

主要是node内部逻辑使用,定位该node的下一条信息。

关键结构


vlib_node_main_t

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
54
55
56
57
58
59
typedef struct
{
//一块连续内存,头部是vec_header_t,数据部分是node指针数组
vlib_node_t **nodes;

//node按名字组成hash表
uword *node_by_name;

//目前只有VLIB_NODE_MAIN_RUNTIME_STARTED一个状态,暂时没啥用,忽略之。
u32 flags;
#define VLIB_NODE_MAIN_RUNTIME_STARTED (1 << 0)

/*
* node有三种类型:VLIB_NODE_TYPE_INTERNAL,VLIB_NODE_TYPE_INPUT,
* VLIB_NODE_TYPE_PRE_INPUT,按类型分类索引
*/
vlib_node_runtime_t *nodes_by_type[VLIB_N_NODE_TYPE];

//以下三个用于类似于网卡napi收包模式,dpdk没用到,忽略之
u32 *pending_interrupt_node_runtime_indices;

//假设node1有n1个下一跳,node2有n2个下一跳....共n1 + n2 +....+ni个下一跳
//连续保存在next_frames指向的vec中。
vlib_next_frame_t *next_frames;

/*
* 数据包从node输出到下一跳,那么下一跳node即是pending frame,会加入pending_frames指向的vec。
* 主循环会遍历该vec,对每个node来调用处理逻辑
*/
vlib_pending_frame_t *pending_frames;

//定时器,信号相关,源码及其恶心,博主暂时放弃阅读
timing_wheel_t timing_wheel;
vlib_signal_timed_event_data_t *signal_timed_event_data_pool;

/*
* VLIB_NODE_TYPE_PROCESS类型的node,每个node有分配在heap上的运行栈,
* 没错就是通常说的在顶部那种栈,但是这里分配在了heap上。
* 这里在单线程中模拟了类似多线程的效果,以后会详细分析。
* processes保存了所有VLIB_NODE_TYPE_PROCESS类型node的描述结构指针。
*/
vlib_process_t **processes;

//VLIB_NODE_TYPE_PROCESS类型的node,执行时都会把
//current_process_index赋值为本node的runtime_index
u32 current_process_index;

//VLIB_NODE_TYPE_PROCESS类型node专用,挂起时在其中保存信息。
vlib_pending_frame_t *suspended_process_frames;

//hash表,把node的scalar_size,vector_size值组合成key,查找对应的vlib_frame_size_t结构。
uword *frame_size_hash;

//通过hash查找到node对应的vlib_frame_size_t值,从中的内存池来分配vlib_frame_t。
vlib_frame_size_t *frame_sizes;

//注册node函数提交的注册信息链接在这里,仅仅初始化时使用
vlib_node_registration_t *node_registrations;
} vlib_node_main_t;

vlib_node_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct vlib_node_t
{
//业务逻辑
vlib_node_function_t *function;

//node类型,之前提到的那四种之一
vlib_node_type_t type;

//vlib_node_t和vlib_node_runtime_t是一一对应的好基友
u32 runtime_index;

//这两个成员组合成key,在vlib_node_main_t->frame_size_hash中查找,
//确定本node相关的frame的内存池
u16 scalar_size, vector_size;

//初始化时用用
char **next_node_names;

//根据next_node_names来生成next_nodes,vec结构,记录了每个可选下一条的index。
u32 *next_nodes;

//统计发给下一条node的数据包总数
u64 *n_vectors_by_next_node;
} vlib_node_t;

vlib_node_runtime_t

1
2
3
4
5
6
7
8
typedef struct vlib_node_runtime_t
{
//本node的多个下一条中,第一个下一条在vlib_node_main_t->next_frames中的索引
u32 next_frame_index;

//vlib_node_t和vlib_node_runtime_t是一一对应的好基友
u32 node_index;
}

vlib_process_t

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
//对应的PROSESS类型node索引号
vlib_node_runtime_t node_runtime;

//如下return_longjmp,resume_longjmp用于保存当前寄存器内容,
//可以参考C库的setjump,longjump原理
clib_longjmp_t return_longjmp;

//PROSESS类型node的业务逻辑运行时栈地址,在heap上
#define VLIB_PROCESS_STACK_MAGIC (0xdead7ead)
u32 stack[0] ALIGN_ON_MULTIPLE_PAGE_BOUNDARY_FOR_MPROTECT;
} vlib_process_t __attribute__ ((aligned (CLIB_CACHE_LINE_BYTES)));

调度逻辑


VPP支持多工作线程模型,这里不考虑多线程,以单线程模型来分析源码。
核心调度函数:

vlib_main_loop

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
static void vlib_main_loop (vlib_main_t * vm)
{
...

//dpdk收包时下面用不到。它们是用来模拟标准网卡NAPI机制的
if (!nm->polling_threshold_vector_length)
nm->polling_threshold_vector_length = 10;
if (!nm->interrupt_threshold_vector_length)
nm->interrupt_threshold_vector_length = 5;

nm->current_process_index = ~0;

/*
* 执行所有VLIB_NODE_TYPE_PROCESS类型node,利用setjump,longjump机制,
* 把node挂起来,等待之后唤醒。
* 可以理解为一种多任务模型。该类型node主要时用在作运行时配置相关。
* 之后会详细论述该类型node。
*/
uword i;
for (i = 0; i < vec_len (nm->processes); i++)
cpu_time_now =
dispatch_process (vm, nm->processes[i], /* frame */ 0, cpu_time_now);

while (1)
{
vlib_node_runtime_t *n;

//目前只有一个epoll相关的node,监听socket,辅助功能
vec_foreach (n, nm->nodes_by_type[VLIB_NODE_TYPE_PRE_INPUT])
cpu_time_now = dispatch_node (vm, n,
VLIB_NODE_TYPE_PRE_INPUT,
VLIB_NODE_STATE_POLLING,
/* frame */ 0,
cpu_time_now);

//收包node,假设使用dpdk的node
vec_foreach (n, nm->nodes_by_type[VLIB_NODE_TYPE_INPUT])
cpu_time_now = dispatch_node (vm, n,
VLIB_NODE_TYPE_INPUT,
VLIB_NODE_STATE_POLLING,
/* frame */ 0,
cpu_time_now);

//memclnt_node会用到,但是博主发现该node没有使用。无视之。
if (PREDICT_TRUE (vm->queue_signal_pending == 0))
vm->queue_signal_callback (vm);

//dpdk没有中断机制,这里不会执行
{
...
}

/*
* 处理超时事件,通过api发生的唤醒node事件。
* 只对VLIB_NODE_TYPE_PROCESS类型node有效。
* VLIB_NODE_TYPE_PROCESS类型node处理需要单独描述,
* 但不是开发人员关注的重点,只是配置相关。
*/
nm->data_from_advancing_timing_wheel
= timing_wheel_advance (&nm->timing_wheel, cpu_time_now,
nm->data_from_advancing_timing_wheel,
&nm->cpu_time_next_process_ready);

...

/*
* 真正开发人员关注的重点,数据包处理的核心逻辑都在VLIB_NODE_TYPE_INTERNAL类型node中。
* nm->pending_frames记录了上一个node转给下一个node信息,
* 最后可以找到传递给下一个node使用的数据包
*/
for (i = 0; i < _vec_len (nm->pending_frames); i++)
cpu_time_now = dispatch_pending_node (vm, nm->pending_frames + i,
cpu_time_now);
}
}
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
54
55
56
57
58
59
60
61
62
63
64
65
u64 dispatch_pending_node (vlib_main_t * vm,
vlib_pending_frame_t * p, u64 last_time_stamp)
{
vlib_node_main_t *nm = &vm->node_main;
vlib_frame_t *f;
vlib_next_frame_t *nf, nf_dummy;
vlib_node_runtime_t *n;
u32 restore_frame_index;

//之后将执行n的业务逻辑
n = vec_elt_at_index (nm->nodes_by_type[VLIB_NODE_TYPE_INTERNAL],
p->node_runtime_index);

//node n的对应frame,里面包含了node需要处理的数据包
f = vlib_get_frame (vm, p->frame_index);
/*
* 不是下一跳传过来的数据包,比如自己生成的包注入某个node,此时if判断为真。
* vlib_put_frame_to_node()完成注入逻辑。
*/
if (p->next_frame_index == VLIB_PENDING_FRAME_NO_NEXT_FRAME)
{
nf = &nf_dummy;
nf->flags = f->flags & VLIB_NODE_FLAG_TRACE;
nf->frame_index = ~p->frame_index;
}
else
//通过node下一跳传递来的数据包
nf = vec_elt_at_index (nm->next_frames, p->next_frame_index);

/*
* node传递数据包到下一个node是通过调用vlib_put_next_frame()接口:
* p->frame_index = nf->frame_index;
* p->node_runtime_index = nf->node_runtime_index;
* p->next_frame_index = nf - nm->next_frames;
*/
if (nf->frame_index == p->frame_index)
{
//有两个位置同时引用了frame,把nf中的清掉,以免干扰本线程对frame的使用。
nf->frame_index = ~0;
nf->flags &= ~VLIB_FRAME_IS_ALLOCATED;
if (!(n->flags & VLIB_NODE_FLAG_FRAME_NO_FREE_AFTER_DISPATCH))
restore_frame_index = p->frame_index;
}

...

//调度node业务逻辑
last_time_stamp = dispatch_node (vm, n,
VLIB_NODE_TYPE_INTERNAL,
VLIB_NODE_STATE_POLLING,
f, last_time_stamp);

f->flags &= ~VLIB_FRAME_PENDING;

//frame已经在业务逻辑中使用完了,重新保存到nf中
if (restore_frame_index != ~0)
{
//vlib_next_frame_change_ownership()可能会修改p->next_frame_index
nf = vec_elt_at_index (nm->next_frames, p->next_frame_index);
nf->frame_index = restore_frame_index;
nf->flags |= VLIB_FRAME_IS_ALLOCATED;
}

return last_time_stamp;
}
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
//对DPDK驱动,该函数基本没有什么复杂处理。
u64 dispatch_node (vlib_main_t * vm,
vlib_node_runtime_t * node,
vlib_node_type_t type,
vlib_node_state_t dispatch_state,
vlib_frame_t * frame, u64 last_time_stamp)
{
...

if (node->n_next_nodes > 0)
{
//node的业务逻辑肯定会用到nf
nf = vec_elt_at_index (nm->next_frames, node->next_frame_index);
CLIB_PREFETCH (nf, 4 * sizeof (nf[0]), WRITE);
}

...

//业务逻辑
n = node->function (vm, node, frame);

...

//跟dpdk没关系了
if ((DPDK == 0 && dispatch_state == VLIB_NODE_STATE_INTERRUPT)
|| (DPDK == 0 && dispatch_state == VLIB_NODE_STATE_POLLING
&& (node->flags
& VLIB_NODE_FLAG_SWITCH_FROM_INTERRUPT_TO_POLLING_MODE)))
{
...
}

return t;
}

有时间再详细分析,该函数背后机制比较复杂,但是跟vpp框架使用者关系不大。

1
2
3
4
5
6
static u64
dispatch_process (vlib_main_t * vm,
vlib_process_t * p, vlib_frame_t * f, u64 last_time_stamp)
{
...
}

注意:所有文章非特别说明皆为原创。为保证信息与源同步,转载时请务必注明文章出处!谢谢合作 :-)

原始链接:http://zhaozhanxu.com/2016/11/05/VPP/2016-11-05-VPP-Node-Framework/

许可协议: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。