第三章:Linux内核实时性改造

好,咱们进入正题。这一章讲的是Linux内核实时性改造,说白了就是让Linux这个“通用操作系统”能干活儿更靠谱、更准时。我在工业控制项目里摸爬滚打这么多年,最深的体会就是:实时性不是靠嘴说出来的,是靠内核一点点“拧”出来的

3.1 内核配置:从源头掐掉延迟

很多人拿到内核源码,直接make menuconfig就开始编译。我建议你先停下来,想想你的应用场景。工业控制里,我们最怕什么?怕中断被屏蔽、怕调度器乱抢、怕大内核锁。

所以第一步,内核配置要选对。我个人习惯用PREEMPT_RT补丁,也就是全抢占内核。在配置菜单里,你会看到这几个选项:

配置项 说明 我的建议
CONFIG_PREEMPT_NONE 无抢占,服务器用 工业控制别选
CONFIG_PREEMPT_VOLUNTARY 自愿抢占,桌面用 勉强能用,但不推荐
CONFIG_PREEMPT 全抢占,实时性基础 选这个!
CONFIG_PREEMPT_RT_FULL 完全实时,带RT补丁 工业控制首选

嗯,这里要注意:PREEMPT_RT_FULL需要打RT补丁,瑞芯平台我测试过RK3568、RK3588都支持。你想想看,如果内核里还留着大段不可抢占的临界区,那你的电机控制周期能准时才怪。

核心配置清单(我常用的):

  • CONFIG_PREEMPT_RT_FULL=y
  • CONFIG_HZ_1000=y (时钟频率1000Hz,周期1ms)
  • CONFIG_NO_HZ_FULL=y (减少时钟中断干扰)
  • CONFIG_RCU_NOCB_CPU=y (RCU回调迁移,后面细说)

3.2 中断线程化:把“野蛮人”关进笼子

传统Linux里,中断处理程序是最高优先级的。它一跑起来,谁都得让路。这在工业控制里是个大问题——你一个网卡中断,凭什么把我的伺服控制线程给抢了?

中断线程化,就是把中断处理程序变成一个内核线程。这样它就可以被调度器管理,可以被其他更高优先级的实时任务抢占。

具体怎么做?在设备树里加一个属性:

// 在设备树中,给中断控制器添加:
interrupts = <0 123 4>;  // 最后一个数字是触发类型
// 或者在内核启动参数里加:
threadirqs

我在项目中遇到过一件事:有个客户用RK3568做PLC,发现每次以太网收包时,伺服电机就会抖动一下。查了半天,发现是网卡中断优先级太高,把电机控制线程给打断了。后来开了threadirqs,把网卡中断线程化,再配合实时优先级调度,问题就解决了。

小技巧:你可以用cat /proc/interrupts查看哪些中断被线程化了。如果看到irq/xxx-thread这样的名字,说明成功了。

3.3 优先级继承:别让低优先级任务“耍流氓”

优先级反转,这个词做嵌入式的应该都听过。简单说就是:一个高优先级任务等着一个资源,但这个资源被一个低优先级任务占着,而低优先级任务又被一个中等优先级任务抢占了CPU。结果高优先级任务反而被“饿死”了。

怎么解决?优先级继承协议。说白了就是:当低优先级任务持有高优先级任务需要的锁时,低优先级任务临时“继承”高优先级任务的优先级,直到它释放锁。

在Linux里,用pthread_mutexattr_setprotocol来设置:

#include <pthread.h>

pthread_mutex_t mutex;
pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);
// 设置优先级继承协议
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&mutex, &attr);

我曾经在一个多轴同步控制项目里,因为没开优先级继承,导致三个轴的运动轨迹总是差那么几微秒。开了之后,同步误差从50微秒降到了5微秒以内。你想想看,这差距有多大。

注意:优先级继承会增加上下文切换的开销。如果你的系统里锁竞争不激烈,可以考虑用PTHREAD_PRIO_PROTECT(优先级上限协议),它更轻量。

3.4 RCU锁优化:读多写少的场景利器

RCU(Read-Copy-Update)是Linux内核里一种非常巧妙的同步机制。它特别适合读操作远多于写操作的场景。比如工业控制里的配置表、路由表、传感器参数表。

RCU的原理是:读操作不需要加锁,直接访问。写操作先复制一份数据,修改完再“原子地”替换指针。旧数据等所有读操作都完成后才被回收。

在实时性改造中,RCU的优化重点在于减少宽限期(grace period)的延迟。宽限期就是等待所有读操作完成的那段时间。如果宽限期太长,写操作就会被阻塞。

我建议做两件事:

  1. 开启RCU_NOCB_CPU:把RCU的回调处理迁移到专门的CPU上,避免干扰实时任务。
  2. 调整RCU的线程优先级:把rcu_preemptrcu_sched内核线程的优先级调高,确保它们能及时处理回调。
// 在内核启动参数里加:
rcu_nocbs=1-3   // 把CPU1-3的RCU回调迁移到CPU0
rcu_nocb_poll   // 轮询模式,减少中断

// 或者运行时调整优先级:
echo 99 > /proc/irq/xxx/rcu_preempt/priority

我的经验:在RK3588上做视觉引导的机器人控制时,RCU的宽限期曾经达到过2ms。优化后降到了200μs以内。怎么做到的?就是把RCU回调绑定到一个独立的大核上,并且用isolcpus把这个核从普通调度器中隔离出来。

3.5 实战:把这些技术串起来

好,理论说完了。咱们看看怎么在瑞芯微平台上落地。我以RK3568为例,给一个典型配置流程:

# 1. 内核配置
make ARCH=arm64 menuconfig
# 开启:
#   CONFIG_PREEMPT_RT_FULL=y
#   CONFIG_HZ_1000=y
#   CONFIG_NO_HZ_FULL=y
#   CONFIG_RCU_NOCB_CPU=y

# 2. 启动参数
# 在extlinux.conf或bootargs里加:
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 threadirqs

# 3. 用户空间程序
# 设置实时优先级和调度策略
struct sched_param param;
param.sched_priority = 80;
pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

# 4. 内存锁定,防止缺页中断
mlockall(MCL_CURRENT | MCL_FUTURE);

嗯,这里要提醒你:隔离CPU之后,别忘了把中断也绑到非隔离的CPU上。否则中断还是会跑到你的实时核上,那就白隔离了。

验证方法:cyclictest工具测试延迟。我一般在RK3568上能跑到50μs以内的最大延迟,RK3588能跑到30μs以内。如果超过100μs,说明哪里还有问题。

最后说一句:实时性改造不是一锤子买卖。你改完配置、写完代码,一定要用示波器或者逻辑分析仪去抓实际的IO响应时间。纸上谈兵没用,得看真家伙。

下一章咱们讲中断子系统的深度优化,包括中断亲和性、NMI的使用、以及如何避免中断风暴。到时候我会分享一个我在光伏逆变器项目里踩过的坑——那个坑差点让整个产线停摆。