第2章:Linux内核实时性改造:PREEMPT_RT补丁原理、内核配置与编译、实时性测试工具(cyclictest)使用

各位同学,咱们今天聊点硬核的。嵌入式Linux要做实时控制,第一道坎就是内核本身。标准Linux内核是个“公平先生”,它尽量让每个任务都分到时间片,但实时系统要的是“说一不二”——我说这个任务5毫秒内必须跑完,那就得跑完。怎么办?打上PREEMPT_RT补丁。

我在2016年做一个工业机器人项目时,就吃过这个亏。当时用的标准内核,电机控制线程时不时被调度延迟卡一下,机器人在高速运动时直接抖成了“帕金森”。后来一测,最大延迟竟然到了20多毫秒。嗯,从那以后,我再也不敢轻视内核实时性改造了。

核心结论:PREEMPT_RT补丁通过将内核中的自旋锁替换为可抢占的互斥锁、增加抢占点、以及将中断线程化,将Linux从一个“尽力而为”的系统,改造成一个“确定性优先”的实时系统。

2.1 PREEMPT_RT补丁原理:内核是怎么变“听话”的?

说白了,标准内核里有很多“不可抢占区”。比如一个系统调用正在操作一个链表,这时候来了个高优先级任务想抢CPU——对不起,你得等着。这个“等着”的时间,就是实时性的死穴。

PREEMPT_RT做了三件核心的事:

  • 中断线程化:把原本在中断上下文执行的中断处理程序,变成内核线程。这样它们就可以被调度、被抢占,不再“霸占”CPU不放。
  • 自旋锁替换:把大部分自旋锁(spinlock)替换成可睡眠的互斥锁(rt_mutex)。任务在等锁的时候可以睡过去,让别的任务先跑。
  • 增加抢占点:在内核代码的关键路径上,插入更多的抢占点。高优先级任务来了,能更快地抢到CPU。

你想想看,这三板斧下去,内核的“不可抢占区”从原来的几百微秒甚至几毫秒,压缩到了几十微秒级别。这就是实时性的本质提升。

我的经验:别以为打了补丁就万事大吉。我曾经在一个项目里发现,某个网卡驱动的中断处理函数特别长,即使线程化了,它还是占了太多时间。最后不得不把驱动里的中断处理逻辑拆成顶半部和底半部,才把延迟降下来。所以,驱动质量也很关键。

2.2 内核配置与编译:动手打造你的实时内核

好,理论讲完了,咱们来点实际的。怎么编译一个带PREEMPT_RT补丁的内核?我习惯用脚本自动化,但第一次还是建议手动走一遍,理解每个步骤的意义。

2.2.1 获取源码与补丁

首先,你得找到匹配的版本。PREEMPT_RT补丁是跟着内核主线版本走的。比如你要用Linux 5.10 LTS,就去下载对应的rt补丁。

# 下载内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.100.tar.xz
tar -xf linux-5.10.100.tar.xz

# 下载PREEMPT_RT补丁
wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.10/patch-5.10.100-rt60.patch.xz
xz -d patch-5.10.100-rt60.patch.xz

# 打补丁
cd linux-5.10.100
patch -p1 < ../patch-5.10.100-rt60.patch

注意:补丁版本必须与内核版本严格对应。差一个次版本号都可能打不上。我曾经因为手快,下了个5.10.99的补丁打在5.10.100上,结果patch报了一堆冲突,排查了半天。别学我。

2.2.2 内核配置:开启实时性选项

配置内核是核心中的核心。我建议基于你当前系统的配置文件来改,这样能省掉很多驱动适配的麻烦。

# 使用当前系统的配置作为基础
cp /boot/config-$(uname -r) .config

# 进入配置界面
make menuconfig

在menuconfig里,你需要找到并开启以下关键选项:

配置项 路径 说明
CONFIG_PREEMPT_RT_FULL General setup → Preemption Model 选择“Fully Preemptible Kernel (Real-Time)”
CONFIG_HIGH_RES_TIMERS General setup → Timers subsystem 高精度定时器,必须开启
CONFIG_NO_HZ_FULL General setup → Timers subsystem → Tickless System 全无滴答模式,减少定时器中断干扰
CONFIG_CPU_ISOLATION General setup → CPU Isolation 允许将CPU隔离出来专门跑实时任务

我个人习惯,还会额外开启CONFIG_RCU_NOCB_CPU,把RCU回调从实时CPU上卸载掉。这个细节很多人会忽略,但它对降低延迟抖动很有帮助。

2.2.3 编译与安装

配置好了,就是编译。注意,别用-j参数开太多线程,否则容易把内存吃满。我一般用物理核心数的一半。

# 编译内核和模块
make -j$(nproc) bzImage
make -j$(nproc) modules

# 安装模块
sudo make modules_install

# 安装内核
sudo make install

# 更新引导
sudo update-grub

重启后,在GRUB菜单里选择新内核。用uname -a确认一下,如果看到PREEMPT RT字样,就说明成功了。

避坑指南:我第一次编译RT内核时,忘了装libelf-dev和openssl-dev,结果编译到一半报错。建议先跑一下sudo apt build-dep linux,把依赖一次性装齐。

2.3 实时性测试工具:cyclictest 使用

内核编译好了,到底实时性怎么样?不能靠感觉,得用数据说话。cyclictest是rt-tests工具集里的核心工具,专门用来测量内核调度延迟。

2.3.1 安装rt-tests

# 从源码编译
git clone git://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git
cd rt-tests
make
sudo make install

# 或者直接用包管理器(版本可能较旧)
sudo apt install rt-tests

2.3.2 运行cyclictest

最基本的用法:

sudo cyclictest -t 5 -p 80 -n -i 1000 -l 10000

参数含义:

参数 含义 我的建议
-t 5 启动5个测量线程 一般和CPU核心数一致
-p 80 线程优先级80 实时线程建议80-90
-n 使用nanosleep 比默认的clock_nanosleep更准
-i 1000 间隔1000微秒(1ms) 模拟典型的实时周期
-l 10000 循环10000次 测试时长 = 10000 * 1ms = 10秒

输出示例:

T: 0 (12345) P: 80 I: 1000 C:  10000 Min:      2 Act:    5 Avg:    4 Max:   18
T: 1 (12346) P: 80 I: 1000 C:  10000 Min:      2 Act:    4 Avg:    3 Max:   15
T: 2 (12347) P: 80 I: 1000 C:  10000 Min:      1 Act:    3 Avg:    3 Max:   12
T: 3 (12348) P: 80 I: 1000 C:  10000 Min:      2 Act:    6 Avg:    4 Max:   20
T: 4 (12349) P: 80 I: 1000 C:  10000 Min:      1 Act:    4 Avg:    3 Max:   14

重点关注Max列,这是最大延迟。如果这个值在几十微秒级别,说明你的内核实时性不错。如果超过100微秒,就得排查问题了。

重要:测试时一定要用sudo,否则线程优先级不生效。我曾经在一个项目里忘了加sudo,测出来的最大延迟有200多微秒,吓我一跳。后来发现是权限问题,加上sudo后直接降到20微秒。嗯,白忙活了一上午。

2.4 知识结构图:PREEMPT_RT改造全景

下面这张图,把PREEMPT_RT改造的核心逻辑串起来了。从标准内核的问题出发,到补丁的三大改造手段,再到配置、编译、测试的完整流程。建议你保存下来,以后做项目时对照着看。

PREEMPT_RT实时内核改造全景图 PREEMPT_RT实时内核改造全景图 标准内核:不可抢占区导致延迟抖动 ① 中断线程化 ② 自旋锁→可睡眠互斥锁 ③ 内核路径增加抢占点 内核配置:开启 PREEMPT_RT_FULL + 高精度定时器 + CPU隔离 编译安装:make bzImage → make modules → make install 测试验证:cyclictest 测量调度延迟(关注 Max 列) 结果判定:Max < 50μs 合格,Max < 20μs 优秀 说明:实际项目中,还需结合CPU隔离(isolcpus)、中断亲和性(irqbalance)等进一步优化

2.5 实战建议:从实验室到产品

最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路:

  • 别迷信“打了RT补丁就万事大吉”:补丁只是基础。你还要做CPU隔离、中断亲和性设置、内存锁页(mlockall)等一系列优化。我见过一个团队,打了补丁后没做任何调优,测出来延迟还是100多微秒,然后抱怨补丁没用。其实不是补丁没用,是没调到位。
  • 测试要跑足够长时间:cyclictest跑10秒看不出问题。我习惯跑至少24小时,甚至72小时。因为有些延迟抖动是偶发的,比如网络风暴、磁盘IO高峰时才会触发。短时间测试容易漏掉这些“定时炸弹”。
  • 记录基线数据:每次修改内核参数或驱动后,都跑一遍cyclictest,把Max、Avg、Min记录下来。这样你能清楚地知道,每次改动是变好了还是变差了。我有个Excel表格,记录了每个内核版本的测试数据,排查问题时特别有用。

一句话总结:PREEMPT_RT补丁是实时Linux的基石,但真正的实时性,是靠“补丁 + 配置 + 调优 + 测试”四步走出来的。每一步都偷懒不得。