1. RCU基础概念

1.1 RCU的起源与历史背景

RCU(Read-Copy-Update)是一种同步机制,由Paul E. McKenney于1990年代在DYNIX/ptx操作系统内核中首创。其诞生背景源于对传统读写锁在高并发读场景下性能瓶颈的深刻洞察:当读操作远多于写操作时,读写锁的原子操作和缓存一致性开销成为主要性能杀手。

关键里程碑: 2002年,RCU被合入Linux 2.5.43内核主线;2006年,RCU成为Linux内核最核心的同步原语之一,支撑着从内存管理到网络子系统的数十个关键模块。

1.2 设计哲学:免锁读路径

RCU的核心设计哲学可以概括为:让读操作完全免于原子操作、内存屏障和锁竞争。读端仅需一个轻量级的标记(rcu_read_lock / rcu_read_unlock),无需任何原子指令或内存屏障(在主流架构上)。

特性 读写锁 RCU
读端开销 原子操作 + 内存屏障 几乎为零(仅编译器屏障)
写端开销 写锁互斥 复制 + 延迟回收
读并发度 受缓存一致性限制 完全无干扰
写阻塞读 是(写锁等待) 否(读始终可进行)
适用读/写比例 读多写少(但写仍影响读) 读极多写极少
工程直觉: 如果读操作需要执行原子CAS或自旋锁,那就不是真正的RCU读端。RCU读端在x86上仅是一个编译器屏障,连`mfence`都不需要。

1.3 核心思想:延迟回收(Deferred Reclamation)

RCU最巧妙的设计在于:写操作不立即释放旧数据,而是等待所有正在读该数据的CPU核心完成读临界区后,再执行回收。这个等待期称为“宽限期”(Grace Period)。

RCU 延迟回收机制:宽限期示意图 t 写者:替换指针 旧数据(待回收) 宽限期(Grace Period) 等待所有在读读者退出临界区 读者1(旧) 读者2(旧) 回收旧数据 关键保证: • 读者在宽限期开始前已进入临界区,则一定看到旧数据 • 宽限期结束后,所有旧数据的引用都已消失,方可安全回收 • 写者无需等待读者完成,只需等待读者退出临界区

1.4 读写锁 vs RCU:适用场景对比

选择读写锁还是RCU,取决于读写比例、临界区长度、实时性要求等工程约束。以下从五个维度进行系统对比:

对比维度 读写锁 RCU 工程建议
读端延迟 数十ns(原子操作) 数ns(无原子操作) RCU在极高频读路径优势明显
写端延迟 低(直接修改) 高(复制+回收) 写频繁时读写锁更优
内存开销 无额外开销 需要维护旧副本 RCU不适合内存受限场景
实时性 写锁可能优先级反转 读端无阻塞,写端可抢占 RCU更适合实时系统
死锁风险 读写锁嵌套易死锁 读端不可睡眠,但无锁嵌套死锁 RCU死锁模式不同,需单独分析
常见误区: 不要认为RCU可以完全替代读写锁。当写操作频繁(写比例超过1%)或数据更新需要原子性多字段修改时,读写锁或顺序锁(seqlock)可能是更合适的选择。RCU擅长的是“读几乎永远发生,写极少发生”的场景。

1.5 RCU的三种基本操作

  1. 读端标记(Read-Side Marking): 使用rcu_read_lock()和rcu_read_unlock()标记读临界区,保证在读期间引用的指针不会被回收。
  2. 写端更新(Write-Side Update): 使用rcu_assign_pointer()原子性地替换共享指针,旧数据进入“待回收”状态。
  3. 同步回收(Synchronize Reclamation): 使用synchronize_rcu()等待宽限期结束,或call_rcu()注册回调异步回收。
RCU 三种基本操作流程 读端 rcu_read_lock() rcu_dereference() 写端 rcu_assign_pointer() 替换为新数据 回收端 synchronize_rcu() 或 call_rcu() 回调 操作顺序约束: 1. 写端先通过 rcu_assign_pointer() 发布新数据 2. 读端通过 rcu_dereference() 获取指针,保证看到最新发布的数据 3. 回收端等待所有读端退出后,才释放旧数据内存

1.6 RCU的典型应用场景

  • 路由表/防火墙规则: 网络包处理路径上读操作极高频,规则更新极少(秒级甚至分钟级)。
  • 文件系统dcache: 路径名查找是内核最频繁的操作之一,目录项变更相对低频。
  • 内核模块链表遍历: 使用list_for_each_entry_rcu()遍历注册的模块列表,模块加载/卸载极少发生。
  • per-CPU统计计数器: 读操作无锁累加,写操作(如重置计数器)通过RCU同步。
性能数据参考: 在x86-64平台上,一个读写锁的读端临界区大约需要20-40ns(含原子操作),而RCU读端仅需2-5ns。在100核以上的系统中,RCU的读端扩展性优势可达10倍以上。

1.7 RCU的局限性

局限性 说明 应对策略
读端不可睡眠 在RCU读临界区内不能调用可能睡眠的函数 使用SRCU(Sleepable RCU)变体
写端延迟不确定 宽限期长度取决于读者行为,可能被阻塞 使用异步call_rcu()避免阻塞写路径
内存占用增加 旧数据在宽限期内不能被释放 控制更新频率,或使用SLAB缓存复用
调试复杂度高 RCU使用不当导致UAF或内存泄漏 使用KASAN、RCU stall检测等工具