Linux 内核 RCU:Read-Copy Update 核心概念、FAQ 与源码级实现剖析
RCU(Read-Copy Update,读-拷贝-更新)是 Linux 内核中专为"读多写少"场景优化的同步机制。本文以 RCU 概念文档 为主体,系统讲解 RCU 的"两段式破坏操作"模型与宽限期(grace period)判定原理,覆盖官方 FAQ 中的全部高频问题,并结合 include/linux/rcupdate.h 与 kernel/rcu/ 的实现源码,说明每个原语在编译期与运行期的真实形态,帮助你在内核开发中正确选用 rcu_read_lock()、synchronize_rcu()、call_rcu() 等原语,并建立可落地的 RCU 代码审查与调试方法论。
一、基本思想:把破坏性操作拆成两半
RCU 文档开篇即给出核心定义:将破坏性操作拆分为两部分——一部分阻止任何人看到正在被销毁的数据项,另一部分真正执行销毁;两部分之间必须经过一个足够长的"宽限期",保证所有正在访问被删数据的读者都已放弃引用。以 RCU 保护下的链表为例,删除流程是:先把节点从链表中摘除 → 等待宽限期结束 → 再释放该节点。链表场景的完整用法可参见 链表中的 RCU。
这套模型的关键收益在于读端。把更新拆成"摘除引用"与"延迟回收"之后,读者无需任何重同步手段即可安全读取旧版本数据:单对齐指针的写在现代 CPU 上是原子的,读者看到的只会是旧指针或新指针,绝不会是"半更新"的引用;并发读者继续访问旧版本,从而省去了原子操作、内存屏障和跨核缓存一致性通信——这些正是当前 SMP 系统上最昂贵的开销(详见 whatisRCU.rst 中 RCU 总览一节的论证)。
二、官方 FAQ 逐条解析
RCU 概念文档 的第二部分是一组高频问题,以下逐一给出解答,并在仓库内找到对应实现证据。
2.1 为什么要用 RCU?
文档给出的答案是:RCU 读者不需要获取任何锁、不需要执行任何原子指令、不需要写共享内存,也不需要在(除 Alpha 以外的)CPU 上执行内存屏障。这些操作在现代 CPU 上开销巨大,正是 RCU 在读多场景下性能优势的来源;读者无需加锁也极大简化了死锁规避代码。
这一点在源码中体现得非常直白。非抢占内核下,rcu_read_lock() 的最终形态就是禁止抢占(include/linux/rcupdate.h 中 __rcu_read_lock() 直接调用 preempt_disable()),而面向开发者的入口 rcu_read_lock() 则附加了 lockdep 上下文锁与合法性检查(include/linux/rcupdate.h):
static __always_inline void rcu_read_lock(void)
__acquires_shared(RCU)
{
__rcu_read_lock();
__acquire_shared(RCU);
rcu_lock_acquire(&rcu_lock_map);
RCU_LOCKDEP_WARN(!rcu_is_watching(),
"rcu_read_lock() used illegally while idle");
}
头文件中还有一段著名的注释:不存在 rcu_write_lock(),因为没有任何办法能把 RCU 读者"锁出去"——这不是缺陷,恰恰是 RCU 性能优势的来源;写者之间必须自行协调(通常用自旋锁)。
2.2 读者不做任何同步,更新者如何知道宽限期已结束?
文档给出的判定规则与自旋锁类似:RCU 读者被禁止阻塞、禁止切换到用户态执行、禁止进入空闲循环。因此,一旦观察到某 CPU 经历过上述三种状态之一,就能确定它已经退出了此前所有 RCU 读侧临界区。把节点从链表摘除后,等到所有 CPU 都完成过一次上下文切换、用户态执行或空闲循环,就可以安全释放该节点。
可抢占的 RCU 变体(CONFIG_PREEMPT_RCU)要达到同样效果,则要求读者维护 CPU 本地计数器,从而允许在读侧临界区中做有限类型的阻塞;SRCU 同样使用 CPU 本地计数器,且允许在临界区中任意阻塞。这类变体通过采样计数器来检测宽限期。对应的实现位于 kernel/rcu/tree.c(树状 RCU 主实现)与 kernel/rcu/srcutree.c(SRCU)。
2.3 单处理器(UP)内核上为何还要等宽限期?
文档将这一问题专门指向 RCU on Uniprocessor Systems。该文档用一个常见误区开篇:在 UP 系统上 call_rcu() 可以立即执行回调是一个危险的想法,并给出三个反例:
- softirq 自杀:进程上下文正在扫描含 A、B、C 的链表且当前引用到 B 时,被 softirq 打断,softirq 删除 B 并调用
call_rcu()延迟释放。若回调被立即执行,softirq 返回后扫描代码将引用一个刚被释放的节点;硬件中断上下文中同样会发生。 - 函数调用致命性:即使只在进程上下文立即执行回调,若扫描过程中调用某元素上的函数、该函数删除元素 B 并
call_rcu(),立即执行回调就违反了 RCU 的根本保证——回调必须等到所有正在执行的读侧临界区结束后才能被调用。 - 死锁:
call_rcu()持锁调用、回调又要抢同一把锁,立即执行回调会造成自死锁,哪怕这次调用发生在整整一个宽限期之后。
结论:即使在 UP 系统上,RCU 基础设施也必须尊重宽限期,并且必须在"不持有任何锁"的已知环境中执行回调。文档同时指出:synchronize_rcu() 在 UP 上立即返回是安全的(包括在 UP 上运行的 PREEMPT SMP 构建),但运行可抢占 RCU 的 UP 系统不行——因为可能有其他任务在 RCU 读侧临界区中间被抢占,此时宽限期尚未真正结束。
2.4 如何在内核中找到 RCU 的使用点?
文档列出的检索关键字即是一组完整的 RCU 原语清单:rcu_read_lock、rcu_read_unlock、call_rcu、rcu_read_lock_bh、rcu_read_unlock_bh、srcu_read_lock、srcu_read_unlock、synchronize_rcu、synchronize_net、synchronize_srcu 等。直接对源码树做全文检索即可得到内核中 RCU 的全景使用图。
2.5 编写 RCU 代码应遵循什么准则?
文档指向 RCU 补丁审查清单,其中最重要的条目包括:
- 确认是读多写少场景:若结构更新占比超过约 10%,应优先考虑其他方案,除非详细性能测量证明 RCU 仍是正确工具。RCU 的本质是用写端开销换取读端零开销。
- 更新侧必须有互斥:RCU 只放读端,写者之间仍需加锁、原子操作或"限制更新者唯一"三者之一。
- 读侧临界区必须正确使用
rcu_read_lock()及其兄弟原语:任何对 RCU 保护指针的解引用都必须被rcu_read_lock()、rcu_read_lock_bh()、rcu_read_lock_sched()或更新侧锁覆盖。获取 raw spinlock、禁用抢占也会隐式进入读侧临界区。清单同时提到,较新的guard(rcu)()与scoped_guard(rcu)清理守卫(cleanup.h 机制)比手工配对 lock/unlock 更不易出错。 - 先摘除,再宽限:必须先移除读者可能跟随的所有路径,然后才调用
call_rcu()/synchronize_rcu();这些原语只等待"已存在"的读者,之后新进入的读者安全由调用者保证。 - 等待原语与读原语必须配对(v4.20 起每个内核只实现一种 RCU flavor:
PREEMPTION=n对应 RCU-sched,PREEMPTION=y对应 RCU-preempt):call_rcu()/synchronize_rcu()对应rcu_read_lock()、任何禁用 softirq 的原语对、任何禁用抢占的原语对;synchronize_srcu()/call_srcu()必须配合同一个srcu_struct的srcu_read_lock()/srcu_read_unlock()。混用会导致内核损坏,甚至已经产生过可被利用的安全问题。 - 上下文限制:
call_rcu()的回调可能从 softirq 上下文调用且半中断(bottom half)处于禁用状态,回调中不能阻塞;如需阻塞,应通过 workqueue 调度(queue_rcu_work()为call_rcu()提供了现成方案)。反之,synchronize_rcu()(以及synchronize_srcu()、synchronize_rcu_expedited()等)可以阻塞,因此不能在任何中断上下文中调用。 synchronize_rcu()优于call_rcu()的默认选择:synchronize_rcu()天然自限流——宽限期被延迟时更新自动变慢,而call_rcu()用户若不限流,可能引发实时延迟甚至 OOM。需要"发射后不管"的内存释放时,kfree_rcu()/kvfree_rcu()通常是最简方案。- 回调并发性:RCU 回调会并行执行,不能假设在发起
call_rcu()的同一 CPU 上执行(CPU 下线时回调会迁移到存活 CPU),也不能假设按入队顺序或串行执行;rcu_nocbs=启动参数指定的 CPU 其回调可能永远由其他 CPU 执行。 - 模块卸载必须等回调排空:把模块内回调传给
call_rcu()后,卸载前必须等所有挂起回调执行完,且仅等一个宽限期是不够的——需要调用对应的 barrier 函数(rcu_barrier()/srcu_barrier()/rcu_barrier_tasks()/rcu_barrier_tasks_trace());barrier 函数本身不保证宽限期,必要时需与synchronize_*()成对调用(详见 rcubarrier.rst)。 - 调试手段:
CONFIG_PROVE_LOCKING检查 RCU 保护数据的访问是否处于正确临界区;CONFIG_DEBUG_OBJECTS_RCU_HEAD检查同一对象在宽限期结束前被重复交给call_rcu();CONFIG_RCU_STRICT_GRACE_PERIOD配合 KASAN 检查泄漏出临界区的指针;给 RCU 指针加__rcu标注后,sparse 会警告未用rcu_dereference()变体的直接访问。
2.6 名字、专利与实时性
- 名字:"RCU" 即 read-copy update,命名由来见 listRCU.rst 中 "read-copy update" 一节。
- 专利:文档如实说明 RCU 确有相关专利,可检索 RTFP.txt("RCU: Read-Copy Updates Frequently Printed")中的 "Patent" 字符串了解详情——其中一项已被受让人放弃,其余以 GPL 贡献给 Linux 内核,许多早已过期;用户态也存在 LGPL 许可的 RCU 实现。
- 实时内核:文档指出实时友好的 RCU 通过
CONFIG_PREEMPTION内核配置启用(对应CONFIG_PREEMPT_RCU一族实现,即 RCU-preempt,允许在读侧临界区内有限阻塞)。清单第 13 条还说明:SRCU 的加急原语synchronize_srcu_expedited()从不向其他 CPU 发 IPI,对实时负载比synchronize_rcu_expedited()更友好;对 IPI 敏感的实时负载还可以用启动参数rcupdate.rcu_normal完全禁用加急宽限期。
三、核心 API 的源码级形态
whatisRCU.rst 将核心 RCU API 归纳为五个原语:rcu_read_lock()、rcu_read_unlock()、synchronize_rcu()/call_rcu()、rcu_assign_pointer()、rcu_dereference()。以下对照仓库源码看它们的真实定义。
3.1 更新侧:rcu_assign_pointer() 是 store-release
include/linux/rcupdate.h 中的实现:
#define rcu_assign_pointer(p, v) \
context_unsafe( \
uintptr_t _r_a_p__v = (uintptr_t)(v); \
rcu_check_sparse(p, __rcu); \
\
if (__builtin_constant_p(v) && (_r_a_p__v) == (uintptr_t)NULL) \
WRITE_ONCE((p), (typeof(p))(_r_a_p__v); \
else \
smp_store_release(&p, RCU_INITIALIZER((typeof(p))_r_a_p__v)); \
)
要点有三:rcu_check_sparse() 让 sparse 校验目标确实带 __rcu 限定符;非空赋值走 smp_store_release(),保证结构体的全部初始化都排在指针发布之前(store-release 语义);常量 NULL 赋值退化为 WRITE_ONCE,因为无数据可见性需要。文档同时强调:它保护的是读者不被更新者干扰,不保护并发更新者彼此不干扰,后者仍需锁。
3.2 读取侧:rcu_dereference() 是受保护的 volatile 读取
#define rcu_dereference(p) rcu_dereference_check(p, 0)
它并不真正解引用指针,而是"为稍后的解引用保护该指针":执行当前架构所需的内存屏障(目前只有 Alpha 真正需要屏障,其他架构编译为一个 volatile load),同时阻止编译器利用地址依赖做破坏性优化。rcu_dereference_check() 附带 lockdep 检查,若在无读侧临界区(且无更新侧锁)时解引用会告警——这正是 checklist.rst 第 4 条要求的。更完整的语义、常见误用与 rcu_dereference_protected() 变体的用法,见 rcu_dereference.rst 与 lockdep.rst。
3.3 宽限与回调:synchronize_rcu() 与 call_rcu()
两个对外接口在 include/linux/rcupdate.h 中声明:
/* Exported common interfaces */
void call_rcu(struct rcu_head *head, rcu_callback_t func);
void rcu_barrier_tasks(void);
void synchronize_rcu(void);
典型"全局指针读-拷贝-更新"用法(改编自 whatisRCU.rst 第 3 节示例):
struct foo { int a; char b; long c; };
DEFINE_SPINLOCK(foo_mutex);
struct foo __rcu *gbl_foo;
void foo_update_a(int new_a)
{
struct foo *new_fp = kmalloc_obj(struct foo);
struct foo *old_fp;
spin_lock(&foo_mutex);
old_fp = rcu_dereference_protected(gbl_foo,
lockdep_is_held(&foo_mutex));
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp);
spin_unlock(&foo_mutex);
synchronize_rcu(); /* 等旧结构的读者全部退出 */
kfree(old_fp);
}
int foo_get_a(void)
{
int retval;
rcu_read_lock();
retval = rcu_dereference(gbl_foo)->a;
rcu_read_unlock();
return retval;
}
若更新者不能阻塞,则改用 call_rcu():给 struct foo 嵌入 struct rcu_head rcu;,摘除后登记 call_rcu(&old_fp->rcu, foo_reclaim);,由基础设施在宽限期后从 softirq 或进程上下文回调 foo_reclaim()(回调不能阻塞)。若回调只是 kfree(),可直接用 kfree_rcu(old_fp, rcu);;允许偶发睡眠时可省略 rcu_head 字段,用单参数形式 kfree_rcu_mightsleep(old_fp)(几乎不阻塞,仅在内存分配失败时回落到 synchronize_rcu())。
synchronize_rcu() 的时序语义有一个易错点(whatisRCU.rst 第 2 节给出的时序):它只等待调用时已存在的读侧临界区,不等待之后新进入的——若 CPU 2 在 synchronize_rcu() 进入后才调用 rcu_read_lock(),宽限期可以先行结束。此外它也不保证在最后一个读者结束后立即返回:调度延迟与实现的批量处理都会带来额外时延。
3.4 三种主流读侧 flavor 及守卫形式
源码结构印证了文档所述的分层:CONFIG_TINY_RCU(单 CPU 简化实现,kernel/rcu/tiny.c)下 rcu_read_unlock_strict() 为空操作(include/linux/rcupdate.h);CONFIG_PREEMPT_RCU 下 __rcu_read_lock()/__rcu_read_unlock() 是真实函数,操作 current->rcu_read_lock_nesting 深度计数(include/linux/rcupdate.h)。按 whatisRCU.rst 的分类,更新侧原语三种 flavor 相同,读侧则分为:
| flavor | 读侧临界区 | 适用场景 |
|---|---|---|
| (a) | rcu_read_lock()/rcu_read_unlock() + rcu_dereference() |
普通数据结构,最常见 |
| (b) | rcu_read_lock_bh()/rcu_read_unlock_bh()、local_bh_disable()/local_bh_enable() + rcu_dereference_bh() |
可能遭受远程拒绝服务攻击的网络数据结构 |
| (c) | rcu_read_lock_sched()/rcu_read_unlock_sched()、preempt_disable()/preempt_enable()、硬中断/NMI 进出 + rcu_dereference_sched() |
调度器与中断/NMI 处理任务 |
SRCU、RCU-Tasks、RCU-Tasks-Rude、RCU-Tasks-Trace 各自有对应的原语关系,其中 SRCU 读侧临界区可睡眠,但必须用显式初始化的 srcu_struct(DEFINE_SRCU()/init_srcu_struct() 等)划定域范围,且同一 srcu_struct 上的更新只等待同一域内的读者——这使 SRCU 比"允许睡眠的 RCU"更不易 OOM(checklist 第 13 条)。
四、内核中的实现布局与验证手段
4.1 kernel/rcu/ 目录速览
| 文件 | 职责 |
|---|---|
| tree.c | 树状 RCU(Tree RCU)主实现:状态机式宽限期管理、回调分段列表驱动 |
| tree_exp.h | 加急(expedited)宽限期实现,含对 IPI 的约束 |
| tree_nocb.h | RCU 回调卸载(rcu_nocbs= 启动参数,rcu_nocb_cpu_offload() 等,见 include/linux/rcupdate.h) |
| tree_plugin.h | 可抢占 RCU(RCU-preempt)CPU 本地计数采样路径 |
| tree_stall.h | 宽限期停滞检测,配合 stallwarn.rst 描述的告警与自诊断 |
| sync.c | synchronize_rcu() 系列及 polling 版宽限期 API |
| srcutree.c / srcutiny.c | SRCU 树状/单 CPU 实现 |
| tiny.c | 单 CPU(Tiny RCU)实现 |
| update.c | 更新侧公共路径(call_rcu() 公共逻辑、queue_rcu_work() 等) |
| rcutorture.c | rcutorture 自检模块,用法见 torture.rst |
| rcu.h | 各实现的公共内部头文件 |
宽限期序列的轮询式快照(struct rcu_gp_seq 的 norm/exp 双通道,include/linux/rcupdate.h)同时服务于普通与加急两种宽限期;CONFIG_RCU_LAZY 下还存在 call_rcu_hurry() 变体(include/linux/rcupdate.h 的 include/linux/rcupdate.h)。
4.2 与文档体系的对应关系
围绕 rcu.rst 的概念与 FAQ,Documentation/RCU/ 目录形成完整文档体系,可按需深入:
- listRCU.rst:RCU 保护链表/哈希链的完整用法;
- NMI-RCU.rst:NMI 上下文使用 RCU 的约束;
- rcubarrier.rst:
rcu_barrier()一族 barrier 语义详解; - lockdep.rst、lockdep-splat.rst:lockdep 告警的成因与
rcu_dereference_protected()处理法; - stallwarn.rst:宽限期停滞告警解读;
- torture.rst:rcutorture 压力测试工具;
- RTFP.txt:RCU 概念、FAQ 与专利的长篇汇总(概念文档多处指回此文件)。
五、要点回顾
- RCU 的本质是将"摘除引用"与"回收内存"用宽限期隔开,使读端获得零锁、零原子操作、(除 Alpha 外)零屏障的读取成本。
- 经典宽限判定依赖"读者不得阻塞/进入用户态/进入 idle"三条禁令;可抢占 RCU 与 SRCU 以 CPU 本地计数器采样替代。
- 写者之间必须自行互斥;先摘除、后宽限、再回收的顺序不可颠倒;读原语与等待原语必须按 flavor 严格配对。
synchronize_rcu()默认优先(自限流、代码简单),不能阻塞时用call_rcu(),纯释放场景用kfree_rcu();回调不得阻塞、并行执行、位置不定。- 用
CONFIG_PROVE_LOCKING、CONFIG_DEBUG_OBJECTS_RCU_HEAD、sparse__rcu检查与 rcutorture 构建从静态到动态的完整验证链路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00