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