实践:配置、调优、验证与红线
把前九章的原理落到操作上:怎样在 6.12 上编出一个 ARM64 RT 内核、启动参数和运行期参数各自在解决哪一段延迟、怎样测量并定位超标的延迟,以及在 RT 内核里写代码不能碰的红线。最后附代码位置速查索引。
10.1先建立全局图:每个旋钮管哪一段延迟
RT 内核只解决"内核软件造成的不可抢占"。一个实时系统的最坏延迟还取决于硬件/固件和你怎样摆放负载。下图把旋钮按层次排开,后面各节逐层展开。
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_COUNT | y | PREEMPT_RT 直接 select PREEMPTION kernel/Kconfig.preempt:73 |
PREEMPT_DYNAMIC | 不可选 | depends on … && !PREEMPT_RT kernel/Kconfig.preempt:98——RT 只能编译期定死 |
PREEMPT_RCU | y | default y if PREEMPTION kernel/rcu/Kconfig:19;读侧可抢占(第 7 章) |
RCU_BOOST | y | default y if PREEMPT_RT kernel/rcu/Kconfig:198;被抢占读者需要 PI |
RCU_NOCB_CPU_CB_BOOST | y(若开 NOCB) | kernel/rcu/Kconfig:282:卸载的回调线程用 FIFO |
RCU_EXP_KTHREAD | 默认 n | kernel/rcu/Kconfig:227:RT 启动后本来就禁用 expedited GP |
RT_MUTEXES | y | default y if PREEMPT_RT init/Kconfig:2026 |
IRQ_FORCED_THREADING | y(ARM64 本就 select) | arch/arm64/Kconfig:250;RT 下 force_irqthreads() 恒真 include/linux/interrupt.h:518 |
QUEUED_RWLOCKS | n | depends on SMP && !PREEMPT_RT kernel/Kconfig.locks:254:rwlock_t 改由 rwbase_rt 实现 |
NUMA_BALANCING | 不可选 | !PREEMPT_RT init/Kconfig:953:自动 NUMA 迁移的扫描与迁页是延迟源 |
| 模块 vermagic | 含 preempt_rt | include/linux/vermagic.h:20:非 RT 编译的模块不能加载到 RT 内核 |
建议一并调整的选项
| 选项 | 建议 | 理由 |
|---|---|---|
HZ | 250 或 1000,视需求 | 默认 HZ_250 kernel/Kconfig.hz:8。高精度定时器不依赖 HZ;HZ 影响 tick 开销、timer_list 精度、RR 时间片。隔离核配 nohz_full 后 tick 本身会停。 |
NO_HZ_FULL | y(需要隔离核时) | 会 select RCU_NOCB_CPU、CPU_ISOLATION、IRQ_WORK 等;只编进去不生效,要配 nohz_full= 启动参数。 |
CPU_FREQ_DEFAULT_GOV_PERFORMANCE | y,或运行时改 governor | DVFS 切频本身会在固件/硬件里停顿;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,]list | domain:移出负载均衡;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=list | RCU 回调交给 rcuog/rcuop 线程,它们可以绑到 housekeeping 核 | RCU 回调批处理(第 7 章);否则隔离核上的 rcuc/N(FIFO 1)会被你的 RT 任务饿住 |
rcu_nocb_poll | rcuog 线程轮询,而不是由隔离核去唤醒它 | 省掉隔离核上的一次唤醒(代价是 housekeeping 核的轮询开销) |
irqaffinity=list | 新中断的默认亲和性 | 设备中断及其 irq/N 线程默认就落在 housekeeping 核(线程会跟随亲和性迁移,第 9 章) |
rcutree.kthread_prio=N | rcuc/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 内核上提前发现线程化后的驱动问题 |
不受这些参数影响、仍会打到隔离核上的东西: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测量:工具各测哪一段
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"]
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 挡不住硬中断
/* 任务侧 */
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
hrtimer_init(&t, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
/* RT: __hrtimer_init 把非 HARD 的改成 soft,
* 回调在 ksoftirqd(SCHED_OTHER)里跑 */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:4787 | lockdep 的 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 那几行判断"外层是什么原子区",再沿栈往下找"是谁要睡"。
- mydrv_update()驱动代码
- raw_spin_lock(&dev->hw_lock)真正关抢占:preempt_count = 1
- kmalloc(len, GFP_ATOMIC) → __kmalloc_cache_noprof()mm/slub.c:4394
- slab_alloc_node() → __slab_alloc_node()mm/slub.c:4209 · 4014
- !USE_LOCKLESS_FAST_PATH() → __slab_alloc() → ___slab_alloc()mm/slub.c:4058 · 3993 · 3741RT 下 USE_LOCKLESS_FAST_PATH() 恒为 false mm/slub.c:204,连快路径都要拿锁
- local_lock_irqsave(&s->cpu_slab->lock, flags)mm/slub.c:3788RT:
flags = 0; __local_lock()include/linux/local_lock_internal.h:144 - migrate_disable(); spin_lock() → rt_spin_lock() → __rt_spin_lock()kernel/locking/spinlock_rt.c:54 · 46
- 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:70 | config PREEMPT_RT(depends on EXPERT && ARCH_SUPPORTS_RT) |
kernel/Kconfig.preempt:98 | PREEMPT_DYNAMIC 依赖 !PREEMPT_RT |
arch/arm64/Kconfig:107 | select ARCH_SUPPORTS_RT |
arch/arm64/Kconfig:250 | select IRQ_FORCED_THREADING |
init/Makefile:29 | 版本串中的 PREEMPT_RT |
include/linux/vermagic.h:20 | 模块 vermagic preempt_rt |
MAINTAINERS:19471 | Real-time Linux (PREEMPT_RT) 条目 |
锁与 PI
| 位置 | 内容 |
|---|---|
include/linux/spinlock_rt.h:42 | RT 下 spin_lock() → rt_spin_lock() |
include/linux/spinlock_rt.h:96 | RT 下 spin_lock_irqsave(flags = 0) |
kernel/locking/spinlock_rt.c:54 | rt_spin_lock() |
include/linux/local_lock_internal.h:127 | RT 下 typedef spinlock_t local_lock_t |
include/linux/lockdep_types.h:20 | LD_WAIT_FREE / SPIN / CONFIG / SLEEP |
include/linux/rtmutex.h:23 | struct rt_mutex_base |
kernel/locking/rtmutex.c:677 | rt_mutex_adjust_prio_chain() |
kernel/locking/rtmutex_api.c:14 | max_lock_depth = 1024 |
kernel/sched/core.c:7123 | rt_mutex_setprio() |
kernel/sched/core.c:2323 | migrate_disable() |
中断、软中断、定时器
| 位置 | 内容 |
|---|---|
include/linux/interrupt.h:518 | RT 下 force_irqthreads() 恒真 |
include/linux/interrupt.h:90 | IRQF_TIMER 含 IRQF_NO_THREAD |
kernel/irq/manage.c:1368 | irq_setup_forced_threading() |
kernel/irq/manage.c:1300 / 1310 | irq_thread() / sched_set_fifo(current) |
kernel/irq/manage.c:1463 | 线程名 irq/%d-%s |
kernel/irq/manage.c:1191 | irq_forced_thread_fn() |
kernel/irq/manage.c:1134 | irq_finalize_oneshot() |
kernel/sched/syscalls.c:856 / 866 | sched_set_fifo()(50)/ sched_set_fifo_low()(1) |
kernel/softirq.c:120 | struct softirq_ctrl(RT) |
kernel/softirq.c:156 / 218 | RT 版 __local_bh_disable_ip() / __local_bh_enable_ip() |
kernel/softirq.c:295 / 444 | invoke_softirq() RT 版 / 非 RT 版 |
kernel/softirq.c:500–501 | MAX_SOFTIRQ_TIME(2ms)/ MAX_SOFTIRQ_RESTART(10) |
kernel/softirq.c:996 | ksoftirqd/%u |
kernel/irq_work.c:317 / 330 | irq_work/%u 线程(仅 RT)FIFO 1 |
kernel/time/hrtimer.c:1622 | RT 下非 HARD 的 hrtimer 改为 soft |
kernel/time/hrtimer.c:2041 | RT/DL 任务的 sleeper 自动 HARD |
kernel/time/hrtimer.c:1849 | hrtimer_interrupt() |
drivers/clocksource/arm_arch_timer.c:681 / 688 | arch_timer_handler_virt / _phys |
RCU、printk、调度
| 位置 | 内容 |
|---|---|
kernel/rcu/tree.c:106 | use_softirq = !IS_ENABLED(CONFIG_PREEMPT_RT) |
kernel/rcu/tree.c:168 | kthread_prio 默认(RCU_BOOST 下为 1) |
kernel/rcu/tree.c:2939 | rcuc/%u |
kernel/rcu/update.c:60 | rcu_normal_after_boot(RT 下默认 1) |
kernel/rcu/tree_plugin.h:162 / 411 / 1157 | rcu_preempt_ctxt_queue() / __rcu_read_lock() / rcu_boost() |
kernel/rcu/Kconfig:198 | RCU_BOOST(default y if PREEMPT_RT) |
kernel/printk/printk.c:3640 / 3652 | pr/legacy,nice −20 |
kernel/printk/nbcon.c:1335 / 1652 | pr/%s%d / nbcon_cpu_emergency_enter() |
kernel/sched/rt.c:21 | sysctl_sched_rt_runtime = 950000 |
kernel/sched/deadline.c:1839 | fair_server 默认 50ms / 1000ms |
kernel/sched/core.c:4142 / 6613 / 5296 | try_to_wake_up() / __schedule() / context_switch() |
kernel/trace/trace_osnoise.c:49 | DEFAULT_TIMERLAT_PRIO 95 |
ARM64
| 位置 | 内容 |
|---|---|
arch/arm64/kernel/entry-common.c:270 | arm64_preempt_schedule_irq() |
arch/arm64/kernel/entry-common.c:130 | do_notify_resume()(返回用户态前的 schedule()) |
arch/arm64/kernel/entry-common.c:619 / 643 | __el1_irq() / el1h_64_irq_handler() |
arch/arm64/kernel/entry-common.c:945 | el0t_64_irq_handler() |
arch/arm64/kernel/entry.S:825 | cpu_switch_to |
arch/arm64/kernel/fpsimd.c:226 | get_cpu_fpsimd_context() RT 分支 |
arch/arm64/kernel/irq.c:78–84 | do_softirq_own_stack()(#ifndef CONFIG_PREEMPT_RT) |
推荐阅读(均在 6.12 源码树内)
Documentation/locking/locktypes.rst—— 锁类型与 RT 语义的权威说明Documentation/trace/timerlat-tracer.rst、Documentation/trace/osnoise-tracer.rstDocumentation/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 的"顺带语义",必须把真实意图写出来。