RCU(Read-Copy Update)让读操作零开销——不需要锁、不需要原子指令、不需要屏障(在很多架构上)。代价是写操作需要等待 grace period。
一、先看图
sequenceDiagram
participant W as Writer
participant R1 as Reader 1
participant R2 as Reader 2
participant GP as Grace Period
R1->>R1: rcu_read_lock
W->>W: 分配新数据
W->>W: rcu_assign_pointer
R2->>R2: rcu_read_lock
R2->>R2: rcu_dereference → 看到新数据
R1->>R1: rcu_read_unlock
W->>GP: synchronize_rcu / call_rcu
GP->>GP: 等所有旧读者退出
GP->>W: 释放旧数据
二、核心思想
- 读者:
rcu_read_lock()/rcu_read_unlock(),不阻塞、不获取锁 - 写者:创建新版本 →
rcu_assign_pointer()发布 → 等旧读者退出 → 释放旧版本 - Grace period:所有持有旧引用的读者都退出临界区的时间段
三、读侧 API
rcu_read_lock();
struct foo *p = rcu_dereference(global_ptr); // 带 address dependency
// 使用 p
rcu_read_unlock();rcu_dereference 展开为
READ_ONCE + 编译器屏障。在 ARM
等弱序架构,地址依赖天然保证顺序。
四、写侧 API
struct foo *new = kmalloc(...);
// 初始化 new
rcu_assign_pointer(global_ptr, new); // smp_store_release 语义
// 同步方式
synchronize_rcu(); // 阻塞等待 grace period
kfree(old);
// 或异步方式
call_rcu(&old->rcu_head, my_callback); // grace period 后回调释放五、Grace Period 实现
5.1 经典 RCU(已废弃)
每个 CPU 经过一次上下文切换 = 不在 RCU 临界区 → grace period 结束。
5.2 Tree RCU
flowchart TD
ROOT[rcu_state<br/>root node] --> L1A[rcu_node<br/>level 1]
ROOT --> L1B[rcu_node<br/>level 1]
L1A --> CPU0[CPU 0<br/>rcu_data]
L1A --> CPU1[CPU 1<br/>rcu_data]
L1B --> CPU2[CPU 2<br/>rcu_data]
L1B --> CPU3[CPU 3<br/>rcu_data]
classDef node fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef cpu fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class ROOT,L1A,L1B node
class CPU0,CPU1,CPU2,CPU3 cpu
树形 quiescent state 报告 → O(log N) 而非 O(N) → 适合大核数。
每 CPU 报告 quiescent state(QS)→ 逐层汇聚到 root → grace period 完成。
六、RCU 变体
| 变体 | 读侧开销 | 可睡眠 | 用途 |
|---|---|---|---|
| 经典 RCU | 零(preempt_disable) | 否 | 大多数 |
| SRCU | 原子 inc/dec | 是 | 需要睡眠的读侧 |
| Tasks RCU | 零 | 是(自愿调度点) | tracing/BPF |
| Tasks Trace RCU | 轻量 | 是 | BPF trampoline |
七、RCU 的代价
- Grace period 有延迟(默认可达数百毫秒)
- 写者的内存释放延迟 → 内存占用增加
- 不适合写密集场景
7.1 Lazy RCU
减少唤醒频率 → 省电。grace period 回调延迟批量处理。
7.2 nocb CPU
rcu_nocbs=1-7
指定 CPU 不处理 RCU 回调 → 回调由专门的 rcuog/rcuop 线程处理 → 减少对实时 CPU 的干扰。
八、常见错误
- rcu_read_lock 里睡眠(非 SRCU)→ grace period 无法推进 → 死锁
- 忘记 rcu_dereference → 编译器优化破坏顺序
- 不等 grace period 就释放 → use-after-free
九、观察
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata # per-CPU RCU 状态
cat /sys/kernel/debug/rcu/rcu_preempt/rcuexp # expedited GP
dmesg | grep -i rcu
bpftrace -e 'kprobe:rcu_gp_kthread { @++ }'十、小结
- RCU 读侧零开销——不需要锁、不需要原子操作
- 写侧通过 grace period 延迟释放
- Tree RCU 扩展到数千核
- SRCU 允许读侧睡眠
- RCU 是 Linux 内核并发的默认选择
参考文献
- Paul McKenney, “What is RCU, Fundamentally?” LWN 2007
kernel/rcu/Documentation/RCU/- 算法百科-20-persistent-ds(交叉引用)
工具
/sys/kernel/debug/rcu/perf stat -e rcu:*rcutorture(内核模块)
上一篇:mutex/rwsem 下一篇:seqlock
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】percpu_refcount 与 SRCU
高频 refcount 每次 inc/dec 都是 cache line bouncing。percpu_refcount 用 per-CPU 计数器解决——活跃时零竞争,关闭时收敛为原子。本文讲两阶段设计、SRCU、kref 与正确关闭顺序。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。