第三章:Linux内核实时性改造:PREEMPT_RT补丁原理与配置

好,咱们进入正题。EtherCAT主站跑在Linux上,最头疼的问题是什么?说白了就是延迟抖动。你想想看,一个实时控制周期是100微秒,结果内核调度器突然跑去处理别的任务,把EtherCAT数据帧的收发给耽误了——这在工业现场就是灾难。

我最早做EtherCAT主站时,用的就是标准Linux内核。结果呢?示波器一抓,周期抖动能达到几百微秒。客户问我:「你这系统能跑伺服吗?」我硬着头皮说能,其实心里虚得很。后来老老实实上了PREEMPT_RT补丁,抖动直接压到10微秒以内。嗯,这才是能用的东西。

3.1 为什么标准Linux内核不适合硬实时?

先搞清楚一个问题:标准Linux内核为什么实时性差?

核心原因就一个——内核抢占点太少。你想想看,当应用程序执行系统调用进入内核态后,如果内核正在处理某个任务,它不会轻易被其他高优先级任务打断。这个「不轻易」在实时场景下就是致命伤。

具体来说,标准内核有几个「实时杀手」:

  • 关中断区域太长:内核在某些临界区会关闭中断,这时候外部事件根本进不来
  • 自旋锁持有时间不可控:一个任务拿着自旋锁,其他任务只能干等
  • 调度器延迟不确定:从事件发生到调度器响应,中间可能隔着好几个不可抢占的内核路径

我在一个项目中遇到过这种情况:EtherCAT从站发来一个紧急停止信号,Linux内核因为正在处理文件系统的写操作,硬是等了3毫秒才响应。3毫秒啊,伺服电机都转了好几圈了。那次之后我就明白了——不做实时改造,工业以太网就是个笑话。

3.2 PREEMPT_RT补丁干了什么?

PREEMPT_RT补丁,说白了就是把Linux内核从一个「粗鲁的大块头」改造成一个「随时可以打断的灵活人」。它做了几件关键的事:

3.2.1 将自旋锁替换为互斥锁

这是最核心的改动。标准内核里,自旋锁持有期间是禁止抢占的。PREEMPT_RT把大部分自旋锁替换成了可睡眠的互斥锁(rt_mutex)。这意味着什么?一个任务拿着锁,如果被更高优先级的任务抢占了,它可以主动让出CPU,而不是死等。

我习惯把这个过程叫做「锁的优先级继承」。嗯,这个名字其实不太准确,但好理解。你想想看,低优先级任务拿着锁,高优先级任务等着要,这时候低优先级任务临时提升优先级,赶紧把锁释放掉——这就是优先级继承。PREEMPT_RT实现了这个机制。

3.2.2 中断线程化

标准内核里,中断处理程序运行在中断上下文,优先级最高,不可被抢占。PREEMPT_RT把中断处理程序变成了内核线程。这样一来,中断处理程序也可以被调度器管理,可以被更高优先级的实时任务抢占。

「那中断响应会不会变慢?」——会有人这么问。其实不会。关键中断(比如时钟中断)仍然保持原样,只有那些非关键的中断被线程化了。EtherCAT的网卡中断,我建议保留为原始中断,不要线程化。这个后面会讲。

3.2.3 增加抢占点

PREEMPT_RT在内核的很多关键路径上插入了抢占点。这些抢占点就像「检查站」,内核执行到这里时会问一句:「有没有更高优先级的任务要运行?」如果有,就切换过去。

核心结论:PREEMPT_RT不是让内核跑得更快,而是让内核的响应时间变得可预测。实时性追求的不是「快」,而是「确定」。

3.3 内核抢占模型选择

Linux内核提供了三种抢占模型,你得根据实际需求选。我见过不少工程师一上来就选Full Preemption,结果系统吞吐量掉了一半,还找不到原因。

抢占模型 配置选项 特点 适用场景
No Forced Preemption CONFIG_PREEMPT_NONE 服务器风格,吞吐量最高,延迟最大 文件服务器、数据库
Voluntary Kernel Preemption CONFIG_PREEMPT_VOLUNTARY 在自愿抢占点可抢占,延迟中等 桌面系统、轻度实时
Full Preemption CONFIG_PREEMPT 内核大部分区域可抢占,延迟最低 硬实时、工业控制

我个人建议:EtherCAT主站必须选Full Preemption。别犹豫,这是底线。Voluntary模型虽然也能用,但遇到高负载时,抖动会明显增加。我曾经在Voluntary模型下跑EtherCAT,负载一上来,周期抖动从20微秒飙到200微秒——这谁能忍?

注意:Full Preemption会带来约5%-15%的吞吐量损失。如果你的系统既要跑实时控制,又要做大量数据记录,得做好性能预算。我一般会单独分配一个CPU核心给EtherCAT主站,其他任务跑在别的核心上。

3.4 内核编译实战

好,理论说完了,咱们动手。下面是我自己用的编译流程,每一步都踩过坑。

3.4.1 获取源码和补丁

首先,你得找到匹配的版本。PREEMPT_RT补丁是跟着内核版本走的,不能乱配。

# 以5.10.200为例
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.200.tar.xz
wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.10/patch-5.10.200-rt89.patch.xz

# 解压并打补丁
tar -xf linux-5.10.200.tar.xz
cd linux-5.10.200
xzcat ../patch-5.10.200-rt89.patch.xz | patch -p1

这里有个坑:补丁版本号里的「rt89」表示这是第89个RT版本。我建议用最新的稳定RT版本,别用太老的。我曾经用过rt50左右的版本,结果有个网卡驱动死活编译不过,折腾了两天。

3.4.2 配置内核

配置内核时,有几个关键选项必须打开:

make menuconfig

# 必须开启的选项
General setup --->
  [*] Timers subsystem --->
    [*] High Resolution Timer Support

Processor type and features --->
  [*] Tickless System (Dynamic Ticks)
  Preemption Model (Fully Preemptible Kernel (RT))  # 选这个!

Kernel Features --->
  [*] Enable RT mutexes
  [*] RCU Subsystem --->
    RCU implementation (Preemptible RCU)  # 选这个

Device Drivers --->
  [*] Real Time Clock --->
    <*>   /sys/class/rtc/rtc0 (sysfs)

嗯,这里要注意:Preemption Model一定要选「Fully Preemptible Kernel (RT)」。如果你看不到这个选项,说明补丁没打成功。我遇到过这种情况,检查后发现是补丁版本和内核版本差了0.0.1——就一个小版本号不对,补丁就挂不上。

3.4.3 编译和安装

# 编译,用-j参数加速
make -j$(nproc)

# 安装模块
make modules_install

# 安装内核
make install

# 更新引导配置
update-grub

编译时间取决于你的机器。我习惯用-j$(nproc),但如果你内存不够(小于8GB),建议用-j2,否则编译到一半会OOM。别问我怎么知道的——有一次我服务器上同时跑了三个编译任务,结果系统直接挂了。

3.4.4 验证RT内核

重启后,检查是否真的跑在RT内核上:

# 查看内核版本
uname -a
# 输出应该包含 PREEMPT_RT 字样

# 检查实时性
cyclictest -m -n -p 99 -h 100 -i 1000 -l 100000

cyclictest的输出会告诉你最大延迟、平均延迟和最小延迟。我一般看最大延迟——如果超过50微秒,说明配置还有问题。正常调优后,EtherCAT主站的最大延迟应该控制在20微秒以内。

个人经验:第一次跑cyclictest时,别急着下结论。先让系统空载跑一轮,再加载EtherCAT主站跑一轮。对比两次结果,你就能看出是内核本身的问题,还是EtherCAT驱动的问题。我习惯把结果保存成文件,方便对比调优前后的效果。

3.5 知识体系总览

下面这张图是我自己画的,把本章的核心逻辑串起来了。你看一眼,应该能对整个改造过程有个全局认识。

Linux内核实时性改造知识体系 标准内核实时性差 原因:关中断长 | 自旋锁 | 调度延迟不确定 PREEMPT_RT补丁三大改造 自旋锁→互斥锁 优先级继承 中断线程化 可被实时任务抢占 增加抢占点 内核路径可抢占 抢占模型选择:No → Voluntary → Full(RT) 目标:延迟可预测,抖动 < 20μs

这张图从左到右,从上到下,就是整个改造的逻辑链条。你照着这个思路走,不会迷路。

好了,这一章的内容就到这儿。PREEMPT_RT补丁的原理和配置,说白了就是让内核变得「听话」——该让路的时候让路,该干活的时候干活。下一章我们会深入EtherCAT主站的实时线程设计,看看怎么把RT内核的能力真正用起来。

课后作业:在你的开发板上编译一个PREEMPT_RT内核,跑cyclictest测试,记录最大延迟。然后换成标准内核再跑一次,对比结果。差距会让你印象深刻的。