第2章:Linux实时内核与PREEMPT_RT补丁
做工业网关这些年,我最大的体会就是:实时性不是靠嘴说出来的,是内核调度器一刀一刀切出来的。很多朋友问我,为什么我的PROFINET网关跑着跑着就丢包了?为什么从站响应时间忽高忽低?说白了,八成是内核没配好。
这一章,我们就来啃下实时内核这块硬骨头。我会把内核编译、PREEMPT_RT补丁、cyclictest测试、中断线程化这些内容串起来讲。嗯,都是我在项目里踩过的坑。
2.1 为什么标准Linux做不了实时?
先问个问题:标准Linux内核为什么不适合工业实时控制?
你想想看,标准内核里中断处理是最高优先级的。一个网卡中断来了,CPU立马放下手头的事去处理。这听起来挺快,对吧?但问题在于——中断处理程序里不能睡眠,而且它会把所有低优先级任务都堵死。
更麻烦的是,内核里到处都是大内核锁(BKL)和关抢占区。一个系统调用进去,可能就把整个内核给锁住了。这时候你PROFINET周期数据来了?等着吧。
实时内核的核心目标:把内核的不可抢占区域压缩到极致,让高优先级实时任务能在确定时间内得到响应。
我记得第一次做PROFINET网关原型时,用的就是标准Ubuntu内核。结果呢?周期抖动动不动就几百微秒,根本没法用。后来换了PREEMPT_RT,抖动直接降到10微秒以内。差距就这么大。
2.2 PREEMPT_RT补丁干了什么?
PREEMPT_RT补丁,说白了就是把Linux内核从一个"粗粒度抢占"的系统,改造成一个"细粒度抢占"的系统。它主要做了这几件事:
| 改造项 | 标准内核 | PREEMPT_RT内核 |
|---|---|---|
| 中断处理 | 在中断上下文中执行,不可睡眠 | 中断线程化,可被高优先级任务抢占 |
| 自旋锁 | 关抢占,忙等待 | 可睡眠互斥锁(rt_mutex) |
| 关抢占区 | 大量不可抢占区域 | 几乎全部可抢占 |
| 优先级继承 | 不支持 | 完整支持 |
| 高精度定时器 | 可选 | 强制启用 |
这里面最关键的,就是中断线程化和优先级继承。我一个个说。
2.2.1 中断线程化
标准内核里,中断来了直接执行,谁都不能拦。但PREEMPT_RT把中断处理程序变成了内核线程。这样一来,中断处理就有了优先级,可以被更高优先级的实时任务抢占。
举个例子:你的PROFINET周期数据是1ms一次,优先级设为99。网卡中断优先级设为50。那当周期任务正在运行时,网卡中断来了,它得等着——因为周期任务优先级更高。这在标准内核里是做不到的。
我的经验:中断线程化后,一定要给每个中断线程分配合理的优先级。我曾经把所有中断都设成最高优先级,结果实时任务反而被中断淹没了。后来把网卡中断降到50,PROFINET周期任务设99,才稳定下来。
2.2.2 优先级继承
优先级继承是个啥?我举个实际场景:
任务A(优先级80)持有一把锁,任务B(优先级99)想拿同一把锁。标准内核里,任务B只能干等,哪怕任务A被低优先级任务抢占了,它也得等着。这叫优先级反转。
PREEMPT_RT的优先级继承机制会临时把任务A的优先级提升到99,让它赶紧把锁释放掉。这样任务B就能尽快拿到锁。嗯,这个机制在PROFINET多任务通信中特别重要。
2.3 内核编译与配置实战
好了,理论说完了,我们来动手。我个人习惯用4.19.x或5.10.x内核搭配PREEMPT_RT补丁。这两个版本在工业界验证得最多。
2.3.1 下载与打补丁
# 下载内核源码
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-rt61.patch.xz
xz -d patch-5.10.100-rt61.patch.xz
# 打补丁
cd linux-5.10.100
patch -p1 < ../patch-5.10.100-rt61.patch
注意:内核版本和补丁版本必须严格对应。差一个次版本号都可能打不上。我曾经因为版本号没对齐,折腾了一整天。
2.3.2 关键配置项
配置内核时,以下选项必须打开:
# 实时性核心配置
CONFIG_PREEMPT_RT_FULL=y # 完全实时抢占
CONFIG_HIGH_RES_TIMERS=y # 高精度定时器
CONFIG_NO_HZ_FULL=y # 全无滴答模式
# 中断线程化
CONFIG_IRQ_FORCED_THREADING=y # 强制中断线程化
# 优先级继承
CONFIG_RT_MUTEXES=y # 实时互斥锁
# 调试与测试工具
CONFIG_FTRACE=y # 函数跟踪
CONFIG_STACKTRACE=y # 栈回溯
CONFIG_DEBUG_PREEMPT=y # 抢占调试
配置方法:
make menuconfig
# 进入 General setup ---> Preemption Model
# 选择 "Fully Preemptible Kernel (Real-Time)"
2.3.3 编译与安装
# 编译内核
make -j$(nproc)
# 安装模块
make modules_install
# 安装内核
make install
# 更新引导
update-grub
重启后,用uname -a确认内核版本。如果看到PREEMPT RT字样,说明成功了。
2.4 实时性测试:cyclictest
内核配好了,到底实时性怎么样?别猜,跑一下cyclictest就知道了。
2.4.1 安装cyclictest
# 从源码编译
git clone git://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git
cd rt-tests
make
make install
# 或者直接用包管理器
apt-get install rt-tests # Debian/Ubuntu
2.4.2 运行测试
# 基本测试:1个线程,优先级99,周期1000微秒
cyclictest -t 1 -p 99 -n -i 1000 -l 100000
# 多线程测试:4个线程,不同优先级
cyclictest -t 4 -p 99 -n -i 1000 -l 100000 -m
参数说明:
-t:线程数-p:优先级-n:使用nanosleep-i:间隔(微秒)-l:循环次数-m:锁定内存,防止页面交换
2.4.3 结果解读
测试结果长这样:
T: 0 ( 1234) P: 99 I: 1000 C: 100000 Min: 2 Act: 5 Avg: 4 Max: 18
关键指标:
- Min:最小延迟(2微秒)
- Act:当前延迟(5微秒)
- Avg:平均延迟(4微秒)
- Max:最大延迟(18微秒)—— 这个最重要
PROFINET网关的实时性要求:对于1ms周期的IRT通信,最大抖动应控制在50微秒以内。如果Max超过100微秒,你的网关在工业现场大概率会出问题。
我曾经在一个项目里,cyclictest跑出来Max是85微秒。当时觉得还行,结果到现场一接PROFINET从站,丢包率3%。后来排查发现是ACPI电源管理没关,关了之后Max降到12微秒,问题解决。
2.5 实时性优化技巧
跑完cyclictest,如果结果不理想,别急着换内核。先试试这些优化:
| 优化项 | 操作 | 预期效果 |
|---|---|---|
| 关闭ACPI | 内核参数:acpi=off |
减少电源管理中断 |
| 关闭CPU调频 | 内核参数:intel_pstate=disable |
稳定CPU频率 |
| 隔离CPU核心 | 内核参数:isolcpus=1,2 |
专用核心跑实时任务 |
| 关闭看门狗 | echo 0 > /proc/sys/kernel/watchdog |
减少NMI中断 |
| 锁定内存 | 使用mlockall()系统调用 |
防止页面交换 |
我的建议:在grub命令行里加上threadirqs参数,强制所有中断线程化。这个参数在PREEMPT_RT内核里默认启用,但手动加上更保险。
2.6 知识结构总览
下面这张图,把本章的核心逻辑串起来了:
2.7 避坑指南
最后,分享几个我踩过的坑:
- 内核版本别追新:我试过5.15+最新补丁,结果PROFINET协议栈的驱动不兼容。工业项目用LTS版本最稳。
- cyclictest别只跑一次:至少跑24小时,看最大延迟的稳定性。我见过白天跑得好好的,晚上温度一降,延迟就飙上去的。
- 别忘了关掉桌面服务:如果你用桌面版Linux做网关,记得把
systemd-journald、NetworkManager这些服务关掉。它们会时不时抢CPU。 - 中断亲和性要设置:把PROFINET相关的中断绑定到隔离的CPU核心上,避免被其他中断干扰。
重要提醒:PREEMPT_RT内核不是万能的。它只能保证内核空间的实时性。如果你的PROFINET协议栈跑在用户空间,还需要配合sched_setscheduler()设置SCHED_FIFO策略和优先级。这个我们下一章会详细讲。
嗯,这一章的内容就到这。内核配好了,测试也跑过了,接下来就可以真正开始PROFINET网关的组态集成了。