什么是 RCU 锁
什么是 RCU 锁
📋 问题定位
- 所属子系统:内核同步原语(
kernel/rcu/),核心 API 定义于include/linux/rcupdate.h - 关键概念:RCU(Read-Copy Update,读-复制更新),2002 年 10 月加入 Linux 内核,出自 Paul McKenney 之手
- 一句话定义:RCU 不是传统意义上的”锁”,而是一种读者与写者不同步的同步机制——读者路径几乎零开销,代价是写者路径必须等待”宽限期(grace period)”后才能安全回收旧数据
🎯 原理说明
1. 核心思想:把”同步”全部推给写者
传统读写锁中,读者必须与写者互斥;而 RCU 的读者完全不与写者直接同步:
传统读写锁: 读者 --互斥--> 写者(读者要加锁,写者要等所有读者)
RCU: 读者 ---无同步---> 写者
读者只标记"我在读"(几乎零成本)
写者发布新版本后,等所有在读读者退出,再回收旧版本
- 读者侧:进入读侧临界区只需一条近乎空操作的指令(见下方源码),因此读路径极快
- 写者侧:更新分两步——先”发布”新数据(
rcu_assign_pointer),再”等待”所有旧读者退出(synchronize_rcu()或call_rcu()),之后才能kfree()旧数据
2. 三种视角理解 RCU(来自您提供的 LWN 系列文章)
| 视角 | 说明 |
|---|---|
| 读写锁替代品 | 读者不阻塞写者,读多写少场景无锁竞争 |
| 受限/批量引用计数 | 把”每个读者持有一份引用”合并为”所有读者共享一个宽限期计数” |
| 穷人版垃圾回收器 | 旧对象延迟释放,由内核保证”没人再用时才回收” |
| 存在性保证 | 只要在临界区内,被解引用的对象一定存活 |
| 等待事物完成 | synchronize_rcu() 本质是”等待所有已开始的读操作结束” |
3. 关键 API 家族
读者侧(快路径)——在 master 分支 include/linux/rcupdate.h 中可验证:
#ifdef CONFIG_PREEMPT_RCU
void __rcu_read_lock(void); /* 可抢占变体:计数器加减 */
#else
static inline void __rcu_read_lock(void)
{
preempt_disable(); /* 经典变体:仅关抢占,几乎零开销 */
}
#endif
rcu_read_lock() / rcu_read_unlock():读侧临界区rcu_dereference(p):带内存屏障的指针解引用,配合__rcu注解供 sparse/lockdep 检查
写者侧(慢路径)——kernel/rcu/tree.c 中的 synchronize_rcu():
void synchronize_rcu(void)
{
...
if (rcu_gp_is_expedited())
synchronize_rcu_expedited(); /* 快速宽限期(IPI 轰击) */
else
synchronize_rcu_normal(); /* 常规宽限期:挂起一个回调 */
}
| API | 语义 | 开销 |
|---|---|---|
synchronize_rcu() |
阻塞等待一个宽限期(所有在读读者退出) | 毫秒级,可睡眠 |
synchronize_rcu_expedited() |
通过 IPI 强制各 CPU 报告静止状态,加速宽限期 | 更快但干扰大 |
call_rcu(callback) |
异步:宽限期结束后在软中断中回调,不阻塞写者 | 适合批量回收 |
rcu_assign_pointer(p, v) |
发布新指针,带写侧屏障 | 屏障开销 |
rcu_dereference(p) |
读者读取指针,带读侧屏障 | 屏障开销 |
4. 宽限期(Grace Period)的运作机制
宽限期是 RCU 的基石。内核通过静止状态(quiescent state,QS)判断读者是否退出:
写者调用 synchronize_rcu()
│
▼
检测所有 CPU 是否都经历了 QS(内核态、用户态、空闲态切换点)
│
├─ CPU 在 idle/dynticks 状态 → 天然是 QS(无需唤醒,节省能耗)
├─ CPU 在内核态但未在 RCU 读临界区 → 也是 QS
└─ CPU 在 RCU 读临界区 → 必须等它退出
│
▼
所有 CPU 报告 QS → 宽限期结束 → 旧对象可安全 kfree()
- 层级结构:
rcu_state -> rcu_node树(kernel/rcu/tree.h),把宽限期检测从 O(N) 降到 O(log N),支撑数千核扩展 - 记忆屏障保证:宽限期结束后,读者看到的一定是新数据(
synchronize_rcu()的注释明确保证了这个内存序)
💡 适用场景
典型用法(读多写极少、指针替换型数据结构):
- 链表遍历:
list_for_each_entry_rcu()遍历 + 写者list_replace_rcu()替换节点,延迟回收(如路由表、struct file的路径缓存) - 指针型配置更新:
rcu_assign_pointer发布新配置,读者rcu_dereference读取 - 模块卸载/对象销毁:等待所有使用者退出后再释放(如网络协议栈的
inet_hashinfo、nf_conntrack等大量使用) - 内核内部基础设施:
percpu_ref(块设备生命周期)、srcu(睡眠型读者,如文件系统)、rcu_sync(kernel/rcu/sync.c,percpu-rwsem 的快慢路径切换,正如源码注释所述 “RCU-based infrastructure for lightweight reader-writer locking”)
限制:
- 经典 RCU 读者临界区内不能睡眠、不能阻塞(可抢占变体放宽了这一点,但仍有约束)
- 写者延迟回收带来内存占用峰值(旧对象不能立即释放)
- 宽限期等待是毫秒级,不适合写频繁场景
💡 收益效果
| 指标 | 传统读写锁 | RCU |
|---|---|---|
| 读者路径开销 | 原子操作 + 可能的自旋/睡眠 | 仅 preempt_disable()(或计数器加一) |
| 读者并发度 | 可被写者阻塞/饿死 | 完全无阻塞,与写者并行执行有用工作 |
| 写者延迟 | 低(等锁即走) | 高(必须等宽限期) |
| 扩展性 | 读锁本身是共享缓存行,多核竞争 | 读者完全无共享写,天然可扩展 |
| 能耗 | 无关 | 空闲 CPU 可深度睡眠(dynticks),宽限期不打扰(2008 年文章的核心主题) |
真实代价:
- 写者侧:
synchronize_rcu()可能阻塞数毫秒;call_rcu()回调堆积时内存压力上升 - 调试难度:延迟释放的 bug 难以复现,需
CONFIG_PROVE_RCU(lockdep)与rcu_torture配合验证 - 内存序:错误使用
rcu_dereference/rcu_assign_pointer(绕过屏障)会导致数据竞争
💡 应用前景
RCU 是 Linux 内核并发设计的”招牌”:从 2002 年的经典实现,到 2008 年可抢占变体与 dynticks 适配(您提供的第二篇材料即讲述这段历史:Paul 用 Promela/Spin 形式化验证宽限期与中断/NMI 嵌套的正确性),再到 2019 年 API 演化(rcu_read_lock_bh/rcu_read_lock_sched 合并、srcu 独立、Tasks RCU 用于 trampoline 卸载)。其”读者零开销、写者集中等待”的思想已被 percpu-refcount、hlist_bl、lwq 等机制广泛吸收。
对内核开发者而言,掌握 RCU 意味着:判断场景是否为”读多写极少 + 指针/链表替换”——是,则用 RCU 获得无锁读的性能;否(写频繁或读者需睡眠),则 SRCU 或传统锁更合适。普通运维层面,理解 RCU 有助于解读 rcu_sched 软中断的 stall 告警(INFO: rcu_sched detected stalls)——通常意味着某 CPU 长期处于内核态不经过静止状态,往往是死循环或长临界区导致宽限期无法结束。
参考资料:您提供的 6 篇 LWN 文章引言(Paul McKenney:《What is RCU, Really?》《Preemptable RCU》《The RCU API, 2010/2014/2019》系列);内核源码:include/linux/rcupdate.h(__rcu_read_lock 定义)、kernel/rcu/tree.c(synchronize_rcu 实现)、kernel/rcu/sync.c(rcu_sync 轻量读写锁设施)。
参考来源
本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。