kernel/rcu 内核 RCU(Read-Copy Update)机制与原理详解

kernel/rcu 内核 RCU(Read-Copy Update)机制与原理详解

源码路径rk3588/kernel-6.1/kernel/rcu/
内核版本:Linux 6.1(RK3588 平台)
平台:RK3588(4×Cortex-A76 + 4×Cortex-A55 big.LITTLE,ARM64)

该目录实现 Linux 内核 Read-Copy Update(RCU) 子系统的核心逻辑:一种以读侧几乎零开销、写侧延迟回收为特征的同步机制。RCU 广泛用于链表遍历、路由表、模块卸载、内存释放(kfree_rcu)等场景。Linux 6.1 在 SMP 系统上默认启用 Tree RCU(层次化合并树),配合 分段回调链表(segcblist) 管理多 grace period 并发的 call_rcu() 回调。

定位:内核最基础的「读多写少」同步原语之一;公开 API 在 include/linux/rcupdate.hinclude/linux/srcu.h,架构相关内存屏障由 include/asm/ 通用头文件提供。


目录


一、RCU 基本概念

1.1 设计思想

RCU 将同步问题拆成三部分:

角色 行为 典型 API
读侧(Reader) 进入临界区后无锁访问共享数据;允许看到旧版本 rcu_read_lock() / rcu_dereference()
写侧(Updater) 复制-修改-发布新版本;旧版本不能立即释放 rcu_assign_pointer() + synchronize_rcu() / call_rcu()
回收(Reclaimer) 等所有读侧退出后再执行回调释放内存 call_rcu(head, func)

Grace Period(宽限期):从写侧发布更新到所有 CPU 上「曾经存在的读侧临界区」全部结束之间的时间窗口。只有 GP 结束后,旧数据才可安全回收。

1.2 RCU 变体对比

变体 配置 读侧临界区 典型用途
RCU-sched TREE_RCU + 非抢占 关抢占/关中断/软中断 传统内核路径
RCU-preempt PREEMPT_RCU rcu_read_lock() 可嵌套、可抢占 抢占内核
SRCU TREE_SRCU 可睡眠、每实例独立 文件系统、KVM
Tasks RCU TASKS_RCU* 基于任务调度状态 特殊场景(tracing 等)
Tiny RCU TINY_RCU UP 非抢占简化版 嵌入式单核

二、源码目录结构

2.1 编译依赖(Makefile)

1
2
3
4
5
6
7
8
9
10
obj-y += update.o sync.o
obj-$(CONFIG_TREE_SRCU) += srcutree.o
obj-$(CONFIG_TINY_SRCU) += srcutiny.o
obj-$(CONFIG_TREE_RCU) += tree.o
obj-$(CONFIG_TINY_RCU) += tiny.o
obj-$(CONFIG_RCU_NEED_SEGCBLIST) += rcu_segcblist.o
# 测试模块(默认不编译进内核)
obj-$(CONFIG_RCU_TORTURE_TEST) += rcutorture.o
obj-$(CONFIG_RCU_SCALE_TEST) += rcuscale.o
obj-$(CONFIG_RCU_REF_SCALE_TEST) += refscale.o
文件 作用
tree.c Tree RCU 主实现:GP kthread、QS 上报、call_rcu、synchronize_rcu
update.c 各 RCU 变体共享的更新侧逻辑、lockdep、expedited 模式
sync.c rcu_sync 结构:动态读写路径切换
rcu_segcblist.c 分段回调链表操作
srcutree.c 层次化 SRCU 实现
srcutiny.c 单核 SRCU
tiny.c 单核非抢占 Tiny RCU
tree.h / tree_plugin.h Tree RCU 内部结构与 PREEMPT/SCHED 插件
tree_nocb.h No-CBs CPU 卸载回调
tree_stall.h CPU stall 警告
tree_exp.h Expedited grace period
tasks.h Tasks RCU 通用框架
rcu.h GP 序列号(rcu_seq_*)等公共内联函数

2.2 头文件包含关系

1
2
3
4
5
include/linux/rcupdate.h     ← 公开 RCU API(读/写侧宏)
include/linux/rcu_segcblist.h ← 分段链表定义
include/linux/srcu.h ← SRCU 公开 API
kernel/rcu/rcu.h ← 内部 GP 序列号管理
kernel/rcu/tree.h ← rcu_node / rcu_data / rcu_state

三、Kconfig 与 RK3588 默认配置

3.1 关键 Kconfig 选项

配置项 默认 说明
CONFIG_TREE_RCU SMP=y 层次化 Tree RCU
CONFIG_PREEMPT_RCU PREEMPTION=y 抢占 RCU(需内核可抢占)
CONFIG_TINY_RCU !SMP && !PREEMPTION 单核简化版
CONFIG_TREE_SRCU SRCU && !TINY_RCU 完整 SRCU
CONFIG_RCU_FANOUT 64(64 位) 合并树扇出
CONFIG_RCU_FANOUT_LEAF 16 叶节点扇出
CONFIG_RCU_NOCB_CPU Expert/NO_HZ_FULL 卸载回调到 kthread
CONFIG_RCU_BOOST PREEMPT_RT 优先级提升防 GP 阻塞

3.2 RK3588 Rockchip defconfig 特点

arch/arm64/configs/rockchip_linux_defconfig 为例:

1
2
3
4
CONFIG_PREEMPT_VOLUNTARY=y      # 自愿抢占,非 PREEMPT_RCU
CONFIG_RCU_EXPERT=y
CONFIG_RCU_NOCB_CPU=y # 支持将回调卸载到 rcuo kthread
CONFIG_RCU_CPU_STALL_TIMEOUT=60

含义

  • RK3588 使用 RCU-schedTREE_RCU),读侧临界区由 关抢占/关中断/软中断 界定,而非 rcu_read_lock() 嵌套计数(那是 PREEMPT_RCU 路径)。
  • 启用了 RCU_EXPERTRCU_NOCB_CPU,可通过启动参数 rcu_nocbs=CPU列表 将指定 CPU 的 RCU 回调卸载,降低 HPC/实时负载的 OS jitter。
  • 8 核(4+4 big.LITTLE)下 Tree RCU 通常为 两层合并树(64 扇出 >> 8 CPU)。

四、整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
                 ┌─────────────────────────────────────┐
│ rcu_gp_kthread │
│ GP 初始化 → FQS 强制 QS → GP 清理 │
└──────────────┬──────────────────────┘

┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ rcu_node │◄──parent───│ rcu_node │◄──parent───│ rcu_node │ (合并树)
│ (root) │ │ (internal)│ │ (leaf) │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ qsmask │ │ qsmask
│ │ ▼
│ │ ┌───────────┐
│ │ │ rcu_data │ (per-CPU)
│ │ │ cblist │
│ │ └───────────┘
▼ ▼
grace period 完成判定 quiescent state 逐级上报

Per-CPU 路径

  1. 读侧:rcu_read_lock() → 访问 RCU 保护数据 → rcu_read_unlock()
  2. 写侧:call_rcu() 将回调入队到本 CPU rcu_data.cblist
  3. GP 线程检测所有 CPU 均报告 QS → 推进 gp_seq → 将对应段回调移至 DONE → rcuc kthread / softirq 执行回调

五、核心数据结构

5.1 struct rcu_state(全局状态)

位于 tree.c,包含:

  • gp_seq:grace period 序列号(rcu_seq_* 管理)
  • gp_flags / gp_wq:GP kthread 唤醒标志与等待队列
  • node[]:合并树节点数组
  • expedited_sequence:加速 GP 序列号

5.2 struct rcu_node(合并树节点)

1
2
3
4
5
6
7
8
9
10
11
struct rcu_node {
raw_spinlock_t lock;
unsigned long gp_seq;
unsigned long qsmask; /* 尚未报告 QS 的 CPU/子节点位图 */
unsigned long qsmaskinit; /* 本 GP 初始 online CPU 掩码 */
int grplo, grphi; /* 管辖 CPU 范围 */
u8 level; /* 0=root */
struct rcu_node *parent;
struct list_head blkd_tasks; /* PREEMPT_RCU:阻塞 GP 的任务 */
// ... boost、expedited、nocb 等字段
};

Leaf 节点每位对应一个 rcu_data;非叶节点每位对应一个子 rcu_node。QS 从叶向根逐级清除 qsmask 位,根节点 qsmask==0 时 GP 完成。

5.3 struct rcu_data(Per-CPU)

1
2
3
4
5
6
7
8
9
10
struct rcu_data {
unsigned long gp_seq;
union rcu_noqs cpu_no_qs;
bool core_needs_qs;
struct rcu_node *mynode;
unsigned long grpmask;
struct rcu_segcblist cblist; /* 分段回调链表 */
long blimit; /* 单次批量处理回调上限 */
// dynticks、expedited、nocb 等
};

定义于 tree.h,每 CPU 一份 DEFINE_PER_CPU(struct rcu_data, rcu_data)

5.4 GP 序列号(rcu.h

1
2
3
4
5
6
7
#define RCU_SEQ_CTR_SHIFT  2
#define RCU_SEQ_STATE_MASK ((1 << RCU_SEQ_CTR_SHIFT) - 1)

static inline void rcu_seq_start(unsigned long *sp); /* GP 开始 */
static inline void rcu_seq_end(unsigned long *sp); /* GP 结束 */
static inline unsigned long rcu_seq_snap(unsigned long *sp); /* 快照 */
static inline bool rcu_seq_done(unsigned long *sp, unsigned long s);

低 2 bit 为状态位,高位为计数器;rcu_seq_snap() 返回「下一次 GP 完成」对应的序列值,供 poll API 使用。


六、Grace Period 生命周期

GP 由 rcu_gp_kthread 驱动(tree.c),主循环三阶段:

6.1 启动 GP(rcu_gp_init

  1. 检查 rcu_state.gp_flags & RCU_GP_FLAG_INIT
  2. rcu_seq_start(&rcu_state.gp_seq) 递增序列号
  3. 处理 CPU hotplug 缓冲的 online/offline 变更
  4. 广度优先遍历 rcu_node 树,设置 qsmask = qsmaskinit
  5. 离线 CPU 直接报告 QS(rcu_report_qs_rnp

6.2 强制 Quiescent State(rcu_gp_fqs_loop

若 GP 长时间未完成:

  • 调度器 tick / idle 入口报告 QS(rcu_check_callbacks
  • rcu_force_quiescent_state() 向其他 CPU 发 IPI 或唤醒
  • PREEMPT_RCU:rcu_preempt_check_blocked_tasks 处理阻塞读侧

6.3 结束 GP(rcu_gp_cleanup

  1. 根节点 qsmask == 0rcu_report_qs_rsp() 唤醒 kthread
  2. rcu_seq_end(&rcu_state.gp_seq)
  3. cblist 中 WAIT 段回调移至 DONE 段
  4. 唤醒 rcuc kthread 或触发 RCU_SOFTIRQ 执行回调
  5. 若仍有 pending 回调,设置 RCU_GP_FLAG_INIT 启动下一轮 GP

6.4 Quiescent State 上报(rcu_report_qs_rnp

1
2
3
4
5
6
7
8
9
10
11
12
13
static void rcu_report_qs_rnp(unsigned long mask, struct rcu_node *rnp,
unsigned long gps, unsigned long flags)
{
for (;;) {
rnp->qsmask &= ~mask; /* 清除本节点 QS 位 */
if (rnp->qsmask || rnp->gp_seq != gps)
break;
mask = rnp->grpmask;
rnp = rnp->parent; /* 向父节点传播 */
}
if (!rnp) /* 到达 root 且 qsmask==0 */
rcu_report_qs_rsp(flags);
}

七、读侧与写侧 API

7.1 读侧(include/linux/rcupdate.h

PREEMPT_RCUCONFIG_PREEMPT_RCU):

1
2
3
4
void __rcu_read_lock(void);   /* current->rcu_read_lock_nesting++ */
void __rcu_read_unlock(void);
#define rcu_read_lock() __rcu_read_lock()
#define rcu_read_unlock() __rcu_read_unlock()

RCU-sched(RK3588 默认路径):

1
2
static inline void __rcu_read_lock(void) { preempt_disable(); }
static inline void __rcu_read_unlock(void) { preempt_enable(); }

此外,关中断、关软中断、NMI 处理程序 也隐式构成 RCU 读侧临界区。

访问 RCU 保护指针

1
2
#define rcu_dereference(p) rcu_dereference_check(p, 0)
#define rcu_assign_pointer(p, v) smp_store_release(&p, v)

7.2 写侧同步

API 行为
synchronize_rcu() 阻塞直到 GP 完成
call_rcu(head, func) 异步 GP 后调用 func
kfree_call_rcu(head, func) 专用内存释放回调
get_state_synchronize_rcu() 非阻塞快照
poll_state_synchronize_rcu(old) 轮询 GP 是否完成
cond_synchronize_rcu(old) 仅在必要时等待

synchronize_rcu() 核心逻辑(tree.c):

1
2
3
4
5
6
7
8
9
10
11
12
void synchronize_rcu(void)
{
if (!rcu_blocking_is_gp()) {
if (rcu_gp_is_expedited())
synchronize_rcu_expedited();
else
wait_rcu_gp(call_rcu);
return;
}
/* 单 CPU 早期启动阶段:简化为内存屏障 + gp_seq 递增 */
...
}

八、call_rcu 与回调管理

8.1 入队流程

1
2
3
4
5
6
7
8
9
void call_rcu(struct rcu_head *head, rcu_callback_t func)
{
head->func = func;
local_irq_save(flags);
rdp = this_cpu_ptr(&rcu_data);
rcu_segcblist_enqueue(&rdp->cblist, head);
__call_rcu_core(rdp, head, flags); /* 必要时强制 GP */
local_irq_restore(flags);
}

8.2 回调执行

  • 默认RCU_SOFTIRQ 或 per-CPU rcuc kthread
  • CONFIG_RCU_NOCB_CPU:offloaded CPU 由 rcuop/N kthread 执行
  • blimit:单次处理回调数量上限,防止长时间占用 CPU

8.3 过载检测

当 per-CPU 回调数超过 qhimark 时,__call_rcu_core() 调用 rcu_force_quiescent_state() 加速 GP;叶节点 cbovldmask 标记过载 CPU。


九、分段回调链表(rcu_segcblist)

rcu_segcblist 将回调按 GP 阶段分为 4 段(include/linux/rcu_segcblist.h):

含义
DONE RCU_DONE_TAIL GP 已完成,可立即调用
WAIT RCU_WAIT_TAIL 等待当前 GP
NEXT_READY RCU_NEXT_READY_TAIL 下一 GP 已确定
NEXT RCU_NEXT_TAIL 新入队,GP 未启动
1
2
head → [DONE...] → [WAIT...] → [NEXT_READY...] → [NEXT...] → NULL
↑tails[0] ↑tails[1] ↑tails[2] ↑tails[3]

GP 结束时,rcu_segcblist_advance() 将 WAIT 段推进到 DONE;rcu_segcblist_accelerate() 在 GP 提前启动时推进 NEXT_READY。


十、update.c 公共逻辑

update.c 被所有 RCU 变体链入(obj-y),主要提供:

功能 说明
rcu_gp_is_normal() / rcu_gp_is_expedited() 运行时 GP 模式(rcu_normal / rcu_expedited 模块参数)
rcu_read_lock_sched_held() lockdep 检查
__wait_rcu_gp() 批量等待多个 GP
finish_rcuwait() GP 等待辅助
debugobjects init_rcu_head() / destroy_rcu_head()
rcu_set_runtime_mode() 启动后切换 expedited/normal 模式

启动参数(rcupdate. 前缀):

  • rcu_expedited=1:所有 GP 走 expedited 路径
  • rcu_normal=1:强制 normal GP
  • rcu_normal_after_boot=1:PREEMPT_RT 默认启动后用 normal GP

十一、rcu_sync 轻量读写切换(sync.c)

struct rcu_sync 提供 动态 fastpath/slowpath 切换,常用于 seqlock 式优化:

1
2
3
void rcu_sync_enter(struct rcu_sync *rsp);  /* 读者走 slowpath */
void rcu_sync_exit(struct rcu_sync *rsp); /* GP 后恢复 fastpath */
bool rcu_sync_is_idle(struct rcu_sync *rsp);

状态机:GP_IDLE → GP_ENTER → GP_PASSED → GP_EXIT → GP_IDLE

  • rcu_sync_enter() 首次调用时 synchronize_rcu() 同步等待
  • 后续通过 call_rcu(rcu_sync_func) 异步完成 GP 转换
  • 紧密连续的 enter/exit 可合并 GP,避免多余等待

十二、SRCU 可睡眠 RCU(srcutree.c)

SRCU(Sleepable RCU)允许在 读侧临界区内睡眠,每个 srcu_struct 实例独立。

12.1 核心 API

1
2
3
4
5
6
int idx = srcu_read_lock(ssp);
... /* 可阻塞 */
srcu_read_unlock(ssp, idx);

synchronize_srcu(ssp);
call_srcu(ssp, head, func);

12.2 实现要点

  • 使用 per-CPU 计数器数组srcu_lock_count / srcu_unlock_count)跟踪读侧
  • 两个计数器组交替使用(类似 classic RCU 的双缓冲)
  • 层次化 srcu_node 树(与 Tree RCU 类似)合并 QS
  • srcu_size_big:CPU 数 ≥ 128 或 contention 高时切换「大」实现

RK3588 上 SRCU 被 KVMarch/arm64/kvm/Kconfig select SRCU)、文件系统等模块使用。


十三、Tasks RCU(tasks.h)

Tasks RCU 不依赖 per-CPU QS,而是通过 扫描任务状态 判断 GP:

变体 配置 QS 条件
Tasks RCU TASKS_RCU voluntary context switch、idle、用户态
Tasks Rude RCU TASKS_RUDE_RCU 含抢占式 context switch
Tasks Trace RCU TASKS_TRACE_RCU rcu_read_lock_trace() 标记

通用框架 struct rcu_tasks 定义 GP 函数、回调队列、IPI 统计等;默认 不启用,需 RCU_EXPERT 强制开启。


十四、Expedited GP 与 Poll API

14.1 Expedited Grace Period

正常 GP 等待自然 tick/idle QS;Expedited GP 主动 IPI 所有 CPU 强制 QS,延迟更低但开销更大。

  • 实现于 tree_exp.h
  • synchronize_rcu_expedited() / rcu_gp_is_expedited()
  • PREEMPT_RT 内核默认 rcu_normal_after_boot=1 禁用 expedited

14.2 Poll 系列 API

适用于 避免阻塞 的场景(如自旋锁内不能睡眠):

1
2
3
4
5
6
unsigned long s = get_state_synchronize_rcu();
/* ... 做其他工作 ... */
if (poll_state_synchronize_rcu(s))
/* GP 已完成 */;
else
cond_synchronize_rcu(s); /* 必要时才阻塞 */

十五、RCU CPU Stall 检测

实现于 tree_stall.hCONFIG_RCU_STALL_COMMON):

  • 若某 CPU 在 GP 期间长时间未报告 QS,触发 stall 警告
  • RK3588 defconfig:CONFIG_RCU_CPU_STALL_TIMEOUT=60(60 秒)
  • 可通过 rcu_cpu_stall_timeout 启动参数调整
  • stall 信息包含 GP 序列号、阻塞任务栈、nocb 状态等

常见 stall 原因:CPU 关中断过长、PREEMPT_RCU 读侧阻塞、驱动 bug。


十六、RCU_NOCB_CPU 与 Boost

16.1 RCU_NOCB_CPU

tree_nocb.h 实现:指定 CPU 的 RCU 回调由 rcuop/N kthread 执行,本 CPU 仅入队。

  • 启动参数:rcu_nocbs=0-3(卸载 CPU 0–3)
  • Rockchip defconfig 启用 CONFIG_RCU_NOCB_CPU,适合 big.LITTLE 上将回调卸载到 little 核
  • 代价:call_rcu() 开销增加、可能增加 context switch

16.2 RCU_BOOST

CONFIG_RCU_BOOST(PREEMPT_RCU + RT_MUTEXES):当高优先级任务被 RCU 读侧阻塞时,提升读侧任务优先级,防止 GP 饥饿。RK3588 自愿抢占配置下通常 不启用 PREEMPT_RCU,故 RCU_BOOST 不适用。


十七、RK3588/ARM64 平台说明

17.1 配置总结

项目 RK3588 典型值
RCU 变体 TREE_RCU + RCU-sched
内核抢占 PREEMPT_VOLUNTARY(非 PREEMPT_RCU)
CPU 数 8(合并树 1–2 层)
RCU_FANOUT 64(64 位默认)
RCU_NOCB_CPU 已启用(Expert)
Stall 超时 60 秒
SRCU 启用(KVM 等依赖)

17.2 内存序

ARM64 为 弱内存序 架构。RCU 通过以下保证可见性:

  • 读侧:rcu_dereference()READ_ONCE + 编译屏障
  • 写侧:rcu_assign_pointer() 使用 smp_store_release()
  • GP 完成:call_rcu() 回调前,各 CPU 至少执行一次 full memory barrier

详见内核文档 Documentation/RCU/Design/Memory-Ordering/Tree-RCU-Memory-Ordering.rst

17.3 big.LITTLE 注意点

  • A76/A55 共享同一 RCU 合并树;GP 需 所有 online CPU 报告 QS
  • CPU hotplug(core off/on)由 rcu_cpu_notify 处理,更新 qsmaskinit
  • 若使用 rcu_nocbs 将回调卸载到 A55,需注意 kthread 亲和性与 cache 一致性
  • idle 状态:context_tracking 将 idle 视为 extended quiescent state,A55 深 idle 不影响 GP

17.4 相关架构选项

  • CONFIG_MMU_GATHER_RCU_TABLE_FREE(arm64 Kconfig):页表释放走 call_rcu
  • KVM 模块 select SRCU

十八、完整 Grace Period 时序

rcuc/softirqrcu_gp_kthreadrcu_data (per-CPU)读侧 (rcu_read_lock)写侧 (call_rcu)rcuc/softirqrcu_gp_kthreadrcu_data (per-CPU)读侧 (rcu_read_lock)写侧 (call_rcu)等待所有 CPU QSrcu_segcblist_enqueue(NEXT段)触发 GP 启动 (gp_flags)rcu_gp_init() 设置 qsmasktick/idle 报告 QSrcu_report_qs_rnp 向上合并rcu_gp_cleanup() gp_seq_endadvance cblist WAIT→DONE唤醒回调执行head->func() 释放旧数据

十九、总结

要点 说明
读侧零锁 RCU 读路径无原子操作,仅关抢占/关中断
延迟回收 写侧通过 GP + 回调异步释放,换取读侧性能
Tree 合并 rcu_node 层次树将 8~数千 CPU 的 QS 合并为 O(log N)
segcblist 分段链表支持多 GP 并发、批量回调
RK3588 RCU-sched + 可选 NOCB 卸载;8 核 big.LITTLE 默认两层树
扩展 SRCU(可睡眠)、Tasks RCU(任务扫描)、rcu_sync(读写切换)

RCU 是 Linux 内核基础设施,networking、VFS、module、mm 等子系统大量依赖本目录实现。调试 RCU 问题时可关注:rcu_cpu_stalltrace/events/rcu.h 追踪点、以及 rcutorture 压力测试模块。


附录:源文件清单

文件 行数 说明
tree.c 4886 Tree RCU 主实现
tasks.h 1962 Tasks RCU 框架
tree_nocb.h 1590 No-CBs CPU 卸载
tree_plugin.h 1304 PREEMPT/SCHED 插件
tree_exp.h 1122 Expedited GP
tree_stall.h 1045 CPU stall 检测
rcuscale.c 924 RCU 规模测试
refscale.c 897 RCU 引用规模测试
srcutree.c 1854 Tree SRCU
rcutorture.c 3654 RCU torture 测试
rcu.h 599 GP 序列号等公共定义
rcu_segcblist.c 633 分段回调链表
update.c 628 公共更新侧逻辑
tree.h 489 内部数据结构
srcutiny.c 273 Tiny SRCU
tiny.c 267 Tiny RCU
sync.c 206 rcu_sync
rcu_segcblist.h 153 segcblist 内联辅助
合计 22486

行数统计:Linux 6.1,wc -l kernel/rcu/*.{c,h}

文章互动

阅读 --

留言

0 条留言

正在加载留言…