3. RCU数据发布机制

RCU(Read-Copy-Update)的核心思想是将数据更新操作拆分为“发布”与“等待”两个阶段,类似于并发编程中的发布-订阅(Publish-Subscribe)模式。读者(订阅者)在任意时刻读取数据,写者(发布者)通过特定的指针更新与同步原语保证数据的一致性与可见性。本章深入剖析 rcu_assign_pointer、synchronize_rcu 以及宽限期(Grace Period)的协同工作机制。

3.1 发布-订阅模式在RCU中的映射

在传统的发布-订阅模式中,发布者产生数据并通知订阅者;订阅者接收通知后获取数据。RCU将这一模式内化到内核数据结构的更新流程中:

  • 发布者(Writer):负责分配新数据、更新指针,并通过 rcu_assign_pointer 发布新版本。
  • 订阅者(Reader):通过 rcu_dereference 读取指针,无需加锁,不阻塞写者。
  • 同步屏障(Grace Period):写者在发布后调用 synchronize_rcu,等待所有正在访问旧数据的读者完成,然后安全回收旧数据。
关键区别: 传统发布-订阅通常依赖显式消息队列或回调;RCU的发布-订阅完全基于内存屏障与宽限期机制,读者零开销(仅需一个原子读操作),写者承担同步等待成本。

3.2 rcu_assign_pointer:原子发布新指针

rcu_assign_pointer 是写者用来安全发布新数据指针的宏。它确保:

  1. 新数据的所有写入操作在指针赋值之前完成(通过写内存屏障 smp_wmb 或 store-release 语义)。
  2. 指针赋值本身是原子的,读者不会读到部分更新的指针值。
// 内核源码示例:rcu_assign_pointer 典型用法
struct foo {
    int data;
    struct rcu_head rcu;
};

struct foo __rcu *gptr;

void writer_update(int new_data)
{
    struct foo *new_ptr;
    struct foo *old_ptr;

    new_ptr = kmalloc(sizeof(*new_ptr), GFP_KERNEL);
    new_ptr->data = new_data;

    // 步骤1:发布新指针,保证 new_ptr->data 的写入在指针赋值前可见
    old_ptr = rcu_dereference_protected(gptr, 1);
    rcu_assign_pointer(gptr, new_ptr);

    // 步骤2:等待宽限期结束,然后回收旧数据
    synchronize_rcu();
    kfree(old_ptr);
}
rcu_assign_pointer 与普通指针赋值的对比
特性 普通指针赋值 rcu_assign_pointer
内存排序保证 无(可能被CPU重排) 写屏障(release语义)
原子性 不保证(可能撕裂) 保证原子写入
读者可见性 可能读到未初始化数据 读者看到完整初始化的数据
适用场景 单线程或外部锁保护 RCU发布-订阅模式

3.3 synchronize_rcu:等待宽限期结束

synchronize_rcu 是写者用来同步等待所有正在运行的读者完成对旧数据的访问。其核心行为是:

  • 阻塞调用写者,直到所有在调用点之前进入RCU读侧临界区的读者都退出。
  • 保证在 synchronize_rcu 返回后,没有读者持有对旧数据的引用。
  • 写者可以安全地释放旧数据内存。
性能陷阱: synchronize_rcu 是同步阻塞调用,可能等待数十毫秒(取决于CPU数量和调度延迟)。在实时或高性能路径中,应使用 call_rcu 异步回调替代。

3.4 宽限期(Grace Period)概念详解

宽限期是RCU同步机制的核心时间窗口。它定义为:从所有当前正在运行的读者完成其读侧临界区,到下一个读者开始读侧临界区之间的时间段。更精确地说:

  1. 开始点:写者调用 synchronize_rcu 的时刻。
  2. 结束点:所有在开始点之前进入RCU读侧临界区的CPU都退出临界区的时刻。
  3. 宽限期长度:取决于最慢的读者(可能被抢占、中断或睡眠)。
理解关键: 宽限期不是固定时间,而是“所有老读者都离开”的逻辑条件。RCU通过每个CPU的 rcu_node 树和 quiescent state(静止状态)来检测宽限期结束。
RCU发布-订阅与宽限期时序图 时间 写者:分配新数据 rcu_assign_pointer synchronize_rcu() kfree(old) 读者A:读旧数据 读者A:退出 读者B:读旧数据 读者B:退出 宽限期(Grace Period) 宽限期结束条件:所有在 synchronize_rcu 之前进入的读者都已退出

3.5 发布-订阅模式的完整工作流程

结合上述组件,RCU发布-订阅模式的典型工作流程如下:

RCU发布-订阅模式步骤分解
步骤 角色 操作 说明
1 写者 分配新数据并初始化 新数据在私有空间完成写入
2 写者 调用 rcu_assign_pointer 原子发布新指针,保证初始化可见
3 读者 调用 rcu_dereference 读取当前指针,可能读到新或旧版本
4 写者 调用 synchronize_rcu 阻塞等待所有老读者退出
5 写者 回收旧数据 宽限期结束后,无读者引用旧数据

3.6 常见故障模式与排查要点

  • 读者读到未初始化数据:未使用 rcu_assign_pointer 或缺少内存屏障,导致读者看到部分初始化的结构体。
  • 宽限期无限等待:读者在RCU读侧临界区内睡眠或持有自旋锁,导致无法退出,写者永久阻塞。
  • 过早释放旧数据:在 synchronize_rcu 返回前调用 kfree,导致读者访问已释放内存(UAF)。
  • RCU回调堆积:使用 call_rcu 但回调处理过慢,导致宽限期无法推进。
排查工具: 使用 rcu_sched 或 rcu_preempt 的跟踪点(tracepoint)、/proc/rcu_stat 以及 CONFIG_RCU_CPU_STALL_INFO 内核配置,可以诊断宽限期停滞和读者阻塞问题。

3.7 小结

RCU的发布-订阅模式通过 rcu_assign_pointer 实现原子发布,通过 synchronize_rcu 和宽限期机制保证安全回收。理解这三者的配合关系是掌握RCU并发控制的基础。写者必须严格遵循“发布 → 等待 → 回收”的顺序,读者则通过轻量级的 rcu_dereference 获得无锁读取能力。任何一步的缺失或顺序错误都会导致数据竞争或内存安全问题。