第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 知识结构总览

下面这张图,把本章的核心逻辑串起来了:

Linux实时内核与PREEMPT_RT知识结构 Linux实时内核与PREEMPT_RT知识结构 PREEMPT_RT补丁 中断线程化 自旋锁→互斥锁 优先级继承 cyclictest测试 关闭ACPI/调频 CPU隔离/锁内存 关闭看门狗 目标:抖动<50μs,满足PROFINET IRT PREEMPT_RT改造 → cyclictest验证 → 系统优化 → 达到工业实时要求

2.7 避坑指南

最后,分享几个我踩过的坑:

  • 内核版本别追新:我试过5.15+最新补丁,结果PROFINET协议栈的驱动不兼容。工业项目用LTS版本最稳。
  • cyclictest别只跑一次:至少跑24小时,看最大延迟的稳定性。我见过白天跑得好好的,晚上温度一降,延迟就飙上去的。
  • 别忘了关掉桌面服务:如果你用桌面版Linux做网关,记得把systemd-journald、NetworkManager这些服务关掉。它们会时不时抢CPU。
  • 中断亲和性要设置:把PROFINET相关的中断绑定到隔离的CPU核心上,避免被其他中断干扰。

重要提醒:PREEMPT_RT内核不是万能的。它只能保证内核空间的实时性。如果你的PROFINET协议栈跑在用户空间,还需要配合sched_setscheduler()设置SCHED_FIFO策略和优先级。这个我们下一章会详细讲。

嗯,这一章的内容就到这。内核配好了,测试也跑过了,接下来就可以真正开始PROFINET网关的组态集成了。