08 · RCU:可抢占读端、优先级提升与线程化回调
结论:PREEMPT_RT 选中
PREEMPTION后自动得到PREEMPT_RCU(读端可抢占,甚至可在 PI 锁上睡眠)、RCU_BOOST默认开(rcub/N线程在宽限期开始 500 ms 后提升被抢占的读者)、use_softirq固定为 0(回调在rcuc/NSCHED_FIFO 1 线程执行,且启动参数不注册)、rcu_normal_after_boot固定为 1(启动后禁止加急宽限期,不再向所有 CPU 发 IPI)。收益:RCU 读区与回调不再是不可抢占区,也不再有 IPI 打断隔离 CPU;代价:宽限期变长、内存回收滞后、synchronize_rcu_expedited()变慢,并且 RT 任务长期霸占 CPU 且优先级高于 1 时会造成 RCU stall。
第一层
先看结论:RT 让 RCU 读端可抢占、回调进线程、被阻塞的读者 500 ms 后被提升、启动后禁止加急宽限期——读区与回调都不再制造延迟,代价是宽限期更长。
第二层
先看结论:四个 Kconfig 默认值(
PREEMPT_RCU/RCU_BOOST/RCU_NOCB_CPU_CB_BOOST/RCU_EXP_KTHREAD)加两个编译期常量(use_softirq=0、rcu_normal_after_boot=1)决定了一切。
Kconfig 联动(kernel/rcu/Kconfig)
| 符号 | 非 RT | RT | 位置 |
|---|---|---|---|
PREEMPT_RCU |
default y if PREEMPTION(PREEMPT/DYNAMIC 内核也有) |
y | :19-22 |
RCU_BOOST |
需 RT_MUTEXES && PREEMPT_RCU && RCU_EXPERT |
default y if PREEMPT_RT,无需 EXPERT |
:198-206 |
RCU_BOOST_DELAY |
500 ms | 500 ms | :211-220 |
RCU_EXP_KTHREAD |
default !PREEMPT_RT && NR_CPUS <= 32 |
默认关(加急 GP 启动后被禁) | :224-234 |
RCU_NOCB_CPU |
default n | default n(推荐手动开 + rcu_nocbs=) |
:238-256 |
RCU_NOCB_CPU_CB_BOOST |
需手动 | default y if PREEMPT_RT:offload 回调用 SCHED_FIFO |
:279-293 |
TASKS_TRACE_RCU_READ_MB |
NR_CPUS < 8 |
default PREEMPT_RT:读端用内存屏障代替 IPI |
:295-298 |
读端
include/linux/rcupdate.h:70-101:PREEMPT_RCU 下 __rcu_read_lock() 是 out-of-line 的嵌套计数,不关抢占;非抢占 RCU 下就是 preempt_disable()。:836-843 的文档分三档:非抢占 RCU 读区不能阻塞;PREEMPT_RCU 读区可被抢占但不能显式阻塞;RT 读区可被抢占、也可以阻塞——但只允许阻塞在受 PI 保护的自旋锁上。Documentation/RCU/whatisRCU.rst:177-179:RT 下 spin_lock() 隐含进入 RCU 读区(rt_spin_lock() 里的 rcu_read_lock(),第 02 章)。
被抢占的读者记入 rnp->blkd_tasks;rcu_read_unlock_special()(kernel/rcu/tree_plugin.h:705-746)在关中断状态退出读区时需要 expedited 处理以及时 deboost,:746 那行 use_softirq && … 是 RT/非 RT 的分叉。
回调执行:softirq → rcuc 线程
kernel/rcu/tree.c:105-109:
static bool use_softirq = !IS_ENABLED(CONFIG_PREEMPT_RT);
#ifndef CONFIG_PREEMPT_RT
module_param(use_softirq, bool, 0444);
#endif
RT 上参数根本不注册(Documentation/admin-guide/kernel-parameters.txt:5214-5222 也这么写)。invoke_rcu_core()(:2877-2885)据此选择 raise_softirq(RCU_SOFTIRQ) 或唤醒 rcuc/N。rcu_cpu_kthread()(:2897-2933)每次最多 10 轮,每轮 local_bh_disable() 包裹,然后 schedule_timeout_idle(2)。
优先级:kthread_prio = IS_ENABLED(CONFIG_RCU_BOOST) ? 1 : 0(:163-169),rcuc/rcub/GP 线程都设为 SCHED_FIFO kthread_prio(tree_plugin.h:1112-1126, 1301-1322;tree.c:5393-5396),rcutree.kthread_prio= 可调(sanitize_kthread_prio() :5541-5558 钳到 1–99)。启动日志会打印 "RCU_SOFTIRQ processing moved to rcuc kthreads" 与 "RCU priority boosting: priority 1 delay 500 ms"。
优先级提升
rcu_boost_kthread(tree_plugin.h:1217-1220)每个叶子 rcu_node 一个,boost_time = GP 开始 + RCU_BOOST_DELAY_JIFFIES(:1287-1294);到点后通过 rt_mutex 把最老的阻塞读者提升到 kthread_prio。加急 GP 阻塞的读者立即提升(Kconfig:219-220)。
禁止加急宽限期
kernel/rcu/update.c:60-62:rcu_normal_after_boot = IS_ENABLED(CONFIG_PREEMPT_RT),RT(无 NO_HZ_FULL)下参数不暴露。所有 synchronize_rcu_expedited() 启动后降级为普通 GP(kernel-parameters.txt:5595-5600)——不再有 IPI 广播,但模块卸载、网络命名空间销毁、部分 sysfs 操作会明显变慢。
与 softirq 的其它交叉
- ksoftirqd 在 RT 上不报告 RCU 静止态(
kernel/softirq.c:591-592)。 - lockdep 等待类型:
rcu_lock_map/rcu_bh_lock_map都是LD_WAIT_CONFIG(update.c:290, 299),因为 RT 下 bh 可抢占。 Documentation/RCU/stallwarn.rst:56-60:RT 内核里一个 CPU 绑定的实时任务、优先级高于 RCU softirq 线程,会让回调永远得不到执行,PREEMPT_RCU 下还会让宽限期永远无法完成。
ARM64 注
- RCU 的 RT 分支与架构无关;arm64 上值得注意的是
TASKS_TRACE_RCU_READ_MB default PREEMPT_RT(kernel/rcu/Kconfig:298)避免向隔离 CPU 发 IPI,而 arm64 的 IPI 在arch/arm64/kernel/smp.c:1077-1084注册、伪 NMI 开启时部分 IPI 变成 NMI(:1016-1025)。 nohz_full+rcu_nocbs组合在 arm64 上同样有效;arch timer 在隔离 CPU 上不再周期触发,只保留单次到期。
性能影响
净影响:无 IPI 打断隔离 CPU;
kfree_rcu回收滞后、expedited 变慢;RT 任务长跑且优先级 > 1 会导致 stall。
| 指标 | 非 RT | RT | 说明 |
|---|---|---|---|
| 读区对最坏延迟的贡献 | 非抢占 RCU:整段 | ≈ 0 | |
| 回调执行 | softirq 就地,零切换 | rcuc 线程,可被抢占 |
多一次切换;RT 任务可压住它 |
| 宽限期长度 | 短(读者不可抢占) | 长(读者可被抢占很久,500 ms 后才 boost) | kfree_rcu 内存回收滞后,内存压力上升 |
| 加急 GP | ms 级完成,IPI 打断所有 CPU | 降级为普通 GP:数十 ms – 数百 ms | 模块卸载/命名空间销毁变慢 |
| 隔离 CPU 被 IPI 打断 | 会 | 不会(加急被禁 + TASKS_TRACE_RCU_READ_MB) |
RT 的隔离收益 |
| stall 风险 | 低 | RT 任务 > prio 1 且长跑时高 | 需 rcu_nocbs + 提高 kthread_prio 或让出 CPU |
源码定位
| 主题 | 位置 |
|---|---|
| Kconfig | kernel/rcu/Kconfig:8-34, 143-170, 198-298 |
use_softirq / invoke_rcu_core / rcuc |
kernel/rcu/tree.c:105-109, 2877-2958 |
kthread_prio |
kernel/rcu/tree.c:163-169, 4928, 4959, 5393-5396, 5541-5558 |
| boost 线程 | kernel/rcu/tree_plugin.h:1112-1126, 1217-1220, 1287-1322 |
| 读端 | include/linux/rcupdate.h:70-101, 832-843;kernel/rcu/tree_plugin.h:705-746 |
rcu_normal_after_boot |
kernel/rcu/update.c:60-62, 667-668 |
| 文档 | Documentation/admin-guide/kernel-parameters.txt:5214-5222, 5595-5600;Documentation/RCU/stallwarn.rst:56-60;Documentation/RCU/whatisRCU.rst:177-179 |
调优要点
- 隔离 CPU 一律
rcu_nocbs=<隔离集>,回调由rcuo线程在 housekeeping CPU 执行;配合CONFIG_RCU_NOCB_CPU=y。 - 长跑的 RT 线程若优先级 > 1,要么周期性让出 CPU,要么
rcutree.kthread_prio=<高于业务线程>(会让 RCU 回调抢占业务,权衡)。 - 需要频繁加/卸载模块或大量创建销毁 netns 的系统,接受 expedited 变慢的现实,或在
NO_HZ_FULL内核上显式rcupdate.rcu_normal_after_boot=0(会重新引入 IPI)。
小结
- RT 上
spin_lock()隐含进入 RCU 读区,读区可以在 PI 锁上阻塞——这是 RCU 与锁替换互相配合的结果。 rcuc/rcub/GP 线程默认 FIFO 1,比 irq 线程(50)低得多;rcutree.kthread_prio=是唯一旋钮。- 加急宽限期被禁是「隔离 CPU 不被 IPI 打扰」的代价,模块卸载等操作会明显变慢。
- 承上启下:RCU 早在 6.12 之前就已 RT 化;真正让 6.12 成为「首个内置 RT 的 LTS」的最后一块是 printk——下一章。