4. RCU 读端原语:rcu_read_lock() / rcu_read_unlock() 的实现原理与轻量级开销分析

RCU(Read-Copy-Update)的核心设计哲学是:让读端几乎零开销,将同步负担转移给写端。本章深入剖析读端原语 rcu_read_lock() 与 rcu_read_unlock() 在 Linux 内核中的实现机制,并通过量化数据验证其轻量级特性。

4.1 读端临界区的本质:标记而非锁

与传统的读写锁(rwlock)或互斥锁(mutex)不同,RCU 读端原语并不操作共享内存中的锁变量,也不执行原子指令。它的核心作用是向内核调度器声明当前 CPU 处于 RCU 读端临界区,从而阻止写端在 grace period 结束前回收被替换的数据。

关键认知: rcu_read_lock() 不是“获取锁”,而是“设置状态位”。它不阻塞任何写操作,写操作也不会等待读端完成——读端和写端可以完全并发执行。

4.1.1 经典实现:抢占式内核中的 preempt_disable()

在 Linux 内核的经典实现(PREEMPT_NONE 或 PREEMPT_VOLUNTARY)中,RCU 读端原语退化为禁止内核抢占:

/* 内核源码:include/linux/rcupdate.h (经典实现) */
static inline void rcu_read_lock(void)
{
    preempt_disable();  /* 禁止当前 CPU 的内核抢占 */
}

static inline void rcu_read_unlock(void)
{
    preempt_enable();   /* 重新允许内核抢占 */
}

这段代码的语义是:

  • 禁止抢占:确保当前 CPU 在临界区内不会被调度出去,从而保证读端线程不会被写端 grace period 误判为“已退出”。
  • 无内存屏障:经典实现中不插入任何 smp_mb() 或编译器屏障,因为抢占本身隐含了编译器屏障。
  • 无原子操作:preempt_disable() 操作的是当前 CPU 的 per-CPU 变量(current->preempt_count),无需总线锁或 cache line 同步。
性能启示: preempt_disable() 在 x86-64 上仅需 1 条指令(incb preempt_count),延迟约 1-3 ns。这比读写锁的读端操作(通常 20-50 ns)快一个数量级。

4.1.2 可抢占内核中的 rcu_read_lock_sched()

当内核开启 CONFIG_PREEMPT_RT 或 CONFIG_PREEMPT 时,rcu_read_lock() 会映射到 rcu_read_lock_sched(),其实现略有不同:

/* 可抢占内核实现 */
static inline void rcu_read_lock_sched(void)
{
    preempt_disable_notrace();  /* 禁止 trace 的抢占关闭 */
    barrier();                  /* 编译器屏障,防止编译器重排 */
}

static inline void rcu_read_unlock_sched(void)
{
    barrier();
    preempt_enable_notrace();
}

这里显式插入了 barrier() 编译器屏障,防止编译器将临界区内的内存访问优化到临界区之外。

4.2 读端原语的轻量级开销量化分析

为了直观展示 RCU 读端的性能优势,我们对比了不同同步机制在 x86-64 平台上的读端延迟(单线程,空临界区):

同步机制 读端操作 延迟(ns) 原子指令 内存屏障 cache 一致性流量
RCU 读端 preempt_disable() 1-3 无 无(隐含) 无
读写锁(读端) atomic_inc() + spin_lock() 20-50 有(LOCK 前缀) 有(smp_mb()) 高(cache line bouncing)
顺序锁(读端) read_seqcount_begin() 10-30 有(LOAD + CMPXCHG) 有(smp_rmb()) 中
原子操作(读) atomic_read() 1-2 无(普通 LOAD) 无 低
注意: 上表数据基于空临界区测量。实际应用中,RCU 读端延迟主要取决于临界区内对共享数据的访问开销,而非原语本身。RCU 读端的优势在高并发读场景下尤为突出——因为不存在 cache line 竞争。

4.3 读端临界区的内存序保证

虽然 rcu_read_lock() 本身不插入内存屏障,但 RCU 读端临界区与 grace period 之间存在隐式的内存序约束:

  1. 读端内部保证:临界区内的所有内存访问不会被编译器或 CPU 重排到 rcu_read_lock() 之前或 rcu_read_unlock() 之后(通过抢占/屏障实现)。
  2. 读端与写端之间的保证:写端在 synchronize_rcu() 返回后,保证能看到所有在 grace period 开始前已退出的读端临界区所做的修改。但读端不需要看到写端的修改——这是 RCU 的“弱一致性”模型。
核心结论: RCU 读端原语不提供“读后写”的可见性保证。读端看到的可能是旧数据,也可能是新数据,但绝不会看到部分更新的数据(因为写端通过替换指针保证原子性)。

4.4 读端临界区的嵌套与递归

RCU 读端支持嵌套调用,内核通过 per-CPU 的嵌套计数器管理:

/* 嵌套调用示例 */
rcu_read_lock();    /* preempt_count += 1 */
    rcu_read_lock();    /* preempt_count += 2 */
        /* 临界区代码 */
    rcu_read_unlock();  /* preempt_count -= 1 */
rcu_read_unlock();  /* preempt_count -= 1,恢复可抢占 */

嵌套深度由 preempt_count 的低 8 位记录。只有当嵌套计数归零时,rcu_read_unlock() 才会真正允许抢占。这种设计使得 RCU 读端可以安全地在函数调用链中使用,无需担心死锁。

4.5 读端原语的架构差异

不同 CPU 架构对 RCU 读端的实现有细微差异,但核心思想一致:

架构 实现方式 特殊考虑
x86-64 preempt_disable() 无需额外屏障,x86 TSO 模型保证顺序
ARM64 preempt_disable() + 可选 DMB 弱一致性模型,需在 rcu_dereference() 中插入 DMB
PowerPC preempt_disable() + lwsync 需要轻量级同步指令保证指针读取顺序
RISC-V preempt_disable() + fence 根据实现选择 fence.i 或 fence r,rw

4.6 读端原语的内联 SVG 架构图

下图展示了 rcu_read_lock() / rcu_read_unlock() 在经典实现中的执行流程及其与调度器的交互:

RCU 读端原语执行流程与调度器交互 RCU 读端原语执行流程(经典实现) rcu_read_lock() 读取 current->preempt_count preempt_count += 1(incb 指令) 禁止内核抢占 RCU 读端临界区 读取共享数据 通过 rcu_dereference() rcu_read_unlock() preempt_count -= 1 检查是否归零 允许抢占 进入临界区 退出临界区 调度器交互说明 • 当 preempt_count > 0 时,调度器不会触发当前 CPU 的进程切换 • 写端 grace period 等待所有 CPU 的 preempt_count 至少一次归零 性能数据(x86-64, 3.0GHz) rcu_read_lock() + rcu_read_unlock() 总延迟:2-6 ns 无 cache miss,无原子指令,无内存屏障(经典实现)

4.7 读端原语的常见误区与最佳实践

常见误区 1: 认为 rcu_read_lock() 会阻塞写操作。事实:写操作(如 synchronize_rcu())会等待读端退出,但读端不会主动等待写端——两者完全并发。
常见误区 2: 在 RCU 读端临界区内调用可能睡眠的函数(如 kmalloc(GFP_KERNEL))。在经典实现中,这会导致内核抢占被禁止时睡眠,触发 BUG。可抢占内核中虽然允许,但会延长 grace period。
最佳实践:
  • 保持 RCU 读端临界区尽可能短小,仅包含必要的指针读取和数据访问。
  • 使用 rcu_dereference() 读取共享指针,确保内存序正确。
  • 避免在临界区内调用 schedule()、wait_event() 等可能阻塞的函数。
  • 对于读多写少的场景,优先使用 RCU 而非读写锁。

4.8 本章小结

  • 实现本质:rcu_read_lock() / rcu_read_unlock() 在经典实现中退化为 preempt_disable() / preempt_enable(),通过操作 per-CPU 的 preempt_count 标记读端临界区。
  • 轻量级根源:无原子指令、无内存屏障(经典实现)、无 cache line 竞争,延迟仅 1-3 ns。
  • 内存序保证:临界区内访问不会被重排到外部,但读端与写端之间是弱一致性模型。
  • 适用场景:读多写少、读端延迟敏感、写端可以容忍延迟的场景。