2. PREEMPT_RT 内核原理:内核抢占模型演进与实时化改造
本章深入剖析 PREEMPT_RT 内核的核心机制。理解这些原理是后续移植与性能调优的基础。我们将从内核抢占模型的演进历史出发,逐步解析自旋锁与互斥锁的实时化改造、中断线程化机制以及高精度定时器的实现原理。
2.1 内核抢占模型演进:从 UP 到 SMP 再到 RT
Linux 内核的抢占模型经历了从不可抢占到完全可抢占的演进过程,这是实现实时性的关键。下表总结了不同内核版本的抢占模型特点:
| 内核版本 / 特性 | 抢占模型 | 特点 | 实时性 |
|---|---|---|---|
| Linux 2.4 及之前 | 不可抢占 (UP) | 进程在内核态运行时无法被抢占,直到主动返回用户态或阻塞。 | 差(非实时) |
| Linux 2.6 (PREEMPT_NONE) | 不可抢占 (SMP) | 与 UP 类似,但引入了自旋锁用于 SMP 保护。 | 差(非实时) |
| Linux 2.6 (PREEMPT_VOLUNTARY) | 自愿抢占 | 在显式的抢占点(如系统调用返回、锁释放)检查是否需要调度。 | 较低 |
| Linux 2.6 (PREEMPT) | 低延迟抢占 | 允许在中断返回、异常返回等时机抢占内核态进程。 | 中等(软实时) |
| PREEMPT_RT (RT) | 完全可抢占 | 通过中断线程化、实时互斥锁等机制,将内核大部分临界区变为可抢占。 | 高(硬实时) |
下图展示了从不可抢占内核到 PREEMPT_RT 内核的抢占模型演进路径:
2.2 自旋锁与互斥锁的实时化改造
锁机制是影响内核实时性的关键因素。在标准 Linux 内核中,自旋锁保护的临界区是不可抢占的,这会导致优先级反转和长延迟。PREEMPT_RT 对此进行了根本性改造。
2.2.1 自旋锁的实时化:变为可睡眠锁
在 PREEMPT_RT 中,自旋锁被实现为基于实时互斥锁的可睡眠锁。当进程试图获取一个已被持有的自旋锁时,它会被阻塞(睡眠),而不是忙等待。这允许更高优先级的任务抢占当前持有锁的进程,从而避免优先级反转。
spin_lock() 在 PREEMPT_RT 中不再禁用内核抢占,而是调用 rt_mutex_lock()。这意味着持有自旋锁的进程可以被更高优先级的任务抢占。
2.2.2 互斥锁的实时化:优先级继承
标准互斥锁存在优先级反转问题。PREEMPT_RT 使用实时互斥锁(rt_mutex),它实现了优先级继承协议。当低优先级任务持有高优先级任务所需的锁时,低优先级任务会临时继承高优先级任务的优先级,从而尽快释放锁。
下表对比了标准锁与 PREEMPT_RT 锁的关键差异:
| 特性 | 标准 Linux 自旋锁 | PREEMPT_RT 自旋锁 | 标准互斥锁 | PREEMPT_RT 互斥锁 |
|---|---|---|---|---|
| 实现方式 | 忙等待 + 禁用抢占 | 基于 rt_mutex 的可睡眠锁 | 快速用户态锁 (futex) | rt_mutex + 优先级继承 |
| 抢占性 | 不可抢占 | 可抢占 | 可抢占 | 可抢占 |
| 优先级反转 | 可能发生 | 通过可睡眠特性缓解 | 可能发生 | 通过优先级继承解决 |
| 适用场景 | 短临界区,SMP | 所有临界区 | 用户态同步 | 内核态与用户态实时同步 |
下图展示了优先级继承协议的工作流程:
2.3 中断线程化 (IRQ Threads) 机制
在标准 Linux 中,中断处理程序运行在中断上下文,具有最高优先级,且不可被抢占。这会导致高优先级任务被中断处理程序延迟。PREEMPT_RT 通过中断线程化解决了这个问题。
核心思想: 将中断处理程序的上半部(Top Half)和下半部(Bottom Half)合并,作为一个内核线程来运行。这个线程可以被调度,可以被其他更高优先级的任务抢占。
- 中断线程化: 在
request_irq()时,内核会为每个中断创建一个内核线程(例如irq/21-eth0)。中断发生时,硬件中断处理程序只做最少的必要工作(如应答中断),然后唤醒对应的中断线程。 - 线程优先级: 中断线程的优先级可以通过
chrt命令或sched_setscheduler()系统调用进行调整。实时任务可以设置比中断线程更高的优先级,从而确保实时性。 - 可抢占性: 中断线程是可抢占的。如果一个高优先级实时任务被唤醒,它可以抢占正在运行的中断线程。
irq_set_affinity_hint() 和 request_threaded_irq() 等接口来精细控制。
下图对比了标准中断处理与中断线程化的流程:
2.4 高精度定时器 (HRTIMER)
标准 Linux 定时器基于周期性时钟滴答(jiffies),精度受限于 HZ 值(通常为 100Hz 或 1000Hz,即 10ms 或 1ms 精度)。PREEMPT_RT 引入了高精度定时器(HRTIMER),它基于硬件定时器(如 TSC、HPET、ACPI PM 定时器)实现,精度可达纳秒级。
2.4.1 HRTIMER 的核心机制
- 红黑树组织: 所有高精度定时器按到期时间组织在一棵红黑树中,内核可以快速找到下一个到期的定时器。
- 硬件定时器编程: 内核根据红黑树中最早到期的定时器,直接编程硬件定时器。当硬件定时器到期时,产生一个中断,内核处理该定时器。
- 动态编程: 与周期性时钟不同,HRTIMER 是事件驱动的。只有在有定时器到期时才会产生中断,避免了无用的时钟滴答,降低了功耗和 CPU 负载。
hrtimer_start() 启动一个高精度定时器。
#include <linux/hrtimer.h>
#include <linux/ktime.h>
struct hrtimer my_timer;
// 定时器回调函数
enum hrtimer_restart my_timer_callback(struct hrtimer *timer) {
// 执行定时任务
printk(KERN_INFO "HRTIMER fired!\n");
return HRTIMER_NORESTART; // 不重启定时器
}
void setup_timer(void) {
ktime_t ktime;
// 初始化定时器
hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_timer.function = my_timer_callback;
// 设置超时时间为 100 微秒
ktime = ktime_set(0, 100000); // 0秒, 100000纳秒
hrtimer_start(&my_timer, ktime, HRTIMER_MODE_REL);
}
2.4.2 HRTIMER 与标准定时器对比
| 特性 | 标准定时器 (timer_list) | 高精度定时器 (hrtimer) |
|---|---|---|
| 精度 | jiffies 粒度 (通常 1-10ms) | 纳秒级 |
| 触发方式 | 基于周期性时钟滴答 | 基于硬件定时器事件驱动 |
| 数据结构 | 链表 | 红黑树 |
| 功耗 | 较高 (周期性唤醒 CPU) | 较低 (事件驱动,空闲时可深度睡眠) |
| 适用场景 | 非实时、低精度任务 | 实时控制、音频、网络等需要高精度定时的场景 |
下图展示了 HRTIMER 的工作流程: