ARTICLE DETAIL

建站实战干货

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

Linux 读-复制-更新 RCU 机制核心原理:Grace Period 与 call_rcu 源码解析

2026/9/25 19:33:56 拓冰建站 浏览量
Linux 读-复制-更新 RCU 机制核心原理:Grace Period 与 call_rcu 源码解析 Linux 读-复制-更新 RCU 机制核心原理Grace Period 与 call_rcu 源码解析在多核对称多处理SMP架构下即便是最轻量的读写自旋锁rwlock_t或读写信号量rwsem当多个 CPU 核心并发读取共享数据时也必须原子修改内部的读者计数器。这会导致严重的硬件级性能瓶颈不同 CPU 核心的 L1/L2 Cache 行在“修改-失效”之间反复剧烈颠簸Cache Line Bouncing总线带宽被跨核缓存一致性协议如 MESI彻底占满。Linux 内核中的RCURead-Copy-Update读-复制-更新机制是解决极高读并发场景的终极解法。它实现了**“读者零锁、零原子指令、零 Cache 颠簸”**的极致只读性能读端开销低到仅仅是一条关闭抢占的指令。一、RCU 核心三部曲与 Grace Period宽限期模型RCU 将对共享链表或数据结构的更新操作解耦为三个独立阶段1. [Read 端]: 读者自由并发读取老数据 (零阻塞、零锁开销) │ ▼ 2. [Copy Update 端]: 写者复制一份新内存 - 在副本上修改 - 原子替换全局指针 (rcu_assign_pointer) │ ▼ 3. [Grace Period (宽限期)]: 等待所有在指针替换前就已进入读临界区的 CPU 全部退出 │ ▼ 4. [Reclaim 端]: 宽限期结束后安全释放老数据内存 (call_rcu / kfree_rcu)时间轴视角下的宽限期 (Grace Period): CPU 0 (读端): [ rcu_read_lock 读老数据 ] ──(退出)── [Quiescent State (静止态)] │ CPU 1 (写端): --[修改并发布新指针]------------------------[ 宽限期结束: 触发 call_rcu 回调释放老内存 ] ▲ CPU 2 (新读端): [ 读新数据 ] │ │ --------------------------------------------------------------┴──────────────────────── Grace Period (必须覆盖所有旧读者)什么是静止状态Quiescent State, QS在经典非抢占式内核中如果一个 CPU 发生了进程上下文切换schedule()、进入了空闲循环cpu_idle、或者从内核态返回到了用户态就证明该 CPU 绝对不可能还处于rcu_read_lock()与rcu_read_unlock()保护的内核临界区之内。这个状态就称为静止状态QS。当系统中的所有 CPU 核心都至少经历了一次静止状态后一段完整的**宽限期Grace Period**宣告结束。二、读端与写端核心源码剖析1. 读端零开销实现在include/linux/rcupdate.h中读端临界区的实现简洁到令人发指static inline void rcu_read_lock(void) { /* 仅仅关闭当前 CPU 的内核抢占防止临界区内被调度出去 */ preempt_disable(); __acquire(RCU); rcu_lock_acquire(rcu_lock_map); barrier(); /* 编译器内存屏障防止指令重排 */ } static inline void rcu_read_unlock(void) { rcu_lock_release(rcu_lock_map); __release(RCU); preempt_enable(); /* 重新使能内核抢占 */ barrier(); }读者不需要修改任何内存变量不需要发送任何总线消息执行耗时小于 1 纳秒2. 写端指针发布与内存屏障写者通过rcu_assign_pointer宏原子发布新结构体指针内部强制插入写内存屏障smp_store_release确保读者看到新指针时新结构体内部的所有字段已经被完整刷入内存#define rcu_assign_pointer(p, v) \ smp_store_release((p), (v)) #define rcu_dereference(p) \ smp_load_acquire((p))三、异步释放机制call_rcu 源码深度追踪如果在写者路径中调用同步等待synchronize_rcu()写者会被阻塞挂起直到宽限期结束严重影响写吞吐。Linux 内核绝大多数场景使用非阻塞的异步注册函数call_rcu()。1. struct rcu_head 回调节点任何需要通过 RCU 释放的结构体必须内嵌一个struct rcu_headstruct rcu_head { struct rcu_head *next; void (*func)(struct rcu_head *head); /* 宽限期结束后的释放回调函数 */ } __attribute__((aligned(sizeof(void *))));2. call_rcu 入队实现/* kernel/rcu/tree.c */ void call_rcu(struct rcu_head *head, rcu_callback_t func) { unsigned long flags; struct rcu_data *rdp; head-func func; head-next NULL; /* 获取当前 CPU 本地专属的 rcu_data 结构体 */ local_irq_save(flags); rdp this_cpu_ptr(rcu_data); /* 将 rcu_head 挂入本 CPU 的 segcblist (分段回调链表) 尾部 */ rcu_segcblist_enqueue(rdp-cblist, head); /* 若当前积累的回调过多通知 RCU 状态机加速开启新一轮 Grace Period */ if (rcu_segcblist_n_cbs(rdp-cblist) rdp-qhimark) rcu_accelerate_cbs(rdp-rsp, rdp); local_irq_restore(flags); }CPU 本地分段回调链表 (segcblist) 状态演进: [ 刚刚加入 (NEXT_TAIL) ] ── [ 等待当前 GP 结束 (WAIT_TAIL) ] ── [ GP 已完成待执行 (DONE_TAIL) ] │ ▼ 由 RCU 软中断批量执行 free()3. 宽限期结束后的批量回调执行rcu_do_batch当 RCU 核心状态机检测到 Grace Period 结束后会触发RCU_SOFTIRQ软中断调用rcu_do_batch()从DONE_TAIL段取出所有就绪的回调批量执行kfree释放内存。四、RCU 与传统读写锁性能全方位对比对比维度rwlock_t(读写自旋锁)rw_semaphore(读写信号量)RCU (读-复制-更新)读端性能中 (跨核原子修改计数)低 (需原子操作及排队判断)极致 (零总线流量纯只读)读端是否可睡眠否 (自旋锁保护)是 (支持长时间睡眠)经典 RCU 否 (SRCU 支持)写端性能阻塞等待所有读者自旋退出阻塞挂入睡眠等待队列极快 (指针发布异步释放)内存开销极小 (仅一个锁变量)小 (信号量结构体)稍大 (老数据需在宽限期内共存)死锁风险高 (读写锁嵌套易死锁)中读端绝对无死锁可能五、RCU 工业级应用三大铁律读者临界区内绝对禁止睡眠针对 Classic Tree-RCU在rcu_read_lock()和rcu_read_unlock()之间绝对不能调用msleep、mutex_lock或发生阻塞 I/O否则会导致该 CPU 长期无法报告静止状态进而阻断全局 Grace Period造成系统内存无法释放发生 RCU Stall 恐慌。若必须在读端睡眠请使用SRCUSleepable RCU。写者之间仍然需要互斥锁保护RCU 解决的是“读者与写者”之间的并发冲突如果有多个写者同时尝试Copy Update写者之间必须通过互斥锁Mutex或自旋锁Spinlock进行串行化保护。警惕 RCU Stall 告警日志如果内核日志中出现INFO: rcu_preempt detected stalls on CPUs/tasks说明某个 CPU 核心长时间关中断或处于忙死循环中未能及时上报静止状态必须优先排查长耗时循环。