PREEMPT_RT 原理手册
基于 Linux 6.12.110 源码,回答五个问题:没有 RT 时实时任务难在哪里,RT 补丁怎么解决,背后的原理是什么,解决之后内核变成了什么样,以及内核线程和用户态线程各受到什么影响。架构细节以 ARM64 + GICv3 为准。
PREEMPT_RT 只做一件事:让内核里几乎所有代码都可以被抢占。
办法有三种:能睡眠的锁换成带优先级继承的 rtmutex;中断和软中断挪进有优先级的线程;剩下真正必须原子执行的少量代码,显式标成 raw。
select ARCH_SUPPORTS_RTCONFIG_PREEMPT_RT 条件编译的文件数,大部分工作是理清语义0.1一张图看懂 RT 改了什么
判断一个内核"实时不实时",就看一件事:最高优先级的任务被唤醒以后,最多还要等多久才能上 CPU。下图左边是普通内核。任务上面压着好几层不可抢占的执行上下文:硬中断、软中断、关抢占的自旋锁区。这几层不管谁在跑,都比任何任务优先,连 SCHED_FIFO 99 也得等。右边是 PREEMPT_RT。这几层大部分被"压平"成普通线程,回到调度器手里,按优先级排队。
RT 并没有让内核跑得更快,平均延迟甚至略有上升,吞吐也会下降。它换来的是最坏情况可以界定:任务能被耽搁多久,只取决于剩下的那点原子区有多长,与系统负载无关。
0.2问题 → 解法 → 章节
每个延迟来源,RT 都给了一个对应的解法,而每个解法又会带出新的问题。比如锁一旦可以睡眠,就会有优先级反转,于是必须有 PI;RCU 读者一旦可以被抢占,宽限期就可能被拖长,于是需要 RCU boost。下图把问题和解法一一对上。
flowchart LR
p1["长时间关中断"]
p2["spinlock 关抢占"]
p3["硬中断 handler 不可调度"]
p4["softirq 在 irq_exit 就地跑"]
p5["优先级反转"]
p6["printk 同步刷串口"]
p7["RCU 读侧 = 关抢占"]
s1["spinlock_t → rtmutex 睡眠锁"]
s2["强制中断线程化"]
s3["softirq 线程化 + local_lock"]
s4["优先级继承 PI"]
s5["printk 线程化 pr/legacy"]
s6["可抢占 RCU + RCU_BOOST"]
s7["hrtimer 硬/软分离"]
p1 --> s1
p2 --> s1
p3 --> s2
p4 --> s3
p5 --> s4
p4 --> s7
p6 --> s5
p7 --> s6
click s1 "03-locks.html"
click s2 "05-irq-thread.html"
click s3 "06-softirq-timer.html"
click s4 "04-pi.html"
click s5 "07-rcu-printk-mm.html"
click s6 "07-rcu-printk-mm.html"
click s7 "06-softirq-timer.html"
click p1 "01-challenges.html"
click p2 "01-challenges.html"
click p3 "01-challenges.html"
click p4 "01-challenges.html"
click p5 "04-pi.html"
click p6 "07-rcu-printk-mm.html"
click p7 "07-rcu-printk-mm.html"
0.3跟着一个中断走一遍
最能体现 RT 改了什么的,是这样一个场景:一个网卡中断到来,而此时一个 SCHED_FIFO 80 的用户态控制线程,正在等定时器把它唤醒。下面把同一件事在两种内核上的执行顺序并排列出来。细节分散在第 5、6、8 章。
el0t_64_irq ← 假设打断的是用户态
gic_handle_irq
ICC_IAR1_EL1 → INTID 72 ← 网卡 SPI
网卡 handler(关中断执行)
ICC_EOIR1_EL1
irq_exit_rcu
handle_softirqs ← 就地执行
net_rx_action ~数百 µs
run_timer_softirq
exit_to_user_mode
do_notify_resume: schedule()
→ 这时才轮到 FIFO 80 线程el0t_64_irq
gic_handle_irq
ICC_IAR1_EL1 → INTID 72
handle_fasteoi_irq: ONESHOT 先 mask
irq_default_primary_handler
→ wake irq/72-eth0 (FIFO 50)
EOI(线程跑完再 unmask)
irq_exit_rcu ← 无 softirq
exit_to_user_mode → schedule()
选中 FIFO 80 控制线程
……控制线程跑完让出 CPU……
irq/72-eth0 运行:驱动 handler
local_bh_enable → net_rx_action右侧硬中断窗口只剩"读 IAR、屏蔽中断线、唤醒线程、EOI"这几步,这一段是 RT 也去不掉的,第 8 章会细讲。其余工作都变成按优先级排队的线程。所以 FIFO 80 的线程只需要等这一小段硬中断窗口,网卡上有多少流量都不影响它。
上面假设中断打断的是用户态(入口 el0t_64_irq),所以抢占点是返回用户态时的 exit_to_user_mode → do_notify_resume → schedule(),任何内核都有这个点。如果打断的是内核态(入口 el1h_64_irq),抢占点换成 __el1_irq 里的 arm64_preempt_schedule_irq()。在非 RT 内核里,只要被打断的代码持着 spinlock(preempt_count != 0),这个点就会失效,FIFO 80 还得等到对方 spin_unlock。在 RT 内核里,持 spinlock_t 不增加 preempt_count,这个点几乎总是有效。详见第 2 章。
0.4贯穿全书的配色与术语
全书的调用栈和架构图用同一套颜色表示执行上下文。读每个调用栈时先问一句:这一帧是在什么上下文里跑的,能不能被抢占?
| 术语 | 含义 | RT 下的变化 |
|---|---|---|
| 原子上下文 | 不能睡眠、不能被调度的上下文:硬中断、关中断、关抢占、持 raw 锁 | 范围大幅缩小,只剩 raw 锁区、关中断区和真正的硬中断 |
preempt_count | per-task 计数,按位域记录关抢占深度、softirq、hardirq、NMI 嵌套。用来决定能否抢占、判断当前上下文、检查原子区睡眠、给 RCU 判断静止态(详解) | spin_lock 不再增加它;softirq 位改由 current->softirq_disable_cnt 提供 |
| 抢占点 | 内核检查 need_resched 并可能切换任务的位置 | 几乎处处都是抢占点 |
| PI | Priority Inheritance,锁的持有者临时继承最高等待者的优先级 | 从只给用户态 PI futex 用,变成所有 spinlock_t、mutex 都有 |
| raw | raw_spinlock_t 等"真自旋"原语 | RT 下唯一的不可抢占锁,用在调度器、irqchip、hrtimer、RCU 节点等 |
| 线程化 | 把中断或软中断处理放进内核线程执行 | RT 下对所有没标 IRQF_NO_THREAD 的中断强制生效 |
0.5阅读路线
延迟从哪里来
实时性的定义,延迟的分解,非 RT 内核里每一种延迟来源的机制和量级。
抢占模型与调度器
四种抢占模型、抢占点、唤醒到运行的完整链路、ARM64 的 preempt_count。
锁的重新划分
spinlock_t 变成 rtmutex,raw 锁、local_lock、seqcount,以及 lockdep 的 wait-type 检查。
优先级继承
rtmutex 数据结构、PI 链遍历算法、死锁检测、用户态 PI futex。
硬中断线程化
从 GICv3 ICC_IAR1_EL1 到 irq/N 线程,ONESHOT 屏蔽,强制线程化的规则和例外。
软中断与定时器
local_bh_disable 的重新定义,ksoftirqd,hrtimer 的硬/软分离,arch timer。
RCU、printk、内存
可抢占 RCU 与 boost,printk 打印线程,SLUB 和页分配器的 local_lock。
ARM64 与 GICv3
异常入口、抢占门槛、FPSIMD、pseudo-NMI、SGI/PPI/SPI/LPI,以及 RT 管不到的硬件延迟。
内核线程与用户态线程
RT 内核里的线程全景与优先级地图,用户态 RT 程序该怎么写。
配置、调优、验证
内核配置,启动参数,cyclictest 和 rtla timerlat,编码红线,代码索引。
- 想快速建立整体认识:00 → 01 → 03 → 05 → 09。
- 写驱动或改内核代码:03 → 04 → 05 → 06 → 10 的编码红线。
- 做 ARM64 实时系统落地:01 → 08 → 09 → 10。
0.6关于 6.12 这个版本
6.12 是 PREEMPT_RT 第一次能在主线直接打开的版本(2024 年 11 月发布)。Thomas Gleixner 在 sched-rt-2024-09-17 这个 merge tag 里写道:"After twenty years of development we finally reached the point to enable PREEMPT_RT support in the mainline kernel. All prerequisites are merged." 最后一块拼图是 printk。
| 事实 | 位置 / 说明 |
|---|---|
| arm64 打开 RT 的提交只有一行 | d8fccd9ca5f9 "arm64: Allow to enable PREEMPT_RT.",arch/arm64/Kconfig:107 |
6.12 里 PREEMPT_RT 仍然 depends on EXPERT | kernel/Kconfig.preempt:70,要先开 CONFIG_EXPERT |
与 PREEMPT_DYNAMIC 互斥 | 只能在编译期选定,不能用 preempt= 启动参数切换 |
还没有 PREEMPT_LAZY | 6.13 才引入,用来给 SCHED_OTHER 任务挽回吞吐 |
还没有 ktimers 线程 | 6.12 RT 下定时器软中断和其他软中断一样,由 ksoftirqd 或 raise 它的线程执行 |
| 代码基线 | linux-6.12.y @ fa5b06866b64(v6.12.110)。文中行号都在这个版本上核对过 |
文中 path/file.c:123 形式的引用都对应上面这个版本。换了版本行号会漂移,建议按函数名 grep -n 去找。调用栈里的函数名也是在这个版本里核对过的。