4、PREEMPT_RT核心机制(上):中断线程化、实时互斥锁、优先级继承协议
各位好,欢迎来到PREEMPT_RT补丁应用实战的第四讲。今天我们要聊的这三个机制——中断线程化、实时互斥锁、优先级继承协议——可以说是整个实时Linux的基石。说白了,没有它们,Linux就只是个“尽力而为”的系统,谈不上什么确定性。
我个人习惯把这三个机制看作一个整体:中断线程化解决了“谁抢谁”的问题,实时互斥锁解决了“谁等谁”的问题,优先级继承协议解决了“谁让谁”的问题。咱们一个一个来看。
核心观点:实时系统的本质不是快,而是可预测。PREEMPT_RT的这三个机制,都是在为“可预测”这三个字服务。
4.1 中断线程化(IRQ Threads)
先说说中断线程化。嗯,这个概念其实很好理解。在标准Linux里,中断处理程序是运行在中断上下文中的,优先级最高,谁都不能打断它。但问题来了——中断处理程序里如果干了太多活,比如操作一个复杂的驱动,那它就会长时间霸占CPU,实时任务只能干等着。
我在项目中遇到过这样一个场景:一个数据采集卡的中断处理程序里做了大量的数据拷贝和协议解析,结果导致一个周期只有500微秒的实时控制任务频繁超时。当时排查了很久,最后发现罪魁祸首就是中断处理时间过长。
PREEMPT_RT的做法很巧妙——它把中断处理程序分成了两部分:
- 上半部(Hard IRQ):只做最紧急的事,比如清中断标志、读硬件寄存器。这部分仍然在中断上下文,但执行时间被压缩到极致。
- 下半部(Threaded IRQ):剩下的所有工作,都放到一个内核线程里去执行。这个线程是有优先级的,可以被调度,可以被抢占。
这样一来,中断处理就不再是“不可打断”的了。实时任务如果优先级比中断线程高,就可以随时抢占它。
小技巧:在设备树或驱动代码里,你可以通过 request_threaded_irq() 来注册一个线程化的中断处理函数。如果只传一个 handler 参数,系统会自动创建一个线程。
代码示例:
/* 传统中断注册 */
request_irq(irq_num, my_handler, IRQF_SHARED, "my_dev", dev);
/* PREEMPT_RT下的线程化中断注册 */
request_threaded_irq(irq_num, my_hard_handler, my_thread_fn,
IRQF_SHARED, "my_dev", dev);
你想想看,第一个参数是硬中断处理函数,第二个才是线程化处理函数。如果硬中断处理函数传NULL,那系统会帮你生成一个默认的,只做最基础的工作。
注意:不是所有中断都适合线程化。比如定时器中断、IPI(处理器间中断)这些对延迟极度敏感的,还是得保留在硬中断上下文。PREEMPT_RT默认会为大部分设备驱动启用线程化,但你可以通过内核参数 threadirqs 来全局控制。
4.2 实时互斥锁(rt_mutex)
接下来聊实时互斥锁。标准Linux里的 mutex 其实已经不错了,但它有一个致命问题——不支持优先级继承。什么意思呢?
假设有三个任务:T1(优先级最高)、T2(优先级中等)、T3(优先级最低)。T3先拿到了一个锁,然后T1来了,想拿同一个锁,但被T3占着。T1只能等。这时候如果T2突然就绪了,它会抢占T3,因为T2优先级比T3高。结果呢?T1反而被T2间接阻塞了——这就是经典的优先级反转问题。
我曾经在一个机器人控制项目里踩过这个坑。一个低优先级的日志任务拿着锁不放,中间被几个中等优先级的网络任务抢来抢去,结果高优先级的运动控制任务硬生生被拖了十几毫秒。机器人差点撞墙。
PREEMPT_RT引入了 rt_mutex,它本质上是一个支持优先级继承的互斥锁。它的核心数据结构是这样的:
struct rt_mutex {
raw_spinlock_t wait_lock;
struct rb_root_cached waiters;
struct task_struct *owner;
/* ... 其他字段 */
};
关键就在于 waiters 这个红黑树。它按优先级排序,当高优先级任务开始等待时,锁的持有者(低优先级任务)会被临时提升到等待者中最高的优先级。这就是优先级继承的核心逻辑。
一句话总结:rt_mutex 让“拿着锁的人”暂时拥有“等锁的人”的优先级,从而避免被中间优先级任务插队。
使用方式跟普通mutex几乎一样:
struct rt_mutex my_lock;
rt_mutex_init(&my_lock);
/* 在实时任务中 */
rt_mutex_lock(&my_lock);
/* 临界区代码 */
rt_mutex_unlock(&my_lock);
嗯,这里要注意:rt_mutex 是PREEMPT_RT补丁的一部分,如果你没有打补丁,内核里是没有这个接口的。在打了补丁的内核中,标准的 mutex 实际上就是基于 rt_mutex 实现的。
4.3 优先级继承协议
优先级继承协议,说白了就是上面说的那套规则。但我想深入一点,讲讲它的实现细节。
优先级继承不是免费的午餐。它需要额外的开销:
- 每次锁操作都要检查是否有更高优先级的任务在等待
- 如果有,需要临时提升当前任务的优先级
- 解锁时,需要恢复原始优先级
- 如果多个任务在等同一个锁,需要维护一个优先级排序的等待队列
我曾经在调试一个多核系统时发现,优先级继承本身也会引入延迟。因为 rt_mutex 内部使用了 raw_spinlock 来保护自己的数据结构,而这个自旋锁在PREEMPT_RT下也是可抢占的。所以,锁的嵌套层级越深,开销越大。
我的建议:在设计实时系统时,尽量让临界区短小精悍。优先级继承不是万能的,它只能解决“持有锁被抢占”的问题,解决不了“持有锁太久”的问题。
优先级继承协议有几个关键点:
- 链式继承:如果T3持有锁L1,T2持有锁L2,T1在等L2,而T2又在等L1,那么T3的优先级会被提升到T1的优先级。这就是链式继承,系统会沿着等待链一路往上提。
- 优先级冲高(Priority Ceiling):有些实现会使用优先级冲高协议,即锁有一个“天花板优先级”,任何持有锁的任务都会被提升到这个天花板。PREEMPT_RT没有默认使用这个,但你可以通过配置实现类似效果。
- 死锁检测:
rt_mutex内部有基本的死锁检测机制,但别完全依赖它。设计时还是要避免循环等待。
下面我用一张图来展示这三个机制的关系:
从这张图可以看出,这三个机制是层层递进的。中断线程化是基础,它让调度器有了更多的控制权。实时互斥锁是工具,它提供了安全的临界区访问。优先级继承协议是保障,它确保了实时任务的响应时间不会因为锁竞争而失控。
总结一下:PREEMPT_RT的这三个机制,本质上都是在做同一件事——把不可控变成可控。中断从不可抢占变成可抢占,锁从无优先级意识变成有优先级意识,任务调度从“先来后到”变成“优先级说了算”。
好了,这一讲的内容就到这里。下一讲我们会继续深入PREEMPT_RT的其他核心机制,包括自旋锁的改造、RCU的实时化处理,以及高精度定时器。到时候见。
公众号:蓝海资料掘金营,微信deep3321