2. RCU 核心思想:无锁读、延迟回收与宽限期
RCU(Read-Copy-Update)的设计哲学从根本上改变了传统锁同步的范式。它不再试图通过锁来强制读写双方互斥,而是通过无锁读、延迟回收和宽限期三个核心机制,将读路径上的竞争开销降为零,同时保证写操作的最终一致性。
2.1 设计哲学:从“互斥”到“共存”
传统读写锁(rwlock)要求读操作与写操作互斥,当写者持有锁时,所有读者必须等待。RCU 则允许读者在写者更新数据的过程中继续访问旧版本,直到所有可能正在访问旧版本的读者都离开临界区后,才真正回收旧数据。
| 对比维度 | 读写锁(rwlock) | RCU |
|---|---|---|
| 读-写互斥 | 是,写者阻塞读者 | 否,读者无等待 |
| 读-读并发 | 是,允许多个读者 | 是,且无任何锁开销 |
| 写-写互斥 | 需要外部锁保护 | 需要外部锁保护 |
| 读者开销 | 原子操作 + 内存屏障 | 几乎为零(仅编译器屏障) |
| 写者延迟 | 取决于读者持有锁时间 | 取决于宽限期长度 |
核心洞察:RCU 将“一致性”的定义从“所有线程看到同一份数据”放宽为“每个读者看到一份在它进入临界区时合法的快照”。这种语义上的让步,换来了读路径上完全的锁消除。
2.2 无锁读(Lock-Free Read)
RCU 的读路径不包含任何原子指令、自旋锁或内存屏障(在大多数架构上)。读者只需要通过 rcu_read_lock() 和 rcu_read_unlock() 标记临界区,内部实现通常只是禁止抢占或关闭中断,不涉及总线锁。
// RCU 读者典型代码模式
rcu_read_lock();
p = rcu_dereference(gp); // 获取共享指针
if (p != NULL) {
// 安全访问 p 指向的数据
do_something_with(p->data);
}
rcu_read_unlock();
性能特征:在 x86 架构上,
rcu_read_lock() 和 rcu_read_unlock() 仅生成一对 barrier() 编译器屏障,不产生任何 LOCK 前缀指令。这意味着读路径的延迟与单线程访问几乎一致。
2.3 延迟回收(Deferred Reclamation)
写者更新数据时,不会立即释放旧版本的内存。它首先创建一个新副本,原子地替换全局指针,然后等待一个“安全时刻”才执行回收。这个安全时刻就是宽限期。
// RCU 写者典型代码模式
spin_lock(&gp_lock); // 写-写互斥
old = gp;
new = alloc_new();
*new = *old; // 复制旧数据
new->data = new_value; // 修改新副本
rcu_assign_pointer(gp, new); // 原子替换指针
spin_unlock(&gp_lock);
// 此时所有后续读者看到新数据
// 但旧数据 old 还不能释放
synchronize_rcu(); // 等待宽限期结束
kfree(old); // 安全回收旧数据
关键约束:在
synchronize_rcu() 返回之前,写者不能释放旧数据。如果提前释放,正在访问旧数据的读者将触发 use-after-free 错误。延迟回收是 RCU 正确性的基石。
2.4 宽限期(Grace Period)
宽限期是 RCU 最核心的概念。它定义为:从写者替换指针开始,到所有在替换前进入 RCU 读临界区的读者都已完成退出为止的时间段。
宽限期的结束意味着:系统中不再有任何线程持有指向旧数据的引用。此时,旧数据可以安全回收。
宽限期判定条件:每个 CPU 上的每个线程都至少经历一次“静止状态”(quiescent state)。静止状态包括:进程上下文切换、用户态执行、空闲循环等。当所有 CPU 都报告了静止状态后,宽限期结束。
2.5 核心流程可视化
下图展示了 RCU 更新操作中,读者、写者与宽限期之间的时序关系:
2.6 三种核心 API 的语义对比
Linux 内核提供了三种等待宽限期结束的 API,适用于不同的上下文:
| API | 阻塞行为 | 适用上下文 | 延迟特性 |
|---|---|---|---|
synchronize_rcu() |
阻塞调用者直到宽限期结束 | 进程上下文(可睡眠) | 可能数十毫秒 |
synchronize_rcu_expedited() |
阻塞调用者,但使用 IPI 加速 | 进程上下文(可睡眠) | 通常亚毫秒级 |
call_rcu() |
不阻塞,注册回调后立即返回 | 任何上下文(包括中断) | 异步,回调在宽限期后执行 |
选择建议:在性能敏感的写路径中,优先使用
call_rcu() 将回收操作推迟到软中断中执行,避免写者阻塞。仅在写者频率极低且需要立即回收内存时使用 synchronize_rcu()。
2.7 设计哲学总结
RCU 的设计哲学可以归纳为三条核心原则:
- 读路径零等待:读者从不因为写者而阻塞,也不使用任何原子操作。这是 RCU 性能优势的根本来源。
- 写者负责同步:写者必须等待宽限期,确保所有旧数据的引用都已消失,才能安全回收。同步代价由写者承担。
- 版本化数据访问:通过指针替换实现原子更新,每个读者看到的是进入临界区时指针指向的版本,不同读者可能看到不同版本,但每个版本都是合法的。
性能权衡:RCU 将读路径的延迟从“数十纳秒(原子操作)”降低到“数个纳秒(普通内存访问)”,但将写路径的延迟从“微秒级”提升到“毫秒级(宽限期等待)”。这种权衡在读多写少的场景下收益巨大。