嵌入式Linux工业实时编程:用PREEMPT_RT补丁保证控制周期抖动小于1ms
标准Linux下控制线程“平均1ms”很容易,但偶尔跳几毫秒甚至几十毫秒——工业伺服、运动控制受不了的是这个长尾。 PREEMPT_RT的价值不是让程序变快,是把调度延迟的尾部收窄:自旋锁改成可抢占的rt_mutex、中断处理线程化、优先级继承防反转,高优先级控制线程能更快拿到CPU。
一、内核配置:先选对抢占模型
6.12+主线直接开CONFIG_PREEMPT_RT;老LTS(5.15/6.1/6.6)要先打对应版本RT补丁,版本号必须严格匹配。
menuconfig路径:General setup → Preemption Model → Fully Preemptible Kernel (Real-Time)。配套选项:
CONFIG_HIGH_RES_TIMERS=y:高分辨率定时器
CONFIG_HZ_1000=y:1ms节拍粒度
调试开关全关:lockdep、ftrace、kgdb会插进调度路径毁掉确定性
电源管理关C-states、关DVFS节能
启动参数把实时核隔离出来:
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3
控制线程绑到2/3核,ksoftirqd、RCU、日志进程全挪走。
二、实时线程:SCHED_FIFO + 绑核
只开RT内核不够,用户线程默认还是SCHED_OTHER。 控制线程必须显式切SCHED_FIFO,且优先级别踩99——watchdog/migration就在99,踩了系统会卡死,应用层一般取20~80。
pthread_attr_t attr;
struct sched_param sp = {.sched_priority = 70};
pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
pthread_attr_setschedparam(&attr, &sp);
/* 关键:否则继承父线程的非实时策略 */
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
pthread_create(&tid, &attr, control_loop, NULL);
/* 绑到隔离核 */
cpu_set_t c;
CPU_ZERO(&c); CPU_SET(2, &c);
pthread_setaffinity_np(tid, sizeof(c), &c);
主循环里别用相对sleep,累积误差会漂。
三、1ms周期:clock_nanosleep绝对时间
相对nanosleep每次重新算剩余时间,信号打断会偏;绝对模式按下一周期时间点睡,零漂移。
#define PERIOD_NS 1000000L
struct timespec next;
void *control_loop(void *arg){
clock_gettime(CLOCK_MONOTONIC, &next);
while(running){
do_control(); // 读编码器/算PID/发PDO
next.tv_nsec += PERIOD_NS;
if(next.tv_nsec >= 1000000000L){
next.tv_sec++; next.tv_nsec -= 1000000000L;
}
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
}
return NULL;
}
CLOCK_MONOTONIC不受系统校时影响,比CLOCK_REALTIME稳。
四、实时路径零分配
周期函数里最致命的两件事:malloc/new触发缺页、printf碰文件系统。 启动阶段做完:
mlockall(MCL_CURRENT|MCL_FUTURE); // 锁内存防换页
缓冲区、PID状态、通信帧全预分配;日志走无锁环形队列,另起低优先级线程异步写。控制循环内不调用任何可能阻塞的系统调用,只读取“上一周期已就绪”的数据。
五、cyclictest验证抖动
别只看平均值,看Max和直方图尾部:
cyclictest -p 80 -a 2 -i 1000 -D 10m -m -h 200
空载合格线:Max < 50μs;带载(stress-ng压CPU+IO)Max < 100μs算可用。
测之前固定CPU频率、关irqbalance、停rsyslog。普通内核对比一组,能直观看到PREEMPT_RT把偶发毫秒尖峰压下去。
六、落坑清单
只装RT内核不改调度策略→还是SCHED_OTHER
pthread忘了EXPLICIT_SCHED→优先级被继承覆盖
控制线程优先级设99→饿死内核线程系统卡死
实时线程绑核但IRQ没隔离→中断照样抢
周期循环用nanosleep相对睡→长运行漂移
循环里printf/malloc→缺页引入毫秒级尖峰
没mlockall→后台内存压力换页卡顿
DVFS开着→同段代码耗时随频率变
高速电流环(>5kHz)放Linux→还是交给MCU
只测空载Max→现场一带载就超
七、落地边界
PREEMPT_RT能压的是“CPU调度延迟”:中断线程化、锁可抢占、高优先级任务快速唤醒。 它管不到NPU/GPU内部队列、DDR带宽竞争、驱动器固件响应、外部总线延迟。
工程上合理分工:
Linux+RT:100~1000Hz上位控制、轨迹规划、CANopen主站、EtherCAT调度
MCU/驱动器:电流环、FOC、PWM、急停、硬件限位
通信层带时间戳+序号+超时降级,任一层错过周期都能进安全态。
真正“抖动<1ms”不是内核补丁单挑出来的,是四件套叠加:RT抢占内核 + SCHED_FIFO绑隔离核 + 周期绝对睡眠 + 实时路径零分配零缺页。少一个,cyclictest直方图右尾就会冒出来;四个都做对,工业1ms控制环通常能稳在几十微秒级抖动。再往上追微秒级电流环,就该让STM32或伺服驱动器接手了——Linux实时不等于全栈硬实时。





