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 是写者用来安全发布新数据指针的宏。它确保:
- 新数据的所有写入操作在指针赋值之前完成(通过写内存屏障
smp_wmb或store-release语义)。 - 指针赋值本身是原子的,读者不会读到部分更新的指针值。
// 内核源码示例: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 |
|---|---|---|
| 内存排序保证 | 无(可能被CPU重排) | 写屏障(release语义) |
| 原子性 | 不保证(可能撕裂) | 保证原子写入 |
| 读者可见性 | 可能读到未初始化数据 | 读者看到完整初始化的数据 |
| 适用场景 | 单线程或外部锁保护 | RCU发布-订阅模式 |
3.3 synchronize_rcu:等待宽限期结束
synchronize_rcu 是写者用来同步等待所有正在运行的读者完成对旧数据的访问。其核心行为是:
- 阻塞调用写者,直到所有在调用点之前进入RCU读侧临界区的读者都退出。
- 保证在
synchronize_rcu返回后,没有读者持有对旧数据的引用。 - 写者可以安全地释放旧数据内存。
性能陷阱:
synchronize_rcu 是同步阻塞调用,可能等待数十毫秒(取决于CPU数量和调度延迟)。在实时或高性能路径中,应使用 call_rcu 异步回调替代。
3.4 宽限期(Grace Period)概念详解
宽限期是RCU同步机制的核心时间窗口。它定义为:从所有当前正在运行的读者完成其读侧临界区,到下一个读者开始读侧临界区之间的时间段。更精确地说:
- 开始点:写者调用
synchronize_rcu的时刻。 - 结束点:所有在开始点之前进入RCU读侧临界区的CPU都退出临界区的时刻。
- 宽限期长度:取决于最慢的读者(可能被抢占、中断或睡眠)。
理解关键: 宽限期不是固定时间,而是“所有老读者都离开”的逻辑条件。RCU通过每个CPU的
rcu_node 树和 quiescent state(静止状态)来检测宽限期结束。
3.5 发布-订阅模式的完整工作流程
结合上述组件,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 获得无锁读取能力。任何一步的缺失或顺序错误都会导致数据竞争或内存安全问题。