目录
什么是RPC
1.1 RPC的基本概念
**RPC(Remote Procedure Call,远程过程调用)**是一种通信机制,允许一个程序调用另一个程序(通常在不同的地址空间或线程)中的函数,就像调用本地函数一样。
生活化理解:
传统方式(本地调用):
- 你在办公室(线程1)需要打印文件
- 直接走到打印机(同一线程)打印
- 简单直接,但只能在自己的办公室打印
RPC方式(远程调用):
- 你在办公室(worker线程)需要打印文件
- 但打印机在另一个办公室(主线程)
- 你发送打印请求(RPC消息)给打印机办公室
- 打印机办公室收到请求后打印
- 你不需要亲自过去,可以继续工作
1.2 VPP中的RPC
在VPP中,RPC不是"远程"调用,而是跨线程调用。具体来说:
- 调用者:Worker线程(处理数据包的线程)
- 被调用者:主线程(Thread 0,负责控制平面操作)
- 目的:Worker线程需要执行某些只能在主线程执行的操作(通常是线程不安全的操作)
关键点:
- 不是网络RPC:VPP的RPC是进程内的线程间通信
- 异步执行:Worker线程发送RPC请求后,不等待结果,继续处理数据包
- 批量处理:主线程批量处理多个RPC请求,提高效率
VPP为什么需要RPC
2.1 多线程架构的挑战
VPP采用多线程架构:
主线程(Thread 0):
- 负责控制平面操作
- 管理路由表、邻居表等全局数据结构
- 处理API请求、CLI命令
Worker线程(Thread 1, 2, 3...):
- 负责数据平面操作
- 处理数据包转发
- 多个worker线程并发处理数据包
问题:Worker线程在处理数据包时,可能需要更新全局数据结构(如IP邻居表),但这些操作是线程不安全的。
2.2 线程安全问题的解决方案
方案1:加锁
c
// Worker线程直接更新(需要加锁)
pthread_mutex_lock(&neighbor_table_lock);
ip_neighbor_add(...); // 更新邻居表
pthread_mutex_unlock(&neighbor_table_lock);
问题:
- 多个worker线程竞争锁,性能下降
- 锁竞争导致延迟增加
- 数据包处理被阻塞
方案2:RPC机制(VPP采用)
c
// Worker线程通过RPC请求主线程更新
vl_api_rpc_call_main_thread(ip_neighbor_learn, data, sizeof(data));
// Worker线程继续处理数据包,不等待
优势:
- Worker线程不阻塞,继续处理数据包
- 主线程批量处理RPC请求,提高效率
- 避免锁竞争,提高性能
2.3 RPC的使用场景
RPC主要用于以下场景:
- 更新全局数据结构:IP邻居表、路由表等
- 调用线程不安全的函数:某些函数只能在主线程执行
- 从非worker线程调用:如DPDK回调线程
VPP如何实现RPC
3.1 RPC实现架构
┌─────────────────┐
│ Worker线程 │
│ (Thread 1-N) │
└────────┬────────┘
│
│ 1. 调用 vl_api_rpc_call_main_thread()
│
▼
┌─────────────────┐
│ 创建RPC消息 │
│ 添加到队列 │
└────────┬────────┘
│
│ 2. pending_rpc_requests 队列
│
▼
┌─────────────────┐
│ 主线程 │
│ (Thread 0) │
└────────┬────────┘
│
│ 3. 批量处理RPC请求
│
▼
┌─────────────────┐
│ 执行函数 │
│ (barrier同步) │
└─────────────────┘
3.2 RPC核心数据结构
源码位置 :src/vlib/main.h:252-255
c
typedef struct vlib_main_t
{
// ... 其他字段 ...
/* RPC requests, main thread only */
uword *pending_rpc_requests; // 待处理的RPC请求队列
uword *processing_rpc_requests; // 正在处理的RPC请求队列
clib_spinlock_t pending_rpc_lock; // 保护pending_rpc_requests的锁
} vlib_main_t;
数据结构说明:
- pending_rpc_requests:Worker线程添加RPC请求的队列
- processing_rpc_requests:主线程处理RPC请求的队列
- pending_rpc_lock:保护pending_rpc_requests的锁(主线程需要加锁)
双队列设计的原因:
- 避免死锁:处理时交换队列,避免在处理时接收新请求
- 批量处理:主线程可以批量处理多个RPC请求
- 无锁添加:Worker线程添加请求时不需要等待
3.3 RPC消息结构
源码位置 :src/vlibmemory/memclnt_api.c:639-645
c
typedef struct vl_api_rpc_call_t
{
u16 _vl_msg_id; // 消息ID
u32 client_index; // 客户端索引
u32 context; // 上下文
uword function; // 函数指针(要调用的函数)
u8 need_barrier_sync; // 是否需要barrier同步
u8 send_reply; // 是否需要回复
u8 multicast; // 是否多播
u8 data[0]; // 数据(可变长度)
} vl_api_rpc_call_t;
字段说明:
- function:要调用的函数指针
- data:传递给函数的参数数据
- need_barrier_sync:是否需要barrier同步(通常为1)
- send_reply:是否需要回复(可选)
3.4 RPC调用流程
3.4.1 Worker线程发起RPC调用
源码位置 :src/vlibmemory/memclnt_api.c:660-665
作用和实现原理:
vl_api_rpc_call_main_thread是RPC调用的入口函数,Worker线程调用此函数将请求发送给主线程。
c
void
vl_api_rpc_call_main_thread (void *fp, u8 *data, u32 data_length)
{
// 调用内联函数,force_rpc=0(如果在主线程,直接调用)
vl_api_rpc_call_main_thread_inline (fp, data, data_length, 0);
}
内联实现 (src/vlibmemory/memclnt_api.c:616-653):
c
always_inline void
vl_api_rpc_call_main_thread_inline (void *fp, u8 *data, u32 data_length,
u8 force_rpc)
{
vl_api_rpc_call_t *mp;
vlib_main_t *vm_global = vlib_get_first_main (); // 主线程
vlib_main_t *vm = vlib_get_main (); // 当前线程
/* 步骤1:检查是否在主线程 */
/* 说明:如果已经在主线程,可以直接调用函数,不需要RPC */
if ((force_rpc == 0) && (vlib_get_thread_index () == 0))
{
void (*call_fp) (void *);
// 同步所有worker线程(确保一致性)
vlib_worker_thread_barrier_sync (vm);
// 直接调用函数
call_fp = fp;
call_fp (data);
// 释放同步
vlib_worker_thread_barrier_release (vm);
return;
}
/* 步骤2:在worker线程中,需要真正的RPC调用 */
// 步骤2.1:分配RPC消息
mp = vl_msg_api_alloc_as_if_client (sizeof (*mp) + data_length);
// 步骤2.2:填充RPC消息
clib_memset (mp, 0, sizeof (*mp));
clib_memcpy_fast (mp->data, data, data_length); // 复制数据
mp->_vl_msg_id = ntohs (VL_API_RPC_CALL);
mp->function = pointer_to_uword (fp); // 函数指针
mp->need_barrier_sync = 1; // 需要同步
// 步骤2.3:将RPC请求添加到主线程的待处理队列
// 说明:pending_rpc_requests是主线程的RPC请求队列
if (vm == vm_global)
clib_spinlock_lock_if_init (&vm_global->pending_rpc_lock);
vec_add1 (vm->pending_rpc_requests, (uword) mp);
if (vm == vm_global)
clib_spinlock_unlock_if_init (&vm_global->pending_rpc_lock);
}
流程说明:
- 检查线程:如果在主线程,直接调用函数(需要barrier同步)
- 创建消息:分配RPC消息,复制数据
- 添加到队列 :将消息添加到
pending_rpc_requests队列 - 返回:Worker线程立即返回,继续处理数据包
关键点:
- 主线程优化:如果在主线程,直接调用,避免不必要的RPC开销
- 数据复制:复制数据到RPC消息,避免数据被修改
- 无锁添加:Worker线程添加请求时,主线程不需要加锁(每个线程有自己的pending_rpc_requests)
3.4.2 主线程处理RPC请求
源码位置 :src/vlibmemory/memory_api.c:923-965
作用和实现原理:
vl_mem_api_handle_rpc是主线程处理RPC请求的函数,在主循环中被调用。
c
int
vl_mem_api_handle_rpc (vlib_main_t * vm, vlib_node_runtime_t * node)
{
api_main_t *am = vlibapi_get_main ();
int i;
uword *tmp, mp;
/*
* 步骤1:交换待处理队列和处理队列
* 说明:避免在处理时接收新的RPC请求,防止死锁
*/
clib_spinlock_lock_if_init (&vm->pending_rpc_lock);
tmp = vm->processing_rpc_requests;
vec_reset_length (tmp); // 清空处理队列
vm->processing_rpc_requests = vm->pending_rpc_requests; // 交换
vm->pending_rpc_requests = tmp; // 清空待处理队列
clib_spinlock_unlock_if_init (&vm->pending_rpc_lock);
/*
* 步骤2:批量处理RPC请求
* 说明:
* - RPC用于将函数调用反射到主线程(Thread 0)
* - 当底层代码不是线程安全时使用
* - 批量处理RPC时使用barrier同步,提高效率
* - 避免barrier同步保持时间过长
*/
if (PREDICT_TRUE (vec_len (vm->processing_rpc_requests)))
{
// 步骤2.1:同步所有worker线程
// 说明:确保在处理RPC时,所有worker线程都暂停
vl_msg_api_barrier_sync ();
// 步骤2.2:逐个处理RPC请求
for (i = 0; i < vec_len (vm->processing_rpc_requests); i++)
{
mp = vm->processing_rpc_requests[i];
// 调用RPC消息中指定的函数
vl_mem_api_handler_with_vm_node (am, am->vlib_rp, (void *) mp, vm,
node, 0);
}
// 步骤2.3:释放同步
vl_msg_api_barrier_release ();
}
return 0;
}
流程说明:
- 交换队列 :将
pending_rpc_requests和processing_rpc_requests交换 - Barrier同步:同步所有worker线程,确保它们暂停
- 批量处理:逐个处理RPC请求
- 释放同步:释放barrier,worker线程继续运行
关键点:
- 双队列设计:避免在处理时接收新请求,防止死锁
- 批量处理:一次处理多个RPC请求,提高效率
- Barrier同步:确保处理RPC时,worker线程不会修改共享数据
3.4.3 Barrier同步机制详解
什么是Barrier同步?
Barrier同步是一种线程同步机制,用于确保所有worker线程在某个点暂停,等待主线程完成某些操作后再继续。
生活化理解:
就像"学校集合":
场景1:没有Barrier(问题)
- 主线程(老师)要更新课程表(全局数据)
- Worker线程1(学生A)正在查询课程表
- Worker线程2(学生B)正在查询课程表
- Worker线程3(学生C)正在查询课程表
- 问题:老师更新课程表时,学生可能读到错误的数据(部分更新)
场景2:使用Barrier(解决方案)
- 主线程(老师)要更新课程表
- 主线程:设置barrier标志("集合!")
- Worker线程1:看到barrier标志,停止查询,等待
- Worker线程2:看到barrier标志,停止查询,等待
- Worker线程3:看到barrier标志,停止查询,等待
- 主线程:等待所有worker线程到达barrier
- 主线程:更新课程表(所有worker线程已暂停)
- 主线程:清除barrier标志("解散!")
- Worker线程:看到barrier清除,继续查询
Barrier同步的三个阶段:
- Sync(同步):主线程设置barrier标志,等待所有worker线程到达
- 执行操作:主线程执行需要同步的操作(如更新全局数据)
- Release(释放):主线程清除barrier标志,worker线程继续运行
关键问题:第一个到达barrier的worker是否需要等待很久?
答案:不需要,原因如下:
-
Worker线程频繁检查barrier
- Worker线程在主循环中每次循环都检查barrier (
src/vlib/main.c:1521) - 如果worker正在处理数据包,它会在当前batch处理完后立即检查
- Worker不会"等待很久",因为它在每次循环都会检查
- Worker线程在主循环中每次循环都检查barrier (
-
Barrier hold-down timer优化
- 如果worker很忙(vector rate > 10),主线程会等待一段时间再设置barrier
- 这给了worker时间完成当前的工作,避免在繁忙时强制暂停
-
Worker到达barrier的速度
- Worker在主循环中频繁检查barrier(每次循环都检查)
- 如果worker正在处理数据包,它会在当前batch处理完后立即检查
- 所以worker到达barrier的速度取决于它们当前的负载,但通常很快(微秒级)
详细机制:
Worker线程的barrier检查 (src/vlib/main.c:1520-1521):
c
while (1) {
// ... 处理数据包 ...
if (!is_main)
vlib_worker_thread_barrier_check(); // ← 每次循环都检查
// ... 继续处理 ...
}
Worker线程的barrier检查实现 (src/vlib/threads.h:352-389):
c
static inline void
vlib_worker_thread_barrier_check (void)
{
if (PREDICT_FALSE (*vlib_worker_threads->wait_at_barrier))
{
// Worker到达barrier,增加计数
clib_atomic_fetch_add (vlib_worker_threads->workers_at_barrier, 1);
// 自旋等待barrier释放
while (*vlib_worker_threads->wait_at_barrier)
; // ← 自旋等待,不会阻塞
// Barrier释放,减少计数
clib_atomic_fetch_add (vlib_worker_threads->workers_at_barrier, -1);
}
}
主线程的barrier同步 (src/vlib/threads.c:1328-1421):
c
void
vlib_worker_thread_barrier_sync_int (vlib_main_t * vm, const char *func_name)
{
// ... 检查worker是否繁忙 ...
// 如果worker很忙(vector rate > 10),等待一段时间再设置barrier
if (max_vector_rate > 10.0)
{
while (1)
{
now = vlib_time_now (vm);
// Barrier hold-down timer过期?
if (now >= vm->barrier_no_close_before)
break;
// 等待worker完成当前工作
}
}
// 设置barrier标志
*vlib_worker_threads->wait_at_barrier = 1;
// 等待所有worker到达barrier
while (*vlib_worker_threads->workers_at_barrier != count)
{
// 等待所有worker到达(通常很快,微秒级)
}
}
实际场景分析:
假设有30个worker线程:
-
场景1:Worker都在处理数据包
- Worker线程正在处理数据包batch(例如256个数据包)
- 主线程需要设置barrier
- Barrier hold-down timer:如果worker很忙,主线程会等待一段时间(最多1ms)
- Worker处理完当前batch后,立即检查barrier并到达
- 第一个到达的worker等待时间:取决于最慢的worker,通常< 1ms
-
场景2:Worker空闲或负载很低
- Worker线程在主循环中快速循环
- 主线程设置barrier
- Worker在下次循环检查时立即到达barrier
- 第一个到达的worker等待时间:< 100微秒(取决于最慢的worker)
-
场景3:Worker负载不均匀
- 有些worker很忙,有些worker空闲
- 空闲的worker会立即到达barrier
- 繁忙的worker会在处理完当前batch后到达
- 第一个到达的worker等待时间:取决于最慢的worker,但通常< 1ms
性能优化:
-
Barrier hold-down timer:
- 如果worker很忙,主线程会等待一段时间再设置barrier
- 避免在worker繁忙时强制暂停,减少丢包风险
-
频繁检查:
- Worker在主循环中每次循环都检查barrier
- 确保worker能快速响应barrier请求
-
自旋等待:
- Worker在barrier处自旋等待,不会阻塞
- 一旦barrier释放,worker立即继续
总结:
- 第一个到达barrier的worker不需要等待很久
- Worker在主循环中频繁检查barrier(每次循环都检查)
- 如果worker很忙,主线程会等待一段时间再设置barrier(hold-down timer)
- Worker到达barrier的速度取决于它们当前的负载,但通常很快(微秒级)
- 即使有30个worker,第一个到达的worker等待时间通常< 1ms
Barrier同步的实现
源码位置 :src/vlib/threads.c:1328-1421
阶段1:Barrier Sync(同步)
c
void
vlib_worker_thread_barrier_sync_int (vlib_main_t * vm, const char *func_name)
{
f64 deadline;
f64 now;
u32 count;
int i;
if (vlib_get_n_threads () < 2)
return; // 单线程,不需要barrier
ASSERT (vlib_get_thread_index () == 0); // 必须在主线程调用
count = vlib_get_n_threads () - 1; // worker线程数量
// 步骤1:检查worker线程是否忙碌
// 说明:如果worker线程很忙(向量速率>10),需要等待barrier hold-down timer
max_vector_rate = 0.0;
for (i = 1; i < vlib_get_n_threads (); i++)
{
vlib_main_t *ovm = vlib_get_main_by_index (i);
max_vector_rate = clib_max (max_vector_rate,
(f64) vlib_last_vectors_per_main_loop (ovm));
}
// 步骤2:如果worker线程忙碌,等待barrier hold-down timer
// 说明:避免频繁barrier导致数据包丢失
if (max_vector_rate > 10.0)
{
while (1)
{
now = vlib_time_now (vm);
// Barrier hold-down timer过期?
if (now >= vm->barrier_no_close_before)
break;
// 避免等待时间过长(时钟变化)
if ((vm->barrier_no_close_before - now) > (2.0 * BARRIER_MINIMUM_OPEN_LIMIT))
{
clib_warning ("clock change: would have waited for %.4f seconds",
(vm->barrier_no_close_before - now));
break;
}
}
}
deadline = now + BARRIER_SYNC_TIMEOUT;
// 步骤3:设置barrier标志,通知worker线程暂停
*vlib_worker_threads->wait_at_barrier = 1;
// 步骤4:等待所有worker线程到达barrier
while (*vlib_worker_threads->workers_at_barrier != count)
{
if ((now = vlib_time_now (vm)) > deadline)
{
fformat (stderr, "%s: worker thread deadlock\n", __FUNCTION__);
os_panic (); // 超时,死锁
}
}
}
关键步骤说明:
- 检查worker线程状态:如果worker线程很忙,等待barrier hold-down timer,避免频繁barrier
- 设置barrier标志 :
wait_at_barrier = 1,通知worker线程暂停 - 等待所有worker线程 :主线程等待
workers_at_barrier == count(所有worker线程到达)
Worker线程的响应 (src/vlib/threads.h:351-389):
c
static inline void
vlib_worker_thread_barrier_check (void)
{
// 检查barrier标志
if (PREDICT_FALSE (*vlib_worker_threads->wait_at_barrier))
{
vlib_main_t *vm = vlib_get_main ();
u32 thread_index = vm->thread_index;
// 步骤1:记录到达barrier
clib_atomic_fetch_add (vlib_worker_threads->workers_at_barrier, 1);
// 步骤2:等待barrier清除(自旋等待)
while (*vlib_worker_threads->wait_at_barrier)
;
// 步骤3:barrier清除后,更新时钟偏移
// 说明:worker线程使用主线程的时间作为参考
{
f64 now;
vm->time_offset = 0.0;
now = vlib_time_now (vm);
vm->time_offset = vgm->vlib_mains[0]->time_last_barrier_release - now;
vm->time_last_barrier_release = vlib_time_now (vm);
}
// 步骤4:记录离开barrier
clib_atomic_fetch_add (vlib_worker_threads->workers_at_barrier, -1);
}
}
Worker线程的流程:
- 检查barrier标志 :在主循环中定期检查
wait_at_barrier - 到达barrier :原子增加
workers_at_barrier计数器 - 等待barrier清除 :自旋等待
wait_at_barrier变为0 - 更新时钟:barrier清除后,同步主线程的时间
- 离开barrier :原子减少
workers_at_barrier计数器
阶段2:执行操作
主线程在barrier同步后,执行需要同步的操作(如更新全局数据):
c
// 在barrier同步后
vl_msg_api_barrier_sync (); // 同步所有worker线程
// 执行RPC函数(更新全局数据)
for (i = 0; i < vec_len (vm->processing_rpc_requests); i++)
{
mp = vm->processing_rpc_requests[i];
vl_mem_api_handler_with_vm_node (am, am->vlib_rp, (void *) mp, vm, node, 0);
}
vl_msg_api_barrier_release (); // 释放barrier
阶段3:Barrier Release(释放)
源码位置 :src/vlib/threads.c:1424-1491
c
void
vlib_worker_thread_barrier_release (vlib_main_t * vm)
{
f64 deadline;
f64 now;
if (vlib_get_n_threads () < 2)
return; // 单线程,不需要barrier
ASSERT (vlib_get_thread_index () == 0); // 必须在主线程调用
now = vlib_time_now (vm);
// 步骤1:记录barrier释放时间
// 说明:worker线程使用这个时间同步时钟
vm->time_last_barrier_release = vlib_time_now (vm);
CLIB_MEMORY_STORE_BARRIER (); // 内存屏障,确保时间写入对所有线程可见
// 步骤2:清除barrier标志,通知worker线程继续
*vlib_worker_threads->wait_at_barrier = 0;
// 步骤3:等待所有worker线程离开barrier
deadline = now + BARRIER_SYNC_TIMEOUT;
while (*vlib_worker_threads->workers_at_barrier > 0)
{
if ((now = vlib_time_now (vm)) > deadline)
{
fformat (stderr, "%s: worker thread deadlock\n", __FUNCTION__);
os_panic (); // 超时,死锁
}
}
}
关键步骤说明:
- 记录释放时间:记录barrier释放的时间,worker线程用于同步时钟
- 清除barrier标志 :
wait_at_barrier = 0,通知worker线程继续 - 等待worker线程离开 :等待
workers_at_barrier == 0(所有worker线程离开)
Barrier同步的完整时序图:
时间轴:
T0: 主线程调用 barrier_sync()
T1: 主线程设置 wait_at_barrier = 1
T2: Worker线程1检查到barrier,到达(workers_at_barrier = 1)
T3: Worker线程2检查到barrier,到达(workers_at_barrier = 2)
T4: Worker线程3检查到barrier,到达(workers_at_barrier = 3)
T5: 主线程等待完成(workers_at_barrier == 3)
T6: 主线程执行操作(更新全局数据)
T7: 主线程调用 barrier_release()
T8: 主线程设置 wait_at_barrier = 0
T9: Worker线程1看到barrier清除,离开(workers_at_barrier = 2)
T10: Worker线程2看到barrier清除,离开(workers_at_barrier = 1)
T11: Worker线程3看到barrier清除,离开(workers_at_barrier = 0)
T12: 主线程等待完成(workers_at_barrier == 0)
T13: 所有线程继续运行
Barrier同步的性能优化
-
Barrier Hold-down Timer:
- 如果worker线程很忙(向量速率>10),等待一段时间再barrier
- 避免频繁barrier导致数据包丢失
- 最小barrier开放时间:
BARRIER_MINIMUM_OPEN_LIMIT
-
批量处理:
- 一次barrier处理多个RPC请求
- 减少barrier次数,提高效率
-
递归支持:
- Barrier支持递归调用(嵌套barrier)
- 使用
recursion_level跟踪嵌套深度
Barrier同步的实际例子
例子1:RPC处理中的Barrier
c
// 主线程处理RPC请求
vl_mem_api_handle_rpc (vm, node)
{
// 步骤1:交换队列
swap_queues ();
// 步骤2:Barrier同步(所有worker线程暂停)
vl_msg_api_barrier_sync ();
// 步骤3:批量处理RPC请求(更新IP邻居表)
for (i = 0; i < vec_len (vm->processing_rpc_requests); i++)
{
ip_neighbor_learn (...); // 更新全局IP邻居表
}
// 步骤4:Barrier释放(worker线程继续)
vl_msg_api_barrier_release ();
}
为什么需要Barrier?
- 线程安全:更新IP邻居表时,worker线程不能同时读取
- 数据一致性:确保worker线程看到的是完整的更新,不是部分更新
- 避免竞争:防止worker线程在更新过程中读取到不一致的数据
例子2:FIB更新中的Barrier
c
// 主线程更新FIB表
fib_table_entry_update (...)
{
// 步骤1:Barrier同步
vlib_worker_thread_barrier_sync (vm);
// 步骤2:更新FIB表(修改mtrie结构)
ip4_fib_table_entry_insert (...);
// 步骤3:Barrier释放
vlib_worker_thread_barrier_release (vm);
}
为什么需要Barrier?
- 数据结构一致性:FIB表(mtrie)的更新需要原子性
- 避免读取到中间状态:worker线程在查找FIB时,不能看到部分更新的mtrie
- RCU机制配合:Barrier确保旧数据不再被使用后,才能删除
Barrier同步的注意事项
- 只能在主线程调用 :
barrier_sync和barrier_release必须在主线程(Thread 0)调用 - 成对使用 :每次
barrier_sync必须对应一次barrier_release - 避免长时间持有:Barrier持有时间应该尽可能短,避免影响数据包处理
- 超时检测:如果worker线程没有及时到达barrier,会触发死锁检测
3.4.4 RPC消息处理函数
源码位置 :src/vlibmemory/memclnt_api.c:567-608
作用和实现原理:
vl_api_rpc_call_t_handler是RPC消息的处理函数,由vl_mem_api_handler_with_vm_node调用。
c
static void
vl_api_rpc_call_t_handler (vl_api_rpc_call_t *mp)
{
vl_api_rpc_call_reply_t *rmp;
int (*fp) (void *);
i32 rv = 0;
vlib_main_t *vm = vlib_get_main ();
// 步骤1:检查函数指针
if (mp->function == 0)
{
rv = -1;
clib_warning ("rpc NULL function pointer");
}
else
{
// 步骤2:如果需要barrier同步,先同步
if (mp->need_barrier_sync)
vlib_worker_thread_barrier_sync (vm);
// 步骤3:获取函数指针并调用
fp = uword_to_pointer (mp->function, int (*) (void *));
rv = fp (mp->data);
// 步骤4:如果需要barrier同步,释放同步
if (mp->need_barrier_sync)
vlib_worker_thread_barrier_release (vm);
}
// 步骤5:如果需要回复,发送回复消息
if (mp->send_reply)
{
svm_queue_t *q = vl_api_client_index_to_input_queue (mp->client_index);
if (q)
{
rmp = vl_msg_api_alloc_as_if_client (sizeof (*rmp));
rmp->_vl_msg_id = ntohs (VL_API_RPC_CALL_REPLY);
rmp->context = mp->context;
rmp->retval = rv;
vl_msg_api_send_shmem (q, (u8 *) &rmp);
}
}
}
流程说明:
- 检查函数指针:确保函数指针有效
- Barrier同步:如果需要,同步所有worker线程
- 调用函数:执行RPC消息中指定的函数
- 释放同步:释放barrier
- 发送回复:如果需要,发送回复消息
关键点:
- 函数指针转换:将uword转换为函数指针
- Barrier同步:确保函数执行时,worker线程暂停
- 可选回复:RPC可以是异步的(不需要回复)
3.4.4 主循环中的RPC处理
源码位置 :src/vlib/main.c:1514-1518
作用和实现原理:
主线程在主循环中检查并处理RPC请求。
c
while (1)
{
vlib_node_runtime_t *n;
// 检查是否有待处理的RPC请求
if (PREDICT_FALSE (_vec_len (vm->pending_rpc_requests) > 0))
{
if (!is_main)
vlib_worker_flush_pending_rpc_requests (vm);
else
vl_mem_api_handle_rpc (vm, node); // 主线程处理RPC
}
// ... 其他处理 ...
}
流程说明:
- 定期检查:主循环每次迭代都检查是否有RPC请求
- 主线程处理 :如果是主线程,调用
vl_mem_api_handle_rpc处理 - Worker线程刷新:如果是worker线程,刷新RPC请求到主线程
RPC在VPP中的应用场景
4.1 IP邻居学习(ARP/ND)
场景:Worker线程收到ARP响应,需要更新IP邻居表
源码位置 :src/vnet/ip-neighbor/ip_neighbor_dp.c:28-31
实现:
c
void
ip_neighbor_learn_dp (const ip_neighbor_learn_t * l)
{
// Worker线程通过RPC调用主线程的学习函数
vl_api_rpc_call_main_thread (ip_neighbor_learn, (u8 *) l, sizeof (*l));
}
主线程处理函数 (src/vnet/ip-neighbor/ip_neighbor.c:784):
c
void
ip_neighbor_learn (const ip_neighbor_learn_t * l)
{
// 在主线程中执行,更新IP邻居表
ip_neighbor_add (&l->ip, &l->mac, l->sw_if_index,
IP_NEIGHBOR_FLAG_DYNAMIC, NULL);
}
流程:
1. Worker线程收到ARP响应
↓
2. 调用 ip_neighbor_learn_dp()
↓
3. 通过RPC发送学习请求给主线程
↓
4. Worker线程继续处理数据包(不等待)
↓
5. 主线程处理RPC请求,更新IP邻居表
为什么需要RPC:
- IP邻居表是全局共享数据结构
- 多个worker线程可能同时需要更新
- 直接更新会导致线程冲突
- 通过RPC统一在主线程更新,避免冲突
是否需要Barrier? :需要,但原因不是IP邻居表本身
关键理解:
-
Worker线程在数据包转发时不直接读取IP邻居表
- 数据包转发时,worker线程使用Adjacency (
adj_get),不是IP邻居表 - Adjacency中已经包含了MAC地址(在rewrite字符串中)
- Worker线程读取Adjacency是只读操作,不修改Adjacency
- 源码证据 :
ip_neighbor_db_find是static函数,只在ip_neighbor.c内部使用- Worker线程在数据包转发时(
ip4_rewrite_inline,ip6_rewrite_inline)只调用adj_get,不调用ip_neighbor_db_find或ip_neighbor_get ip_neighbor_add有ASSERT (0 == vlib_get_thread_index ()),确保只在主线程执行
- 数据包转发时,worker线程使用Adjacency (
-
Adjacency的rewrite字符串来自IP邻居表
-
控制平面 :当IP邻居表更新时(
ip_neighbor_add),会调用ip_neighbor_mk_complete -
读取IP邻居表 :
ip_neighbor_mk_complete从IP邻居表读取MAC地址(ipn->ipn_mac.bytes) -
构建rewrite字符串 :调用
ethernet_build_rewrite构建rewrite字符串 -
更新Adjacency :调用
adj_nbr_update_rewrite更新Adjacency的rewrite字符串 -
源码证据 (
src/vnet/ip-neighbor/ip_neighbor.c:413-421):cstatic void ip_neighbor_mk_complete (adj_index_t ai, ip_neighbor_t * ipn) { // 从IP邻居表读取MAC地址,构建rewrite字符串 adj_nbr_update_rewrite (ai, ADJ_NBR_REWRITE_FLAG_COMPLETE, ethernet_build_rewrite (vnet_get_main (), ipn->ipn_key->ipnk_sw_if_index, adj_get_link_type (ai), ipn->ipn_mac.bytes)); // ← 从IP邻居表读取MAC }
-
-
IP邻居表更新本身不需要barrier,但更新Adjacency需要barrier
ip_neighbor_add更新IP邻居表(哈希表、链表)→ 不需要barrier(因为worker线程不直接读取)- 但是
ip_neighbor_add会调用adj_nbr_walk_nh→ip_neighbor_mk_complete→adj_nbr_update_rewrite→ 需要barrier(因为worker线程会读取Adjacency的rewrite字符串) - 原因:虽然worker线程不直接读取IP邻居表,但Adjacency的rewrite字符串是从IP邻居表预先填充的,当IP邻居表更新时,需要更新Adjacency,这时需要barrier
-
Adjacency更新需要barrier
ip_neighbor_add会调用adj_nbr_walk_nh→ 更新Adjacency → 这里需要barrier- 原因:Worker线程在转发数据包时会读取Adjacency的rewrite字符串
-
Barrier的真正原因:更新Adjacency的rewrite字符串
源码证据 (src/vnet/adj/adj_nbr.c:484-578):
c
void
adj_nbr_update_rewrite_internal (ip_adjacency_t *adj, ...)
{
/*
* Updating a rewrite string is not atomic;
* - the rewrite string is too long to write in one instruction
* - when swapping from incomplete to complete, we also need to update
* the VLIB graph next-index of the adj.
* ideally we would only want to suspend forwarding via this adj whilst we
* do this, but we do not have that level of granularity - it's suspend all
* worker threads or nothing.
*/
// ... 准备更新 ...
// Barrier同步所有worker线程
vlib_worker_thread_barrier_sync(vm);
// 更新Adjacency的字段(非原子操作)
adj->lookup_next_index = adj_next_index;
adj->ia_node_index = this_node;
// 更新rewrite字符串(太长,无法原子更新)
vnet_rewrite_set_data_internal(&adj->rewrite_header,
sizeof(adj->rewrite_data),
rewrite,
vec_len(rewrite));
// Barrier释放
vlib_worker_thread_barrier_release(vm);
}
为什么需要Barrier?
- Rewrite字符串太长:无法在一个指令中完成写入,需要多次写入
- Worker线程在读取 :数据包转发时,worker线程会读取
adj->rewrite_header来获取MAC地址 - 避免读取到中间状态:如果worker线程在更新过程中读取,可能读到部分更新的rewrite字符串,导致错误转发
完整流程:
时间轴:
T0: Worker线程1收到ARP响应,调用 ip_neighbor_learn_dp()
T1: Worker线程2收到ARP响应,调用 ip_neighbor_learn_dp()
T2: Worker线程3收到ARP响应,调用 ip_neighbor_learn_dp()
↓
RPC请求都添加到 pending_rpc_requests 队列
↓
T3: 主线程调用 vl_mem_api_handle_rpc()
T4: 主线程交换队列(pending → processing)
T5: 主线程调用 vl_msg_api_barrier_sync() ← Barrier同步
↓
Worker线程1、2、3都暂停(到达barrier)
↓
T6: 主线程处理第1个RPC请求 → ip_neighbor_add()
→ adj_nbr_walk_nh() → adj_nbr_update_rewrite()
→ vlib_worker_thread_barrier_sync() ← 嵌套barrier(支持递归)
→ 更新Adjacency的rewrite字符串
→ vlib_worker_thread_barrier_release()
T7: 主线程处理第2个RPC请求 → 同上
T8: 主线程处理第3个RPC请求 → 同上
↓
(所有RPC请求都在barrier保护下处理)
↓
T9: 主线程调用 vl_msg_api_barrier_release() ← Barrier释放
↓
Worker线程1、2、3继续运行
关键点:
- IP邻居表本身可能不需要barrier(因为worker线程不直接读取)
- 但更新Adjacency需要barrier(因为worker线程在转发数据包时会读取)
- RPC批量处理使用barrier是为了保护Adjacency更新,而不是IP邻居表本身
- 嵌套barrier :RPC批量处理有barrier,
adj_nbr_update_rewrite也有barrier,VPP支持递归barrier
为什么这样设计?
- 批量处理效率高:一次barrier处理多个RPC请求,而不是每个RPC请求都单独barrier
- 避免频繁barrier:减少barrier次数,提高性能
- 代码职责分离:RPC批量处理统一管理barrier,函数内部也可以有自己的barrier(支持递归)
关于丢包的担忧:
- Barrier时间很短:只暂停worker线程更新Adjacency的时间(微秒级)
- Barrier hold-down timer:如果worker线程很忙,会等待一段时间再barrier,避免频繁barrier
- 批量处理:一次barrier处理多个RPC请求,减少barrier次数
- 实际影响:ARP学习是低频操作,barrier对数据包处理的影响很小
为什么rewrite字符串不使用RCU机制?
这是一个很好的问题。FIB中的mtrie使用了RCU机制(通过原子指针切换),但rewrite字符串没有使用RCU,原因如下:
1. FIB中的RCU机制体现
源码位置 :src/vnet/ip/ip4_mtrie.c
c
// mtrie更新时使用原子操作切换指针
clib_atomic_store_rel_n (&old_ply->leaves[i], new_leaf);
RCU机制原理:
- mtrie的leaf是指针 (
ip4_mtrie_leaf_t,通常是8字节) - 原子更新 :使用
clib_atomic_store_rel_n原子地切换指针 - Worker线程读取 :使用
clib_atomic_load_acq原子地读取指针 - 优势:只需要一个原子操作(8字节指针),不需要barrier
2. Rewrite字符串为什么不能使用RCU
源码证据:
-
Rewrite字符串大小 (
src/vnet/buffer.h:418):c#define VNET_REWRITE_TOTAL_BYTES 128 -
Rewrite字符串是内嵌的,不是指针 (
src/vnet/adj/adj.h:305):ctypedef struct ip_adjacency_t_ { // ... 其他字段 ... /** Rewrite in second and third cache lines */ VNET_DECLARE_REWRITE; // ← 内嵌在结构体中,不是指针 } ip_adjacency_t; -
VNET_DECLARE_REWRITE的定义 (
src/vnet/adj/rewrite.h:119-126):c#define VNET_DECLARE_REWRITE \ struct \ { \ vnet_rewrite_header_t rewrite_header; \ u8 rewrite_data[(VNET_REWRITE_TOTAL_BYTES) - \ sizeof (vnet_rewrite_header_t)]; \ }
为什么不能使用RCU:
-
大小问题:
- Rewrite字符串是128字节
- 无法在一个原子操作中完成更新(现代CPU通常只支持8字节的原子操作)
- 需要多次写入,无法保证原子性
-
结构设计问题:
- Rewrite字符串是内嵌在Adjacency结构体中,不是指针
- 如果使用RCU,需要:
- 分配新的rewrite字符串(128字节)
- 复制数据
- 切换指针指向新的rewrite字符串
- 但rewrite字符串不是指针,无法切换
-
性能考虑:
- 如果改为指针,每次读取rewrite字符串都需要间接访问(增加cache miss)
- 内嵌设计可以保证Adjacency和rewrite在同一个cache line,性能更好
对比:FIB的mtrie vs Rewrite字符串
| 特性 | FIB的mtrie | Rewrite字符串 |
|---|---|---|
| 大小 | 8字节(指针) | 128字节(数据) |
| 存储方式 | 指针 | 内嵌在结构体中 |
| 更新方式 | 原子指针切换 | 多次写入 |
| 是否使用RCU | ✅ 是 | ❌ 否 |
| 是否需要Barrier | ❌ 否 | ✅ 是 |
总结:
- FIB的mtrie使用RCU:因为leaf是指针(8字节),可以原子切换
- Rewrite字符串不使用RCU:因为它是128字节的内嵌数据,无法原子更新,且不是指针
- Barrier是必要的:确保worker线程在更新rewrite字符串时不会读取到中间状态
4.2 WireGuard握手发送
场景:Worker线程需要发送WireGuard握手消息
源码位置 :src/plugins/wireguard/wireguard_send.c:242
实现:
c
static void
wg_send_handshake_dp (wg_peer_t * peer, vlib_buffer_t * b0)
{
wg_send_handshake_args_t a = {
.peer = peer,
.b0 = b0,
};
bool handshake =
__atomic_load_n (&peer->handshake_is_sent, __ATOMIC_ACQUIRE);
if (handshake == false)
{
handshake = true;
__atomic_store_n (&peer->handshake_is_sent, handshake, __ATOMIC_RELEASE);
// Worker线程通过RPC调用主线程发送握手
vl_api_rpc_call_main_thread (wg_send_handshake_thread_fn, (u8 *) &a,
sizeof (a));
}
}
为什么需要RPC:
- WireGuard握手需要访问主线程管理的状态
- 发送操作可能涉及线程不安全的操作
- 通过RPC确保在主线程执行
是否需要Barrier? :不需要
- 原因 :
wg_send_handshake_thread_fn只是发送数据包,不更新全局共享数据结构 - 说明:函数只是创建和发送握手包,不修改全局状态,所以不需要barrier
4.3 BFD事件通知
场景:Worker线程检测到BFD状态变化,需要通知监听者
源码位置 :src/vnet/bfd/bfd_main.c:622-630
实现:
c
static void
bfd_event_rpc (u32 bs_idx)
{
const u32 data_size = sizeof (bfd_rpc_event_t);
u8 data[data_size];
bfd_rpc_event_t *event = (bfd_rpc_event_t *) data;
event->bs_idx = bs_idx;
// Worker线程通过RPC通知主线程
vl_api_rpc_call_main_thread (bfd_rpc_event_cb, data, data_size);
}
为什么需要RPC:
- BFD事件通知需要访问主线程管理的监听者列表
- 通知操作可能涉及线程不安全的操作
- 通过RPC确保在主线程执行
是否需要Barrier? :不需要
- 原因 :
bfd_rpc_event_cb只是读取session数据并发送API事件,不更新全局共享数据结构 - 说明 :函数使用锁保护读取session数据(
bfd_lock),然后发送事件通知,不修改全局状态,所以不需要barrier
4.4 IGMP查询处理
场景:Worker线程收到IGMP查询,需要处理
源码位置 :src/plugins/igmp/igmp_input.c:305
实现:
c
// Worker线程收到IGMP查询
if (igmp_is_query (h))
{
// 通过RPC调用主线程处理查询
vl_api_rpc_call_main_thread (igmp_handle_query, (u8 *) &args,
sizeof (args));
}
为什么需要RPC:
- IGMP查询处理需要更新组播组状态
- 组播组状态是全局共享数据结构
- 通过RPC统一在主线程更新
是否需要Barrier? :不需要(或数据结构本身有保护)
- 原因 :
igmp_handle_query更新IGMP组状态、定时器等,但代码中没有barrier - 说明:这些数据结构可能有锁保护,或者更新操作本身是线程安全的(在主线程执行),所以不需要barrier
4.5 DHCP客户端报告
场景:Worker线程需要报告DHCP客户端状态
源码位置 :src/plugins/dhcp/client.c:406
实现:
c
// Worker线程报告DHCP客户端状态
vl_api_rpc_call_main_thread (dhcp_client_proc_callback, (u8 *) &args,
sizeof (args));
为什么需要RPC:
- DHCP客户端状态管理在主线程
- 状态更新操作是线程不安全的
- 通过RPC确保在主线程执行
是否需要Barrier? :部分需要
dhcp_client_proc_callback:不需要barrier- 原因:只是发送事件给process节点,不更新全局数据结构
dhcp_client_addr_callback:不需要barrier (或操作本身是线程安全的)- 原因:更新接口地址、路由等,但代码中没有barrier
- 说明:这些操作在主线程执行,且可能本身是线程安全的,所以不需要barrier
4.6 RPC中Barrier使用总结
判断标准:RPC处理函数是否需要barrier,取决于它是否更新全局共享数据结构
| RPC场景 | 是否需要Barrier | 原因 |
|---|---|---|
| IP邻居学习 | ✅ 需要 | 更新全局IP邻居表(哈希表、链表),通过RPC批量处理机制统一使用barrier |
| WireGuard握手发送 | ❌ 不需要 | 只是发送数据包,不更新全局共享数据结构 |
| BFD事件通知 | ❌ 不需要 | 只是读取数据并发送事件,不更新全局共享数据结构 |
| IGMP查询处理 | ❌ 不需要 | 更新组播组状态,但数据结构可能有锁保护或操作本身是线程安全的 |
| DHCP客户端报告(proc_callback) | ❌ 不需要 | 只是发送事件,不更新全局数据结构 |
| DHCP客户端报告(addr_callback) | ❌ 不需要 | 更新接口地址、路由,但操作在主线程执行且可能是线程安全的 |
关键点:
-
RPC批量处理机制 :
vl_mem_api_handle_rpc会统一使用barrier批量处理RPC请求- 如果RPC请求通过批量处理机制,会自动获得barrier保护
- 如果RPC请求直接调用(不在批量处理队列中),需要函数内部自己使用barrier
-
更新全局数据结构:如果RPC处理函数更新全局共享数据结构,通常需要barrier
- 例如:更新哈希表、链表、pool等
-
只读操作:如果RPC处理函数只是读取数据或发送事件,通常不需要barrier
- 例如:读取session数据、发送API事件
-
数据结构保护:如果数据结构本身有锁保护或操作是线程安全的,可能不需要barrier
- 例如:使用锁保护的数据结构、原子操作等
4.7 强制RPC调用
场景:从非worker线程(如DPDK回调线程)调用
源码位置 :src/vlibmemory/memclnt_api.c:671-676
实现:
c
/*
* 总是通过shmem进行RPC调用
* 适用于非worker线程,如DPDK回调线程
*/
void
vl_api_force_rpc_call_main_thread (void *fp, u8 *data, u32 data_length)
{
vl_api_rpc_call_main_thread_inline (fp, data, data_length, /*force_rpc */ 1);
}
为什么需要强制RPC:
- 非worker线程不在VPP的线程管理系统中
- 不能直接调用主线程函数
- 必须通过RPC机制
使用场景:
- DPDK回调线程
- 外部库的回调函数
- 信号处理函数
总结
5.1 RPC机制的核心价值
- 解决线程安全问题:将线程不安全的操作集中到主线程执行
- 提高性能:Worker线程不阻塞,继续处理数据包
- 批量处理:主线程批量处理RPC请求,提高效率
- 避免锁竞争:不需要在worker线程加锁,避免性能下降
5.2 RPC机制的设计特点
- 双队列设计:pending和processing队列,避免死锁
- Barrier同步:确保处理RPC时,worker线程暂停
- 批量处理:一次处理多个RPC请求,提高效率
- 主线程优化:如果在主线程,直接调用,避免RPC开销
5.3 RPC机制的使用原则
- 低频操作:RPC有延迟,适合低频操作(如ARP学习)
- 线程不安全:需要更新全局共享数据结构时使用
- 控制平面操作:需要访问主线程管理的状态时使用
- 非worker线程:从非worker线程调用时使用强制RPC
5.4 RPC机制的性能考虑
优势:
- Worker线程不阻塞,继续处理数据包
- 批量处理提高效率
- 避免锁竞争
开销:
- RPC消息创建和复制
- 队列操作
- Barrier同步延迟
权衡:
- 适合:低频操作(如ARP学习、BFD事件)
- 不适合:高频操作(如每个数据包都需要RPC)
5.5 最佳实践
- 最小化RPC调用:只在必要时使用RPC
- 批量处理:尽量将多个操作合并到一个RPC请求
- 避免在热点路径使用:数据包处理的热点路径避免RPC
- 使用强制RPC :从非worker线程调用时使用
vl_api_force_rpc_call_main_thread
附录:RPC相关API
A.1 主要API函数
c
// 标准RPC调用(如果在主线程,直接调用)
void vl_api_rpc_call_main_thread (void *fp, u8 *data, u32 data_length);
// 强制RPC调用(总是通过RPC,即使是在主线程)
void vl_api_force_rpc_call_main_thread (void *fp, u8 *data, u32 data_length);
// 处理RPC请求(主线程调用)
int vl_mem_api_handle_rpc (vlib_main_t * vm, vlib_node_runtime_t * node);
A.2 RPC消息结构
c
typedef struct vl_api_rpc_call_t
{
u16 _vl_msg_id; // 消息ID
u32 client_index; // 客户端索引
u32 context; // 上下文
uword function; // 函数指针
u8 need_barrier_sync; // 是否需要barrier同步
u8 send_reply; // 是否需要回复
u8 multicast; // 是否多播
u8 data[0]; // 数据(可变长度)
} vl_api_rpc_call_t;
A.3 使用示例
c
// 示例1:IP邻居学习
void
ip_neighbor_learn_dp (const ip_neighbor_learn_t * l)
{
vl_api_rpc_call_main_thread (ip_neighbor_learn, (u8 *) l, sizeof (*l));
}
// 示例2:从非worker线程调用
void
some_dpdk_callback (void)
{
my_rpc_args_t args = { ... };
vl_api_force_rpc_call_main_thread (my_rpc_function, (u8 *) &args,
sizeof (args));
}
文档版本 :1.0
最后更新 :2024年
作者:VPP源码分析