Linux RT 分析Linux 6.12.107

案例 01 · munmap 压测中的 rcuc:回调从哪来、为什么在 RT 上「卡」

结论munmap 是 RCU 回调的大生产者——arm64 上页表页经 MMU_GATHER_RCU_TABLE_FREEcall_rcu 释放,VMA 和 maple tree 节点也各走一次 call_rcu。非 RT 内核把这些回调在 RCU_SOFTIRQ 里就地跑掉;RT 内核(use_softirq=0)把它们全部排给每 CPU 一个的 rcuc/N 线程(SCHED_FIFO 1),而 RT 的宽限期更长(读者可抢占、无加急 GP),于是回调先堆积到上万、再成批爆发:每批只受 rcu_resched_ns=3 ms 限制,只让给 ≥ FIFO 1 的线程,10 轮后再睡 2 个 jiffies——SCHED_OTHER 的测试线程被整段压住,top 里看到 rcuc/N 高 CPU。堆积还带来第二个恶果:页表页迟迟不归还 → 空闲页下降 → tlb_remove_table()GFP_NOWAIT 分配失败 → 走 tlb_remove_table_sync_one() 向所有 CPU 发 IPI 并等待,munmap 自己变慢、隔离核也被打断。首选对策是 rcu_nocbs= 把回调挪到 housekeeping 核的 rcuo 线程。

现象

第一层:回调在哪里执行

先看结论:非 RT 在 softirq 里就地跑、不可抢占但零切换;RT 在 rcuc/N 线程里跑、可抢占但先堆后爆、以 FIFO 1 压住普通线程。

非 RTmunmap 的 RCU 回调在 RCU_SOFTIRQ 里就地执行
非 RT:munmap 的 RCU 回调在 RCU_SOFTIRQ 里就地执行
同一场景 · 换成 PREEMPT_RT 内核
PREEMPT_RT同样的回调全部排给 rcuc/N,先堆积后成批爆发
RT:同样的回调全部排给 rcuc/N,先堆积后成批爆发

第二层:两条释放路径与 rcuc 的执行细节

先看结论:页表释放正常走「批量 + RCU」,内存紧张时退化为「IPI 同步 + 立即释放」;rcuc 的批大小、时间片、睡眠间隔和优先级四个参数决定了「卡」的形状。

非 RTmunmap 释放页表的两条路(批量 RCU vs IPI 同步)
非 RT:munmap 释放页表的两条路(批量 RCU vs IPI 同步)
同一场景 · 换成 PREEMPT_RT 内核
PREEMPT_RTrcuc/N 的执行细节与调参点
RT:rcuc/N 的执行细节与调参点

机制细节

1. 一次 munmap 留下什么

来源 路径 位置
页表页 unmap_region → tlb_finish_mmu → tlb_flush_mmu_free → tlb_table_flush → tlb_remove_table_free → call_rcu(&batch->rcu, tlb_remove_table_rcu);一批最多 MAX_TABLE_BATCH ≈ (4096-16)/8 = 510 页,宽限期后 __tlb_remove_table_free() 归还 mm/mmap.c:2382-2400mm/mmu_gather.c:223, 281-288, 321-349, 363-365, 464-498include/asm-generic/tlb.h:208arch/arm64/Kconfig:239
VMA vm_area_free() → call_rcu(&vma->vm_rcu, vm_area_free_rcu_cb)(per-VMA lock 要求 VMA 经 RCU 释放) kernel/fork.c:521-535
maple tree 节点 ma_free_rcu() → call_rcu(&node->rcu, mt_free_rcu) lib/maple_tree.c:182-199
其它 kfree_rcu 用户(anon_vma 链、文件映射相关对象) 各处

几万次/秒的 munmap 就是每秒十几万个回调,全部进入本 CPU 的 rcu_data.cblist(未 offload 时)。

2. 非 RT 怎么消化

宽限期结束后 invoke_rcu_core() → raise_softirq(RCU_SOFTIRQ)kernel/rcu/tree.c:2877-2885),在 irq_exit 的 softirq 循环里 rcu_core() → rcu_do_batch()bl = max(blimit=10, pending >> 7),softirq 模式下 count >= bl && (need_resched() || !is_idle_task()) 就让出(:2582-2584)。加急宽限期可用,堆积通常不深。

3. RT 为什么变成「rcuc 卡」

环节 机制 位置
执行者 use_softirq = !IS_ENABLED(CONFIG_PREEMPT_RT) → 0;invoke_rcu_core_kthread() 唤醒 rcuc/N kernel/rcu/tree.c:105-109, 2877-2885, 2935-2942
优先级 kthread_prio = RCU_BOOST ? 1 : 0 → RT 上 FIFO 1 :168tree_plugin.h:1112-1126
循环 rcu_cpu_kthread():最多 10 轮 local_bh_disable → rcu_core → local_bh_enable,仍有活则 schedule_timeout_idle(2)(HZ=250 → 8 ms;HZ=1000 → 2 ms) :2902-2933
批大小 bl = max(blimit, pending >> rcu_divisor);pending ≥ qhimark=10000 后不限条数 :421-447, 2539
时间片 kthread 模式 tlimit = local_clock() + rcu_resched_ns(3 ms)rcu_do_batch_check_time() 到点让出 :2483-2491, 2540-2548, 2589
让出对象 每批后 local_bh_enable() + cond_resched_tasks_rcu_qs()——只让给 ≥ FIFO 1 的线程;SCHED_OTHER 测试线程排在后面 :2596-2600
bh 锁 RT 的 local_bh_disable() = 拿 per-CPU softirq_ctrl.lock;rcuc 跑回调期间 ksoftirqd/irq 线程等它(PI) kernel/softirq.c:120-185
宽限期更长 读者可抢占;rcu_normal_after_boot = IS_ENABLED(CONFIG_PREEMPT_RT) 禁加急;RCU_BOOST 500 ms 后才提升 kernel/rcu/update.c:60kernel/rcu/Kconfig:211-215

叠加起来:队列先积到上万 → 一波 3 ms 的 FIFO 1 独占 → 让出一下 → 再 3 ms …… 10 轮后停 2 个 jiffies → 再来。测试线程看到的是周期性的几十 ms 停顿。

4. IPI 旁路(内存紧张时)

tlb_remove_table()__get_free_page(GFP_NOWAIT | __GFP_NOWARN) 申请批缓冲(mmu_gather.c:337);失败时 tlb_table_invalidate() + tlb_remove_table_one()tlb_remove_table_sync_one()smp_call_function(tlb_remove_table_smp_sync, NULL, 1):264-278, 315-319)——同步 IPI 到所有在线 CPU(arm64 IPI_CALL_FUNCarch/arm64/kernel/smp.c:816, 847)。回调堆积让页表页迟迟不归还,空闲页减少,GFP_NOWAIT 更容易失败,于是 munmap 本身变慢、隔离核被 IPI 打断。这条路与 RT 无关,但 RT 的堆积让它更常出现。

复现与确认

# 1. 回调来源与批形状
trace-cmd record -e rcu:rcu_callback -e rcu:rcu_batch_start -e rcu:rcu_batch_end -e rcu:rcu_invoke_callback -a sleep 5
trace-cmd report | grep -E 'tlb_remove_table_rcu|vm_area_free_rcu_cb|mt_free_rcu' | wc -l
trace-cmd report | grep rcu_batch_end | head          # count 与间隔
# 2. rcuc 的 CPU 时间
for p in $(pgrep -f 'rcuc/'); do echo "$(cat /proc/$p/comm) utime+stime=$(awk '{print $14+$15}' /proc/$p/stat)"; done
# 3. IPI 旁路是否触发
grep -E 'Function call interrupts' /proc/interrupts     # munmap 期间是否暴涨
# 4. 测试线程被谁抢占
perf sched record -a -- sleep 5 && perf sched latency | grep -E 'rcuc|<测试线程名>'

对策

优先级 做法 效果 参数/位置
1 rcu_nocbs=<测试/隔离核>CONFIG_RCU_NOCB_CPU 回调交给 housekeeping 核的 rcuo 线程;rcuc 只剩静止态处理,CPU 时间接近 0 kernel/rcu/tree_nocb.h:950, 1399;性能分析第 13 章
2 测试线程 SCHED_FIFO ≥ 2,或接受 rcuc 优先 不再被 FIFO 1 的 rcuc 整段压住 rcutree.kthread_prio=
3 CONFIG_HZ_1000;运行时 echo 1000000 > /sys/module/rcutree/parameters/rcu_resched_ns 空档 8 ms → 2 ms;单批 3 ms → 1 ms tree.c:446-447, 2928
4 保证空闲内存、控制 munmap 速率 避免 GFP_NOWAIT 失败 → IPI 旁路 mmu_gather.c:337
5 应用侧:复用映射 + MADV_DONTNEED/MADV_FREE;大页减少页表页;预分配 从源头减少回调
6 NO_HZ_FULL 内核 rcupdate.rcu_normal_after_boot=0 恢复加急 GP、缩短堆积;但带回 IPI,隔离核慎用 kernel/rcu/update.c:60-62

munmap 微基准在 RT 上退化 10%–30% 是预期内的(性能分析第 06/08/11 章);目标不是让它和非 RT 一样快,而是让它不再干扰控制回路——这正是 rcu_nocbs + 分区的意义。

源码定位

主题 位置
munmap → tlb mm/mmap.c:2382-2400mm/vma.c:351-359, 1118mm/mmu_gather.c:223, 264-349, 363-365, 464-498
回调来源 kernel/fork.c:521-535lib/maple_tree.c:182-199arch/arm64/Kconfig:239include/asm-generic/tlb.h:208-240
rcuc 线程 kernel/rcu/tree.c:105-109, 168, 2877-2958kernel/rcu/tree_plugin.h:1112-1126
批处理 kernel/rcu/tree.c:421-447, 2483-2600
nocb offload kernel/rcu/tree_nocb.h:883-950, 1399
宽限期 kernel/rcu/update.c:60-62kernel/rcu/Kconfig:198-215
bh 锁 kernel/softirq.c:120-185
IPI arch/arm64/kernel/smp.c:69, 816, 847
trace 事件 include/trace/events/rcu.h:512, 604, 631, 748

小结