案例 01 · munmap 压测中的 rcuc:回调从哪来、为什么在 RT 上「卡」
结论:
munmap是 RCU 回调的大生产者——arm64 上页表页经MMU_GATHER_RCU_TABLE_FREE走call_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线程。
现象
top/ps里rcuc/N周期性占满一颗核;munmap 循环的吞吐下降,偶发几十 ms 的停顿。- ftrace 里
rcu:rcu_batch_end的count很大(数千到上万),事件间隔呈固定周期;rcu:rcu_callback的函数名以tlb_remove_table_rcu、vm_area_free_rcu_cb、mt_free_rcu为主。 /proc/interrupts的 IPI「Function call interrupts」计数随 munmap 速率一起涨(走了 IPI 旁路)。
第一层:回调在哪里执行
先看结论:非 RT 在 softirq 里就地跑、不可抢占但零切换;RT 在
rcuc/N线程里跑、可抢占但先堆后爆、以 FIFO 1 压住普通线程。
第二层:两条释放路径与 rcuc 的执行细节
先看结论:页表释放正常走「批量 + RCU」,内存紧张时退化为「IPI 同步 + 立即释放」;rcuc 的批大小、时间片、睡眠间隔和优先级四个参数决定了「卡」的形状。
机制细节
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-2400;mm/mmu_gather.c:223, 281-288, 321-349, 363-365, 464-498;include/asm-generic/tlb.h:208;arch/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 |
:168;tree_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:60;kernel/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_FUNC,arch/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-2400;mm/vma.c:351-359, 1118;mm/mmu_gather.c:223, 264-349, 363-365, 464-498 |
| 回调来源 | kernel/fork.c:521-535;lib/maple_tree.c:182-199;arch/arm64/Kconfig:239;include/asm-generic/tlb.h:208-240 |
| rcuc 线程 | kernel/rcu/tree.c:105-109, 168, 2877-2958;kernel/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-62;kernel/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 |
小结
- munmap 每次至少留下页表批、VMA、maple 节点三类 RCU 回调;RT 把它们交给 FIFO 1 的
rcuc/N,宽限期更长导致先堆后爆。 - 「卡」的形状由四个数决定:
qhimark=10000(放开条数)、rcu_resched_ns=3 ms(单批)、schedule_timeout_idle(2)(空档)、kthread_prio=1(压住谁)。 - 堆积的副作用是
tlb_remove_table()走 IPI 同步旁路,munmap 自己变慢、隔离核被打断。 - 对策首选
rcu_nocbs=,其次是测试线程优先级、HZ/时间片、内存余量和应用侧减少 munmap。