首页
/ Linux 内核 RCU:Read-Copy Update 核心概念、FAQ 与源码级实现剖析

Linux 内核 RCU:Read-Copy Update 核心概念、FAQ 与源码级实现剖析

2026-09-04 19:03:43作者:蔡怀权

RCU(Read-Copy Update,读-拷贝-更新)是 Linux 内核中专为"读多写少"场景优化的同步机制。本文以 RCU 概念文档 为主体,系统讲解 RCU 的"两段式破坏操作"模型与宽限期(grace period)判定原理,覆盖官方 FAQ 中的全部高频问题,并结合 include/linux/rcupdate.hkernel/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() 可以立即执行回调是一个危险的想法,并给出三个反例:

  1. softirq 自杀:进程上下文正在扫描含 A、B、C 的链表且当前引用到 B 时,被 softirq 打断,softirq 删除 B 并调用 call_rcu() 延迟释放。若回调被立即执行,softirq 返回后扫描代码将引用一个刚被释放的节点;硬件中断上下文中同样会发生。
  2. 函数调用致命性:即使只在进程上下文立即执行回调,若扫描过程中调用某元素上的函数、该函数删除元素 B 并 call_rcu(),立即执行回调就违反了 RCU 的根本保证——回调必须等到所有正在执行的读侧临界区结束后才能被调用。
  3. 死锁call_rcu() 持锁调用、回调又要抢同一把锁,立即执行回调会造成自死锁,哪怕这次调用发生在整整一个宽限期之后。

结论:即使在 UP 系统上,RCU 基础设施也必须尊重宽限期,并且必须在"不持有任何锁"的已知环境中执行回调。文档同时指出:synchronize_rcu() 在 UP 上立即返回是安全的(包括在 UP 上运行的 PREEMPT SMP 构建),但运行可抢占 RCU 的 UP 系统不行——因为可能有其他任务在 RCU 读侧临界区中间被抢占,此时宽限期尚未真正结束。

2.4 如何在内核中找到 RCU 的使用点?

文档列出的检索关键字即是一组完整的 RCU 原语清单:rcu_read_lockrcu_read_unlockcall_rcurcu_read_lock_bhrcu_read_unlock_bhsrcu_read_locksrcu_read_unlocksynchronize_rcusynchronize_netsynchronize_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_structsrcu_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 读取

include/linux/rcupdate.h

#define rcu_dereference(p) rcu_dereference_check(p, 0)

它并不真正解引用指针,而是"为稍后的解引用保护该指针":执行当前架构所需的内存屏障(目前只有 Alpha 真正需要屏障,其他架构编译为一个 volatile load),同时阻止编译器利用地址依赖做破坏性优化。rcu_dereference_check() 附带 lockdep 检查,若在无读侧临界区(且无更新侧锁)时解引用会告警——这正是 checklist.rst 第 4 条要求的。更完整的语义、常见误用与 rcu_dereference_protected() 变体的用法,见 rcu_dereference.rstlockdep.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_structDEFINE_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_seqnorm/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/ 目录形成完整文档体系,可按需深入:

五、要点回顾

  1. RCU 的本质是将"摘除引用"与"回收内存"用宽限期隔开,使读端获得零锁、零原子操作、(除 Alpha 外)零屏障的读取成本。
  2. 经典宽限判定依赖"读者不得阻塞/进入用户态/进入 idle"三条禁令;可抢占 RCU 与 SRCU 以 CPU 本地计数器采样替代。
  3. 写者之间必须自行互斥;先摘除、后宽限、再回收的顺序不可颠倒;读原语与等待原语必须按 flavor 严格配对。
  4. synchronize_rcu() 默认优先(自限流、代码简单),不能阻塞时用 call_rcu(),纯释放场景用 kfree_rcu();回调不得阻塞、并行执行、位置不定。
  5. CONFIG_PROVE_LOCKINGCONFIG_DEBUG_OBJECTS_RCU_HEAD、sparse __rcu 检查与 rcutorture 构建从静态到动态的完整验证链路。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384