vpp本身支持线程之间的数据包调度——handoff。可以用这种特性来分发数据包至指定线程。
这里就从nat44的handoff开始来分析一下。简化掉拥塞,trace等处理,只看核心逻辑。
参考vpp代码版本号:2005
[ckun@localhost vpp]$ git status
# On branch 2005
初始化
设定nat44接口
snat_feature_command_fn函数
1. enable nat44-in2out-worker-handoff/nat44-out2in-worker-handoff node
2. 初始化分发队列 vlib_frame_queue_main_init
vlib_frame_queue_main_init函数
1. new 一个vlib_frame_queue_main_t 结构, 追加到全局变量 vlib_thread_main的frame_queue_mains数组中。
这个 vlib_thread_main.frame_queue_mains在线程轮询时会去check,并get其中的数据包。
2. vlib_frame_queue_main_t结构内的初始化
typedef struct
{
u32 node_index; // 分发后线程转到对应node处理包
u32 frame_queue_nelts; // 队列中元素个数,默认64
u32 queue_hi_thresh; // 门限 64 - 线程数
vlib_frame_queue_t **vlib_frame_queues; // 线程数个frame_queues
vlib_frame_queue_per_thread_data_t *per_thread_data; //线程数个thread data
/* for frame queue tracing */
frame_queue_trace_t *frame_queue_traces;
frame_queue_nelt_counter_t *frame_queue_histogram;
} vlib_frame_queue_main_t;
vlib_frame_queues
申请线程数个队列,每个队列中申请64个元素
typedef struct
{
/* enqueue side */
CLIB_CACHE_LINE_ALIGN_MARK (cacheline0);
volatile u64 tail;
u64 enqueues;
u64 enqueue_ticks;
u64 enqueue_vectors;
u32 enqueue_full_events;
/* dequeue side */
CLIB_CACHE_LINE_ALIGN_MARK (cacheline1);
volatile u64 head;
u64 dequeues;
u64 dequeue_ticks;
u64 dequeue_vectors;
u64 trace;
u64 vector_threshold;
/* dequeue hint to enqueue side */
CLIB_CACHE_LINE_ALIGN_MARK (cacheline2);
volatile u64 head_hint;
/* read-only, constant, shared */
CLIB_CACHE_LINE_ALIGN_MARK (cacheline3);
vlib_frame_queue_elt_t *elts; // 64个元素
u32 nelts; // 64个
}
vlib_frame_queue_t;
per_thread_data
申请线程数个thread_data。
每个thread_data中,再申请线程数个handoff_queue_elt_by_thread_index
typedef struct
{
vlib_frame_queue_elt_t **handoff_queue_elt_by_thread_index; // 记录本线程转发至目的线程的队列
vlib_frame_queue_t **congested_handoff_queue_by_thread_index; //判断阻塞用的
} vlib_frame_queue_per_thread_data_t;
数据包分发至队列
1. 为数据包选择目的线程
这里按照数据包源地址哈希确定目的线程,确保某一用户的数据包一定落在同一个线程。
这里是in2out的例子,out2in根据端口决定线程,也能确保同一个用户一个线程。
static u32
snat_get_worker_in2out_cb (ip4_header_t * ip0, u32 rx_fib_index0,
u8 is_output)
{
snat_main_t *sm = &snat_main;
u32 next_worker_index = 0;
u32 hash;
next_worker_index = sm->first_worker_index;
hash = ip0->src_address.as_u32 + (ip0->src_address.as_u32 >> 8) +
(ip0->src_address.as_u32 >> 16) + (ip0->src_address.as_u32 >> 24);
if (PREDICT_TRUE (is_pow2 (_vec_len (sm->workers))))
next_worker_index += sm->workers[hash & (_vec_len (sm->workers) - 1)];
else
next_worker_index += sm->workers[hash % _vec_len (sm->workers)];
return next_worker_index;
}
2. 将数据包塞入分发队列 vlib_buffer_enqueue_to_thread
vlib_buffer_enqueue_to_thread (vlib_main_t * vm,
u32 frame_queue_index, // fqm的index,在vlib_thead_main.frame_queue_mains中
u32 * buffer_indices, // 数据包的index们
u16 * thread_indices, // 目的线程标识,一个数据包对应一个线程id
u32 n_packets, // 数据包数
int drop_on_congestion)
1. 获取队列元素
(每一个包处理)
vlib_frame_queue_elt_t* hf = fqm->vlib_frame_queues[worker_thread_index]->elts[x]
从对应线程队列取一个可用的元素(闲置slot)
2. 将数据包index放入 hf->buffer_index数组中。
N个包处理完,某个线程的hf->buffer_index中会有多个数据包索引。
同时
fqm->per_thread_data[current_thread_index]->handoff_queue_elt_by_thread_index[worker_thread_index] = hf;
3. 遍历handoff_queue_elt_by_thread_index,获得每个操作过的hf
hf置为可用,工作线程置为需检查分发队列
vlib_put_frame_queue_elt (hf); // hf->valid = 1
vlib_mains[i]->check_frame_queues = 1;
ptd->handoff_queue_elt_by_thread_index[i] = 0;
从队列取数据包
vlib_main_or_worker_loop 函数
函数循环体中,遍历所有fqm,尝试从fqm中取包
if (!is_main)
{
vlib_worker_thread_barrier_check ();
if (PREDICT_FALSE (vm->check_frame_queues +
frame_queue_check_counter))
{
u32 processed = 0;
if (vm->check_frame_queues)
{
frame_queue_check_counter = 100;
vm->check_frame_queues = 0;
}
vec_foreach (fqm, tm->frame_queue_mains) //遍历所有fqm
processed += vlib_frame_queue_dequeue (vm, fqm);
/* No handoff queue work found? */
if (processed)
frame_queue_check_counter = 100;
else
frame_queue_check_counter--;
}
if (PREDICT_FALSE (vec_len (vm->worker_thread_main_loop_callbacks)))
clib_call_callbacks (vm->worker_thread_main_loop_callbacks, vm);
}
vlib_frame_queue_dequeue
1. 获取当前线程的队列 fq = fqm->vlib_frame_queues[worker_thread_index]
2. fp->elts获取每一个有效的elt(etl->valid ==1)
3. 新建一个frame数据帧,隶属于fqm->node_index
4. 将elt元素中存有多个buffer_index, 传入到新建的frame数据帧里。
从而数据包顺利转入至目标线程和目标node。