Linux rcu经典实现GP宽限期检测与quiescent state
Linux rcu经典实现GP宽限期检测与quiescent statekernel/rcu/tree.c中的grace periodGP检测是Tree RCU最核心的闭环逻辑。GP宽限期是指从rcu_gp_init()设置-gp_seq到rcu_gp_cleanup()递增-gp_seq的整个区间。期间所有RCU读者都必须经过一个quiescent stateGP才被认为完成。整个检测链路涉及四个层次的交互per-CPU的rcu_data、中间节点的rcu_node、全局的rcu_state以及GP kthread的主循环rcu_gp_kthread()。---## 一、Grace Period生命周期的状态机GP kthread的主循环由rcu_gp_kthread()驱动定义在kernel/rcu/tree.c。其核心是一个四阶段状态机cstatic int __noreturn rcu_gp_kthread(void *unused){for (;;) {/* 阶段1: 等待GP请求 */wait_event_interruptible(rsp-gp_wq,READ_ONCE(rsp-gp_flags) RCU_GP_FLAG_INIT);/* 阶段2: 初始化GP */rcu_gp_init(rsp);/* 阶段3: 等待QS FQS循环 */rcu_gp_fqs_loop(rsp);/* 阶段4: 清理 */rcu_gp_cleanup(rsp);}}状态转换通过rsp-gp_state追踪取值包括RCU_GP_DONE_GPS、RCU_GP_ONOFF、RCU_GP_INIT、RCU_GP_WAIT_GPS和RCU_GP_WAIT_FQS。其中RCU_GP_WAIT_FQS是耗时最长的阶段等待所有CPU报告QS或FQS定时器超时触发强制扫描。## 二、GP初始化qsmask的建立rcu_gp_init()的核心工作是将当前在线CPU集合的快照写入每个rcu_node的-qsmask字段作为GP完成的判定依据。cstatic bool rcu_gp_init(struct rcu_state *rsp){unsigned long oldmask;struct rcu_node *rnp_root rcu_get_root(rsp);WRITE_ONCE(rsp-gp_state, RCU_GP_ONOFF);/* 阶段A: 应用热插拔缓冲 */rcu_for_each_leaf_node(rsp, rnp) {raw_spin_lock_irq_rcu_node(rnp);rnp-qsmaskinit rnp-qsmaskinitnext;WRITE_ONCE(rnp-gp_seq, rsp-gp_seq);raw_spin_unlock_irq_rcu_node(rnp);}/* 阶段B: 将qsmaskinit复制到qsmask */rcu_for_each_node_breadth_first(rsp, rnp) {raw_spin_lock_irq_rcu_node(rnp);WRITE_ONCE(rnp-qsmask, rnp-qsmaskinit);rnp-rcu_gp_init_mask rnp-qsmaskinit;if (rnp rnp_root)break;raw_spin_unlock_irq_rcu_node(rnp);}WRITE_ONCE(rsp-gp_state, RCU_GP_WAIT_FQS);return true;}这里存在一个关键的竞态条件rcu_gp_init()运行时可能正好有CPU hotplug事件发生。rcutree_report_cpu_dead()会设置rnp-qsmaskinitnext而rcu_gp_init()通过读取qsmaskinitnext来初始化qsmask。如果在线CPU在rcu_gp_init()之后立即下线其位仍在qsmask中但该CPU永远不会再主动报告QS。为此rcu_gp_init()在复制qsmask之后立即处理已离线CPUcmask rnp-qsmask ~rnp-qsmaskinitnext;if (mask) {raw_spin_lock_irq_rcu_node(rnp);rcu_report_qs_rnp(mask, rnp, rnp-gp_seq, flags);raw_spin_unlock_irq_rcu_node(rnp);}如果不执行这一步离线CPU的位会永远卡死在qsmask中导致GP永远无法完成最终触发RCU stall。## 三、QS检测路径与传播协议QS的检测发生在以下四个上下文1. **调度器切换**rcu_qctr_help()在__schedule()末尾被调用递增当前CPU的rdp-rcu_qs_ctr_snap。2. **用户模式退出**rcu_user_enter()/rcu_user_exit()在context_tracking框架中标记QS。3. **idle进入/退出**rcu_dynticks_eqs_enter()/rcu_dynticks_eqs_exit()通过dynticks counter标记扩展QSEQS。4. **RCU_SOFTIRQ**rcu_core()中的rcu_check_quiescent_state()按tick周期性检查。### 3.1 rcu_report_qs_rdp() - per-CPU上报入口这是CPU级QS上报的单一入口点cstatic void rcu_report_qs_rdp(struct rcu_data *rdp){unsigned long flags;unsigned long mask;bool needwake false;struct rcu_node *rnp;rnp rdp-mynode;raw_spin_lock_irqsave_rcu_node(rnp, flags);/* 边界条件1: GP已经过期 */if (rdp-gp_seq ! rnp-gp_seq) {rdp-passed_quiesc 0;raw_spin_unlock_irqrestore_rcu_node(rnp, flags);return;}mask rdp-grpmask;if ((rnp-qsmask mask) 0) {/* 边界条件2: 其他CPU已经代为上报 */rdp-core_needs_qs false;raw_spin_unlock_irqrestore_rcu_node(rnp, flags);return;}WRITE_ONCE(rnp-qsmask, rnp-qsmask ~mask);rdp-passed_quiesc 0;rdp-core_needs_qs false;if (rnp-qsmask ! 0 || rcu_preempt_blocked_readers_cgp(rnp)) {raw_spin_unlock_irqrestore_rcu_node(rnp, flags);return;}/* 向父节点传播 */needwake rcu_report_qs_rnp(mask, rnp, rnp-gp_seq, flags);if (needwake)rcu_gp_kthread_wake();}边界条件1处理的是典型的GP过期场景当前CPU的GP序列号落后于rcu_node的-gp_seq说明该CPU之前错过了某个GP现在才补报QS——必须丢弃。边界条件2处理的是force_qs_rnp()已经抢先清除了该位的情况此时rdp-core_needs_qs如果不清零会导致后续不必要的RCU softirq重入。### 3.2 rcu_report_qs_rnp() - 树形传播rcu_report_qs_rnp()负责将QS从叶子节点逐级向根节点传播cstatic bool rcu_report_qs_rnp(unsigned long mask, struct rcu_node *rnp,unsigned long gp_seq, unsigned long flags){bool wake false;raw_spin_lock_irqsave_rcu_node(rnp, flags);if (rnp-gp_seq ! gp_seq) {/* GP已经前进当前上报作废 */raw_spin_unlock_irqrestore_rcu_node(rnp, flags);return false;}rnp-qsmask ~mask;if (rnp-qsmask ! 0) {raw_spin_unlock_irqrestore_rcu_node(rnp, flags);return false;}/* 检查是否有阻塞的preemptible RCU读者需要优先提升 */if (rcu_preempt_blocked_readers_cgp(rnp)) {if (rnp-gp_tasks ! NULL)raw_spin_unlock_irqrestore_rcu_node(rnp, flags);return false;}/* 向父节点传播 */if (rnp-parent ! NULL) {return rcu_report_qs_rnp(rnp-grpmask, rnp-parent,gp_seq, flags);}/* 到达根节点GP完成 */WRITE_ONCE(rsp-gp_state, RCU_GP_DONE_GPS);/* 唤醒GP kthread进入清理 */wake true;raw_spin_unlock_irqrestore_rcu_node(rnp, flags);return wake;}传播过程中rnp-gp_seq ! gp_seq检查是关键同步边界。如果父节点上的GP序号已经前进即嵌套GP已经开始当前QS上报的掩码引用的是旧GP的位图必须丢弃。这防止了qsmask在新旧GP之间交叉访问导致的无限等待。### 3.3 rcu_report_qs_rsp() - GP终止信号当根节点的qsmask归零后rcu_gp_fqs_loop()退出等待进入rcu_gp_cleanup()。清理函数会检查cstatic void rcu_gp_cleanup(struct rcu_state *rsp){unsigned long gp_duration;struct rcu_node *rnp rcu_get_root(rsp);struct rcu_data *rdp;WRITE_ONCE(rsp-gp_state, RCU_GP_CLEANUP);/* 诊断检查所有qsmask必须归零 */rcu_for_each_node_breadth_first(rsp, rnp) {if (WARN_ON_ONCE(rcu_preempt_blocked_readers_cgp(rnp)))continue;if (WARN_ON_ONCE(rnp-qsmask))continue;}/* 记录GP持续时间 */gp_duration rsp-gp_seq - rdp-gp_seq;trace_rcu_grace_period(rsp-name, gp_duration, end);/* 递增GP序列号 */WRITE_ONCE(rsp-gp_seq, rsp-gp_seq 1);WRITE_ONCE(rsp-gp_state, RCU_GP_DONE_GPS);}其中的WARN_ON_ONCE是捕获严重Bug的诊断手段。如果存在因race condition导致的qsmask残留或者rcu_preempt_blocked_readers_cgp(rnp)非空preemptible RCU中还有读者在临界区内阻塞rcu_gp_cleanup()不能简单归零——它会尝试通过RCU priority boosting让阻塞读者退出。## 四、Force Quiescent State的退化路径当CPU处于长时间idle状态时tick可能被停止dynticksCPU不会主动检查QS。rcu_gp_fqs_loop()通过hrtimer定期进入FQS路径。cstatic void rcu_gp_fqs_loop(struct rcu_state *rsp){unsigned long gf;unsigned long jiffies_start jiffies;for (;;) {/* 等待FQS定时器超时或QS全部到达 */wait_event_idle_timeout(xxx, xxx, jiffies_till_next_fqs);/* 检查是否有紧急FQS请求 */gf READ_ONCE(rsp-gp_flags);if (gf RCU_GP_FLAG_FQS) {WRITE_ONCE(rsp-gp_flags, gf ~RCU_GP_FLAG_FQS);cond_resched_tasks_rcu_qs(longterm);force_qs_rnp(rsp, dyntick_save_progress_counter);rcu_gp_fqs(rsp, jiffies_start, jiffies);}/* GP完成条件 */if (!READ_ONCE(rnp-qsmask) !rcu_preempt_blocked_readers_cgp(rnp))break;}}### 4.1 force_qs_rnp() - 遍历扫描cstatic void force_qs_rnp(struct rcu_state *rsp,int (*f)(struct rcu_data *rdp)){int cpu;unsigned long flags;unsigned long mask;struct rcu_node *rnp;rcu_for_each_leaf_node(rsp, rnp) {raw_spin_lock_irqsave_rcu_node(rnp, flags);if (rnp-qsmask 0) {raw_spin_unlock_irqrestore_rcu_node(rnp, flags);continue;}for_each_leaf_node_possible_cpu(rnp, cpu) {unsigned long bit leaf_node_cpu_bit(rnp, cpu);if ((rnp-qsmask bit) 0)continue;if (f(per_cpu_ptr(rsp-rda, cpu))) {mask | bit;}}if (mask) {rcu_report_qs_rnp(mask, rnp, rnp-gp_seq, flags);} else {raw_spin_unlock_irqrestore_rcu_node(rnp, flags);}}}### 4.2 dyntick_save_progress_counter() - idle CPU检测对于dynticks idle的CPU不能依赖它主动上报QS。dyntick_save_progress_counter()通过dynticks counter的快照判断CPU是否经历了dyntick idle状态cstatic int dyntick_save_progress_counter(struct rcu_data *rdp){unsigned long cbs;cbs READ_ONCE(rdp-dynticks-dynticks);if (rdp-dynticks_snap cbs) {/* dynticks未变化CPU可能在长时间idle */return 0;}/* dynticks已变化CPU经过了EQS即等同于QS */rdp-dynticks_snap cbs;return 1;}### 4.3 rcu_implicit_dynticks_qs() - 强制探测当dynticks counter始终不变时即CPU在idle中完全没有中断活动判断逻辑升级cstatic int rcu_implicit_dynticks_qs(struct rcu_data *rdp){unsigned long jtsq;int *rcrmp;unsigned long rjlmc;struct rcu_node *rnp rdp-mynode;/* 已过信号量检查 */if (rdp-rcu_qs_ctr_snap ! per_cpu(rcu_qs_ctr, rdp-cpu))return 1;/* 如果CPU已下线直接上报QS */if (!rcu_cpu_online(rdp-cpu)) {rcu_report_qs_rdp(rdp);return 1;}/* CPU长时间无响应 - 触发RCU stall警告 */jtsq jiffies - rdp-gp_start;/* 检查是否已经超时 */if (jtsq jiffies_till_stall_check) {rcu_dump_cpu_stacks(rnp);if (jtsq urc) {/* 最终触发紧急RCU stall */rcu_dump_cpu_stacks(rnp);return 0;}}return 0;}这里存在一个隐蔽的边界条件rcu_qs_ctr_snap在调度点通过rcu_qsctr_help()递增但如果CPU处于完全无调度的idle循环中如ARCH_HAS_TICK_BROADCAST下的deep idleQS永远无法通过调度触发。此时dynticks EQS是唯一的QS来源。## 五、竞态场景深度分析### 5.1 GP边界上的QS误报当GP kthread完成rcu_gp_init()后有一个时间窗口rsp-gp_seq已经递增但rnp-qsmask还没有在全部rcu_node上写完成。如果此时某个CPU在softirq中调用rcu_report_qs_rdp()它会比较rdp-gp_seq ! rnp-gp_seq——相等才继续。但rcu_gp_init()在内核遍历rcu_node树时是广度优先存在部分节点已经更新-gp_seq而部分节点尚未更新的时刻。这种不对称会导致CPU对未初始化完成的树节点上报QS收到错误的qsmask判断。解决方法是在rcu_gp_init()结束前设置rsp-gp_state RCU_GP_WAIT_FQS而rcu_check_quiescent_state()检查gp_state RCU_GP_WAIT_FQS才允许上报。### 5.2 并发GP请求的合并多个call_rcu()在同一时间触发rcu_gp_kthread_wake()时rsp-gp_flags的RCU_GP_FLAG_INIT位通过__call_rcu_core()原子置位。如果GP kthread已经开始初始化后续的call_rcu()通过rcu_accelerate_cbs()将回调迁移至当前GP的等待队列中不再触发新的GP启动。### 5.3 热插拔与qsmask的再同步当CPU hotplug发生时rcu_report_dead()在CPU death announcement路径中被调用该CPU在rcu_node中的位会被从-qsmaskinitnext中清除。但如果此时GP已经在进行中清除-qsmaskinitnext并不影响正在运行的GP的-qsmask。处理方式分为两个路径- 如果rcu_gp_init()尚未运行GP处于wait阶段rcutree_report_cpu_dead()直接清除rnp-qsmask的对应位。- 如果rcu_gp_init()已经完成了qsmask复制则等待FQS路径中的force_qs_rnp()在发现该CPU不在线时通过rcu_implicit_dynticks_qs()自动上报。### 5.4 expedited GP的short-circuit当rsp-gp_exp_help置位时expedited GP可以截断正在运行的normal GP。在rcu_gp_fqs_loop()中额外检查rcu_exp_gp_seq_done(rsp)cif (rcu_exp_gp_seq_done(rsp)) {/* expedited GP已经完成normal GP可以提前终止 */rcu_gp_fqs(rsp, jiffies_start, jiffies);WRITE_ONCE(rsp-gp_exp_help, false);}在rcu_gp_cleanup()中若gp_exp_help trueWARN_ON被压制qsmask和gp_tasks被显式清零而非触发诊断警告。## 六、Cache line与性能影响rnp-lock是RCU子系统中最热门的锁之一。每个QS上报都需要对叶子rcu_node加锁当系统有数百个CPU时树形层次可以显著缓解锁竞争(CPU0) rcu_data - rnp_leaf[0] --|(CPU1) rcu_data - rnp_leaf[1] ---- rnp_internal[0] -- rnp_root... |(CPU255) - rnp_leaf[15] ---------|每个叶子节点只覆盖16个CPUCONFIG_RCU_FANOUT_LEAF锁的粒度被控制在该范围内。现代内核还引入了rcu_node的-lock的raw spinlock优先级的优化在force_qs_rnp()中如果发现rnp-qsmask全零直接跳过而不加锁这避免了大量空轮询时的缓存行写入。rnp-qsmask本身是一个unsigned long其读写使用WRITE_ONCE/READ_ONCE而非原子操作因为所有操作都在-lock保护下进行。唯一的无锁读取是rcu_gp_kthread()主循环中检查!READ_ONCE(rnp-qsmask)——这只是一个快速路径的peek后续确认仍需要加锁。rdp-passed_quiesc的写操作分布是另一个性能陷阱。在每个tick的rcu_check_quiescent_state()中如果rdp-passed_quiesc已经为true则跳过后续流程。但force_qs_rnp()可能通过dyntick_save_progress_counter()间接置位rdp-passed_quiesc导致rcu_report_qs_rdp()中的passed_quiesc 0写回与tick路径产生竞争。由于passed_quiesc没有用-lock保护这是一个单线程读写优化其可见性依赖于rcu_node锁的间接同步。## 七、边界条件备忘录- **CPU 0的特殊性**rcu_gp_kthread()运行在CPU 0上但触发QS后rcu_report_qs_rdp()不会特殊处理CPU 0的QS。不过rcu_gp_fqs_loop()中调用cond_resched_tasks_rcu_qs()时GP kthread本身会为运行它的CPU隐式上报一个QS。- **NMI上下文中的QS**rcu_nmi_enter()/rcu_nmi_exit()会在dynticks counter上增加偏移量即使idle CPU处理NMI时也不会被误判为始终idle。- **nocbno-CBcallback offloading**当rcu_nocbs启用了特定CPU的callback offloadingrdp-cblist的管理权移交给rcuog/rcuop kthread但QS上报仍然通过相同的rcu_report_qs_rdp()路径只是rcu_accelerate_cbs()的调用被绕过以避免GP kthread在callback处理上产生额外唤醒。- **bypass list**对于offloaded CPUcall_rcu()先写入rdp-nocb_bypass链表当该链表超过阈值或经过一个GP后再刷新到rdp-cblist。这减少了rcu_node锁的竞争频率。- **RCU stall**当jiffies - rdp-gp_start jiffies_till_stall_check且rnp-qsmask仍有未清位时rcu_implicit_dynticks_qs()调用rcu_dump_cpu_stacks()打印堆栈并触发hard lockup检测。这是一条最后一公里的兜底逻辑不应该在正常系统中被触发。- **kfree_rcu()的特殊性**kfree_rcu()背后使用kvfree_rcu_bulk()在新的rcu_tasks等变体中被隔离管理不直接参与rcu_gp_fqs_loop()的传统GP生命周期。- **SMP memory ordering**rcu_gp_init()中WRITE_ONCE(rnp-qsmask, ...)之后必须有smp_mb__after_unlock_lock()内嵌于raw_spin_unlock之后的隐含屏障以保证其他CPU读取qsmask时能看到完整的GP序列号更新。- **tick_nohz_full的干扰**在adaptive-ticks模式下housekeeping CPU之外的CPU在用户态运行时不产生tick。这些CPU只能通过rcu_user_exit()路径上报QS而force_qs_rnp()必须依赖dyntick_save_progress_counter()来检测这导致GP完成时间被FQS定时器间隔jiffies_till_next_fqs默认约3ms钳制。c/* kernel/rcu/tree_plugin.h: rcu_qsctr_help - 调度路径QS计数器 */static void rcu_qsctr_help(struct rcu_data *rdp){rdp-rcu_qs_ctr_snap __this_cpu_read(rcu_qs_ctr);WRITE_ONCE(rdp-passed_quiesc, 1);}/* kernel/rcu/tree.c: __note_gp_changes - GP边界检查 */static bool __note_gp_changes(struct rcu_state *rsp, struct rcu_node *rnp,struct rcu_data *rdp){if (rdp-gp_seq rnp-gp_seq)return false;rdp-gp_seq rnp-gp_seq;/* 如果当前GP需要当前CPU的QS */if (rnp-qsmask rdp-grpmask) {rdp-qs_pending true;rdp-core_needs_qs true;}return true;}kernel/rcu/tree_plugin.h中的rcu_qsctr_help()是调度路径上的QS辅助函数在__schedule()的末尾被调用。它递增per-CPU的rcu_qs_ctr变量该变量被rcu_implicit_dynticks_qs()作为判断依据之一。这个per-CPU计数器没有原子操作开销因为它是单写者仅当前CPU写、多读者FQS扫描时其他CPU读的模式。kernel/rcu/tree.c是整个Grace Period QS检测逻辑的主战场。其状态机设计保证了每个CPU在GP宽限期内恰好报告一次QS而树形qsmask传播在O(log N)的锁获取次数内完成GP终止判定。当CPU长时间处于dynticks idle状态时FQS路径通过dynticks counter快照对比实现无侵入检测当dynticks counter不变化时rcu_implicit_dynticks_qs()逐步升级到RCU stall警告——这一整套机制保证了GP检测在极端条件下的完备性。