第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改造的核心逻辑串起来了。从标准内核的问题出发,到补丁的三大改造手段,再到配置、编译、测试的完整流程。建议你保存下来,以后做项目时对照着看。
2.5 实战建议:从实验室到产品
最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路:
- 别迷信“打了RT补丁就万事大吉”:补丁只是基础。你还要做CPU隔离、中断亲和性设置、内存锁页(mlockall)等一系列优化。我见过一个团队,打了补丁后没做任何调优,测出来延迟还是100多微秒,然后抱怨补丁没用。其实不是补丁没用,是没调到位。
- 测试要跑足够长时间:cyclictest跑10秒看不出问题。我习惯跑至少24小时,甚至72小时。因为有些延迟抖动是偶发的,比如网络风暴、磁盘IO高峰时才会触发。短时间测试容易漏掉这些“定时炸弹”。
- 记录基线数据:每次修改内核参数或驱动后,都跑一遍cyclictest,把Max、Avg、Min记录下来。这样你能清楚地知道,每次改动是变好了还是变差了。我有个Excel表格,记录了每个内核版本的测试数据,排查问题时特别有用。
一句话总结:PREEMPT_RT补丁是实时Linux的基石,但真正的实时性,是靠“补丁 + 配置 + 调优 + 测试”四步走出来的。每一步都偷懒不得。