Linux RT 分析Linux 6.12.107

01 · 抢占模型:不可抢占区的消失

结论:非 RT 内核里,任何持有 spinlock_t、处理 softirq、关中断的代码段都是「不可抢占区」,高优先级任务醒来后必须等它们结束;PREEMPT_NONE/VOLUNTARY 更进一步,连 preempt_enable() 和中断返回都不调度。PREEMPT_RT 把这些区域绝大部分改成可抢占,只留下 raw_spinlock_t、hardirq 桩和显式 preempt_disable() 三类极短区域。代价:抢占更频繁、上下文切换更多,且 6.12 还没有 PREEMPT_LAZY 来缓解普通任务的过度抢占。

第一层:一颗 CPU 上的时间线

先看结论:非 RT 的调度延迟由「最长不可抢占区」决定;RT 把它压缩到 hardirq 桩 + raw 区的长度。

非 RT任务 B 醒来后被 spin_lock 临界区 + softirq 挡住
非 RT:任务 B 醒来后被 spin_lock 临界区 + softirq 挡住
同一场景 · 换成 PREEMPT_RT 内核
PREEMPT_RThardirq 桩一结束就切换到任务 B
RT:hardirq 桩一结束就切换到任务 B

左图是 CONFIG_PREEMPT(低延迟桌面)内核:任务 A 在系统调用里拿了一把 spinlock_t,此时网卡中断唤醒了 SCHED_FIFO 的任务 B。虽然 NEED_RESCHED 已经置位,但 preempt_count 非零,调度只能等 A 释放锁、softirq 跑完、preempt_enable() 把计数减到零之后才发生。驱动里一段几百 µs 的临界区,就是几百 µs 的抖动。

右图是同一场景在 RT 内核上:spin_lock() 不再增加 preempt_countinclude/linux/preempt.h:157-162PREEMPT_LOCK_OFFSET 定义为 0),hardirq 处理函数只做唤醒,中断返回时 arm64_preempt_schedule_irq()arch/arm64/kernel/entry-common.c:270)发现 64 位 preempt_count 为零(计数为零且需要调度)立即调度。A 的临界区被切开,等 B 跑完再继续——这之所以安全,是因为 RT 下 spinlock_t 是可睡眠的 rt_mutex(第 02 章)。

第二层:preempt_count 的来源与抢占点

先看结论:RT 没有改 preempt_count 的结构,改的是「谁往里加计数」——spinlock、bh、RCU 读区三大来源都换成了不计数的机制。

非 RT谁把 preempt_count 加上去,内核在哪里才检查 NEED_RESCHED
非 RT:谁把 preempt_count 加上去,内核在哪里才检查 NEED_RESCHED
同一场景 · 换成 PREEMPT_RT 内核
PREEMPT_RT主要来源被 migrate_disable / 睡眠锁 / 线程化替换
RT:主要来源被 migrate_disable / 睡眠锁 / 线程化替换

preempt_count 的布局(两种内核相同)

include/linux/preempt.h:27-55 定义了 32 位计数的分段:PREEMPT [0..7]SOFTIRQ [8..15]HARDIRQ [16..19]NMI [20..23]arm64 把 need_resched 放在计数之外的高 32 位arch/arm64/include/asm/thread_info.h:29-40 是一个 union { u64 preempt_count; struct { u32 count; u32 need_resched; } preempt; }arch/arm64/include/asm/preempt.h:8 定义 PREEMPT_NEED_RESCHED = BIT(32),极性反转——need_resched == 0 表示需要调度(:32, :37, :42)。好处是「可抢占且需要调度」变成一次 64 位比零(:79-80 should_resched());代价是 arm64 的计数 RMW 不是原子的(:45-57READ_ONCE/WRITE_ONCE),__preempt_count_dec_and_test() 要在 :74 重读整个 u64 以防中断在两半操作之间清了 need_resched。x86 则把 NEED_RESCHED 反向塞在同一个 32 位字的 bit 31。SOFTIRQ_DISABLE_OFFSET = 2 * SOFTIRQ_OFFSET:55):bit 8 表示「正在跑 softirq」,bit 9 表示「bh 已关闭」。

非 RT:三种模型的抢占点

抢占点 PREEMPT_NONE PREEMPT_VOLUNTARY PREEMPT 源码
返回用户态 kernel/entry/common.c exit_to_user_mode_loop
显式 schedule() / 阻塞
cond_resched() / might_sleep() kernel/sched/core.c:7251-7273 __cond_resched()
preempt_enable() 计数归零 include/linux/preempt.h:229-235(无 PREEMPTION 时 :250-255 只减计数)
中断返回内核态 arm64arch/arm64/kernel/entry-common.c:270-302 arm64_preempt_schedule_irq(),由 __el1_irq():628-630 调用(arm64 没有 select GENERIC_ENTRY,不走 kernel/entry/common.c);x86 对应 kernel/entry/common.c:303-313

kernel/Kconfig.preempt:14-16 里 choice 的默认值仍是 PREEMPT_NONE;发行版内核普遍打开 PREEMPT_DYNAMIC:96-101),用 preempt=none|voluntary|full 启动参数在三者间切换——注意它 depends on !PREEMPT_RT,所以 RT 与 DYNAMIC 互斥,一个内核镜像不可能既是发行版通用内核又是 RT 内核。

RT:计数来源被替换成什么

非 RT 会增加 preempt_count 的操作 RT 的替代 源码
spin_lock() / spin_lock_irqsave()preempt_disable() rt_spin_lock()migrate_disable() + rcu_read_lock(),不动 preempt_count kernel/locking/spinlock_rt.c:46-52include/linux/preempt.h:161
rcu_read_lock()(非抢占 RCU)→ preempt_disable() PREEMPT_RCU:只加 rcu_read_lock_nesting include/linux/rcupdate.h:70-101
local_bh_disable()+SOFTIRQ_DISABLE_OFFSET 拿 per-CPU softirq_ctrl.lock;计数存到 current->softirq_disable_cnt kernel/softirq.c:156-185include/linux/preempt.h:110-116
get_cpu() / kmap_atomic() migrate_disable() include/linux/highmem-internal.h:97-132
raw_spin_lock() / 显式 preempt_disable() 不变,仍是真正的不可抢占区
hardirq 入口 HARDIRQ_OFFSET 不变,但处理函数缩短为「唤醒线程」 第 03 章

RT 引入了两个新的 per-task 计数来承接这些语义:task->migration_disabledinclude/linux/sched.h:888-890,16 位)和 task->softirq_disable_cnt:1225-1227)。in_task()softirq_count() 在 RT 下的定义也随之改变(include/linux/preempt.h:110-133)。

RT 对调度器本身的调整

非 RT RT 源码 为什么
TTWU_QUEUE 特性 true false kernel/sched/features.h:75-83 远程唤醒排队要靠 IPI 延后处理,增加唤醒延迟
RT_PUSH_IPI false true features.h:108-116 用 IPI 把 RT 任务推到别的 CPU,避免在本 CPU 长时间关抢占遍历
SCHED_NR_MIGRATE_BREAK 32 8 kernel/sched/sched.h:2833-2837 负载均衡一次搬更少任务,缩短持 rq 锁时间
schedule_rtlock() 新增 kernel/sched/core.c:6883-6889 在 RT 自旋锁上阻塞的专用入口(SM_RTLOCK_WAIT
RT 限流 95%(sysctl_sched_rt_runtime = 950000 kernel/sched/rt.c:17-21 RT 内核也默认限流,长跑 RT 线程每秒被停 50ms,需 sched_rt_runtime_us=-1
PREEMPT_LAZY 无(6.13 才有) 全树无 PREEMPT_LAZY/NEED_RESCHED_LAZY 6.12 RT 上 SCHED_OTHER 任务也会在每个抢占点被立刻抢占

ARM64 的中断返回抢占路径

arm64 没有选 GENERIC_ENTRYarch/arm64/kernel/entry-common.c 自己实现了入口/出口;__exit_to_kernel_mode() 的注释(:70)明说它只做 trace/lockdep/RCU,「preemption handled elsewhere」。真正的抢占点是 arm64_preempt_schedule_irq():270-302),它按顺序做四件事:

  1. need_irq_preemption():262-267):PREEMPT_DYNAMIC 下是静态键 sk_dynamic_irqentry_exit_cond_resched,RT 下退化为 IS_ENABLED(CONFIG_PREEMPTION),编译期恒真。
  2. READ_ONCE(current_thread_info()->preempt_count) != 0 就返回(:280-281)——一次 64 位比较同时判断「计数为零」和「需要调度」。
  3. 伪 NMI 例外:284-290):if (system_uses_irq_prio_masking() && read_sysreg(daif)) return;——开启 irqchip.gicv3_pseudo_nmi=1 后,从伪 NMI 返回时不做抢占el1_interrupt() 也把 !interrupts_enabled(regs) 的情况分流到 __el1_pnmi():637-640),那条路径根本不调用抢占函数。
  4. system_capabilities_finalized() 之前不抢占(:294-301):早期启动与 secondary CPU 上线期间存在一个不可抢占窗口。

另两处 arm64 特有的不可抢占区:调试异常上下文显式 preempt_disable():445-462);arch/arm64/Kconfig:58-83ARCH_INLINE_*_LOCK if !PREEMPTION——RT 下锁操作全部走 out-of-line 函数,比非抢占 arm64 内核多一次调用开销。

ARM64 的入口开销:KPTI 与 Spectre 缓解

抢占点在异常入口/出口上,所以入口本身的开销直接进「唤醒→运行」延迟:

性能影响

净影响:最坏延迟下降约两个数量级;抢占更频繁、切换更多,吞吐型负载损失 0–35%。

指标 非 RT(PREEMPT) RT 说明
唤醒→运行延迟(最坏) 取决于最长不可抢占区:常见 100 µs – 数 ms 取决于最长 raw 区 + hardirq 桩:常见 10 – 50 µs 见第 11 章量级
抢占频率 RT 无 LAZY,普通任务的时间片经常被切碎
上下文切换总数 基线 +20% ~ +100%(中断密集时更高) 线程化中断 / softirq 线程 / 锁睡眠都要切换
缓存局部性 变差 频繁切换 + 被钉住的任务无法迁移
吞吐(CPU 密集) 100% 95% – 100% 纯计算几乎无差别
吞吐(调度密集,如 hackbench) 100% 65% – 90% 切换与锁成本叠加

源码定位

主题 位置
抢占模型 choice kernel/Kconfig.preempt:14-87(PREEMPT_RT :70-73,帮助文本 :75-82
PREEMPT_DYNAMIC 与 RT 互斥 kernel/Kconfig.preempt:96-101
ARCH_SUPPORTS_RT arch/Kconfig:1359-1360;x86 arch/x86/Kconfig:128、arm64 :105、riscv :68
preempt_count 位布局 include/linux/preempt.h:27-65
PREEMPT_LOCK_OFFSET = 0 include/linux/preempt.h:157-162
preempt_enable()__preempt_schedule() include/linux/preempt.h:229-255
中断返回抢占(arm64) arch/arm64/kernel/entry-common.c:262-302, 628-640;x86 参考 kernel/entry/common.c:303-358
arm64 preempt_count 布局 arch/arm64/include/asm/preempt.h:8-90arch/arm64/include/asm/thread_info.h:29-40
arm64 KPTI / Spectre arch/arm64/kernel/cpufeature.c:1731-1789, 1982arch/arm64/kernel/proton-pack.c:84-94, 413, 829-1034arch/arm64/Kconfig:1696-1708
preempt_schedule() / preempt_schedule_irq() kernel/sched/core.c:6891-7058
preempt_disable_nested()(RT 才真关抢占) include/linux/preempt.h:438-478
migrate_disable() 设计说明 include/linux/preempt.h:373-426

调优要点

小结