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.h、include/linux/srcu.h,架构相关内存屏障由include/asm/通用头文件提供。
目录
- 一、RCU 基本概念
- 二、源码目录结构
- 三、Kconfig 与 RK3588 默认配置
- 四、整体架构
- 五、核心数据结构
- 六、Grace Period 生命周期
- 七、读侧与写侧 API
- 八、call_rcu 与回调管理
- 九、分段回调链表(rcu_segcblist)
- 十、update.c 公共逻辑
- 十一、rcu_sync 轻量读写切换(sync.c)
- 十二、SRCU 可睡眠 RCU(srcutree.c)
- 十三、Tasks RCU(tasks.h)
- 十四、Expedited GP 与 Poll API
- 十五、RCU CPU Stall 检测
- 十六、RCU_NOCB_CPU 与 Boost
- 十七、RK3588/ARM64 平台说明
- 十八、完整 Grace Period 时序
- 十九、总结
- 附录:源文件清单
一、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 | obj-y += update.o sync.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 | include/linux/rcupdate.h ← 公开 RCU API(读/写侧宏) |
三、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 | CONFIG_PREEMPT_VOLUNTARY=y # 自愿抢占,非 PREEMPT_RCU |
含义:
- RK3588 使用 RCU-sched(
TREE_RCU),读侧临界区由 关抢占/关中断/软中断 界定,而非rcu_read_lock()嵌套计数(那是PREEMPT_RCU路径)。 - 启用了
RCU_EXPERT和RCU_NOCB_CPU,可通过启动参数rcu_nocbs=CPU列表将指定 CPU 的 RCU 回调卸载,降低 HPC/实时负载的 OS jitter。 - 8 核(4+4 big.LITTLE)下 Tree RCU 通常为 两层合并树(64 扇出 >> 8 CPU)。
四、整体架构
1 | ┌─────────────────────────────────────┐ |
Per-CPU 路径:
- 读侧:
rcu_read_lock()→ 访问 RCU 保护数据 →rcu_read_unlock() - 写侧:
call_rcu()将回调入队到本 CPUrcu_data.cblist - GP 线程检测所有 CPU 均报告 QS → 推进
gp_seq→ 将对应段回调移至 DONE →rcuckthread / 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 | struct rcu_node { |
Leaf 节点每位对应一个 rcu_data;非叶节点每位对应一个子 rcu_node。QS 从叶向根逐级清除 qsmask 位,根节点 qsmask==0 时 GP 完成。
5.3 struct rcu_data(Per-CPU)
1 | struct rcu_data { |
定义于 tree.h,每 CPU 一份 DEFINE_PER_CPU(struct rcu_data, rcu_data)。
5.4 GP 序列号(rcu.h)
1 |
|
低 2 bit 为状态位,高位为计数器;rcu_seq_snap() 返回「下一次 GP 完成」对应的序列值,供 poll API 使用。
六、Grace Period 生命周期
GP 由 rcu_gp_kthread 驱动(tree.c),主循环三阶段:
6.1 启动 GP(rcu_gp_init)
- 检查
rcu_state.gp_flags & RCU_GP_FLAG_INIT rcu_seq_start(&rcu_state.gp_seq)递增序列号- 处理 CPU hotplug 缓冲的 online/offline 变更
- 广度优先遍历
rcu_node树,设置qsmask = qsmaskinit - 离线 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)
- 根节点
qsmask == 0→rcu_report_qs_rsp()唤醒 kthread rcu_seq_end(&rcu_state.gp_seq)- 将
cblist中 WAIT 段回调移至 DONE 段 - 唤醒
rcuckthread 或触发RCU_SOFTIRQ执行回调 - 若仍有 pending 回调,设置
RCU_GP_FLAG_INIT启动下一轮 GP
6.4 Quiescent State 上报(rcu_report_qs_rnp)
1 | static void rcu_report_qs_rnp(unsigned long mask, struct rcu_node *rnp, |
七、读侧与写侧 API
7.1 读侧(include/linux/rcupdate.h)
PREEMPT_RCU(CONFIG_PREEMPT_RCU):
1 | void __rcu_read_lock(void); /* current->rcu_read_lock_nesting++ */ |
RCU-sched(RK3588 默认路径):
1 | static inline void __rcu_read_lock(void) { preempt_disable(); } |
此外,关中断、关软中断、NMI 处理程序 也隐式构成 RCU 读侧临界区。
访问 RCU 保护指针:
1 |
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 | void synchronize_rcu(void) |
八、call_rcu 与回调管理
8.1 入队流程
1 | void call_rcu(struct rcu_head *head, rcu_callback_t func) |
8.2 回调执行
- 默认:
RCU_SOFTIRQ或 per-CPUrcuckthread CONFIG_RCU_NOCB_CPU:offloaded CPU 由rcuop/Nkthread 执行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 | head → [DONE...] → [WAIT...] → [NEXT_READY...] → [NEXT...] → NULL |
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 GPrcu_normal_after_boot=1:PREEMPT_RT 默认启动后用 normal GP
十一、rcu_sync 轻量读写切换(sync.c)
struct rcu_sync 提供 动态 fastpath/slowpath 切换,常用于 seqlock 式优化:
1 | void rcu_sync_enter(struct rcu_sync *rsp); /* 读者走 slowpath */ |
状态机: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 | int idx = srcu_read_lock(ssp); |
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 被 KVM(arch/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 | unsigned long s = get_state_synchronize_rcu(); |
十五、RCU CPU Stall 检测
实现于 tree_stall.h(CONFIG_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 时序
十九、总结
| 要点 | 说明 |
|---|---|
| 读侧零锁 | 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_stall、trace/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}。
正在加载留言…