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做了三件大事:

  1. 将自旋锁替换为可睡眠的互斥锁
  2. 将中断处理程序线程化
  3. 引入“抢占优先级”机制

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抢占了,能尽快释放锁。

我画了一张图来说明这个流程:

优先级继承流程示意图 任务L(低优先级) 任务M(中优先级) 任务H(高优先级) 🔒 共享锁 ① 先拿到锁 ② 请求锁,被阻塞 ③ 优先级继承生效 L临时提升到H的优先级 ④ L释放锁,恢复原优先级

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