很多人第一次做实时系统,容易有一个错觉:
把实时补丁打上,线程优先级拉高,再跑一遍 cyclictest,事情就差不多了。
真正到了现场,很快会发现不是这样。
实时系统最麻烦的地方,不是“某一个机制不会用”,而是很多小问题单独看都不致命,但叠在一起,就足够把关键线程拖垮。
比如:
• 优先级关系乱
• 锁拿得太久
• 中断打在关键核上
• 网卡和控制线程混跑
• 关键路径里动态分配内存
• 热路径里同步打日志
• CPU 绑了,但环境并不干净
这些问题每一个都可能只带来几十微秒、几百微秒的抖动。
但工业现场最怕的,正是这些抖动叠加。
这一篇不讲抽象原理,直接讲实时系统里最常见的 10 个坑,以及怎么避。
一句话先讲明白
实时系统最怕的,不是某个参数没调好,而是关键线程前面同时堆了优先级错误、锁等待、中断干扰、CPU 混跑、内存抖动、驱动慢路径。
避坑的核心,也不是猛调某一个参数,而是顺着关键线程,把“唤醒、调度、执行”这条主路径一层层清干净。
第一坑:把“高优先级”当成万能药
最常见的错误是:
关键线程不稳,那就继续加优先级。
问题是,优先级不是奖励,而是抢占权。
如果系统里一堆线程都被提成高优先级,最后通常会变成:
• 关键线程互相抢
• 后台线程被压死
• 日志、喂狗、恢复线程跑不起来
• 问题看起来更随机
很多现场问题不是关键线程优先级不够高,而是高优先级线程太多,层级关系乱了。
怎么避?
• 按 deadline 排,不按“感觉重要”排
• 高优先级线程数量要少
• 明确谁必须高于谁,谁可以低一点
• 不要让一堆线程挤在同一个实时优先级
• 给日志、恢复、喂狗线程留下运行空间
实时系统里,优先级不是越高越好,而是关系越清楚越好。
第二坑:只看单个线程优先级,不看整体优先级关系
很多人会说:
这个线程我已经给很高优先级了。
但真正决定系统稳不稳的,不是某个线程“高不高”,而是它和其他线程的相对顺序对不对。
尤其在 PREEMPT_RT 系统里,很多中断会线程化。
这时不只要看用户态控制线程,还要看 IRQ 线程、通信线程、采样线程、后台线程。
比如控制线程优先级很高,但它依赖网卡收包。
如果网卡 IRQ 线程优先级太低,数据迟迟进不来,控制线程再高也只能等。
怎么避?
• 列出所有关键线程
• 把控制链、采样链、通信链、后台链分清楚
• 把 IRQ 线程优先级也纳入排序
• 看清关键线程依赖谁,谁又可能打断它
• 不要只排用户线程,忽略内核线程和 IRQ 线程
调优第一步,不是调参数,而是把优先级关系画清楚。
第三坑:锁用得太重
实时系统里,锁不是不能用。
真正危险的是:
• 临界区太大
• 锁里做复杂逻辑
• 锁里做 I/O
• 锁里打日志
• 高低优先级线程共用同一把重锁
这类问题最容易造成一个假象:
高优先级线程明明已经被调度了,但就是推进不下去。
原因不是它没拿到 CPU,而是它卡在锁前面。
怎么避?
• 锁里只放最小必要动作
• 不在锁里做日志、I/O、复杂计算和长循环
• 热路径尽量少共享资源
• 关键线程尽量不要和后台线程共用重锁
• 关键锁优先使用支持优先级继承的机制
锁的问题不在于“有没有锁”,而在于高优先级线程会不会被低优先级路径挡住。
第四坑:忽略优先级反转
很多实时问题表面看像“线程偶发超时”,根因其实是优先级反转。
典型场景是:
低优先级线程拿着锁。
高优先级线程需要这把锁,于是等待。
中优先级线程不断运行,把低优先级线程压住。
低优先级线程迟迟不能释放锁,高优先级线程也就一直等。
最麻烦的是,它表面看起来不像“低优先级线程影响了高优先级线程”。
因为真正占 CPU 的,可能是中优先级线程。
怎么避?
• 缩短持锁时间
• 减少跨优先级共享资源
• 关键路径使用支持优先级继承的锁
• 避免低优先级线程持有高优先级线程需要的资源
• trace 时重点看高优先级线程是不是卡在锁前
• 不要只看谁在跑,还要看谁在等谁
很多“偶发卡顿”不是 CPU 不够,而是等待关系错了。
第五坑:中断路径太重
很多驱动和系统抖动问题,最后都能落到一句话:
中断里做太多。
中断路径太重,常见后果是:
• 周期线程晚醒
• 调度延迟尖峰变大
• 实时线程被中断前置工作挡住
• 平均负载不高,但最大延迟很难看
尤其是网卡、采集卡、串口、CAN、USB、存储设备,一旦中断频率高或者后续处理重,就容易影响实时线程。
PREEMPT_RT 会把很多中断线程化,但不等于中断没有影响。
线程化以后,它仍然要占 CPU,仍然要参与优先级竞争。
怎么避?
• ISR 只做最少必要动作
• 大量处理后移到线程上下文
• 高频中断源重点盯
• 检查 /proc/interrupts,看中断是否集中打在关键 CPU 上
• 对 IRQ 线程设置合理优先级
• 出现尖峰时,优先排查中断路径是否过长
中断不是背景噪声,它经常就是延迟尖峰的来源。
第六坑:把网卡和实时线程放在同一个核上
这是现场特别常见的一类坑。
系统里只要有:
• 高频收包
• TCP/UDP 通信
• SSH 远程访问
• 日志上传
• 监控服务
• NAPI poll 活跃
如果它们和关键实时线程跑在同一个 CPU 上,实时性通常会明显变差。
很多人只看到实时线程绑在 CPU2 上,却没看网卡 IRQ、软中断、NAPI 处理是不是也在 CPU2 上。
结果就是:
线程是绑住了,但干扰也绑过来了。
怎么避?
• 实时线程和高频网卡 IRQ 分核
• 检查 /proc/interrupts,确认网卡中断落在哪些 CPU 上
• 迁走网卡 IRQ 和相关高频干扰
• 注意软中断和 NAPI poll 对 CPU 的影响
• 不要只绑线程,不管 IRQ
• 实时控制核尽量不要承担普通网络业务
很多抖动不是控制线程的问题,而是网络路径把 CPU 打乱了。
第七坑:只做任务绑核,不做 IRQ 绑核
很多人会把关键线程绑到某个 CPU 上。
这一步没错,但只做这一步远远不够。
因为线程虽然固定了,中断可能还在这个 CPU 上乱打。
最后就会出现:
关键线程没有迁移,调度策略也没问题,优先级也不低,但延迟还是抖。
原因可能就是:
你只管了任务,没有管 IRQ。
怎么避?
• 任务绑核和 IRQ 绑核一起做
• 看关键 CPU 上到底有哪些 IRQ
• 把网卡、存储、USB、串口等高频 IRQ 移出关键核
• 必要时停掉 irqbalance,再手工规划 IRQ affinity
• 调整后用 /proc/interrupts 验证,不要凭感觉
绑核不是把线程固定住就结束了。
真正要做的是让关键 CPU 少被无关工作打扰。
第八坑:绑核了,但 CPU 其实并不干净
很多人把实时线程绑到 CPU2,就以为 CPU2 已经是“实时核”了。
但这个 CPU 上可能还在跑:
• 周期 tick
• RCU 回调
• 内核 workqueue
• 普通服务线程
• 软中断
• 定时器回调
所以线程虽然没有迁移,CPU 环境仍然不干净。
从应用层看,线程 affinity 是对的。
但从系统层看,这个 CPU 仍然在承载杂活。
怎么避?
• 先做任务绑核
• 再做 IRQ 迁移
• 检查关键 CPU 上还有哪些内核线程和后台任务
• 必要时配置 isolcpus、nohz_full、rcu_nocbs
• 把普通服务线程移出关键 CPU
• 不要只看 affinity,要看这个 CPU 上到底还在跑什么
实时核不是“绑了一个线程的核”,而是“尽量只服务关键路径的核”。
第九坑:在关键路径里动态分配内存、打大量日志
这类问题特别像:
平时正常,偶发变慢。
动态分配内存可能走到不可预测的慢路径。
日志可能引入字符串格式化、锁竞争、缓冲区等待、I/O 阻塞。
大量数据访问还可能带来 cache 抖动和内存带宽竞争。
放到实时关键路径里,这些都会变成延迟尖峰。
尤其是同步日志,非常危险。
很多现场问题最后发现,不是控制算法慢,而是关键线程在打印、刷盘、抢日志锁。
怎么避?
• 关键路径预分配内存
• 关键线程不频繁 malloc/new
• 热路径少打日志,尤其不要同步打印
• 日志尽量异步化、降频、限流
• 把调试信息和实时路径解耦
• 关键线程启动后尽量避免触发缺页和大块内存申请
实时路径里最贵的,不一定是算法,而是那些“顺手写上去”的辅助动作。
第十坑:一上来就拍脑袋查问题,没有顺着关键线程走
这是最大的一类坑。
实时系统一出问题,很多人会马上到处看:
• CPU 负载
• 调度策略
• 中断数量
• 网卡流量
• 锁竞争
• PREEMPT_RT 配置
• 内核参数
• 驱动实现
这些都要看,但不能一上来乱看。
越是复杂系统,越要先抓主线:
先盯关键线程。
先确认它到底卡在哪一段:
• 没按时醒
• 醒了但没拿到 CPU
• 拿到 CPU 但执行推进太慢
这三个问题,对应的排查方向完全不同。
• 没按时醒:看定时器、中断、唤醒路径
• 醒了没跑:看调度、优先级、CPU 是否被更高优先级任务占住
• 跑了还慢:看锁、内存、I/O、cache、驱动路径和共享资源
怎么避?
• 永远从关键线程倒推
• 先分清问题发生在唤醒、调度还是执行阶段
• 不要一上来就认定是 CPU 或调度器
• 用 trace 看关键线程的实际时间线
• 先看主路径,再看旁路噪声
• 每次只验证一个假设,不要同时乱改一堆参数
实时问题最怕凭感觉查。
先把关键线程的时间线拉出来,很多问题自然会变清楚。
最实用的一套避坑顺序
如果只想记一套现场可用的方法,可以按这个顺序来:
1. 先把关键线程找出来
不要一上来优化全系统。
先明确谁是真正有 deadline 的线程,谁只是辅助线程,谁只是后台线程。
2. 把优先级关系排清楚
不要只问“这个线程优先级高不高”。
要问:
谁绝不能被谁挡住?
谁依赖谁?
谁可以慢一点?
IRQ 线程应该排在哪一层?
3. 把线程和 IRQ 分开
关键线程不要和高频中断共核。
尤其是网卡、采集卡、存储、USB 这类设备,要重点检查 IRQ affinity。
只绑任务不绑 IRQ,很多时候等于只做了一半。
4. 把锁和共享资源收紧
沿着关键路径查一遍:
有没有重锁?
有没有跨优先级共享资源?
有没有锁里打日志、做 I/O、跑复杂逻辑?
高优先级线程是不是在等低优先级线程释放资源?
5. 把 CPU 环境清干净
如果关键线程对延迟很敏感,就不要让它所在的 CPU 同时承担一堆杂活。
必要时做:
• CPU affinity
• IRQ affinity
• isolcpus
• nohz_full
• rcu_nocbs
• 后台服务迁移
目标不是参数好看,而是让关键 CPU 尽量少被打扰。
6. 再看内存、DMA、cache 和驱动路径
前面几层清完以后,如果还有尖峰,再往更深处看:
• 动态内存分配
• 缺页
• DMA 同步
• cache miss
• 驱动慢路径
• 总线竞争
• 设备固件响应时间
这一步通常不是解决大方向问题,而是收尾尖峰。
最后怎么一句话记住?
实时系统最常见的坑,不是某一个参数没配,而是优先级、锁、中断、绑核、内存和驱动路径一起把关键线程拖慢。
避坑的核心也不是头痛医头,而是顺着关键线程,把“唤醒、调度、执行”这条主路径一层层清干净。
先清主路径,再清噪声。