ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

第 10 章 RCU 与 SRCU:宽限期驱动的无锁读

2026/9/12 18:46:32 拓冰建站 浏览量
第 10 章 RCU 与 SRCU:宽限期驱动的无锁读 第 8 章的seqcount用序号与重试实现了无锁读但在小结里留了一个伏笔它只适用于读取值一旦读侧要顺着指针访问对象就得叠加 RCU——因为重试能保证读到的值前后一致却无法保证指针指向的对象在读的过程中没有被释放。本章补齐这块RCURead-Copy-Update正是专门解决对象生命周期的无锁读机制它让读者近乎零成本把全部代价搬到更新者一侧。RCU 的实现规模庞大本章只做综述锚定语义与关键接口深入的宽限期推进机制在 mmu-notifier 专栏 12.3 展开。本章源码为include/linux/rcupdate.h、kernel/rcu/tree.c与kernel/rcu/srcutree.c。说明以 x86-64、非PREEMPT_RT配置为主线。RCU 在不同抢占配置下读侧开销不同下文以经典可抢占语义描述。10.1 守护什么读者不设防更新者担全责RCU 的适用场景是读多写极少、且读侧要解引用指针的共享数据结构典型如链表、哈希表、路由表。它的核心约定是一组不对称的分工读者进入rcu_read_lock()到rcu_read_unlock()之间的读侧临界区其中可以放心解引用受 RCU 保护的指针全程不加锁、不自旋、不写任何共享状态。更新者采用“读—复制—更新”不原地修改而是复制一份、改好新副本、用一次指针发布切换过去随后等待一个宽限期确认所有可能看见旧对象的读者都已退出才释放旧对象。于是读侧几乎没有成本一致性完全由更新者通过“延迟释放”来保证——旧对象绝不会在还有读者引用时被回收。10.2 发布—订阅与宽限期RCU 的两根支柱都在屏障上。发布侧用rcu_assign_pointer订阅侧用rcu_dereference/* 发布smp_store_release确保对象初始化先于指针可见 */#definercu_assign_pointer(p,v)\smp_store_release(p,RCU_INITIALIZER((typeof(p))_r_a_p__v))/* 订阅READ_ONCE 依赖序读到指针后解引用不会被重排到前面 */prcu_dereference(gp);这对屏障保证读者要么看到旧对象、要么看到完整初始化后的新对象绝不会看到构造到一半的中间态。这正是第 8 章seqcount无法独立完成、必须叠加 RCU 的原因。另一根支柱是宽限期grace period。更新者摘除旧指针后用两种方式等待synchronize_rcu();/* 同步阻塞直到宽限期结束再返回 */call_rcu(obj-rcu,free_fn);/* 异步登记宽限期后由内核回调释放 */宽限期的定义是“所有在它开始前就已存在的读侧临界区都已结束”的一段时间。跨过它就再没有读者能引用旧对象回收才安全。等待策略在这里彻底移到了更新者身上读者零等待更新者用一段可能不短的延迟换取读侧的无锁。下图给出一次更新的完整时序新旧读者并发更新者发布新对象后等待宽限期待旧读者全部退出再释放旧对象。读者B 新更新者受保护指针读者A 旧读者B 新更新者受保护指针读者A 旧旧读者陆续 rcu_read_unlock所有旧读者已退出rcu_dereference 看到旧对象rcu_assign_pointer 发布新对象rcu_dereference 看到新对象synchronize_rcu 进入宽限期kfree 释放旧对象10.3 SRCU可睡眠的 RCU经典 RCU 的读侧临界区内不允许睡眠会拖住宽限期、乃至无法界定。当读侧确实需要阻塞——例如持读侧锁期间要做内存分配、等 I/O——就用 SRCUSleepable RCU。它把 RCU 按独立域切分每个域是一个struct srcu_struct域之间的宽限期互不牵连structsrcu_structsp;intidxsrcu_read_lock(sp);/* 返回一个 idx读侧可睡眠 *//* ...可阻塞的读侧临界区... */srcu_read_unlock(sp,idx);/* 必须回传同一个 idx */synchronize_srcu(sp);/* 更新者只等本域的读者 */与经典 RCU 相比有两处关键差异读侧srcu_read_lock返回一个idx且解锁时必须原样传回对应它内部翻转的一对 per-CPU 计数器宽限期按域计算一个域里的慢读者不会拖累其他域。代价是读侧比经典 RCU 略重要更新 per-CPU 计数。mmu-notifier 子系统正是用 SRCU 保护通知链——回调里可能睡眠这一场景在专栏 12.3 详述。10.4 与前几章的关系把 RCU 放回全书的坐标它与第 8 章seqcount是互补的两种无锁读。seqcount用序号重试保证读到的值自洽成本在读侧可能重试RCU 用宽限期保证指针指向的对象存活成本在更新侧等宽限期。读一个会被并发改写的标量快照用seqcount读一个会被并发替换、释放的对象用 RCU两者叠加才能安全地无锁遍历一个动态的指针结构。本章小结RCU 以极致不对称的分工实现无锁读读者在rcu_read_lock/rcu_read_unlock之间近乎零成本地解引用指针更新者用rcu_assign_pointer发布新对象、用宽限期synchronize_rcu或call_rcu延迟释放旧对象确保没有读者还在引用时才回收。SRCU 通过按域划分与可睡眠读侧覆盖读侧需要阻塞的场景代价是读侧略重。回到“锁 原子操作 屏障 等待策略”RCU 几乎放弃了读侧的原子操作只靠rcu_assign_pointer/rcu_dereference一对屏障维系可见性顺序而把等待策略整体挪到更新者的宽限期上。它补上了seqcount留下的“含指针须叠加 RCU”这一块两者合起来构成内核无锁读的完整图景。技术交流和投稿欢迎加入社区GPUers。