从打补丁到一行配置:PREEMPT_RT 主线化之后,实时内核到底该怎么配?
摘要:Linux 6.12(2024-11-17,LTS)把 PREEMPT_RT 收进主线,二十年的 out-of-tree 补丁史就此收尾。但"能选了"不等于"会配了"------真正的差异不在
make menuconfig少打几条命令,而在 Kconfig 语义、架构覆盖面、以及随后的 preemption 模型重构。本文从 PREEMPT 概念讲起,给出 RT 相关配置项全解,对齐到 7.2 时代的现状,并提供可直接复用的配置、验证与调优清单。
一、先厘清概念,再给结论
1.1 PREEMPT 到底是什么
**抢占(preemption)**说的是:一个正在运行的任务,能不能被更高优先级的任务中途赶下 CPU。真正的麻烦在于------这个任务此刻是不是在内核态。
在 PREEMPT_NONE 模型下,任务一旦通过系统调用陷入内核,就一路跑到返回用户态为止。这期间哪怕实时任务被唤醒,也只能干等,而这个"等"的上限不可控。抢占模型就是为解决它而生的:
| 模型 | 内核态代码何时可被抢占 | 延迟 | 典型场景 |
|---|---|---|---|
PREEMPT_NONE |
仅在返回用户态等少数几个点 | 最高 | 服务器、HPC、数据库 |
PREEMPT_VOLUNTARY |
内核代码里显式埋好的让出点(cond_resched()) |
中 | 桌面发行版 |
PREEMPT |
除临界区外,几乎处处可抢 | 低 | 低延迟桌面、软实时 |
【配图位 1:PREEMPT_NONE 与 PREEMPT 的调度延迟时序对比图】
内核靠每个任务身上的 preempt_count 计数来判断"此刻能不能抢":
- 计数 > 0 → 不可抢占
spin_lock()、preempt_disable()、处于中断上下文 → 计数加一- 高优先级任务被唤醒时只是置上
TIF_NEED_RESCHED标志,真正的切换发生在检查点 :preempt_enable()时、中断返回内核态时、异常返回用户态时
所以 PREEMPT 并不是"处处可抢"------抢占请求要是落在临界区里,只能等 preempt_count 归零才生效。这段等待正是延迟尾巴的主要来源之一。
【配图位 2:preempt_count 决定的抢占边界图】
顺着这个思路,三个配置项其实是三个递进的问题:
PREEMPT 管的是"内核态能不能抢",
PREEMPT_LAZY 管的是"抢得急不急",
PREEMPT_RT 管的是"临界区算不算"。
PREEMPT让内核态可抢,代价是吞吐下降------持有锁的任务被抢占,别人只能自旋干等PREEMPT_LAZY(6.13 引入)把普通任务的抢占推迟到 tick 边界,缓解上述代价PREEMPT_RT再进一步:把spinlock_t换成可睡眠的 rt_mutex,连临界区都变成可抢占的
完整的抢占模型对照表见 6.3 节。另外注意两个容易混的符号:
CONFIG_PREEMPT是抢占模型菜单项,CONFIG_PREEMPTION是"该内核支持抢占"的内部符号,后者不要手动改。
1.1.1 一个高频混淆:内核态 ≠ 临界区
读表 1.1 时最容易卡住的就是"临界区"三个字。"内核态"和"临界区"根本不是一个维度上的概念,混为一谈会把后面所有配置项都理解偏:
- 内核态说的是 CPU 特权级------能不能执行特权指令、能不能访问内核地址空间,由硬件决定。
- 临界区说的是互斥------这段访问共享数据的代码不允许被并发执行,由锁决定。
两者正交,组合出四种情况:
| 位置 | 能否被抢占 | 取决于 |
|---|---|---|
| 用户态(任何配置) | 能 | 时钟中断,与 CONFIG_PREEMPT 无关 |
| 内核态 · 非临界区 | PREEMPT 下能 |
CONFIG_PREEMPT |
| 内核态 · 临界区 | 不能(RT 除外) | preempt_count |
| 用户态 · 临界区 | 能(锁拦不住) | 这是坑,挡不住 |
【配图位:内核态 × 临界区 四象限图,标注各象限抢占行为】
为什么这俩总被搅在一起? Linux 语境下"能不能抢占"恰好要同时看两个维度,于是早年"内核里不能抢占"的说法传久了,让人默认"内核态 = 临界区"。三个纠偏:
- 用户态抢占一直存在,跟
CONFIG_PREEMPT无关。 时钟中断一来,只要TIF_NEED_RESCHED被置上,返回用户态时就切换。PREEMPT_NONE并不是"不能抢占",而是"内核态不给额外抢占点"。 CONFIG_PREEMPT只管一种情况:进程陷在内核态、又没在临界区里时,要不要允许抢占。- 内核态临界区之所以抢不动,不是因为它在内核态,而是因为
preempt_count > 0------spin_lock()会同时给你"互斥"和"禁止抢占"两件事;而用户态自旋锁只能给互斥,给不了禁抢占。
一个直观类比:
内核态 ------ 能不能用公章(特权级,硬件说了算)
临界区 ------ 会议室是不是被你占着(互斥范围,锁说了算)
在走廊走(内核态非临界区)可以被叫走;在会议室开会(临界区)你锁了门------但用户态程序的会议室门只是个牌子,调度器照样能把你拎出来,门口排队自旋的人就白等了。
这一点恰好补完了后文 §7 的 PostgreSQL 案例:7.0 移除 PREEMPT_NONE 后,持有用户态自旋锁的进程也可能被抢占,其余核上自旋等待的线程跟着空转,吞吐掉到约 51%。理解"用户态临界区挡不住抢占",才能理解这个根因。
1.2 一图看懂:6.12 前后的差异
| 维度 | 6.12 之前(补丁时代) | 6.12 及之后(主线时代) |
|---|---|---|
| 获取方式 | 下载与内核版本一一对应的 -rtNN 补丁,patch -p1 打入 |
主线源码自带,无需任何外部补丁 |
| 配置项 | 打了补丁后才出现(早期叫 CONFIG_PREEMPT_RT_FULL,后期统一为 CONFIG_PREEMPT_RT) |
CONFIG_PREEMPT_RT,依赖 EXPERT + ARCH_SUPPORTS_RT |
| 在 Kconfig 中位置 | Preemption Model 的四选一成员 | 6.12 仍是四选一;6.13 起被移出 choice,成为独立开关 |
| 版本约束 | 补丁版本必须与内核版本完全一致,差一个小版本就冲突 | 任意主线/LTS 版本,随取随用 |
| 架构覆盖 | x86 为主,arm64 时好时坏 | x86(32/64)、arm64、riscv(6.12)→ loongarch(6.13)→ arm32(7.1) |
| 与 PREEMPT_DYNAMIC | 互斥(depends on ... && !PREEMPT_DYNAMIC) |
6.13 起可共存,但启动参数只保留 preempt=full |
| 长期维护 | 自己维护补丁与 rebase | 随内核主线演进,LTS 6.12 支持至 2028-12-31 |
一句话:以前是"给内核做手术",现在是"打开一个开关";而变化真正的分水岭是 6.13------PREEMPT_RT 从"一种抢占模型"变成了"一个正交的开关"。
二、补丁时代:那些年我们踩过的坑
典型流程(以 5.10 为例):
bash
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.100.tar.xz
wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.10/older/patch-5.10.100-rt64.patch.xz
tar -xf linux-5.10.100.tar.xz && cd linux-5.10.100
xzcat ../patch-5.10.100-rt64.patch.xz | patch -p1
make menuconfig
# General setup → Preemption Model → Fully Preemptible Kernel (Real-Time)
这条路上有五个老兵都熟悉的雷:
- 版本锁死 :
patch-5.10.100-rt64只对 5.10.100 有效,换成 5.10.101 就可能满屏Hunk FAILED。 - 只覆盖少数 LTS:官方只为少数长期支持版本发布补丁,用非 LTS 内核基本无解。
- 与厂商 BSP 打架:树莓派、NXP、全志等厂商内核已被改过,再叠一层 RT 补丁,冲突概率陡增。
- 滞后:新内核发布后要等 RT 团队 rebase,往往晚数周甚至一个版本。
- 技术债无法回流 :RT 补丁里那些为了规避主线缺陷的 hack(最典型的是
printk相关处理)长期躲在主线之外,无法被社区检验------这也正是合入拖了二十年的最后一个卡点。
2024 年 9 月,Thomas Gleixner 提交了那封著名的 pull request:
Enable PREEMPT_RT on supported architectures: After twenty years of development we finally reached the point to enable PREEMPT_RT support in the mainline kernel. All prerequisites are merged, so enable it on the supported architectures ARM64, RISCV and X86(32/64-bit).
对应改动只有三行------三个架构的 Kconfig 各加一句 select ARCH_SUPPORTS_RT。压垮骆驼的最后一块拼图,是 printk 的重写(nbcon)。
三、主线时代:从 6.12 起的标准流程
bash
# 以 6.12 LTS 为例,也可以直接用当前主线 7.x
git clone --depth 1 -b v6.12 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
cp /boot/config-$(uname -r) .config && make olddefconfig
# 关键:CONFIG_PREEMPT_RT 依赖 EXPERT,默认不可见
scripts/config --enable CONFIG_EXPERT
scripts/config --enable CONFIG_PREEMPT_RT
make olddefconfig
make -j$(nproc)
sudo make modules_install && sudo make install
验证:
bash
uname -v # 应含 "SMP PREEMPT_RT"
zcat /proc/config.gz | grep PREEMPT_RT # CONFIG_PREEMPT_RT=y
cat /sys/kernel/realtime # 1(部分内核/发行版提供该接口)
坑位提醒 :如果在 Preemption Model 里看不到 "Fully Preemptible Kernel (Real-Time)",先检查两件事------
CONFIG_EXPERT是否打开、目标架构是否select ARCH_SUPPORTS_RT。PowerPC 至今仍未合入支持。
不想自己编译的话,主流发行版也都有现成 RT 内核:Ubuntu Pro 的 linux-realtime、Debian 的 linux-image-rt-amd64、RHEL 的 kernel-rt、Yocto 的 linux-yocto-rt。
四、真正的差异:Kconfig 语义变了
这才是本文想讲的重点。光说"不用打补丁了"太浅,6.12 和 6.13 之间还有一次语义重构。
版本 6.12:四选一里的第四项
刚合入时,PREEMPT_RT 被塞进了原本的 Preemption Model choice 里:
kconfig
choice
prompt "Preemption Model"
config PREEMPT_NONE ...
config PREEMPT_VOLUNTARY ...
config PREEMPT ...
config PREEMPT_RT
bool "Fully Preemptible Kernel (Real-Time)"
depends on EXPERT && ARCH_SUPPORTS_RT
endchoice
config PREEMPT_DYNAMIC
depends on HAVE_PREEMPT_DYNAMIC && !PREEMPT_RT # 与 RT 互斥
也就是说,RT 被当成了"第四种抢占模型",与 PREEMPT_DYNAMIC 水火不容。
版本 6.13:RT 被移出 choice(commit 35772d627b55)
Peter Zijlstra 在 2024 年 10 月提的这组补丁(2024-11-05 合入,落在 6.13 周期)把 RT 拿了出来:
kconfig
choice
config PREEMPT_NONE ...
config PREEMPT_VOLUNTARY ...
config PREEMPT ...
config PREEMPT_LAZY ... # 6.13 新增
endchoice # ← endchoice 提前
config PREEMPT_RT
bool "Fully Preemptible Kernel (Real-Time)"
depends on EXPERT && ARCH_SUPPORTS_RT
config PREEMPT_DYNAMIC
depends on HAVE_PREEMPT_DYNAMIC # 不再 !PREEMPT_RT
commit message 里那句话很值得记住:
Strictly speaking PREEMPT_RT is not a change in how preemption works, but rather it makes a ton more code preemptible.
严格来说,PREEMPT_RT 不是"另一种抢占模型",而是"让更多代码变得可抢占"。 它和 NONE/VOLUNTARY/FULL 不在同一个维度上,硬塞进四选一本身是概念错误。
配套地,调度器代码里把 none/voluntary 两个分支用 #ifndef CONFIG_PREEMPT_RT 包了起来------RT 内核上即使传了 preempt=none 也不会生效,只认 full。
这次重构的直接收益是:RT 内核也能开 PREEMPT_DYNAMIC,为后续统一铺路。
五、2025---2026:主线化之后的连锁反应
PREEMPT_RT 进主线不是终点,而是把一系列重构的闸门拉开了。按时间线看:
| 版本 | 时间 | 事件 |
|---|---|---|
| 6.12 (LTS) | 2024-11-17 | PREEMPT_RT 主线化,支持 x86(32/64)、arm64、riscv |
| 6.13 | 2025-01-19 | 引入 CONFIG_PREEMPT_LAZY;RT 移出 choice;loongarch 支持 RT |
| 6.18 (LTS) | 2025-11-30 | 持续打磨;softirq 大锁在 RT 上可去除(CONFIG_PREEMPT_RT_NEEDS_BH_LOCK 作为回退开关) |
| 7.0 | 2026-04-12 | 主流架构(x86、arm64、loongarch、powerpc、riscv、s390)移除 PREEMPT_NONE 与 PREEMPT_VOLUNTARY,只留 full 与 lazy |
| 7.1 | 2026-06-14 | 32 位 ARM 支持 PREEMPT_RT |
| 7.2 | 2026-08-16 | 当前稳定线(截至 2026-09 为 7.2.5),7.3-rc 已开 |
两个值得单独说的点:
(1)PREEMPT_LAZY 是冲着 RT 的开销来的。 它把 SCHED_NORMAL 任务的抢占请求推迟到 tick 边界(平均延迟 TICK_NSEC/2),但对 RR/FIFO/DEADLINE 类仍是完全抢占。目标是让"完全可抢占"的开销接近自愿抢占,从而最终干掉 cond_resched() 满天飞的局面。
(2)7.0 那次"删选项"是有代价的。 2026 年 4 月,AWS 工程师报告 PostgreSQL 在 7.0-rc 上吞吐掉到基线的约 51%,根因正是 PREEMPT_NONE 消失后,持有用户态自旋锁的进程可能被抢占,导致其余进程忙等。Peter Zijlstra 拒绝 revert,给出的解法是新合入的 RSEQ time-slice extension。这场争论说明:抢占模型的统一不只是内核内部的事,用户态的锁设计也被牵连进来了。
六、RT 相关配置项全解:开什么、关什么、别动什么
前面讲了"怎么选",这一节讲"选完之后还要配什么"。很多人以为勾上 CONFIG_PREEMPT_RT 就完事了,实际差得远。
6.1 五组最容易搞混的对照
| 对照 | 区别在哪 |
|---|---|
PREEMPT vs PREEMPT_RT |
前者让非临界区 的内核代码可抢;后者进一步把临界区本身 变成可抢占的睡眠锁(spinlock_t → rt_mutex)。RT 是 PREEMPT 的超集,开 RT 会自动 select PREEMPT |
PREEMPT_LAZY vs PREEMPT_VOLUNTARY |
都为降抢占开销。VOLUNTARY 靠代码里显式埋 cond_resched();LAZY 由调度器把 SCHED_NORMAL 的抢占请求推迟到 tick 边界,对 RT 类仍是完全抢占。7.0 后主流架构上 VOLUNTARY 已被移除 |
NO_HZ_IDLE vs NO_HZ_FULL |
前者(旧称 NO_HZ)只停空闲 CPU 的周期性 tick;后者在 CPU 上只有一个可运行任务时也停 tick,是实时隔离核的关键 |
RCU_NOCB_CPU vs RCU_BOOST |
前者把 RCU 回调卸载到专门的 kthread(配合 rcu_nocbs=),让隔离核不再被打扰;后者提升"被抢占的 RCU 读侧临界区持有者"的优先级,缩短其持锁时间。作用在不同环节,实时系统通常两个都要 |
PREEMPT_DYNAMIC vs PREEMPT_RT |
前者是运行时 维度(启动时用 preempt= 选模型);后者是编译期 维度(扩展可抢占范围)。6.13 起两者可共存,但 RT 内核上 preempt=none/voluntary 已被编译期剔除,只认 full |
冷知识:看老资料时会遇到
CONFIG_PREEMPT_LL这个名字。2019 年 Gleixner 引入CONFIG_PREEMPT_RT那次,为了把CONFIG_PREEMPT腾出来做内部符号,曾把菜单项 "Preemptible Kernel (Low-Latency Desktop)" 改名成PREEMPT_LL;后来 PREEMPT_LAZY 引入时又重组回现在的结构。
6.2 核心开关(必开)
| 选项 | 说明 |
|---|---|
CONFIG_EXPERT |
RT 的"门"。不打开它,menuconfig 里根本看不到 RT 选项 |
CONFIG_ARCH_SUPPORTS_RT |
架构能力位,由架构自己的 Kconfig select,用户改不了。7.2 时 x86、arm64、arm32、riscv、loongarch 已置位,powerpc 仍未合入 |
CONFIG_PREEMPT_RT |
总开关,依赖 EXPERT && ARCH_SUPPORTS_RT |
CONFIG_IRQ_FORCED_THREADING |
强制中断线程化。个别必须留在硬中断上下文的中断用 IRQF_NO_THREAD 标记(时钟源、perf、级联中断控制器等) |
CONFIG_PREEMPTION、CONFIG_PREEMPT_COUNT、CONFIG_UNINLINE_SPIN_UNLOCK |
内部符号,由 RT 自动 select,不要手动干预 |
6.3 抢占模型(非 RT 场景下的四选一)
| 模型 | 抢占时机 | 延迟 | 吞吐 | 典型场景 | 7.2 现状 |
|---|---|---|---|---|---|
PREEMPT_NONE |
仅在返回用户态等显式点 | 最高 | 最好 | 数据库 / HPC | 仅剩不支持抢占的架构 |
PREEMPT_VOLUNTARY |
额外的显式让出点 | 中 | 中 | 桌面 | 仅剩不支持 LAZY 的架构 |
PREEMPT(full) |
除临界区外处处可抢 | 低 | 略降 | 低延迟桌面、软实时 | 主流架构保留 |
PREEMPT_LAZY |
NORMAL 类推迟到 tick 边界,RT 类完全抢占 | 低(对 RT 类) | 接近 VOLUNTARY | 通用推荐 | 主流架构默认推荐 |
PREEMPT_RT |
几乎处处可抢,含临界区 | 最低 | 再降几个百分点 | 硬实时 | 独立开关,与上表正交 |
注意最后一行:PREEMPT_RT 已经不在这个"四选一"里了------这正是第四节讲的 6.13 语义重构的结果。7.0 之后主流架构上实际是 full / lazy 二选一,RT 另行叠加。
6.4 时钟与定时器
| 选项 | 建议 | 说明 |
|---|---|---|
CONFIG_HIGH_RES_TIMERS |
必开 | 微秒级定时的前提,没有它一切都免谈 |
CONFIG_HZ_1000 |
推荐 | tick 频率提到 1000 Hz。配合 nohz_full 时对隔离核影响变小,但对 housekeeping 核仍有意义 |
CONFIG_NO_HZ_FULL |
推荐 | 配合启动参数 nohz_full= 使用,见下节 |
一个容易忽略的点:hrtimer 默认仍在硬中断上下文执行 ,只有以 HRTIMER_MODE_SOFT 初始化的才走 softirq。所以定时器回调写长了照样会咬人。
6.5 RCU 三兄弟
CONFIG_PREEMPT_RCU=y # RCU 读侧临界区可抢占(RT 下自动生效)
CONFIG_RCU_NOCB_CPU=y # 配合启动参数 rcu_nocbs=2-3
CONFIG_RCU_BOOST=y # 提升被抢占的 RCU 读侧持有者优先级
CONFIG_RCU_BOOST_PRIO=1 # 提升到的优先级(默认 1,通常够用)
CONFIG_RCU_BOOST_DELAY=500 # 提升前的等待毫秒数
RCU_BOOST 解决的是一个很隐蔽的场景:低优先级任务在 RCU 读侧临界区里被抢占,导致高优先级任务等 grace period 干等。它靠优先级继承把前者临时提上去。
6.6 建议关闭或固定的选项
| 选项 | 建议 | 原因 |
|---|---|---|
CONFIG_CPU_FREQ |
关,或固定 performance governor | 变频切换瞬间引入延迟 |
CONFIG_CPU_IDLE / CONFIG_INTEL_IDLE |
保留但限制深睡,BIOS 层关深 C-State | 深 C-State 唤醒可达毫秒级 |
CONFIG_SUSPEND / CONFIG_HIBERNATION |
关 | 实时设备用不上,纯增路径 |
CONFIG_SWAP |
关 | 换页是延迟炸弹 |
CONFIG_TRANSPARENT_HUGEPAGE |
关,或设为 madvise | khugepaged 与大页合并制造延迟尖峰 |
CONFIG_KSM |
关 | 页面扫描带来周期性抖动 |
CONFIG_SCHED_AUTOGROUP |
关 | 面向桌面交互,干扰确定性 |
CONFIG_RT_GROUP_SCHED |
实时场景通常关 | 启用后 cgroup 层的 RT 带宽限制会覆盖全局 sched_rt_runtime_us |
CONFIG_LOCKDEP / CONFIG_PROVE_LOCKING |
仅调试期开 | 开销大且改变时序,开着它测出来的延迟不作数 |
CONFIG_DEBUG_ATOMIC_SLEEP、CONFIG_DEBUG_RT_MUTEXES |
仅调试期开 | 同上 |
6.7 只在测量期开启
CONFIG_FTRACE / CONFIG_FUNCTION_TRACER # 追踪基础设施
CONFIG_IRQSOFF_TRACER / CONFIG_PREEMPT_TRACER # irqsoff、preemptoff、preemptirqsoff
CONFIG_OSNOISE_TRACER / CONFIG_TIMERLAT_TRACER # 对应 rtla osnoise / rtla timerlat
测完记得关掉------tracer 自身有观测开销,留着跑生产等于给自己挖坑。
6.8 RT 下语义会发生变化的机制(避坑要点)
这部分来自 kernel.org 官方的 real-time 文档,是驱动开发者最容易踩的:
- 锁 :
spinlock_t/rwlock_t在 RT 上不再关中断 (中断已线程化),蜕变为可睡眠的 rt_mutex 并带优先级继承。只有raw_spinlock_t保持"关抢占 / 关中断"的传统语义,用于调度器、底层中断处理等极少数路径。 - 中断 :全部强制线程化,例外是带
IRQF_NO_THREAD、IRQF_PERCPU、IRQF_ONESHOT的中断。线程化后可用chrt调整irq/N-*的优先级。 - softirq :在线程上下文执行且可被抢占 。因此不能再靠
local_bh_disable()保护 per-CPU 数据,要改用local_lock_nested_bh()。 - per-CPU 数据 :改用
local_lock_t(RT 上实现为 per-CPU 的spinlock_t);确实需要显式关抢占的极端场合用preempt_disable_nested()。 - 控制台 :RT 下
printk输出由专门线程处理,好处是原子上下文可以安全打印,代价是内核崩溃时若切不到打印线程,最后那几条消息就丢了(panic 路径有例外)。这正是 nbcon 重写成为主线化最后一块拼图的原因------第二节那句"卡了二十年"不是修辞。
七、开了 RT 不等于实时:必做的调优
CONFIG_PREEMPT_RT=y 只保证"内核里该可抢的地方都可抢",剩下的活在你手上。内核侧的选项上一节已经列全,这里只讲内核之外的部分。
启动参数(把 CPU 2、3 划给实时任务)
isolcpus=domain,managed_irq,2-3 nohz_full=2-3 rcu_nocbs=2-3 irqaffinity=0-1
运行时
bash
echo never > /sys/kernel/mm/transparent_hugepage/enabled # THP 会制造延迟尖峰
echo -1 > /proc/sys/kernel/sched_rt_runtime_us # 关闭 RT 带宽节流(慎用)
ps -eLo pid,rtprio,comm | grep irq # 查看中断线程优先级,默认 50
chrt -f -p 90 <irq_thread_pid> # 关键中断优先级提到实时任务之上
应用层
mlockall(MCL_CURRENT | MCL_FUTURE)------缺页是实时任务的头号杀手sched_setscheduler(SCHED_FIFO)+ 合理优先级- 用户态互斥量设
PTHREAD_PRIO_INHERIT,内核的 PI 管不到用户态锁 - 绑核:
pthread_setaffinity_np或 cpuset cgroup - BIOS 层:关深度 C-State、关 Turbo、视情况关 SMT;能用
hwlatdetect先探一下 SMI
测量工具
bash
cyclictest -m -Sp99 -i1000 -d0 -h800 # rt-tests,最经典
rtla timerlat top # tools/tracing/rtla,定位 timer 路径延迟
rtla osnoise top # 定位操作系统噪声
hwlatdetect # 检测 SMI 等硬件层延迟
trace-cmd record -e ... # 配合 wakeup_rt / osnoise tracer
调优到位后,x86 平台上 cyclictest 的 Max 通常落在几十微秒量级;ARM 平台视 SoC 与驱动质量,常见在 50--150 µs。这些是经验值,必须用自己硬件的实测数据说话,不能直接写进论文。
八、方案光谱:PREEMPT_RT 该摆在什么位置
结合异构与虚拟化的视角,把常见实时方案横着排一遍:
| 方案 | 类型 | 典型最坏延迟 | 优势 | 代价 |
|---|---|---|---|---|
| PREEMPT_RT | 单一 Linux 内核 | 数十 µs | 完整 Linux 生态、主线维护、零补丁 | 延迟取决于全内核与驱动质量 |
| Xenomai 4 / EVL | co-kernel(Dovetail) | 约 10 µs 级 | 与 Linux 内核解耦,延迟更低更稳 | 仍需 out-of-tree 补丁,API 自成体系 |
| Jailhouse | 静态分区 hypervisor | 由裸机 RTOS 决定 | Linux 与 RTOS 硬件级隔离,cell 之间互不干扰 | 静态分区、资源独占,跨 cell 通信靠 IVSHMEM |
| RTOS(Zephyr / FreeRTOS) | 独立实时内核 | 个位数 µs | 延迟由构造保证 | 无 Linux 软件生态 |
一个有意思的推论:RT 主线化放大了双核方案的维护成本劣势。Xenomai 的 Dovetail 补丁如今和当年的 RT 补丁一样,需要跟着内核版本 rebase;而 PREEMPT_RT 已经不需要了。所以对多数新项目来说,选型顺序建议是:
- 先试 PREEMPT_RT,测出来的 Max 能满足 deadline 就用它;
- 满足不了,再考虑 co-kernel(Xenomai/EVL);
- 如果还要功能安全隔离或"Linux 崩了控制回路照跑"级别的确定性,那才是 Jailhouse 这类静态分区方案的战场------它卖的不是延迟数字,是隔离性。
九、给不同读者的建议
- 工程落地:直接上 6.12 LTS 或 6.18 LTS,别追最新主线。LTS 支持到 2028 年底,够一个产品周期了。
- 教学 :这件事本身就是操作系统课的绝佳案例------一个 out-of-tree 项目如何用二十年时间、以"每次回流一小块"的方式进入主线,期间还顺手改造了
printk、RCU、中断线程化。讲"开源协作"比讲概念有说服力得多。 - 科研/论文:写实时性对比实验时,务必注明内核版本、preemption 模型、cyclictest 参数、隔离核配置、BIOS 设置,否则数据不可复现。7.0 之后抢占模型只剩两种,实验基线也要跟着更新。
参考资料
- Thomas Gleixner, GIT PULL sched/rt for v6.12-rc1(2024-09)
- Peter Zijlstra, sched: Enable PREEMPT_DYNAMIC for PREEMPT_RT(commit 35772d627b55)
- Peter Zijlstra, sched: Further restrict the preemption modes(commit 7dadeaa6e851,2026-01)
- KernelNewbies: Linux 6.12 / Linux 7.0
- LWN: The first half of the 7.0 merge window
- kernel.org Releases(版本与 LTS 状态查询)
- 内核文档:How realtime kernels differ(锁、中断、softirq、per-CPU 在 RT 下的语义变化)
- 内核文档:Porting an architecture to support PREEMPT_RT(架构移植要求,第六节 6.2 与 6.8 的依据)
- 内核文档:Real-time Linux(theory of operation)、Scheduler、Deadline scheduling
本文基于 2026 年 9 月的主线状态撰写(当前稳定版 7.2.5)。内核演进很快,引用前请以 kernel.org 为准。