VPP中进程同步模块:RPC机制详解

目录

  1. 什么是RPC
  2. VPP为什么需要RPC
  3. VPP如何实现RPC
  4. RPC在VPP中的应用场景
  5. 总结

什么是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主要用于以下场景:

  1. 更新全局数据结构:IP邻居表、路由表等
  2. 调用线程不安全的函数:某些函数只能在主线程执行
  3. 从非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);
}

流程说明

  1. 检查线程:如果在主线程,直接调用函数(需要barrier同步)
  2. 创建消息:分配RPC消息,复制数据
  3. 添加到队列 :将消息添加到pending_rpc_requests队列
  4. 返回: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;
}

流程说明

  1. 交换队列 :将pending_rpc_requestsprocessing_rpc_requests交换
  2. Barrier同步:同步所有worker线程,确保它们暂停
  3. 批量处理:逐个处理RPC请求
  4. 释放同步:释放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同步的三个阶段

  1. Sync(同步):主线程设置barrier标志,等待所有worker线程到达
  2. 执行操作:主线程执行需要同步的操作(如更新全局数据)
  3. Release(释放):主线程清除barrier标志,worker线程继续运行

关键问题:第一个到达barrier的worker是否需要等待很久?

答案:不需要,原因如下:

  1. Worker线程频繁检查barrier

    • Worker线程在主循环中每次循环都检查barriersrc/vlib/main.c:1521
    • 如果worker正在处理数据包,它会在当前batch处理完后立即检查
    • Worker不会"等待很久",因为它在每次循环都会检查
  2. Barrier hold-down timer优化

    • 如果worker很忙(vector rate > 10),主线程会等待一段时间再设置barrier
    • 这给了worker时间完成当前的工作,避免在繁忙时强制暂停
  3. 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. 场景1:Worker都在处理数据包

    • Worker线程正在处理数据包batch(例如256个数据包)
    • 主线程需要设置barrier
    • Barrier hold-down timer:如果worker很忙,主线程会等待一段时间(最多1ms)
    • Worker处理完当前batch后,立即检查barrier并到达
    • 第一个到达的worker等待时间:取决于最慢的worker,通常< 1ms
  2. 场景2:Worker空闲或负载很低

    • Worker线程在主循环中快速循环
    • 主线程设置barrier
    • Worker在下次循环检查时立即到达barrier
    • 第一个到达的worker等待时间:< 100微秒(取决于最慢的worker)
  3. 场景3:Worker负载不均匀

    • 有些worker很忙,有些worker空闲
    • 空闲的worker会立即到达barrier
    • 繁忙的worker会在处理完当前batch后到达
    • 第一个到达的worker等待时间:取决于最慢的worker,但通常< 1ms

性能优化

  1. Barrier hold-down timer

    • 如果worker很忙,主线程会等待一段时间再设置barrier
    • 避免在worker繁忙时强制暂停,减少丢包风险
  2. 频繁检查

    • Worker在主循环中每次循环都检查barrier
    • 确保worker能快速响应barrier请求
  3. 自旋等待

    • 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 ();  // 超时,死锁
        }
    }
}

关键步骤说明

  1. 检查worker线程状态:如果worker线程很忙,等待barrier hold-down timer,避免频繁barrier
  2. 设置barrier标志wait_at_barrier = 1,通知worker线程暂停
  3. 等待所有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线程的流程

  1. 检查barrier标志 :在主循环中定期检查wait_at_barrier
  2. 到达barrier :原子增加workers_at_barrier计数器
  3. 等待barrier清除 :自旋等待wait_at_barrier变为0
  4. 更新时钟:barrier清除后,同步主线程的时间
  5. 离开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 ();  // 超时,死锁
        }
    }
}

关键步骤说明

  1. 记录释放时间:记录barrier释放的时间,worker线程用于同步时钟
  2. 清除barrier标志wait_at_barrier = 0,通知worker线程继续
  3. 等待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同步的性能优化

  1. Barrier Hold-down Timer

    • 如果worker线程很忙(向量速率>10),等待一段时间再barrier
    • 避免频繁barrier导致数据包丢失
    • 最小barrier开放时间:BARRIER_MINIMUM_OPEN_LIMIT
  2. 批量处理

    • 一次barrier处理多个RPC请求
    • 减少barrier次数,提高效率
  3. 递归支持

    • 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同步的注意事项

  1. 只能在主线程调用barrier_syncbarrier_release必须在主线程(Thread 0)调用
  2. 成对使用 :每次barrier_sync必须对应一次barrier_release
  3. 避免长时间持有:Barrier持有时间应该尽可能短,避免影响数据包处理
  4. 超时检测:如果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);
        }
    }
}

流程说明

  1. 检查函数指针:确保函数指针有效
  2. Barrier同步:如果需要,同步所有worker线程
  3. 调用函数:执行RPC消息中指定的函数
  4. 释放同步:释放barrier
  5. 发送回复:如果需要,发送回复消息

关键点

  • 函数指针转换:将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邻居表本身

关键理解

  1. Worker线程在数据包转发时不直接读取IP邻居表

    • 数据包转发时,worker线程使用Adjacencyadj_get),不是IP邻居表
    • Adjacency中已经包含了MAC地址(在rewrite字符串中)
    • Worker线程读取Adjacency是只读操作,不修改Adjacency
    • 源码证据
      • ip_neighbor_db_findstatic函数,只在ip_neighbor.c内部使用
      • Worker线程在数据包转发时(ip4_rewrite_inline, ip6_rewrite_inline)只调用adj_get,不调用ip_neighbor_db_findip_neighbor_get
      • ip_neighbor_addASSERT (0 == vlib_get_thread_index ()),确保只在主线程执行
  2. 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):

      c 复制代码
      static 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
      }
  3. IP邻居表更新本身不需要barrier,但更新Adjacency需要barrier

    • ip_neighbor_add更新IP邻居表(哈希表、链表)→ 不需要barrier(因为worker线程不直接读取)
    • 但是ip_neighbor_add会调用adj_nbr_walk_nhip_neighbor_mk_completeadj_nbr_update_rewrite需要barrier(因为worker线程会读取Adjacency的rewrite字符串)
    • 原因:虽然worker线程不直接读取IP邻居表,但Adjacency的rewrite字符串是从IP邻居表预先填充的,当IP邻居表更新时,需要更新Adjacency,这时需要barrier
  4. Adjacency更新需要barrier

    • ip_neighbor_add会调用adj_nbr_walk_nh → 更新Adjacency → 这里需要barrier
    • 原因:Worker线程在转发数据包时会读取Adjacency的rewrite字符串
  5. 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继续运行

关键点

  1. IP邻居表本身可能不需要barrier(因为worker线程不直接读取)
  2. 但更新Adjacency需要barrier(因为worker线程在转发数据包时会读取)
  3. RPC批量处理使用barrier是为了保护Adjacency更新,而不是IP邻居表本身
  4. 嵌套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

源码证据

  1. Rewrite字符串大小src/vnet/buffer.h:418):

    c 复制代码
    #define VNET_REWRITE_TOTAL_BYTES 128
  2. Rewrite字符串是内嵌的,不是指针src/vnet/adj/adj.h:305):

    c 复制代码
    typedef struct ip_adjacency_t_ {
        // ... 其他字段 ...
        
        /** Rewrite in second and third cache lines */
        VNET_DECLARE_REWRITE;  // ← 内嵌在结构体中,不是指针
    } ip_adjacency_t;
  3. 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

  1. 大小问题

    • Rewrite字符串是128字节
    • 无法在一个原子操作中完成更新(现代CPU通常只支持8字节的原子操作)
    • 需要多次写入,无法保证原子性
  2. 结构设计问题

    • Rewrite字符串是内嵌在Adjacency结构体中,不是指针
    • 如果使用RCU,需要:
      • 分配新的rewrite字符串(128字节)
      • 复制数据
      • 切换指针指向新的rewrite字符串
    • 但rewrite字符串不是指针,无法切换
  3. 性能考虑

    • 如果改为指针,每次读取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) 不需要 更新接口地址、路由,但操作在主线程执行且可能是线程安全的

关键点

  1. RPC批量处理机制vl_mem_api_handle_rpc会统一使用barrier批量处理RPC请求

    • 如果RPC请求通过批量处理机制,会自动获得barrier保护
    • 如果RPC请求直接调用(不在批量处理队列中),需要函数内部自己使用barrier
  2. 更新全局数据结构:如果RPC处理函数更新全局共享数据结构,通常需要barrier

    • 例如:更新哈希表、链表、pool等
  3. 只读操作:如果RPC处理函数只是读取数据或发送事件,通常不需要barrier

    • 例如:读取session数据、发送API事件
  4. 数据结构保护:如果数据结构本身有锁保护或操作是线程安全的,可能不需要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机制的核心价值

  1. 解决线程安全问题:将线程不安全的操作集中到主线程执行
  2. 提高性能:Worker线程不阻塞,继续处理数据包
  3. 批量处理:主线程批量处理RPC请求,提高效率
  4. 避免锁竞争:不需要在worker线程加锁,避免性能下降

5.2 RPC机制的设计特点

  1. 双队列设计:pending和processing队列,避免死锁
  2. Barrier同步:确保处理RPC时,worker线程暂停
  3. 批量处理:一次处理多个RPC请求,提高效率
  4. 主线程优化:如果在主线程,直接调用,避免RPC开销

5.3 RPC机制的使用原则

  1. 低频操作:RPC有延迟,适合低频操作(如ARP学习)
  2. 线程不安全:需要更新全局共享数据结构时使用
  3. 控制平面操作:需要访问主线程管理的状态时使用
  4. 非worker线程:从非worker线程调用时使用强制RPC

5.4 RPC机制的性能考虑

优势

  • Worker线程不阻塞,继续处理数据包
  • 批量处理提高效率
  • 避免锁竞争

开销

  • RPC消息创建和复制
  • 队列操作
  • Barrier同步延迟

权衡

  • 适合:低频操作(如ARP学习、BFD事件)
  • 不适合:高频操作(如每个数据包都需要RPC)

5.5 最佳实践

  1. 最小化RPC调用:只在必要时使用RPC
  2. 批量处理:尽量将多个操作合并到一个RPC请求
  3. 避免在热点路径使用:数据包处理的热点路径避免RPC
  4. 使用强制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源码分析

相关推荐
奋进的电子工程师2 小时前
软件定义汽车的背后:一场架构的“深层次革命”
网络协议·系统架构·汽车·安全威胁分析·安全架构
中杯可乐多加冰2 小时前
openEuler网络优化:TCP/IP协议栈性能深度测评
网络·网络协议·tcp/ip·计算机网络·openeuler
虎头金猫2 小时前
openEuler 22.03 LTS 时序数据库实战:InfluxDB 深度性能评测与优化指南
网络·数据库·python·网络协议·tcp/ip·负载均衡·时序数据库
Yan-英杰2 小时前
openEuler 数据库性能深度评测:PostgreSQL 全方位压测
网络·人工智能·网络协议·tcp/ip·http
小曹要微笑2 小时前
HTTP与WebSocket协议深度解析
websocket·网络协议·http·js
飞行增长手记2 小时前
了解真实属性,从4个核心维度选对静态住宅IP
网络·网络协议·tcp/ip
会头痛的可达鸭2 小时前
HTTP 请求报文详解
网络·网络协议·http
m0_619731193 小时前
TCP协议实战
网络·网络协议·tcp/ip
Arva .3 小时前
TCP 的粘包 / 拆包机制
网络·网络协议·tcp/ip