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

发布时间:2026/9/5 20:48:49
Linux 内核 RCU:Read-Copy Update 核心概念、FAQ 与源码级实现剖析 Linux 内核 RCURead-Copy Update 核心概念、FAQ 与源码级实现剖析【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxRCURead-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.hstatic __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.cSRCU。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 flavorPREEMPTIONn对应 RCU-schedPREEMPTIONy对应 RCU-preemptcall_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.txtRCU: 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-releaseinclude/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.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.hCONFIG_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更不易 OOMchecklist 第 13 条。四、内核中的实现布局与验证手段4.1 kernel/rcu/ 目录速览文件职责tree.c树状 RCUTree RCU主实现状态机式宽限期管理、回调分段列表驱动tree_exp.h加急expedited宽限期实现含对 IPI 的约束tree_nocb.hRCU 回调卸载rcu_nocbs启动参数rcu_nocb_cpu_offload()等见 include/linux/rcupdate.htree_plugin.h可抢占 RCURCU-preemptCPU 本地计数采样路径tree_stall.h宽限期停滞检测配合 stallwarn.rst 描述的告警与自诊断sync.csynchronize_rcu()系列及 polling 版宽限期 APIsrcutree.c / srcutiny.cSRCU 树状/单 CPU 实现tiny.c单 CPUTiny RCU实现update.c更新侧公共路径call_rcu()公共逻辑、queue_rcu_work()等rcutorture.crcutorture 自检模块用法见 torture.rstrcu.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 的概念与 FAQDocumentation/RCU/ 目录形成完整文档体系可按需深入listRCU.rstRCU 保护链表/哈希链的完整用法NMI-RCU.rstNMI 上下文使用 RCU 的约束rcubarrier.rstrcu_barrier()一族 barrier 语义详解lockdep.rst、lockdep-splat.rstlockdep 告警的成因与rcu_dereference_protected()处理法stallwarn.rst宽限期停滞告警解读torture.rstrcutorture 压力测试工具RTFP.txtRCU 概念、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 构建从静态到动态的完整验证链路。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考