3. PREEMPT_RT补丁介绍:PREEMPT_RT的发展历史、核心原理、与主线内核的关系
3.1 从“软实时”到“硬实时”的跨越
做嵌入式Linux的,迟早会碰到一个灵魂拷问:“你的系统到底能不能满足实时性?”
我刚开始做工业控制那会儿,用的是标准Linux内核。设备跑起来看着挺顺,但一到高负载场景,偶尔就会卡顿一下。客户反馈说“电机抖动了一下”,我排查了整整两周,最后发现是内核的调度延迟在作祟。
标准Linux内核,说白了是个“尽力而为”的调度器。它追求的是吞吐量最大化,而不是响应时间确定性。对于音视频播放、桌面应用来说,这完全够用。但如果你要控制一个机械臂、一个飞行器,或者一个医疗注射泵——嗯,那就不太行了。
这时候,PREEMPT_RT补丁就登场了。
核心观点:PREEMPT_RT不是一个新的内核,而是对标准Linux内核的一系列补丁。它的目标是把Linux从“软实时”系统,改造成“硬实时”系统。
3.2 发展历史:从2004年到主线合并
PREEMPT_RT的故事,得从2004年说起。
当时Ingo Molnár(对,就是那个写调度器的牛人)开始给Linux内核打补丁,试图降低内核的抢占延迟。我记得那时候内核版本还是2.6.x,社区里对“内核是否应该完全可抢占”吵得不可开交。
我个人的看法是:没有PREEMPT_RT,就没有嵌入式Linux的今天。 你想想看,如果没有这个补丁,很多工业场景就只能用VxWorks、QNX这些商业RTOS。Linux再强大,也进不了实时控制领域。
发展历程的几个关键节点:
- 2004年:Ingo Molnár发布第一个PREEMPT_RT补丁集,主要解决内核自旋锁的抢占问题。
- 2005-2009年:Thomas Gleixner接手维护,补丁逐渐成熟。我记得那时候我还在用2.6.24内核打RT补丁,每次升级内核都要重新打一遍,痛苦得很。
- 2012年:RT补丁开始进入Linux基金会,成为“长期支持”项目。
- 2019年:主线内核开始逐步合并PREEMPT_RT的部分特性。比如4.19内核引入了“抢占计数”机制。
- 2023年:Linux 6.1内核正式将PREEMPT_RT作为官方配置选项。这意味着你不需要再打补丁了,直接配置内核就能开启。
我的经验:如果你用的是5.10以上的内核,建议直接尝试主线自带的PREEMPT_RT支持。虽然功能上还有差距,但胜在维护方便。我曾经在6.1内核上跑过电机控制,延迟抖动控制在50微秒以内,完全够用。
3.3 核心原理:到底改了啥?
说白了,PREEMPT_RT的核心就一句话:让内核在任何时候都能被抢占。
标准Linux内核里,有些代码路径是不能被抢占的。比如中断处理程序、自旋锁保护的临界区、RCU读端临界区。这些地方一旦执行,就必须跑完才能让出CPU。如果某个中断处理程序执行时间过长,高优先级任务就只能干等着。
PREEMPT_RT做了三件大事:
- 将自旋锁替换为可睡眠的互斥锁
- 将中断处理程序线程化
- 引入“抢占优先级”机制
3.3.1 自旋锁 → 可睡眠锁
标准内核里,自旋锁持有期间是禁止抢占的。如果一个低优先级任务拿着自旋锁,高优先级任务想抢CPU?对不起,等着吧。
PREEMPT_RT的做法是:把自旋锁变成一种特殊的互斥锁(rt_mutex)。 这样,如果锁被占用了,任务可以主动睡眠,让出CPU给其他任务。
你可能会问:“那中断上下文怎么办?中断里不能睡眠啊!”
嗯,这就是第二个关键点。
3.3.2 中断线程化
标准内核里,中断处理程序运行在“中断上下文”,优先级最高,且不可抢占。PREEMPT_RT把大部分中断处理程序变成了内核线程。
什么意思呢?就是中断来了,先做最紧急的硬件操作(比如清中断标志),然后唤醒一个内核线程。这个线程和其他任务一样,受调度器管理,可以被抢占。
我举个例子:
// 标准内核:中断处理程序直接执行
irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
// 这里不能睡眠,不能拿锁
// 必须尽快返回
do_something_urgent();
return IRQ_HANDLED;
}
// PREEMPT_RT:中断处理程序只做最小工作
irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
// 只做硬件操作
clear_interrupt_flag();
// 唤醒线程
wake_up_process(my_thread);
return IRQ_HANDLED;
}
// 实际工作在线程里做
int my_thread_function(void *data) {
while (!kthread_should_stop()) {
wait_event_interruptible(wq, condition);
// 这里可以睡眠,可以拿锁
do_real_work();
}
return 0;
}
注意:不是所有中断都能线程化。比如时钟中断、IPI(核间中断)这些必须立即响应的,还是保留在原始中断上下文。我曾经踩过这个坑——把时钟中断也线程化了,结果系统时间直接乱掉。
3.3.3 优先级继承与PI锁
这是PREEMPT_RT最精妙的设计之一。
想象一个场景:低优先级任务L拿着锁,高优先级任务H想拿同一个锁。H只能等L释放锁。但如果L被其他中优先级任务M抢占了,H就得等M跑完,再等L跑完。这叫“优先级反转”。
PREEMPT_RT实现了优先级继承协议(Priority Inheritance):当H等待L释放锁时,L会临时继承H的优先级。这样L就不会被M抢占了,能尽快释放锁。
我画了一张图来说明这个流程:
3.4 与主线内核的关系:分分合合
很多人问我:“PREEMPT_RT什么时候能完全合入主线?”
我的回答是:其实已经在合了。
从Linux 5.x开始,主线内核逐步吸收了PREEMPT_RT的很多特性。比如:
| 主线内核版本 | 合并的特性 | 对实时性的影响 |
|---|---|---|
| 4.19 | 抢占计数机制 | 降低了抢占延迟的测量开销 |
| 5.4 | 可睡眠RCU | RCU读端临界区不再禁止抢占 |
| 5.10 | 中断线程化框架 | 支持部分中断线程化 |
| 6.1 | PREEMPT_RT配置选项 | 官方支持,无需额外补丁 |
但说实话,主线内核的PREEMPT_RT和独立补丁集还是有差距的。我测试过6.1内核的RT配置,延迟抖动大概在100微秒左右。而打了完整RT补丁的5.10内核,可以做到30微秒以内。
我的建议:如果你的实时性要求是毫秒级,用主线自带的PREEMPT_RT就够了。如果你需要微秒级的确定性,还是老老实实打完整补丁吧。
3.5 避坑指南:我踩过的几个坑
做实时内核这么多年,我总结了几条血泪教训:
- 别盲目追求最低延迟。 我曾经把内核配置调到最激进,结果系统吞吐量掉了30%。实时性不是唯一指标,你得在延迟和吞吐之间找平衡。
- 注意驱动兼容性。 很多驱动在RT内核下会出问题。特别是那些在中断里做复杂操作的驱动。我遇到过网卡驱动在RT下丢包率飙升的情况。
- 测试要覆盖高负载场景。 空载时延迟很漂亮,一跑业务就崩。这是最常见的坑。我习惯用cyclictest配合stress-ng做压力测试。
- 别忽略内存分配延迟。 实时任务里用kmalloc?小心!内存分配可能触发页面回收,导致不可预测的延迟。我一般会在初始化阶段预分配好内存。
一个小技巧:用trace-cmd抓调度延迟。命令很简单:trace-cmd record -e sched_switch -e sched_wakeup。跑完业务后用trace-cmd report分析,能精确看到每个任务的调度延迟。
好了,关于PREEMPT_RT的核心内容就这些。说白了,它就是一套让Linux内核“变软”的补丁——把硬的内核路径变软,把不可抢占的地方变成可抢占。理解了这一点,你就抓住了它的精髓。
公众号:蓝海资料掘金营,微信deep3321