CHAPTER 10

实践:配置、调优、验证与红线

把前九章的原理落到操作上:怎样在 6.12 上编出一个 ARM64 RT 内核、启动参数和运行期参数各自在解决哪一段延迟、怎样测量并定位超标的延迟,以及在 RT 内核里写代码不能碰的红线。最后附代码位置速查索引。

10.1先建立全局图:每个旋钮管哪一段延迟

RT 内核只解决"内核软件造成的不可抢占"。一个实时系统的最坏延迟还取决于硬件/固件和你怎样摆放负载。下图把旋钮按层次排开,后面各节逐层展开。

应用层 SCHED_FIFO/DEADLINE、mlockall、预触页、PI mutex、绝对时间睡眠、绑核(第 9 章) 运行期调优 chrt 调 irq 线程、/proc/irq/N/smp_affinity、sched_rt_runtime_us、cpufreq governor、/dev/cpu_dma_latency 启动参数 isolcpus、nohz_full、rcu_nocbs、irqaffinity、rcutree.kthread_prio、skew_tick、nosoftlockup、cpuidle.off 内核配置 PREEMPT_RT(锁、线程化、RCU、printk 全套机制)、HZ、NO_HZ_FULL、RCU_NOCB_CPU、关掉调试选项 硬件 / 固件(RT 管不到) EL3 固件 SMC/PSCI、SDEI、深度 idle 退出、DVFS 切换、cache/TLB、总线与内存控制器争用(第 1、8 章)
从下往上:硬件定下限,内核配置决定"能不能可抢占",启动参数和运行期调优决定"干扰落在哪",应用决定"能不能用上"。

10.2内核配置(6.12 · ARM64)

打开 PREEMPT_RT

6.12 里 PREEMPT_RT 仍然 depends on EXPERT && ARCH_SUPPORTS_RT kernel/Kconfig.preempt:70,ARM64 通过 select ARCH_SUPPORTS_RT 满足后者 arch/arm64/Kconfig:107。所以菜单路径是:

General setup  --->
    [*] Configure standard kernel features (expert users)      ← CONFIG_EXPERT
    Preemption Model (Fully Preemptible Kernel (Real-Time))  --->
        ( ) No Forced Preemption (Server)
        ( ) Voluntary Kernel Preemption (Desktop)
        ( ) Preemptible Kernel (Low-Latency Desktop)
        (X) Fully Preemptible Kernel (Real-Time)               ← CONFIG_PREEMPT_RT

命令行方式(在一份 arm64 defconfig 的基础上):

make ARCH=arm64 defconfig
./scripts/config -e EXPERT -d PREEMPT_NONE -d PREEMPT_VOLUNTARY -d PREEMPT \
                 -e PREEMPT_RT -d PREEMPT_DYNAMIC
make ARCH=arm64 olddefconfig
grep -E 'PREEMPT_RT|PREEMPT_RCU|RCU_BOOST|RT_MUTEXES|IRQ_FORCED' .config

打开它之后,自动随之改变的选项

选项RT 下的值原因 / 出处
PREEMPTION / PREEMPT_COUNTyPREEMPT_RT 直接 select PREEMPTION kernel/Kconfig.preempt:73
PREEMPT_DYNAMIC不可选depends on … && !PREEMPT_RT kernel/Kconfig.preempt:98——RT 只能编译期定死
PREEMPT_RCUydefault y if PREEMPTION kernel/rcu/Kconfig:19;读侧可抢占(第 7 章)
RCU_BOOSTydefault y if PREEMPT_RT kernel/rcu/Kconfig:198;被抢占读者需要 PI
RCU_NOCB_CPU_CB_BOOSTy(若开 NOCB)kernel/rcu/Kconfig:282:卸载的回调线程用 FIFO
RCU_EXP_KTHREAD默认 nkernel/rcu/Kconfig:227:RT 启动后本来就禁用 expedited GP
RT_MUTEXESydefault y if PREEMPT_RT init/Kconfig:2026
IRQ_FORCED_THREADINGy(ARM64 本就 select)arch/arm64/Kconfig:250;RT 下 force_irqthreads() 恒真 include/linux/interrupt.h:518
QUEUED_RWLOCKSndepends on SMP && !PREEMPT_RT kernel/Kconfig.locks:254:rwlock_t 改由 rwbase_rt 实现
NUMA_BALANCING不可选!PREEMPT_RT init/Kconfig:953:自动 NUMA 迁移的扫描与迁页是延迟源
模块 vermagic含 preempt_rtinclude/linux/vermagic.h:20:非 RT 编译的模块不能加载到 RT 内核

建议一并调整的选项

选项建议理由
HZ250 或 1000,视需求默认 HZ_250 kernel/Kconfig.hz:8。高精度定时器不依赖 HZ;HZ 影响 tick 开销、timer_list 精度、RR 时间片。隔离核配 nohz_full 后 tick 本身会停。
NO_HZ_FULLy(需要隔离核时)会 select RCU_NOCB_CPU、CPU_ISOLATION、IRQ_WORK 等;只编进去不生效,要配 nohz_full= 启动参数。
CPU_FREQ_DEFAULT_GOV_PERFORMANCEy,或运行时改 governorDVFS 切频本身会在固件/硬件里停顿;schedutil 的升频滞后也会拉长唤醒后的执行时间。
CPU_IDLE保留,但运行时限制深度深度 idle(PSCI CPU_SUSPEND 到 cluster/系统级)退出延迟可达数百微秒以上;用 PM QoS 限制而不是编译掉。
ARM64_PSEUDO_NMI按需需配 irqchip.gicv3_pseudo_nmi=1 才启用。好处:perf/hardlockup 用 NMI;代价:local_irq_disable() 改写 ICC_PMR_EL1,开关中断稍贵(第 8 章)。
LOCKDEP / PROVE_LOCKING / DEBUG_PREEMPT / DEBUG_ATOMIC_SLEEP / KASAN开发期开、量产/测延迟时关调试选项在每次加锁、抢占计数变化时插入检查,显著拉长关键路径;它们是验证正确性的工具,不是测延迟的环境。
IRQSOFF_TRACER、PREEMPT_TRACER、SCHED_TRACER、OSNOISE_TRACER、TIMERLAT_TRACER建议编进去kernel/trace/Kconfig:346–475。timerlat/osnoise 平时几乎零开销,出问题时现场就能抓。irqsoff/preempt tracer 打开后会给每次开关中断加钩子,按需使用。

确认跑的是 RT 内核

$ uname -v
#1 SMP PREEMPT_RT Thu Oct  9 12:00:00 CST 2026     # init/Makefile:29 把 PREEMPT_RT 写进版本串
$ zcat /proc/config.gz | grep PREEMPT_RT              # 需要 CONFIG_IKCONFIG_PROC
CONFIG_PREEMPT_RT=y
$ ps -eo comm | grep -E '^irq/|^irq_work/|^rcuc/' | head  # RT 特有线程(第 9 章)
注意

旧 RT 补丁集提供过 /sys/kernel/realtime,6.12 主线 kernel/ksysfs.c 里没有这个文件。脚本里判断 RT 请用 uname -v 或 config。

10.3启动参数:把干扰赶出实时核

以一台 8 核 ARM64 为例:CPU 0–1 做 housekeeping(系统杂务、普通中断),CPU 2–7 跑实时任务。

isolcpus=nohz,domain,managed_irq,2-7 nohz_full=2-7 rcu_nocbs=2-7 irqaffinity=0-1
rcu_nocb_poll rcutree.kthread_prio=… skew_tick=1 nosoftlockup
cpuidle.off=1                       # 或者运行时用 /dev/cpu_dma_latency 精细控制
irqchip.gicv3_pseudo_nmi=1          # 可选,需 CONFIG_ARM64_PSEUDO_NMI
参数做什么对应的延迟来源 / 机制
isolcpus=[flags,]listdomain:移出负载均衡;nohz:单任务时停 tick;managed_irq:尽力不让托管中断(如 NVMe/多队列网卡)投到这些核其他任务被迁移过来、tick、托管中断。文档标注 isolcpus 已不推荐,建议用 cpuset 实现 domain 隔离 kernel-parameters.txt:2436
nohz_full=list只有一个可运行任务时停掉 tick;同时把这些核的 RCU 回调卸载(等价于也写进 rcu_nocbs)周期 tick(arch timer PPI)的打断;kernel-parameters.txt:3984。需要 CONFIG_NO_HZ_FULL
rcu_nocbs=listRCU 回调交给 rcuog/rcuop 线程,它们可以绑到 housekeeping 核RCU 回调批处理(第 7 章);否则隔离核上的 rcuc/N(FIFO 1)会被你的 RT 任务饿住
rcu_nocb_pollrcuog 线程轮询,而不是由隔离核去唤醒它省掉隔离核上的一次唤醒(代价是 housekeeping 核的轮询开销)
irqaffinity=list新中断的默认亲和性设备中断及其 irq/N 线程默认就落在 housekeeping 核(线程会跟随亲和性迁移,第 9 章)
rcutree.kthread_prio=Nrcuc/rcub/rcu_preempt 的 FIFO 优先级,RCU_BOOST 下默认 1 kernel/rcu/tree.c:168系统里 RT 负载重时,可把 RCU 线程抬高一点避免 GP 被拖住;但要低于关键任务
skew_tick=1各 CPU 的 tick 错开所有核同一时刻进 tick 造成的锁争用(jiffies、RCU);文档明确建议给 HPC/RT 用
nosoftlockup / nowatchdog关软锁检测 / 关全部 lockup 检测softlockup 会周期性在每个核的 migration/N(stop 类,最高优先级)上跑一次 softlockup_fn;量产 RT 系统常关
cpuidle.off=1完全禁用 cpuidle(只剩 WFI 级)深度 idle 退出延迟。更细的做法见 10.4 的 /dev/cpu_dma_latency
irqchip.gicv3_pseudo_nmi=1启用 GICv3 优先级做 pseudo-NMI让 perf/hardlockup 在关中断区也能采样,便于定位关中断造成的延迟
threadirqs非 RT 内核上强制线程化中断RT 内核上无意义(已恒真);用来在非 RT 内核上提前发现线程化后的驱动问题
ARM64 / GICv3

不受这些参数影响、仍会打到隔离核上的东西:IPI(SGI,例如 TLB 广播之外的 smp_call_function、调度 IPI)、未关闭时的 arch timer tick、以及广播式 TLB 维护指令(TLBI …IS)——后者不是中断,但会让所有核的 TLB 被硬件失效,在别的核做大量 munmap/mprotect 时可观测到抖动。

10.4运行期调优

# 1) 看清所有 RT 线程的现状
ps -eLo pid,cls,rtprio,ni,psr,comm --sort=-rtprio | head -40

# 2) 关键中断线程抬到应用之上(或之下,取决于依赖关系,见第 9 章)
pgrep -f 'irq/[0-9]+-can0'   | xargs -n1 chrt -f -p 85
# 不重要的大流量中断绑到 housekeeping 核(线程会跟着迁移)
echo 0-1 > /proc/irq/45/smp_affinity_list

# 3) 用户态 RT 线程
chrt -f 80 taskset -c 3 ./rt_app

# 4) RT throttling:默认每 1s 留 50ms 给非 RT 任务
cat /proc/sys/kernel/sched_rt_runtime_us      # 950000(kernel/sched/rt.c:21)
#   设成 -1 关闭;只在确定应用不会失控时做,否则一个死循环就挂死整核
# 6.12 另有 fair_server(deadline server),每 CPU 默认 50ms/1000ms
cat /sys/kernel/debug/sched/fair_server/cpu3/runtime

# 5) 频率:固定在高频
for c in /sys/devices/system/cpu/cpufreq/policy*; do echo performance > $c/scaling_governor; done

# 6) idle 深度:持有 /dev/cpu_dma_latency 打开期间生效,写 0 = 只允许最浅 idle
exec 3>/dev/cpu_dma_latency; printf '\x00\x00\x00\x00' >&3   # 进程退出即恢复

# 7) 停掉会搬动中断的守护进程
systemctl stop irqbalance

第 6 条的原理:cpu_dma_latency 是 PM QoS 的全局约束 kernel/power/qos.c:414,cpuidle governor 选状态时会排除退出延迟超过该值的 idle 状态;它只在文件保持打开期间生效,所以不要 echo 0 > 了事。

10.5测量:工具各测哪一段

时间 定时器到期 中断延迟(关中断区) handler 调度延迟(等抢占点) 切换 线程运行 t0 t1 timerlat IRQ latency:t0 → 进入 timerlat_irq() timerlat Thread latency / cyclictest:t0 → 线程开始运行 irqsoff tracer:最长关中断区 preemptoff / preemptirqsoff tracer:最长不可抢占区 wakeup_rt tracer:最高优先级 RT 任务从被唤醒到运行 osnoise:线程在 CPU 上运行期间被"偷走"的时间(中断、软中断、其他线程、硬件噪声)
一次定时唤醒的延迟分段,以及各工具覆盖的区间。timerlat 一次给出两个数:IRQ latency 告诉你问题在中断之前(关中断/硬件),Thread latency 减去 IRQ latency 告诉你问题在中断之后(调度/不可抢占区)。

cyclictest

# rt-tests 包;每核一个 FIFO 95 线程,1ms 周期,mlockall,用 clock_nanosleep 绝对时间
cyclictest --mlockall --smp --priority=95 --interval=1000 --distance=0 \
           --duration=12h --histogram=400 --histfile=hist.txt -q
# 同时施加负载:stress-ng --cpu 4 --io 2 --vm 2 --vm-bytes 512M、hackbench、iperf3 …

只看最大值和直方图尾部,平均值对实时系统几乎没有意义;测试要足够长(小时级),并且在有代表性的负载下进行——空载下的 cyclictest 数字只说明硬件和空闲路径。

rtla timerlat / osnoise

rtla 在 tools/tracing/rtla/,是内核 timerlat/osnoise tracer 的前端。timerlat 每核起一个 FIFO 95 的内核线程 kernel/trace/trace_osnoise.c:49,用 HRTIMER_MODE_ABS_PINNED_HARD 的 hrtimer 周期唤醒自己 trace_osnoise.c:1868——HARD 意味着即使在 RT 内核上,定时器回调 timerlat_irq() 也在硬中断里执行,因此能单独量出 IRQ latency。

# 实时观察,单位 us;超过 50us 自动停止并做自动分析(-a = -T 50 -s 50 -t 等组合)
rtla timerlat top -c 2-7 -a 50
# 直方图
rtla timerlat hist -c 2-7 -d 1h -E 200 --dma-latency 0
# 用户态线程模式(-u):测量路径经过真实的系统调用返回
rtla timerlat top -u -c 2-7
# 噪声:线程在跑时被打断了多少
rtla osnoise top -c 2-7

-a 触发的 auto analysis(timerlat_aa.c)会把超标那一次拆成:IRQ handler 之前的延迟、被哪些中断/软中断/线程挡住、各自耗时多少,以及最后阻塞的内核栈——这是 RT 排障最省事的第一步。

ftrace tracer

cd /sys/kernel/tracing
echo wakeup_rt > current_tracer      # 最高优先级 RT 任务的唤醒延迟最大值
echo 0 > tracing_max_latency; echo 1 > tracing_on
sleep 60; cat tracing_max_latency; cat trace | head -80
echo preemptirqsoff > current_tracer  # 最长"关中断或关抢占"区,含起止调用栈
echo nop > current_tracer
# 事件:谁在什么时候占着 CPU
echo 'sched:sched_switch sched:sched_wakeup irq:irq_handler_entry irq:softirq_entry' > set_event

排障决策流程

flowchart TD
  A["cyclictest / timerlat 最大值超标"] --> B{"timerlat 的 IRQ latency 高吗"}
  B -- "高" --> C["问题在中断到达之前"]
  C --> C1["preemptirqsoff 或 irqsoff tracer 找最长关中断区"]
  C --> C2["检查 idle 深度、cpufreq、EL3 固件调用、SDEI"]
  C --> C3["是否有 IRQF_NO_THREAD 的慢 handler 或 raw 锁长临界区"]
  B -- "不高,但 Thread latency 高" --> D["问题在中断之后"]
  D --> E{"同核上有更高或同优先级的 RT 线程吗"}
  E -- "有" --> E1["调整优先级或绑核,见第 9 章依赖链原则"]
  E -- "没有" --> F{"阻塞在锁上吗,看 auto analysis 的栈"}
  F -- "是" --> F1["找持锁者,看 PI 是否生效,是否在 raw 区里"]
  F -- "否" --> G["preemptoff 区:preempt_disable、raw 锁区、HARD hrtimer 与 irq_work 回调"]
  D --> H["osnoise 看是否有周期性干扰:tick、RCU、workqueue、softlockup"]
  H --> H1["nohz_full、rcu_nocbs、workqueue cpumask、nosoftlockup"]
延迟超标时的定位顺序:先用 timerlat 的两个数字把问题切到"中断前"或"中断后",再分别用对应的 tracer 找元凶。

10.6编码红线(错误写法 vs 正确写法)

每条都源自前面章节的某个机制。共同的根:RT 把"顺带"的语义拆开了——非 RT 下 spinlock 顺带关抢占、spin_lock_irqsave 顺带关中断、local_bh_disable 顺带关抢占,RT 下这些"白嫖"都不存在了,必须显式写出真实意图。

① raw 区 / 关中断区里不能分配或释放内存

错误
raw_spin_lock(&dev->hw_lock);
buf = kmalloc(len, GFP_ATOMIC);   /* RT: slub 用 local_lock → 睡眠锁 */
...
raw_spin_unlock(&dev->hw_lock);
正确
buf = kmalloc(len, GFP_KERNEL);   /* 锁外预分配 */
raw_spin_lock(&dev->hw_lock);
...                               /* 只做寄存器级操作 */
raw_spin_unlock(&dev->hw_lock);
kfree(old);                       /* 锁外释放 */

原因见 第 7 章:SLUB 的 per-CPU 快路径由 local_lock_t 保护 mm/slub.c:396,RT 下它是 spinlock_t。GFP_ATOMIC 只说明"不进回收",不说明"不拿睡眠锁"。

② spin_lock_irqsave 挡不住硬中断

错误(假设 handler 是 IRQF_NO_THREAD)
/* 任务侧 */
spin_lock_irqsave(&d->lock, flags);  /* RT: 不关中断, flags=0 */
d->state = X;
spin_unlock_irqrestore(&d->lock, flags);
/* 硬中断侧(未线程化)*/
spin_lock(&d->lock);   /* RT: 硬中断里拿睡眠锁 → BUG */
正确
/* 真要和硬中断互斥,就用 raw 锁,并保持临界区极短 */
raw_spin_lock_irqsave(&d->lock, flags);
d->state = X;
raw_spin_unlock_irqrestore(&d->lock, flags);
/* 更好:去掉 IRQF_NO_THREAD,让 handler 线程化,
 * 两侧都用 spinlock_t 即可 */

出处:spin_lock_irqsave 在 RT 下的定义里只有 flags = 0 include/linux/spinlock_rt.h:96。之所以普通驱动不出事,是因为 handler 已被线程化,它和任务侧都在线程上下文里,用睡眠锁互斥完全正确(第 5 章)。

③ 保护 per-CPU 数据用 local_lock,别裸用 preempt_disable

错误
preempt_disable();
p = this_cpu_ptr(&pcp_cache);
spin_lock(&p->lock);    /* RT: 关抢占区里拿睡眠锁 */
...
spin_unlock(&p->lock);
preempt_enable();
正确
static DEFINE_PER_CPU(struct pcp, pcp_cache) = {
	.llock = INIT_LOCAL_LOCK(llock),
};
local_lock(&pcp_cache.llock);  /* RT: migrate_disable + per-CPU spinlock */
p = this_cpu_ptr(&pcp_cache);
...
local_unlock(&pcp_cache.llock);

local_lock 在非 RT 下编译成 preempt_disable(),零额外开销;在 RT 下是 typedef spinlock_t local_lock_t include/linux/local_lock_internal.h:127,可抢占且有名字、可被 lockdep 追踪(第 3 章)。

④ 不要"先关中断,再拿 spinlock_t"

错误
local_irq_save(flags);
spin_lock(&q->lock);     /* RT: 关中断区里拿睡眠锁 */
...
spin_unlock(&q->lock);
local_irq_restore(flags);
正确
spin_lock_irqsave(&q->lock, flags);  /* RT 下自动退化为普通加锁 */
...
spin_unlock_irqrestore(&q->lock, flags);

⑤ 必须在硬中断里到期的 hrtimer 要显式标 HARD

非 RT 下正常,RT 下被推到软中断
hrtimer_init(&t, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
/* RT: __hrtimer_init 把非 HARD 的改成 soft,
 * 回调在 ksoftirqd(SCHED_OTHER)里跑 */
正确(回调本身必须是 RT 安全的:只用 raw 锁、不分配)
hrtimer_init(&t, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);

出处:if (IS_ENABLED(CONFIG_PREEMPT_RT) && !(mode & HRTIMER_MODE_HARD)) kernel/time/hrtimer.c:1622。反过来,别为了"低延迟"把所有 hrtimer 都标 HARD——HARD 回调执行在不可抢占的硬中断里,正是 RT 想消灭的东西(第 6 章)。

⑥ 自旋等待另一个上下文完成,在 RT 下可能是活锁

错误
while (!READ_ONCE(d->done))   /* d->done 由某个线程置位 */
	cpu_relax();
/* RT: 置位的线程可能比你优先级低、在同一核上,
 * 你自旋它就永远跑不了 */
正确
wait_for_completion(&d->done);  /* 对方 complete(&d->done) */
/* 或 wait_event(d->wq, d->done);
 * 内核自己的例子:tasklet_unlock_spin_wait() 在 RT 下
 * 改用 local_bh_disable/enable 让出 CPU */

⑦ 不要用上下文谓词做决策

错误
gfp_t gfp = in_atomic() ? GFP_ATOMIC : GFP_KERNEL;
/* RT: 持 spinlock_t 时 in_atomic() 为假,
 * 选了 GFP_KERNEL 后在锁内进入回收 */
正确
int foo_alloc(struct foo *f, gfp_t gfp);  /* 由调用者明确传入 */

⑧ 别把 kernel_neon_begin() 当成"关抢占"

错误
kernel_neon_begin();
p = this_cpu_ptr(&scratch);   /* 以为 NEON 区间里不会换 CPU */
neon_transform(p, src, len);
kernel_neon_end();
正确
local_lock(&scratch.llock);   /* 显式声明 per-CPU 保护 */
p = this_cpu_ptr(&scratch);
kernel_neon_begin();
neon_transform(p, src, len);
kernel_neon_end();
local_unlock(&scratch.llock);

6.12 的 kernel_neon_begin() 只在内部用 get_cpu_fpsimd_context()(RT 下即 preempt_disable(),arch/arm64/kernel/fpsimd.c:226)短暂保护"保存用户态 FPSIMD 状态"这一步,随后就 put_cpu_fpsimd_context() 放开;它给任务打上 TIF_KERNEL_FPSTATE,让内核态 NEON 寄存器随上下文切换保存/恢复 fpsimd.c:1886–1924。所以任务上下文里的 NEON 区间本身是可抢占、可迁移的——好消息是长的加密/校验计算不再是不可抢占区;坏消息是任何依赖"NEON 区间不会换 CPU"的代码都是错的(第 8 章)。

⑨ 新驱动:线程化中断,不用 tasklet

/* primary:只做"确认硬件 + 屏蔽设备中断源",返回 IRQ_WAKE_THREAD */
static irqreturn_t mydev_hardirq(int irq, void *data) { ... return IRQ_WAKE_THREAD; }
/* thread:可以睡眠、拿 mutex、分配内存 */
static irqreturn_t mydev_thread(int irq, void *data)  { ... return IRQ_HANDLED; }

ret = request_threaded_irq(irq, mydev_hardirq, mydev_thread,
                           IRQF_ONESHOT, "mydev", dev);

RT 下 request_irq() 也会被强制线程化,但显式写成 threaded 能在非 RT 内核上获得同样的行为,并让 primary/thread 的分工清楚。tasklet 已被标注 "Do not use in new code",替代品是 threaded IRQ 或 BH workqueue(第 6 章)。

10.7常见故障模式

现象根因处理
BUG: sleeping function called from invalid context kernel/sched/core.c:8671在 raw 锁 / 关中断 / 关抢占区里拿了 spinlock_t、分配了内存,或驱动的 IRQF_NO_THREAD handler 里拿了 spinlock_t按 10.6 ①②③④ 改;或去掉 NO_THREAD
[ BUG: Invalid wait context ] kernel/locking/lockdep.c:4787lockdep 的 wait-type 检查:外层锁的等待类型比内层更严格(例如 raw 锁里拿 spinlock_t)非 RT 内核上开 PROVE_RAW_LOCK_NESTING 也能提前发现
系统卡顿,日志里有 sched: RT throttling activated某 FIFO 任务死循环或忙等,吃满 95% 时间修应用;这是 throttling 在救命,不要先去把它关掉
网络/存储变得极慢,但 CPU 并不忙于它们同核的 RT 应用线程优先级高于 irq/N 线程,把中断处理饿住了"依赖链在上":抬中断线程,或分核(第 9 章)
RCU stall 警告 / 内存持续增长隔离核上 RT 任务压住了 rcuc/N(FIFO 1),回调和 GP 推进不了rcu_nocbs + 把 rcuo 线程放到 housekeeping 核;或调 rcutree.kthread_prio
soft hrtimer / timer_list 回调大幅推迟它们在 ksoftirqd(SCHED_OTHER)里执行,CPU 被 RT 负载占满在该核留出空闲时间,或把相关工作挪核;6.12 之后有专门的 ktimers 线程
模块加载失败:version magic 不匹配非 RT 内核编译的模块 vermagic 不含 preempt_rt针对 RT 内核重新编译
cyclictest 偶发几百微秒尖峰,tracer 看不到内核原因硬件/固件:深度 idle、DVFS、EL3 调用、SDEI、温控降频限 idle、固定频率;用 rtla hwnoise / osnoise 的 hw noise 字段确认

剖析一次 "sleeping function called from invalid context"

以红线 ① 为例(raw 锁里 kmalloc(GFP_ATOMIC)),RT 内核上报错时的调用链如下。读这类 splat 时,先看 in_atomic()/irqs_disabled()/preempt_count 那几行判断"外层是什么原子区",再沿栈往下找"是谁要睡"。

raw_spin_lock 里调用 kmalloc(GFP_ATOMIC) PREEMPT_RT · mm/slub.c
  1. mydrv_update()驱动代码
  2. raw_spin_lock(&dev->hw_lock)真正关抢占:preempt_count = 1
  3. kmalloc(len, GFP_ATOMIC) → __kmalloc_cache_noprof()mm/slub.c:4394
  4. slab_alloc_node() → __slab_alloc_node()mm/slub.c:4209 · 4014
  5. !USE_LOCKLESS_FAST_PATH() → __slab_alloc() → ___slab_alloc()mm/slub.c:4058 · 3993 · 3741RT 下 USE_LOCKLESS_FAST_PATH() 恒为 false mm/slub.c:204,连快路径都要拿锁
  6. local_lock_irqsave(&s->cpu_slab->lock, flags)mm/slub.c:3788RT:flags = 0; __local_lock() include/linux/local_lock_internal.h:144
  7. migrate_disable(); spin_lock() → rt_spin_lock() → __rt_spin_lock()kernel/locking/spinlock_rt.c:54 · 46
  8. rtlock_might_resched() → __might_resched()spinlock_rt.c:35 · kernel/sched/core.c:8648发现 preempt_count != 0 → 打印 "BUG: sleeping function called from invalid context"

注意这里即使锁没有争用也会报错:__rt_spin_lock() 在尝试获取之前就无条件调用 rtlock_might_resched() 做检查。这是 RT 的设计选择——问题在"可能睡眠"而不在"这次睡没睡",所以一跑到就报,不必等到极小概率的争用时才暴露。

10.86.12 之后的演进(以实际版本为准)

本手册只描述 6.12。下面这些是 6.12 之后主线陆续发生的变化,用 6.12 LTS 时不要假设它们存在,升级时要重新评估:

  • PREEMPT_LAZY / NEED_RESCHED_LAZY(6.13 起):对 SCHED_OTHER 的抢占推迟到 tick 或返回用户态,挽回 RT 内核上普通负载的吞吐;6.12 全树 grep 不到。
  • 去掉 PREEMPT_RT 对 EXPERT 的依赖:之后不再需要先开 CONFIG_EXPERT。
  • ktimers 线程:RT 下把 timer/hrtimer 软中断从 ksoftirqd 分离出来,用专门的高优先级线程执行,解决 10.7 表中"soft timer 被饿住"的问题。
  • 更多 nbcon console 驱动(如 8250):printk 可以在 emergency 时原子输出且不再需要 pr/legacy。

10.9附录:代码位置速查索引

全部基于 linux-6.12.y @ fa5b06866b64(v6.12.110),本章写作时逐条 grep 核验。行号会随 stable 更新漂移,核对时请按符号名搜索。

配置与使能

位置内容
kernel/Kconfig.preempt:70config PREEMPT_RT(depends on EXPERT && ARCH_SUPPORTS_RT)
kernel/Kconfig.preempt:98PREEMPT_DYNAMIC 依赖 !PREEMPT_RT
arch/arm64/Kconfig:107select ARCH_SUPPORTS_RT
arch/arm64/Kconfig:250select IRQ_FORCED_THREADING
init/Makefile:29版本串中的 PREEMPT_RT
include/linux/vermagic.h:20模块 vermagic preempt_rt
MAINTAINERS:19471Real-time Linux (PREEMPT_RT) 条目

锁与 PI

位置内容
include/linux/spinlock_rt.h:42RT 下 spin_lock() → rt_spin_lock()
include/linux/spinlock_rt.h:96RT 下 spin_lock_irqsave(flags = 0)
kernel/locking/spinlock_rt.c:54rt_spin_lock()
include/linux/local_lock_internal.h:127RT 下 typedef spinlock_t local_lock_t
include/linux/lockdep_types.h:20LD_WAIT_FREE / SPIN / CONFIG / SLEEP
include/linux/rtmutex.h:23struct rt_mutex_base
kernel/locking/rtmutex.c:677rt_mutex_adjust_prio_chain()
kernel/locking/rtmutex_api.c:14max_lock_depth = 1024
kernel/sched/core.c:7123rt_mutex_setprio()
kernel/sched/core.c:2323migrate_disable()

中断、软中断、定时器

位置内容
include/linux/interrupt.h:518RT 下 force_irqthreads() 恒真
include/linux/interrupt.h:90IRQF_TIMER 含 IRQF_NO_THREAD
kernel/irq/manage.c:1368irq_setup_forced_threading()
kernel/irq/manage.c:1300 / 1310irq_thread() / sched_set_fifo(current)
kernel/irq/manage.c:1463线程名 irq/%d-%s
kernel/irq/manage.c:1191irq_forced_thread_fn()
kernel/irq/manage.c:1134irq_finalize_oneshot()
kernel/sched/syscalls.c:856 / 866sched_set_fifo()(50)/ sched_set_fifo_low()(1)
kernel/softirq.c:120struct softirq_ctrl(RT)
kernel/softirq.c:156 / 218RT 版 __local_bh_disable_ip() / __local_bh_enable_ip()
kernel/softirq.c:295 / 444invoke_softirq() RT 版 / 非 RT 版
kernel/softirq.c:500–501MAX_SOFTIRQ_TIME(2ms)/ MAX_SOFTIRQ_RESTART(10)
kernel/softirq.c:996ksoftirqd/%u
kernel/irq_work.c:317 / 330irq_work/%u 线程(仅 RT)FIFO 1
kernel/time/hrtimer.c:1622RT 下非 HARD 的 hrtimer 改为 soft
kernel/time/hrtimer.c:2041RT/DL 任务的 sleeper 自动 HARD
kernel/time/hrtimer.c:1849hrtimer_interrupt()
drivers/clocksource/arm_arch_timer.c:681 / 688arch_timer_handler_virt / _phys

RCU、printk、调度

位置内容
kernel/rcu/tree.c:106use_softirq = !IS_ENABLED(CONFIG_PREEMPT_RT)
kernel/rcu/tree.c:168kthread_prio 默认(RCU_BOOST 下为 1)
kernel/rcu/tree.c:2939rcuc/%u
kernel/rcu/update.c:60rcu_normal_after_boot(RT 下默认 1)
kernel/rcu/tree_plugin.h:162 / 411 / 1157rcu_preempt_ctxt_queue() / __rcu_read_lock() / rcu_boost()
kernel/rcu/Kconfig:198RCU_BOOST(default y if PREEMPT_RT)
kernel/printk/printk.c:3640 / 3652pr/legacy,nice −20
kernel/printk/nbcon.c:1335 / 1652pr/%s%d / nbcon_cpu_emergency_enter()
kernel/sched/rt.c:21sysctl_sched_rt_runtime = 950000
kernel/sched/deadline.c:1839fair_server 默认 50ms / 1000ms
kernel/sched/core.c:4142 / 6613 / 5296try_to_wake_up() / __schedule() / context_switch()
kernel/trace/trace_osnoise.c:49DEFAULT_TIMERLAT_PRIO 95

ARM64

位置内容
arch/arm64/kernel/entry-common.c:270arm64_preempt_schedule_irq()
arch/arm64/kernel/entry-common.c:130do_notify_resume()(返回用户态前的 schedule())
arch/arm64/kernel/entry-common.c:619 / 643__el1_irq() / el1h_64_irq_handler()
arch/arm64/kernel/entry-common.c:945el0t_64_irq_handler()
arch/arm64/kernel/entry.S:825cpu_switch_to
arch/arm64/kernel/fpsimd.c:226get_cpu_fpsimd_context() RT 分支
arch/arm64/kernel/irq.c:78–84do_softirq_own_stack()(#ifndef CONFIG_PREEMPT_RT)

推荐阅读(均在 6.12 源码树内)

  • Documentation/locking/locktypes.rst —— 锁类型与 RT 语义的权威说明
  • Documentation/trace/timerlat-tracer.rst、Documentation/trace/osnoise-tracer.rst
  • Documentation/tools/rtla/ —— rtla timerlat / osnoise / hwnoise 手册
  • Documentation/RCU/Design/Requirements/Requirements.rst —— RCU 的正式需求,含实时部分
  • Documentation/admin-guide/kernel-parameters.txt —— 本章所有启动参数的原文
  • kernel/locking/rtmutex.c 顶部注释 —— PI 链遍历的逐步说明

10.10小结

  • 配置:EXPERT → PREEMPT_RT,它会带出 PREEMPT_RCU、RCU_BOOST、RT_MUTEXES 等;测延迟时关掉调试选项。
  • 启动参数和运行期调优的本质是把干扰从实时核搬走:tick、RCU 回调、中断、迁移、idle 退出、频率切换。
  • 测量先用 timerlat 把问题切成"中断前 / 中断后",再用对应 tracer 定位。
  • 编码红线的共同根源:RT 拆掉了 spinlock / irqsave / bh_disable 的"顺带语义",必须把真实意图写出来。