4. RCU 数据结构:rcu_head 结构体、rcu_node 树、全局控制块、per-CPU 数据结构

RCU 机制的实现依赖于一套精心设计的内核数据结构。这些数据结构共同构成了 RCU 的“骨架”,负责管理回调、跟踪宽限期状态以及协调多核间的同步。理解这些结构是深入掌握 RCU 工作原理的关键。

4.1 核心回调结构体:rcu_head

rcu_head 是 RCU 机制中最基础的数据结构,它被嵌入到需要被 RCU 保护的资源(如链表节点)中。当写者需要延迟释放一个被 RCU 保护的旧数据对象时,会将一个 rcu_head 实例挂入回调链表,等待宽限期结束后由内核自动调用其回调函数。

成员 类型 描述
next struct rcu_head * 指向下一个待处理回调的指针,形成单向链表。
func rcu_callback_t 回调函数指针,宽限期结束后执行,通常为 kfree() 或自定义释放函数。
// 内核源码 include/linux/types.h
struct rcu_head {
    struct rcu_head *next;
    void (*func)(struct rcu_head *head);
};
使用模式:开发者通常将 rcu_head 作为成员嵌入到自己的数据结构中。例如,在释放一个被 RCU 保护的 struct my_data 时,调用 kfree_rcu(my_data, rcu),其中 rcu 就是该结构体中的 struct rcu_head rcu 成员。

4.2 宽限期管理树:rcu_node

为了高效管理大规模 SMP 系统中的宽限期(Grace Period),RCU 引入了一棵 层次化的 rcu_node 树。这棵树将 CPU 分组,通过自底向上的方式追踪每个 CPU 是否已经度过了一个静止状态(Quiescent State),从而避免全局扫描,显著提升可扩展性。

rcu_node 树的关键成员包括:

  • lock:保护本节点及子节点状态的 spinlock。
  • qsmask:位掩码,标记哪些子节点(或 CPU)尚未报告静止状态。
  • gpnum:本节点观察到的当前宽限期编号。
  • completed:本节点观察到的已完成的宽限期编号。
  • level:节点在树中的层级(根节点为 0)。
  • parent:指向父节点的指针。
设计意图:当所有叶子节点(代表 CPU 或 CPU 组)都报告了静止状态后,信息逐级向上合并,最终根节点检测到宽限期结束。这种树形结构将 O(N) 的全局扫描优化为 O(log N) 的层级传播。
rcu_node 层次树结构 根 rcu_node 中间节点 0 中间节点 1 CPU 组 0 CPU 组 1 CPU 组 2 CPU 组 3 每个叶子节点管理一组 CPU,通过 qsmask 追踪静止状态,逐级向上合并至根节点

4.3 全局控制块:rcu_state

rcu_state 是 RCU 子系统的全局控制结构体,每个 RCU 变体(如 rcu_sched、rcu_bh)都拥有一个独立的 rcu_state 实例。它维护了宽限期的全局状态、根节点指针以及各种统计信息。

关键成员 描述
gpnum 当前正在进行的宽限期编号(全局递增)。
completed 最近一个完成的宽限期编号。
gp_flags 宽限期控制标志,如 GP_FLAG_INIT 表示正在初始化。
node[0] 指向 rcu_node 树根节点的指针(或内嵌数组)。
orphan_don 用于管理 CPU 热插拔时遗留的回调链表。
注意:全局控制块是 RCU 的“大脑”,但对其的访问必须通过精心设计的锁机制(如 root->lock)进行保护,以避免多核竞争。在调试 RCU 相关问题时,检查 rcu_state 的 gpnum 和 completed 值是常用的诊断手段。

4.4 per-CPU 数据结构

为了减少锁竞争和缓存行 bouncing,RCU 大量使用 per-CPU 数据结构。每个 CPU 都维护自己独立的回调链表和状态信息,从而允许读者在本地 CPU 上无锁地执行操作。

主要的 per-CPU 数据结构包括:

  • rcu_data:每个 CPU 一个,包含:
    • mynode:指向该 CPU 所属的 rcu_node 叶子节点。
    • gpnum / completed:该 CPU 感知到的宽限期编号。
    • nxtlist:待处理回调链表头。
    • qsmask:该 CPU 的静止状态掩码。
    • ticks_this_gp:当前宽限期内的时钟 tick 计数。
  • rcu_dynticks:用于跟踪 CPU 是否处于空闲状态(dyntick-idle),这对于判断 CPU 是否可能处于静止状态至关重要。
per-CPU 数据结构与全局控制块的关系 rcu_state (全局) rcu_node 根 CPU 0: rcu_data nxtlist / gpnum CPU 1: rcu_data nxtlist / gpnum CPU 2: rcu_data nxtlist / gpnum CPU 3: rcu_data nxtlist / gpnum 每个 CPU 拥有独立的 rcu_data,通过 rcu_node 树与全局 rcu_state 交互,实现无锁本地操作

4.5 数据结构协同工作流程

当写者调用 synchronize_rcu() 或 call_rcu() 时,这些数据结构按以下步骤协同:

  1. 发起宽限期:rcu_state 递增 gpnum,并通过 rcu_node 树向所有叶子节点广播新的宽限期开始。
  2. 注册回调:call_rcu() 将 rcu_head 挂入当前 CPU 的 rcu_data.nxtlist。
  3. 报告静止状态:每个 CPU 在上下文切换或 idle 时,更新自己的 rcu_data 状态,并通过 rcu_node 树向上报告。
  4. 宽限期结束:当根 rcu_node 检测到所有 CPU 都已报告静止状态后,rcu_state.completed 更新,并触发回调执行。
  5. 执行回调:每个 CPU 遍历自己的 rcu_data.nxtlist,调用 rcu_head.func 完成资源释放。
关键设计哲学:RCU 通过将全局状态(rcu_state)与本地数据(rcu_data)分离,并利用 rcu_node 树进行层级化状态聚合,实现了在数百个 CPU 的系统上仍能保持高效的宽限期管理。读者路径上完全不需要访问这些数据结构,从而保证了极致的读性能。