第3章 PREEMPT_RT补丁深度解析

这一章,我们来啃硬骨头。

PREEMPT_RT补丁,说白了就是把Linux从一个“尽量公平”的系统,改造成一个“必须准时”的系统。我最早接触它是在2009年,当时给一个工业机器人控制器做移植。那会儿补丁还叫「-rt」后缀,打个补丁要编译大半天,动不动就panic。嗯,现在好多了,但核心思想没变。

3.1 PREEMPT_RT的发展历史与社区现状

先聊聊历史。PREEMPT_RT项目始于2004年左右,由Ingo Molnar主导。当时Linux内核的抢占模型只有两种:

  • 非抢占内核(PREEMPT_NONE):适合服务器,吞吐量高,但延迟不可控
  • 低延迟抢占(PREEMPT_VOLUNTARY):在自愿调度点可抢占,延迟有所改善
  • 完全抢占(PREEMPT):普通桌面内核,大部分内核代码可抢占

但即便用了PREEMPT,最差情况下的延迟还是能达到几十毫秒。对于工业控制、航空航天、音频处理这些场景,这完全不可接受。

PREEMPT_RT补丁要解决的核心问题就一个:把内核的不可抢占区域压缩到极致。

我记得2015年给一个医疗设备做认证时,对方要求最差情况响应时间不超过150微秒。普通内核根本做不到,上了PREEMPT_RT之后,实测最差延迟在80微秒左右。那是我第一次真正信服这个补丁的价值。

社区现状如何?截至2024年,PREEMPT_RT补丁已经合并了大约85%到主线内核。剩下的部分主要是:

  • 中断线程化的基础设施(已合入主线)
  • 高精度定时器(已合入主线)
  • RCU优先级继承(部分合入)
  • raw_spinlock与spinlock的分离(已合入主线)
  • 一些驱动和架构相关的补丁(仍在维护)

说白了,现在主线内核已经具备了RT的大部分能力,但完整的PREEMPT_RT补丁集仍然需要额外打上。我个人建议,如果你做的是硬实时项目,还是老老实实打完整补丁集,别只依赖主线。

关键里程碑:

  • 2004年:Ingo Molnar发布首个PREEMPT_RT补丁
  • 2012年:Thomas Gleixner接管维护
  • 2019年:主线内核开始逐步合并RT基础设施
  • 2024年:约85%的补丁已合入主线,剩余部分持续演进

3.2 核心补丁分析

PREEMPT_RT补丁集包含上百个补丁,但核心的就三个。我一个个讲。

3.2.1 中断线程化

这是PREEMPT_RT最核心的改动。传统Linux中,中断处理程序运行在中断上下文,优先级最高,不可抢占。这意味着:

  • 中断处理期间,任何用户态任务都无法运行
  • 如果中断处理时间过长,实时任务就会错过截止时间

中断线程化的思路很简单:把中断处理程序的下半部变成一个内核线程,可以被调度器管理,可以被更高优先级的实时任务抢占。

具体来说,中断处理被拆成两部分:

  • 上半部(hardirq):极短的处理,比如ACK中断、拷贝数据。仍然在中断上下文运行。
  • 下半部(threaded IRQ):耗时的处理逻辑,作为内核线程运行,可被抢占。

我在项目中遇到过一个问题:某个网卡驱动的中断处理函数里做了大量的数据拷贝和协议解析,导致音频播放出现卡顿。打上PREEMPT_RT补丁后,把中断处理改成线程化,音频线程优先级设高,问题就解决了。

避坑指南:我曾经在调试时发现,某些老旧驱动不支持中断线程化。如果你遇到中断延迟异常高的情况,先检查驱动是否使用了request_threaded_irq()而不是request_irq()。

3.2.2 高精度定时器

传统Linux定时器基于jiffies,精度只有1/HZ秒(比如HZ=1000时,精度1ms)。对于实时系统来说,这太粗糙了。

PREEMPT_RT引入了高精度定时器(hrtimer),基于硬件定时器实现,精度可以达到纳秒级。关键点在于:

  • hrtimer使用红黑树管理,而不是传统的定时器链表
  • hrtimer支持绝对时间和相对时间两种模式
  • hrtimer可以在中断上下文或进程上下文运行

你想想看,一个实时任务需要每500微秒执行一次,用传统定时器根本做不到。hrtimer就能轻松搞定。

// 使用hrtimer的示例代码
#include <linux/hrtimer.h>
#include <linux/ktime.h>

static struct hrtimer my_timer;

static enum hrtimer_restart my_timer_callback(struct hrtimer *timer)
{
    // 定时器触发时的处理逻辑
    // 返回HRTIMER_RESTART表示重复定时,HRTIMER_NORESTART表示单次
    return HRTIMER_RESTART;
}

void setup_rt_timer(void)
{
    ktime_t interval = ktime_set(0, 500000); // 500微秒
    hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    my_timer.function = my_timer_callback;
    hrtimer_start(&my_timer, interval, HRTIMER_MODE_REL);
}

3.2.3 RCU优先级继承

RCU(Read-Copy-Update)是Linux内核中一种高效的同步机制。但在实时系统中,RCU有个大问题:优先级反转。

简单说,一个低优先级的任务持有RCU锁,高优先级的任务在等待这个锁,中间优先级的任务抢占了低优先级任务,导致高优先级任务被无限期阻塞。这就是经典的优先级反转问题。

PREEMPT_RT补丁为RCU引入了优先级继承机制:当高优先级任务等待RCU时,低优先级任务会临时继承高优先级,防止被中间优先级任务抢占。

注意:RCU优先级继承的补丁在主线内核中尚未完全合入。如果你使用主线内核做实时系统,需要额外打上这部分补丁。我曾经因为忽略这一点,导致一个多核系统上出现随机性的任务超时,排查了整整两天。

3.3 RT补丁的配置与编译实战

理论讲完了,咱们动手。下面是我个人习惯的RT内核编译流程。

3.3.1 获取补丁

PREEMPT_RT补丁可以从kernel.org的pub/scm/linux/kernel/git/rt/linux-rt-devel.git获取。我建议使用长期支持(LTS)版本对应的RT补丁,稳定性更好。

# 下载主线内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.30.tar.xz
tar -xf linux-6.6.30.tar.xz

# 下载对应的RT补丁
wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/6.6/patch-6.6.30-rt42.patch.xz
xz -d patch-6.6.30-rt42.patch.xz

# 打补丁
cd linux-6.6.30
patch -p1 < ../patch-6.6.30-rt42.patch

3.3.2 内核配置

打上补丁后,配置内核时会出现新的RT相关选项。关键配置项如下:

配置项 说明 建议值
CONFIG_PREEMPT_RT 启用完整RT抢占模型 y
CONFIG_HZ_1000 设置时钟频率为1000Hz y
CONFIG_HIGH_RES_TIMERS 启用高精度定时器 y
CONFIG_IRQ_FORCED_THREADING 强制中断线程化 y
CONFIG_RCU_BOOST 启用RCU优先级提升 y
CONFIG_RCU_NOCB_CPU 将RCU回调卸载到专用线程 y
# 配置内核
make menuconfig
# 进入 General setup -> Preemption Model
# 选择 "Fully Preemptible Kernel (Real-Time)"

# 编译
make -j$(nproc)
make modules_install
make install

个人经验:第一次编译RT内核时,建议先做make olddefconfig,然后手动修改关键配置。直接make menuconfig容易漏掉一些隐藏选项。我曾经因为没开CONFIG_IRQ_FORCED_THREADING,导致中断线程化没生效,排查了半天。

3.4 RT内核启动日志解读

内核启动时,日志里会透露很多信息。我教你几招快速判断RT内核是否正常工作。

3.4.1 关键日志行

启动后执行dmesg | grep -i rt,你应该看到类似下面的输出:

[    0.000000] Linux version 6.6.30-rt42 (user@host) (gcc-12) #1 SMP PREEMPT_RT
[    0.000000] Command line: ... isolcpus=1-3 nohz_full=1-3 ...
[    0.000000] RCU: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=4
[    0.000000] RCU: Enabling RCU priority boosting
[    0.000000] RCU: Defer RCU callbacks to kthreads (RCU_NOCB_CPU)
[    0.000000] Switched to clocksource tsc
[    0.000000] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x1d...
[    0.000000] clocksource: Switched to clocksource tsc
[    0.123456] IRQ: Forced threading enabled

重点关注这几行:

  • PREEMPT_RT:确认内核是RT版本
  • RCU priority boosting:RCU优先级继承已启用
  • RCU_NOCB_CPU:RCU回调已卸载到专用线程
  • Forced threading enabled:中断强制线程化已生效

3.4.2 检查中断线程化

执行ps -eo pid,comm,pri | grep irq,你会看到类似:

  123 irq/18-eth0       50
  124 irq/19-sata       50
  125 irq/20-usb        50

每个中断都有一个对应的内核线程,优先级50(默认)。你可以用chrt命令调整它们的优先级:

# 将网卡中断线程优先级设为99(最高)
chrt -f -p 99 123

警告:我曾经在生产环境中把中断线程优先级设得过高,导致系统响应异常。建议中断线程优先级不要超过80,给关键任务留出空间。另外,调整优先级前一定要确认中断号对应的设备,别把关键设备的中断优先级调低了。

3.5 知识结构图

下面这张图总结了PREEMPT_RT的核心知识体系,方便你对照理解。

PREEMPT_RT核心知识体系 PREEMPT_RT核心知识体系 PREEMPT_RT补丁集 中断线程化 高精度定时器 RCU优先级继承 上半部(hardirq) 下半部(threaded IRQ) 红黑树管理 纳秒级精度 优先级反转 优先级继承 配置与编译实战 启动日志解读 CONFIG_PREEMPT_RT CONFIG_HIGH_RES_TIMERS PREEMPT_RT标记 Forced threading 核心思路:将不可抢占区域压缩到极致,通过中断线程化、高精度定时器和RCU优先级继承实现可预测的低延迟

这张图把PREEMPT_RT的三大核心补丁、配置要点和启动验证串在了一起。你对照着看,思路会清晰很多。

好了,这一章的内容就到这儿。PREEMPT_RT补丁的深度解析,说白了就是理解「怎么让Linux变得准时」。中断线程化解决的是「谁先跑」的问题,高精度定时器解决的是「什么时候跑」的问题,RCU优先级继承解决的是「跑的时候别被堵住」的问题。三个点抓住了,RT内核你就懂了一半。