首页
/ Linux 内核 TREE RCU 宽限期(Grace Period)内存序保证深度解析

Linux 内核 TREE RCU 宽限期(Grace Period)内存序保证深度解析

2026-09-07 23:49:11作者:劳婵绚Shirley

本文以 Linux 内核官方文档 Tree-RCU-Memory-Ordering.rst(由 Paul E. McKenney 撰写,2017-08-08)为骨架,结合 kernel/rcu/tree.ctree.hrcu.h 的当前源码实现,系统讲解树形 RCU(TREE RCU)如何借助 rcu_node 层次结构、自旋锁临界区与内存屏障,为所有参与宽限期的 CPU(含空闲、离线的 CPU)编织出"读侧临界区前后访问"之间的全局内存序。读完你将能够:从源码层面说清 synchronize_rcu()/call_rcu() 的完整排序链路,理解 raw_spin_lock_rcu_node() 系列宏为何必须内嵌 smp_mb__after_unlock_lock(),并掌握回调注册、宽限期初始化、自愿报告静止态、dyntick、CPU 热插拔、强制静止态、宽限期收尾与回调调用这八大组件的排序职责。

RCU 宽限期的内存序保证到底是什么

RCU 宽限期对非空闲、非离线(non-idle non-offline)的代码提供了极强的内存排序保证,其本质是两条对称的承诺:

  • 后宽限期方向的保证:任何在某个 RCU 宽限期结束之后发生的代码,必然能观察到该宽限期开始之前、位于 RCU 读侧临界区(read-side critical section)内的所有访问的效果;
  • 前宽限期方向的保证:任何在该宽限期开始之前发生的代码,必然观察不到该宽限期结束之后、位于 RCU 读侧临界区内的所有访问的效果。

值得注意:RCU-sched 的读侧临界区包含任何"关闭了抢占"的代码区间。由于每条机器指令都可以看作一段极小的禁止抢占区域,因此可以把 synchronize_rcu() 理解为"打了激素的 smp_mb()"。

RCU 更新方(updater)正是利用这一保证,把一次更新拆成两个阶段

  1. 阶段一:在宽限期之前执行,最常见的情形是把一个元素从 RCU 保护的链式数据结构中摘除;
  2. 阶段二:在宽限期之后执行,最常见的情形是释放(free)这个元素。

这个模型成立的前提是:任何观测到阶段一之前状态的读者,都不得观测到阶段二之后的状态。也就是说,只要有一个读者仍然可能持有着被摘除元素的引用,就不能提前释放它——宽限期就是用来"拦截"这些旧读者的。

RCU 实现通过一整套由锁临界区、内存屏障和每 CPU 处理构成的有序网络来兑现上述承诺。下面从最核心的构建块讲起。

内存序的核心构建块:rcu_node->lock 临界区

Tree RCU 宽限期内存序的"主力"(workhorse)是 rcu_node 结构体 ->lock 字段所保护的临界区。注意 rcu_node->lock__private 字段,内核不允许直接对它调用 raw_spin_lock*/raw_spin_unlock*,而必须使用一组专用包装宏。这些宏定义在 kernel/rcu/rcu.h 的 455~526 行:

  • 加锁:raw_spin_lock_rcu_node()raw_spin_lock_irq_rcu_node()raw_spin_lock_irqsave_rcu_node()
  • 解锁:raw_spin_unlock_rcu_node()raw_spin_unlock_irq_rcu_node()raw_spin_unlock_irqrestore_rcu_node()
  • 尝试加锁:raw_spin_trylock_rcu_node()raw_spin_trylock_irqsave_rcu_node()

它们的实现模式完全一致,以最基本的两个为例:

#define raw_spin_lock_rcu_node(p)					\
do {									\
	raw_spin_lock(&ACCESS_PRIVATE(p, lock));			\
	smp_mb__after_unlock_lock();					\
} while (0)

#define raw_spin_unlock_rcu_node(p)					\
do {									\
	lockdep_assert_irqs_disabled();					\
	raw_spin_unlock(&ACCESS_PRIVATE(p, lock));			\
} while (0)

关键点:所有的加锁函数(包括 raw_spin_trylock_rcu_node(),只要加锁成功)都会在获得锁之后立即调用 smp_mb__after_unlock_lock()

源码注释(kernel/rcu/rcu.h 458~464 行)解释了为什么必须额外补这一道屏障:

Because the rcu_nodes form a tree, the tree traversal locking will observe different lock values, this in turn means that an UNLOCK of one level followed by a LOCK of another level does not imply a full memory barrier; and most importantly transitivity is lost. In order to restore full ordering between tree levels, augment the regular lock acquire functions with smp_mb__after_unlock_lock().

也就是说:rcu_node 是一个树状层次结构,宽限期处理线程会沿树在不同层级的锁之间穿梭。普通自旋锁的"解锁 A 之后再锁 B"并不构成完整内存屏障,跨层传递性(transitivity)会丢失;而 RCU 的宽限期保证恰恰需要跨越多层 rcu_node 的全局传递排序,因此必须在每次锁获取后补上 smp_mb__after_unlock_lock()

由此得到两条基本排序法则:

  1. 同一 rcu_node 结构上:任意一个释放锁函数之前发生的访问,对所有 CPU 而言都先于其后任意一次加锁函数之后发生的访问;
  2. 同一 CPU 跨结构:同一 CPU 上,任意一个释放锁函数之前发生的访问,对所有 CPU 而言都先于该 CPU 稍后执行的(即使是作用在另一个 rcu_node 结构上的)加锁函数之后发生的访问。

Tree RCU 正是用这两条法则,在"任何以某种方式参与了宽限期"的 CPU 之间(包括宽限期中途上线/下线的 CPU)编织出一张有序网络。这与 struct rcu_node 中的位掩码字段(kernel/rcu/tree.h 41 行起)密切配合:qsmask 记录当前宽限期仍需哪些 CPU/子节点报告静止态,qsmaskinit/qsmaskinitnext 记录每 CPU 组的在线状态,gp_seq/gp_seq_needed 追踪宽限期序号。

用 litmus 测试验证屏障的作用

文档给出了一个经典的 litmus 测试来演示这批加锁/解锁函数产生的排序效果:

 1 int x, y, z;
 2
 3 void task0(void)
 4 {
 5   raw_spin_lock_rcu_node(rnp);
 6   WRITE_ONCE(x, 1);
 7   r1 = READ_ONCE(y);
 8   raw_spin_unlock_rcu_node(rnp);
 9 }
10
11 void task1(void)
12 {
13   raw_spin_lock_rcu_node(rnp);
14   WRITE_ONCE(y, 1);
15   r2 = READ_ONCE(z);
16   raw_spin_unlock_rcu_node(rnp);
17 }
18
19 void task2(void)
20 {
21   WRITE_ONCE(z, 1);
22   smp_mb();
23   r3 = READ_ONCE(x);
24 }
25
26 WARN_ON(r1 == 0 && r2 == 0 && r3 == 0);

其中的 WARN_ON() 是在"时间尽头"(所有写入都已传播到整个系统之后)被求值的。如果没有加锁函数提供的 smp_mb__after_unlock_lock(),这个 WARN_ON()可能触发的——例如在 PowerPC 架构上;而正是这些 smp_mb__after_unlock_lock() 调用阻止了它的触发。这一测试直观地表明:任务 0 与任务 1 通过同一把 rcu_node 锁串成了 x → y → z 的传递链,而 task2 的 smp_mb() 又保证了 z 的写入先于 x 的读取,最终三个 r 不可能同时为 0。

快速测验(Quick Quiz):但是 rcu_node 结构锁获取的链条已经保证了"新读者会看到更新方宽限期前的全部访问",也保证了"更新方宽限期后的访问会看到旧读者的全部访问"。那为什么还需要这么多 smp_mb__after_unlock_lock() 调用呢?

解答:因为我们必须为 RCU 的轮询式宽限期原语(polling grace-period primitives)提供排序,例如 get_state_synchronize_rcu()poll_state_synchronize_rcu()。考虑下面的代码:

 CPU 0                                     CPU 1
 ----                                      ----
 WRITE_ONCE(X, 1)                          WRITE_ONCE(Y, 1)
 g = get_state_synchronize_rcu()           smp_mb()
 while (!poll_state_synchronize_rcu(g))    r1 = READ_ONCE(X)
         continue;
 r0 = READ_ONCE(Y)

RCU 保证 r0 == 0 && r1 == 0 这一结局不会出现——即使 CPU 1 正处于 RCU 扩展静止态(空闲或离线),完全不会与 RCU 核心处理产生直接交互。这只能靠那些在锁获取点上的完整屏障来实现。

扩展到空闲 CPU:ct_kernel_exit_state()/ct_kernel_enter_state()

上述方案还必须扩展到空闲(idle)CPU:RCU 宽限期内存序保证要能够覆盖空闲前后跨越了整段 idle 停留期的任何读侧临界区。这一情形由带强排序语义的读-改-写原子操作 atomic_add_return() 处理,它在:

  • idle 进入时于 ct_kernel_exit_state() 中被调用;
  • idle 退出时于 ct_kernel_enter_state() 中被调用。

宽限期内核线程则先调用 ct_rcu_watching_cpu_acquire()(前面配一道完整内存屏障)以及 rcu_watching_snap_stopped_since()(二者都依赖 acquire 语义)来检测空闲 CPU。

快速测验:那对于整个宽限期都保持离线的 CPU 呢?

解答:这样的 CPU 在宽限期开始时就是离线的,因此宽限期根本不会向它们索取静止态。宽限期启动与 CPU 热插拔操作之间的竞争,则由该 CPU 的叶子 rcu_node 结构的 ->lock 按前述机制协调。

扩展到最后一种情形:唤醒阻塞在 synchronize_rcu() 中的任务

还需要处理最后一种情形:唤醒一个阻塞在 synchronize_rcu() 中的任务。该任务可能被亲和(affine)绑定到一颗"还不知道宽限期已经结束"的 CPU 上,因此该 CPU 可能尚未受到宽限期内存序的约束。为此,synchronize_rcu() 代码路径中在从 wait_for_completion() 返回之后,存在一道 smp_mb()

快速测验:什么?在哪里???我根本看不到 wait_for_completion() 返回之后有 smp_mb()!!!

解答:那是因为笔者(Paul E. McKenney)是在撰写本文档的过程中才发现需要这道 smp_mb(),因此它不太可能在 v4.14 之前进入主线。感谢 Lance Roy、Will Deacon、Peter Zijlstra 和 Jonathan Cameron 提出的问题,它们让我意识到那串相当复杂的、足以证明需要这道内存屏障的事件序列。

以当前内核源码(kernel/rcu/tree.c 3400 行起的 synchronize_rcu())为参照,可以看到这条路径已经演化为两条子路径:synchronize_rcu_normal()(3327 行起)通过 rcu_sr_normal_add_req() 把请求挂入队列后用 wait_for_completion(&rs.completion) 睡眠(3355 行),由宽限期内核线程在宽限期真正完成后再将其唤醒;synchronize_rcu() 内核线程的收尾循环在处理根 rcu_node 时于 tree.c 2230 行放置了 smp_mb(),注释明确指出它是"Order against failing poll_state_synchronize_rcu_full(), and also against rcu_nocb_gp_cleanup() -> swait_active()",即确保唤醒者与等待者之间的排序。这正是文档所述那道屏障在现代实现中的落脚之处。

图中的缩略表示法

由于 Tree RCU 宽限期内存序保证最重度依赖 rcu_node 结构的 ->lock,在后续章节的流程图中,文档约定把"加锁 → 临界区 → 解锁 + 屏障"缩略为一个盒子表示。以 rcu_prepare_for_idle() 为例,它是若干负责"把新到达的 RCU 回调与未来宽限期排序"的函数之一(注意 233~235 行正是整个排序的关键三行):

static void rcu_prepare_for_idle(void)
{
  bool needwake;
  struct rcu_data *rdp = this_cpu_ptr(&rcu_data);
  struct rcu_node *rnp;
  int tne;

  lockdep_assert_irqs_disabled();
  if (rcu_rdp_is_offloaded(rdp))
    return;

  /* Handle nohz enablement switches conservatively. */
  tne = READ_ONCE(tick_nohz_active);
  if (tne != rdp->tick_nohz_enabled_snap) {
    if (!rcu_segcblist_empty(&rdp->cblist))
      invoke_rcu_core(); /* force nohz to see update. */
    rdp->tick_nohz_enabled_snap = tne;
    return;
  }
  if (!tne)
    return;

  /*
   * If we have not yet accelerated this jiffy, accelerate all
   * callbacks on this CPU.
   */
  if (rdp->last_accelerate == jiffies)
    return;
  rdp->last_accelerate = jiffies;
  if (rcu_segcblist_pend_cbs(&rdp->cblist)) {
    rnp = rdp->mynode;
    raw_spin_lock_rcu_node(rnp); /* irqs already disabled. */
    needwake = rcu_accelerate_cbs(rnp, rdp);
    raw_spin_unlock_rcu_node(rnp); /* irqs remain disabled. */
    if (needwake)
      rcu_gp_kthread_wake();
  }
}

就本次讨论而言,真正重要的是 第 32~34 行:拿叶子 rcu_node 的锁 → 调用 rcu_accelerate_cbs() 加速回调 → 释放锁。因此文档把整个函数缩略为一张 rcu_node-lock.svg 示意图:盒子代表 rcu_node->lock 临界区,盒子顶部的双线代表额外附加的 smp_mb__after_unlock_lock()

宽限期内存序的八大组件

Tree RCU 的宽限期内存序保证由若干 RCU 组件共同提供,文档按以下顺序逐一剖析:

  1. 回调注册(Callback Registry)
  2. 宽限期初始化(Grace-Period Initialization)
  3. 自愿报告静止态(Self-Reported Quiescent States)
  4. 动态时钟接口(Dynamic Tick Interface,dyntick)
  5. CPU 热插拔接口(CPU-Hotplug Interface)
  6. 强制静止态(Forcing Quiescent States)
  7. 宽限期收尾(Grace-Period Cleanup)
  8. 回调调用(Callback Invocation)

1. 回调注册(Callback Registry)

如果宽限期保证有任何意义,那么任何发生在某次 call_rcu() 调用之前的访问,都必须先于对应的宽限期发生。该部分保证的实现见 TreeRCU-callback-registry.svg

由于 call_rcu() 通常只操作 CPU 本地状态,它自身不提供任何排序保证(无论对自身还是对阶段一,即通常的"摘除链表元素")。它只是把 rcu_head 结构挂到某条每 CPU 队列上;这个 rcu_head 要等到稍后某次 rcu_accelerate_cbs() 调用时才会与某个宽限期建立关联(该函数实现见 kernel/rcu/tree.c 1139 行)。图中的三条路径殊途同归:

  • 左侧路径:经由 note_gp_changes() 调用 rcu_accelerate_cbs()——既可能从 call_rcu() 直接调用(当当前 CPU 被排队的 rcu_head "淹没"时),更可能从 RCU_SOFTIRQ 软中断处理程序中调用;
  • 中间路径:仅当内核以 CONFIG_RCU_FAST_NO_HZ=y 构建时才会走,通过 rcu_prepare_for_idle() 调用 rcu_accelerate_cbs()(即上节展示的代码);
  • 右侧路径:仅当内核以 CONFIG_HOTPLUG_CPU=y 构建时才会走,经由 rcu_advance_cbs()rcu_migrate_callbacksrcutree_migrate_callbacks() 直至 takedown_cpu() 调用 rcu_accelerate_cbs()——后者由一颗幸存的 CPU 在退场 CPU 被完全下线之后调用。

宽限期处理中还有几条会"顺路"调用 rcu_accelerate_cbs() 的代码路径。无论走哪条路,该 CPU 近期排队的所有 rcu_head 结构都会在该 CPU 叶子 rcu_node 结构的 ->lock 保护下与一个未来的宽限期序号(grace-period number)关联起来。在所有这些情况下,都对该 rcu_node 锁的任一先前临界区具有完全排序,也对该任务/该 CPU 此前在任何 rcu_node 锁上的临界区具有完全排序。这种排序保证了下节所述"call_rcu() 之前的访问(尤其是阶段一更新)先于相应宽限期开始"这一结论。

快速测验:那 synchronize_rcu() 呢?

解答synchronize_rcu() 会把 call_rcu() 传给 wait_rcu_gp(),由后者来调用它。所以无论哪条路,最终都会归结到 call_rcu()。(当前源码中,正常路径见 kernel/rcu/tree.csynchronize_rcu_normal(),它把请求加入队列后等待唤醒,本质上仍是"注册一个代表本任务的回调"。)

2. 宽限期初始化(Grace-Period Initialization)

宽限期初始化由宽限期内核线程(grace-period kthread,见 kernel/rcu/tree.c 2304 行的 rcu_gp_kthread())在 rcu_gp_init() 函数中完成,它会多次遍历 rcu_node。由于这棵树在遍历期间状态不断变化(文档以赫拉克利特的河流作喻),遍历被拆成多个阶段展示。

第一步(排序相关的首个动作):推进 rcu_state 结构的 ->gp_seq 宽限期序号计数器,如图 TreeRCU-gp-init-1.svg。序号递增通过 smp_store_release() 完成,这有助于排除 RCU CPU 停驻(stall)检测的误报;注意此时只触碰根 rcu_node

第二步:首次遍历 rcu_node 树,根据自上个宽限期开始以来 CPU 的上线/下线情况更新位掩码。在常见情形下(该 rcu_node 的在线 CPU 数没有在 0 与非 0 之间翻转),这次遍历只扫描叶子 rcu_node。但如果某叶子的在线 CPU 数从 0 转为非 0,会为第一个上线的 CPU 调用 rcu_init_new_rnp();反之若转为 0,会为最后一个退出的 CPU 调用 rcu_cleanup_dead_rnp()。文档用 TreeRCU-gp-init-2.svg 展示了"最左 rcu_node 上线其首颗 CPU、且相邻 rcu_node 无在线 CPU"(或对称的下线场景)时的排序路径。

第三步(最后一趟)rcu_gp_init()广度优先遍历整棵 rcu_node 树,把每个 rcu_node->gp_seq 都置为来自 rcu_state 的新推进值,见 TreeRCU-gp-init-3.svg。这一变更会让每颗 CPU 的下一次 __note_gp_changes() 调用注意到"新宽限期已开始"(详见下一节)。由于宽限期线程是先在根处(推进 rcu_state->gp_seq)启动宽限期、之后才设置各叶子 rcu_node->gp_seq,所以每颗 CPU 观察到宽限期开始的时刻必然晚于宽限期的真实开始时刻

快速测验:那启动宽限期的 CPU 呢?为什么它不能在自己启动宽限期的那一刻就"看到"宽限期开始?

解答:在某种深刻的哲学(且过度拟人化)意义上,启动宽限期的 CPU 确实立刻就知道自己启动了。但如果我们假设 RCU 不具备自我意识,那么即便是启动宽限期的 CPU,也要等第一次调用 __note_gp_changes() 时才真正意识到宽限期开始了。另一方面,这颗 CPU 可能会获得早期通知,因为它在 rcu_gp_init() 最后一趟经过它的叶子 rcu_node 时就会调用 __note_gp_changes()

3. 自愿报告静止态(Self-Reported Quiescent States)

当所有可能阻塞宽限期的实体都报告了静止态(quiescent state),宽限期便可结束。在线且非空闲的 CPU 自行报告静止态,图见 TreeRCU-qs.svg。图中展示的是最后一颗报告静止态的 CPU——正是它宣告了宽限期结束;更早的静止态只会沿 rcu_node 树向上推进,直到撞上仍在等待更多静止态的 rcu_node 为止。但排序依然得以保持,因为之后总会有某个静止态取得该 rcu_node->lock

触发 note_gp_changes()(或直接调用 __note_gp_changes())的事件多种多样;一旦发生,该 CPU 会在持有叶子 rcu_node的情况下注意到新宽限期的开始。因此图中所示的一切执行都发生在宽限期开始之后。并且该 CPU 会把任何在 __note_gp_changes() 调用之前开始的 RCU 读侧临界区视为"宽限期之前开始",也就是宽限期必须等待的临界区。

快速测验:但某个 RCU 读侧临界区可能是在宽限期开始(即前面的 ->gp_seq 推进)之后才开始的,宽限期凭什么要等它?

解答:宽限期确实没有必要等这样的临界区。但允许等它,而且等待它很重要——因为这种"慵懒"的渐进式宽限期启动,比那种瞬间"大爆炸"式的一把梭启动要可扩展得多。

具体执行路径:若 CPU 发生上下文切换,由左侧的 rcu_note_context_switch() 记录静止态;若 CPU 在用户态执行期间收到调度时钟中断,则由右侧的 rcu_sched_clock_irq() 记录静止态。无论哪种,都会把"经过静止态"记入某个每 CPU 变量。随后该 CPU 的下一次 RCU_SOFTIRQ 处理(例如在下一次调度时钟中断之后)中,rcu_core() 会调用 rcu_check_quiescent_state(),发现记录的静止态,进而调用 rcu_report_qs_rdp()kernel/rcu/tree.c 的声明见 163 行)。若它验证该静止态确实适用于当前宽限期,就调用 rcu_report_rnp(),沿 rcu_node 树向上遍历(对应图中底部),不断清除各 rcu_node->qsmask 位,并在结果为零时继续向父节点传播。其中 rcu_report_qs_rnp() 的实际实现位于 kernel/rcu/tree.c 2372 行。

注意:只有当前 CPU 报告的恰是某个 rcu_node 子树所需的最后一个静止态时,遍历才会越过该 rcu_node 继续向上。一个关键推论是:若某 CPU 的遍历停在某个 rcu_node 上,那么之后必然有另一颗 CPU(也可能是它自己)从该点继续向上遍历,而 rcu_node->lock 保证了"第一颗 CPU 的静止态先于第二颗 CPU 遍历的其余部分"。反复套用这一推理即可得出:所有 CPU 的静止态都先于"最后一颗 CPU"穿过根 rcu_node——这颗 CPU 正是清除根 rcu_node->qsmask 最后一位的那一颗。

4. 动态时钟接口(Dynamic Tick Interface,dyntick)

出于能效考虑,RCU 被禁止打扰空闲 CPU。因此空闲 CPU 必须在自己进入/离开 idle 状态时通知 RCU——通知方式是对某个每 CPU 变量执行全序的、带返回值的原子操作。排序效果见 TreeRCU-dyntick.svg

RCU 宽限期内核线程持有该 CPU 对应叶子 rcu_node->lock 来采样这个每 CPU 空闲变量。这意味着:空闲期之前(图中上方的椭圆)的任何 RCU 读侧临界区,都会先于当前宽限期结束;同理,当前宽限期的开始会先于空闲期之后(图中下方的椭圆)的任何 RCU 读侧临界区。把这一点接入完整的宽限期执行流程,详见下文"强制静止态"一节(实现上对应文档所述的 atomic_add_return() + acquire 语义组合,即 ct_kernel_exit_state()/ct_kernel_enter_state()ct_rcu_watching_cpu_acquire()/rcu_watching_snap_stopped_since())。

5. CPU 热插拔接口(CPU-Hotplug Interface)

同理,RCU 也被禁止打扰离线 CPU——它们可能已经被断电甚至从系统中物理移除。因此 CPU 必须在热插拔操作中通知 RCU 自己的来去。排序效果见 TreeRCU-hotplug.svg

由于 CPU 热插拔远没有 idle 切换频繁,其处理更为"重量级":会获取该 CPU 叶子 rcu_node->lock,并更新其 ->qsmaskinitnext。RCU 宽限期内核线程通过采样该掩码来检测"自本次宽限期开始以来有哪些 CPU 已经下线"(这也是 rcu_gp_init() 首次遍历树时更新位掩码的数据来源)。

6. 强制静止态(Forcing Quiescent States)

如前所述,空闲与离线 CPU 无法自行报告静止态,因此宽限期内核线程必须代表它们报告。这一过程称为"强制静止态"(forcing quiescent states),每隔几个 jiffy 重复一次,其排序效果见 TreeRCU-gp-fqs.svg(对应源码为 kernel/rcu/tree.c 2055 行的 rcu_gp_fqs())。

每轮强制都保证至少遍历叶子 rcu_node;若没有因近期 idle/离线而产生的新的静止态,就只遍历叶子。但只要出现了图中左侧"新离线的 CPU"或右侧"新进入 idle 的 CPU",对应的静止态就会被向根方向驱动。与自愿报告静止态一致:向上驱动在遇到"仍有其他 CPU 静止态未到"的 rcu_node 处停下。

快速测验:最左侧那次向根的驱动没有抵达根 rcu_node,意味着该结构之下仍有 CPU 欠着当前宽限期的静止态。既然如此,最右侧那次向根的驱动怎么可能就结束了宽限期?

解答:分析得好!在没有 RCU bug 的前提下这确实不可能。但这张图本身已经够复杂了,于是"简洁性压倒了精确性"。你可以把它当作一种诗意的自由发挥,也可以把它当作一种误导——它的谜底在"整合总览"一节的完整拼接图中才会揭晓(详见拼接图)。

7. 宽限期收尾(Grace-Period Cleanup)

宽限期收尾先广度优先扫描 rcu_node 树推进所有 ->gp_seq 字段,之后再推进 rcu_state->gp_seq。排序效果见 TreeRCU-gp-cleanup.svg

当前源码中,这一收尾逻辑就嵌在宽限期内核线程的循环里(kernel/rcu/tree.c 2195~2254 行附近):先用 rcu_seq_end(&new_gp_seq) 结束旧序号、开启新一轮计数,然后 rcu_for_each_node_breadth_first(rnp) 广度优先地为每个 rcu_node 加锁并 WRITE_ONCE(rnp->gp_seq, new_gp_seq)。其中的注释解释了收尾顺序的良苦用心:先让"当前宽限期结束"完整地记录到所有 rcu_node,再让"下一个宽限期开始"记录到任何 rcu_node,从而避免恶性的宽限期初始化竞争。另外,这段代码还顺带调用 __note_gp_changes() 让本 CPU 立刻推进回调。如图底部椭圆所示,收尾一旦完成,下一个宽限期即可开始

快速测验:那宽限期到底在哪个精确时刻结束?

解答:并不存在一个有用的"单一时刻"能被称为宽限期的结束。最早一个合理候选是"最后一颗 CPU 报告其静止态"的那一刻,但 RCU 可能要再过几毫秒才会意识到这一点;最晚的合理候选是 rcu_state->gp_seq 被更新之时,但在那之前很可能已经有 CPU 完成了各自更新的阶段二。简而言之,要跟 RCU 打交道,你就得学会拥抱不确定性。

8. 回调调用(Callback Invocation)

一旦某 CPU 的叶子 rcu_node->gp_seq 被更新,该 CPU 就可以开始调用那些等待当前宽限期结束的 RCU 回调了。这些回调由 rcu_advance_cbs()kernel/rcu/tree.c 1215 行)识别,而它通常由 __note_gp_changes() 调用。触发来源(见 TreeRCU-callback-invocation.svg):

  • 左侧:调度时钟中断(rcu_sched_clock_irq());
  • 右侧:idle 退出(rcu_cleanup_after_idle(),仅 CONFIG_RCU_FAST_NO_HZ=y 内核)。

无论哪种,最终都会触发 RCU_SOFTIRQ,进而由 rcu_do_batch() 调用回调,使回调得以执行(直接或通过唤醒间接执行)各次更新的阶段二处理。回调调用还可能被大量边界路径触发(例如 CPU 发现自己排队了过多回调)。在所有这些情况下,CPU 都会在调用回调前获取其叶子 rcu_node->lock,从而保持与刚完成宽限期之间所必需的排序——smp_mb__after_unlock_lock() 在节点加锁时建立的屏障保证回调执行完全排在对应 rcu_node->gp_seq 更新之后(kernel/rcu/tree.c 2602~2608 行的注释明确说明了这一"回调执行与先前宽限期完成之间的完全排序")。

但要注意:如果回调函数需要与其他 CPU 通信(例如做一次唤醒),维持排序就是该函数自身(以及被唤醒任务)的责任。举例来说:回调运行在最左叶子 rcu_node 对应的 CPU 上,它唤醒一个将运行在最右叶子 rcu_node 对应 CPU 上的任务,而此时宽限期内核线程可能还没推进到最右叶子——宽限期的内存序尚未抵达那颗 CPU,于是回调函数与被唤醒任务必须自行提供正确的排序(可对照 宽限期收尾 示意图的上半部分理解)。

整合总览:一条完整的排序链

把上面八个组件的排序图"缝合"在一起,就得到了完整的宽限期内存序全景图 TreeRCU-gp.svg。沿这条链路从左到右依次是:

  1. 回调注册:更新方在 call_rcu() 之前完成阶段一(摘除元素),rcu_accelerate_cbs() 在叶子 rcu_node 锁内把回调与未来宽限期关联;
  2. 宽限期初始化:宽限期内核线程先以 smp_store_release() 推进 rcu_state.gp_seq,再广度优先地把新序号写入每个 rcu_node.gp_seq
  3. 静止态采集:在线非空闲 CPU 通过上下文切换/调度时钟中断自报静止态,rcu_report_qs_rdp()/rcu_report_rnp() 沿树向上清除 qsmask;空闲与离线 CPU 则由宽限期线程在 rcu_gp_fqs() 中代为报告;
  4. 宽限期收尾:再次广度优先推进各 rcu_nodegp_seq,宣告宽限期完成;
  5. 回调调用:各 CPU 在持有叶子 rcu_node 锁的前提下执行 rcu_do_batch(),完成更新的阶段二(释放元素)。

整条链路的每一跳都由"持有 rcu_node 锁临界区 + 锁获取处的 smp_mb__after_unlock_lock()"(外加 idle/离线场景下的强原子操作与 acquire 语义、唤醒路径的 smp_mb())提供排序,从而把最初那两条宽限期保证兑现为可供内核同步原语依赖的硬保证。

延伸阅读与源码索引

对希望继续深入当前仓库的读者,推荐以下入口:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

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