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 更新操作中,读者、写者与宽限期之间的时序关系:

RCU 宽限期与延迟回收时序图 时间 写者:替换指针 写者:等待宽限期 写者:回收旧数据 读者1:访问旧数据 读者2:访问旧数据 读者3:访问新数据 宽限期(Grace Period) 宽限期结束条件 • 所有在替换前进入的读者 • 都已退出读临界区 • 每个CPU至少经历一次静止状态

2.6 三种核心 API 的语义对比

Linux 内核提供了三种等待宽限期结束的 API,适用于不同的上下文:

API 阻塞行为 适用上下文 延迟特性
synchronize_rcu() 阻塞调用者直到宽限期结束 进程上下文(可睡眠) 可能数十毫秒
synchronize_rcu_expedited() 阻塞调用者,但使用 IPI 加速 进程上下文(可睡眠) 通常亚毫秒级
call_rcu() 不阻塞,注册回调后立即返回 任何上下文(包括中断) 异步,回调在宽限期后执行
选择建议:在性能敏感的写路径中,优先使用 call_rcu() 将回收操作推迟到软中断中执行,避免写者阻塞。仅在写者频率极低且需要立即回收内存时使用 synchronize_rcu()。

2.7 设计哲学总结

RCU 的设计哲学可以归纳为三条核心原则:

  1. 读路径零等待:读者从不因为写者而阻塞,也不使用任何原子操作。这是 RCU 性能优势的根本来源。
  2. 写者负责同步:写者必须等待宽限期,确保所有旧数据的引用都已消失,才能安全回收。同步代价由写者承担。
  3. 版本化数据访问:通过指针替换实现原子更新,每个读者看到的是进入临界区时指针指向的版本,不同读者可能看到不同版本,但每个版本都是合法的。
性能权衡:RCU 将读路径的延迟从“数十纳秒(原子操作)”降低到“数个纳秒(普通内存访问)”,但将写路径的延迟从“微秒级”提升到“毫秒级(宽限期等待)”。这种权衡在读多写少的场景下收益巨大。