3. 实时Linux内核基础

在工业边缘控制器中,Linux 之所以常被认为“不够实时”,根源并不在调度器本身,而在于内核中大量不可抢占的执行路径:中断处理、软中断、自旋锁临界区、以及被关中断保护的短小代码段。PREEMPT_RT 补丁集的目标,就是把这些“不可抢占窗口”尽可能压缩到可预测的微秒级,使最坏情况延迟(Worst-Case Latency)从标准内核的毫秒级降到几十微秒量级。理解这一章,关键要抓住四条主线:抢占模型如何改变、中断如何被线程化、优先级反转如何被抑制、以及延迟到底怎么测。

一、PREEMPT_RT 改变了什么

标准 Linux 提供四种抢占模型:PREEMPT_NONE、PREEMPT_VOLUNTARY、PREEMPT(低延迟桌面)和 PREEMPT_RT。前三种只允许在特定点被抢占,而 PREEMPT_RT 通过以下机制实现“几乎处处可抢占”:

  • 自旋锁转为可睡眠的 rt_mutex:普通 spinlock_t 在 RT 内核中变为可抢占的互斥量,持锁期间高优先级任务仍可抢占,代价是临界区不能太长。
  • 中断线程化:硬中断处理被拆成“顶半部(top half)+ 线程化底半部”,顶半部只做最小唤醒工作。
  • 软中断线程化:ksoftirqd 承担大部分软中断处理,避免软中断长时间占用 CPU。
  • 关中断临界区最小化:用 local_lock 替代 local_irq_disable,把不可抢占窗口压缩到极短。

在工业边缘设备上,通常使用 PREEMPT_RT 内核配置,并在启动参数中加入 threadirqs(若内核未默认开启)以及 isolcpus 隔离实时核。例如隔离 CPU 2 和 3:

# 内核启动参数示例
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3
# 查看当前抢占模型
uname -a
cat /sys/kernel/realtime   # 1 表示 RT 内核

二、中断线程化:把硬中断变成可调度线程

中断线程化的核心思想是:硬中断只做“确认中断、唤醒对应线程”两件事,真正的数据处理交给一个具有实时优先级的线程。这样,中断处理就进入了调度器的管辖范围,可以被更高优先级任务抢占,也可以被优先级继承机制保护。

在 RT 内核中,每个中断线对应一个内核线程,命名形如 irq/<irq号>-<设备名>。可以通过 ps 查看:

ps -eLo pid,tid,cls,rtprio,comm | grep irq/
# 示例输出
# 1234  1234  FF  50  irq/45-eth0
# 1235  1235  FF  50  irq/46-uart1

线程化后,中断线程的优先级可以调整。对于工业现场总线(如 EtherCAT、PROFINET)对应的网卡中断,通常将其优先级设为 80~90,高于普通应用线程但低于关键控制线程。调整方式:

chrt -f -p 85 $(pgrep -f "irq/45-eth0")

需要注意的是,线程化会带来额外的上下文切换开销,因此对于极高频、极短的中断(如某些定时器),仍可能保留为硬中断。判断依据是:如果中断处理时间远小于一次线程唤醒开销(约几微秒),则线程化收益有限。

三、优先级继承:解决优先级反转

优先级反转是实时系统中最隐蔽的延迟来源:低优先级任务持有锁,中优先级任务抢占 CPU,导致高优先级任务被无限期阻塞。PREEMPT_RT 通过 rt_mutex 实现优先级继承(Priority Inheritance, PI):当高优先级任务等待低优先级任务持有的锁时,低优先级任务临时提升到高优先级,直到释放锁。

在用户空间,POSIX 互斥量需要显式设置 PI 属性:

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&mutex, &attr);

如果没有设置 PTHREAD_PRIO_INHERIT,即使内核支持 PI,用户态互斥量也不会触发继承。这是工业边缘应用中最常见的配置遗漏之一。验证方法:用 cyclictest 配合一个故意制造反转的测试程序,观察最大延迟是否因 PI 而收敛。

四、实时调度类与优先级范围

Linux 实时调度类主要有两种:SCHED_FIFO(先进先出)和 SCHED_RR(时间片轮转)。工业控制中通常使用 SCHED_FIFO,因为控制周期固定,不需要时间片轮转。实时优先级范围为 1~99,数值越大优先级越高。普通 SCHED_OTHER 任务优先级为 0。

典型分配策略(示例):

任务类型调度类优先级说明
关键控制线程SCHED_FIFO90周期 1ms,抖动要求 <50μs
现场总线中断线程SCHED_FIFO85EtherCAT 网卡中断
数据采集线程SCHED_FIFO70周期 5ms
日志/通信线程SCHED_OTHER0非实时

设置方式:

chrt -f 90 ./control_task
# 或在代码中
struct sched_param param;
param.sched_priority = 90;
pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

注意:实时优先级 99 通常保留给内核看门狗和迁移线程,应用不应占用。此外,SCHED_FIFO 任务若陷入死循环,会永久占用 CPU,因此必须配合 RLIMIT_RTTIME 或看门狗机制。

五、延迟测量方法

延迟测量是验证实时性的唯一手段。工业边缘常用工具是 cyclictest,它创建一个周期性线程,记录每次唤醒的实际时间与预期时间的差值,输出最小、平均、最大延迟。

cyclictest -t 4 -p 90 -i 1000 -l 100000 -m -n -h 400
# -t 4      : 4 个线程
# -p 90     : 优先级 90
# -i 1000   : 周期 1000 微秒
# -l 100000 : 循环 10 万次
# -m        : 锁定内存
# -n        : 使用 clock_nanosleep
# -h 400    : 延迟直方图,最大 400 微秒

典型输出解读:Min 反映最佳情况,Avg 反映平均负载,Max 才是实时性判据。工业控制通常要求 Max 小于控制周期的 10%~20%。例如 1ms 周期,Max 应小于 100~200μs。若 Max 超标,需排查:BIOS 中的 C-State、SMI、电源管理、网卡中断合并、以及是否有非实时任务占用隔离核。

PREEMPT_RT 实时性关键机制与延迟测量链路 标准 Linux 内核 硬中断长时间关中断 自旋锁不可抢占 软中断延迟处理 最坏延迟:毫秒级 PREEMPT_RT 内核 中断线程化 irq/xx rt_mutex 可抢占 优先级继承 PI 最坏延迟:几十微秒 调度与测量 SCHED_FIFO 优先级 1-99 cyclictest 周期唤醒 Max 延迟判据 目标:Max < 周期 10% cyclictest 延迟测量与判读流程 创建周期线程 SCHED_FIFO / 90 clock_nanosleep 周期 1000μs 记录唤醒偏差 actual - expected 统计 Min/Avg/Max 直方图 -h 400 判据 Max 若 Max 超标:检查 BIOS C-State / SMI、网卡中断合并、隔离核上的非实时任务、以及用户态互斥量是否启用 PTHREAD_PRIO_INHERIT 工业控制经验判据:Max 延迟应小于控制周期的 10%~20%,且多次运行结果稳定

六、工程实践中的关键检查点

把上述机制落到工业边缘设备上,建议按以下顺序检查:

  1. 确认内核为 PREEMPT_RT,cat /sys/kernel/realtime 返回 1。
  2. 确认关键中断已线程化,ps -eLo cls,rtprio,comm | grep irq/ 显示 FF 类。
  3. 确认用户态互斥量设置了 PTHREAD_PRIO_INHERIT。
  4. 确认实时任务使用 SCHED_FIFO,优先级分配合理,且未占用 99。
  5. 用 cyclictest 在隔离核上运行至少 10 万次,记录 Max 值。
  6. 若 Max 超标,逐项排查 BIOS、电源管理、中断合并、内存锁定(mlockall)和 CPU 频率调节器(建议 performance)。
在工业边缘场景中,实时性不是“平均快”,而是“最坏情况可预测”。PREEMPT_RT 提供的是可预测性,而不是绝对速度。测量 Max 延迟并使其稳定收敛,才是工程验收的核心依据。