实时Linux实战:从CPU核心隔离、IRQ绑定到cyclictest,搭建一个可复现的实时性测试环境
前面的文章,我们讨论了一个问题:
很多人第一反应是运行一个 cyclictest,看到终端上出现几十微秒甚至几微秒的最大延迟,然后得出结论:“这台机器的实时性不错。”
但真正做过实时系统的人通常不会这么简单地判断。
因为一个实时系统的性能,并不只由CPU频率决定,也不只是由Linux内核的实时补丁决定。一个看似普通的后台进程、一次网络中断、一次磁盘访问、一个SoftIRQ,甚至另一个CPU核心上的高负载任务,都可能通过共享资源影响实时任务的执行。
尤其是在机器人控制、工业控制、运动控制、实时仿真等场景中,系统关注的往往不是“平均情况下有多快”,而是:
当系统处于高负载、存在中断、网络通信和后台任务同时运行时,关键控制任务还能不能按照规定的时间窗口稳定执行?
这也是为什么实时Linux的测试不能只停留在“跑一个工具”。
真正有意义的实时性测试,需要把测试环境本身也纳入控制范围。
本文就以一个典型的多核Linux系统为例,从零开始搭建一套简单、可复现的实时性测试环境。我们不追求复杂的实验平台,而是通过几个关键步骤,把一个普通Linux环境逐渐变成一个“实时任务专属运行区域”,然后使用 cyclictest 对优化前后的结果进行对比。
需要提前说明的是,下面的命令主要用于理解实时Linux测试方法。不同Linux发行版、内核版本、CPU架构以及实时内核配置可能存在差异,具体参数应根据实际环境调整。测试结果也只能说明特定硬件、特定内核、特定负载条件下的观测结果,不能简单等同于系统的绝对最坏情况。
一、第一步不是跑cyclictest,而是先认识你的CPU
很多实时Linux实验一上来就是:
cyclictest -p 80 -t 4 -m
然后开始看输出。
其实更合理的第一步应该是:
先搞清楚这台机器到底有几个CPU、CPU之间是什么拓扑关系、哪些CPU正在处理系统任务。
因为后面所有的CPU绑定、核心隔离和IRQ调整,都建立在这些信息之上。
最简单的方法是:
lscpu
可以看到类似:
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
这里至少需要关注几个信息。
第一是逻辑CPU数量。
例如系统有:
CPU 0-7
意味着Linux当前可以看到8个逻辑CPU。
但“8个逻辑CPU”并不一定意味着存在8个完全独立的物理计算核心。如果CPU开启了SMT/超线程,那么两个逻辑CPU可能共享同一个物理核心的部分执行资源。
这对于实时系统非常重要。
假设:
CPU 0
CPU 1
实际上属于同一个物理核心,那么即使我们把实时任务分别绑定到CPU 0和CPU 1,也不能简单理解成两个完全互不影响的执行单元。
因此,在进行高要求实时测试时,不能只看:
CPU编号
还需要理解:
CPU编号
↓
物理核心
↓
Socket
↓
NUMA节点
也就是说,CPU拓扑本身就是实时性测试环境的一部分。
接下来可以观察当前系统上的进程:
ps -eLo pid,tid,psr,cls,rtprio,pri,comm
其中:
-
PID:进程ID -
TID:线程ID -
PSR:当前运行CPU -
CLS:调度策略 -
RTPRIO:实时优先级 -
PRI:调度优先级 -
COMM:线程名称
如果看到某个线程不断在不同CPU之间迁移,就需要注意。
因为Linux为了提高整体系统吞吐量,会进行任务迁移和负载均衡。
普通服务器当然希望CPU资源能够被充分利用。
但实时系统关注的是另一件事:
关键任务能不能尽量稳定地留在预期CPU上执行。
因此,实时系统经常需要对CPU调度范围进行更明确的约束。
这就是CPU affinity,也就是我们经常说的:
CPU亲和性。
例如:
taskset -c 3 ./your_rt_task
表示让程序主要运行在CPU 3上。
如果想查看某个进程当前允许使用哪些CPU:
taskset -cp <PID>
例如:
taskset -cp 1234
可能得到:
pid 1234's current affinity list: 0-7
说明这个任务目前可以在0到7号CPU之间运行。
如果调整为:
taskset -cp 3 1234
那么它的运行范围就被限制到了CPU 3。
但是这里必须强调一个非常容易被误解的问题:
CPU affinity不等于CPU核心隔离。
CPU affinity解决的是:
这个任务可以在哪些CPU上运行?
CPU核心隔离解决的则是:
哪些任务应该被允许进入这个CPU?
这两个问题看起来很接近,但实际上完全不同。
假设我们把实时控制线程绑定到CPU 3:
实时控制任务 → CPU 3
但系统的其他任务同样可以运行在CPU 3上。
那么实际情况可能变成:
实时控制任务 ─┐
├── CPU 3
后台任务 ┤
│
系统任务 ┤
│
其他线程 ┘
这时候,实时任务虽然“指定”了CPU 3,但仍然可能受到其他任务的竞争。
所以真正的实时测试环境,需要进一步解决:
谁可以进入这个CPU。
二、第二步:把CPU划分成“实时区”和“普通区”
实时系统经常采用一种非常典型的设计:
不要让所有任务共享所有CPU。
例如一台8核机器,可以进行类似这样的逻辑划分:
CPU 0、1、2、3
普通系统区
CPU 4、5
AI/视觉/计算区
CPU 6、7
实时控制区
于是整个系统形成多个运行域:
┌──────────────────────────────┐
│ Linux系统环境 │
├──────────┬──────────┬────────┤
│ 普通任务 │ AI/计算 │ 实时任务│
│ CPU 0-3 │ CPU 4-5 │ CPU 6-7 │
└──────────┴──────────┴────────┘
这样做的核心目的并不是让CPU“更快”。
而是:
减少实时任务受到无关任务干扰的机会。
在Linux中,可以通过多种方式实现这种隔离。
一种传统方法是在内核启动参数中使用:
isolcpus=
例如:
isolcpus=6,7
表示将CPU 6、7从普通调度负载中进行隔离。
一些实时系统还会结合:
nohz_full=
rcu_nocbs=
等参数进一步减少特定CPU上的系统噪声。
不过需要注意,这些参数的具体行为会随着Linux内核版本和配置发生变化,现代Linux环境也可以结合cpuset、cgroup以及其他CPU管理机制实现更细粒度的资源控制。
因此,不应该把:
isolcpus
理解成“实时Linux的唯一标准答案”。
真正重要的是背后的设计思想:
给关键实时任务建立一个低干扰CPU执行域。
这时候,再回到我们的测试。
假设选择:
CPU 6
作为实时测试CPU。
那么可以让:
cyclictest
实时控制任务
主要运行在CPU 6。
而:
stress-ng
网络任务
后台服务
普通计算任务
尽可能运行在其他CPU。
这时候测试才开始具备一定的可控性。
但这里还有一个非常容易被忽略的问题。
即使我们已经把普通任务赶出了CPU 6:
中断仍然可能进入CPU 6。
这就是实时Linux测试中经常出现的第二层问题。
三、CPU隔离还不够:为什么IRQ绑定是实时测试的关键一步?
Linux系统运行过程中会不断产生中断。
例如:
网卡
磁盘
USB
定时器
GPU
PCIe设备
传感器
工业控制设备
这些硬件设备都可能产生IRQ。
可以通过:
cat /proc/interrupts
观察系统当前的中断情况。
输出可能非常庞大,例如:
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
16: ... ... ... ... ... ... ... ...
24: ... ... ... ... ... ... ... ...
35: ... ... ... ... ... ... ... ...
这里最重要的是观察:
实时CPU是否仍然承担大量设备中断。
例如我们已经决定:
CPU 6 = 实时CPU
但网卡IRQ大量进入CPU 6:
网卡IRQ ───────→ CPU 6
实时任务 ──────→ CPU 6
那么实时CPU仍然可能出现明显抖动。
尤其是在网络流量较大的情况下。
例如机器人控制系统中,网络上可能持续传输:
传感器数据
控制指令
状态数据
视觉数据
诊断数据
如果这些网络中断全部进入实时CPU,那么即使普通进程已经被隔离,实时任务仍然可能受到影响。
因此,实时Linux测试通常需要进一步做:
IRQ affinity。
Linux中可以通过:
cat /proc/irq/<IRQ号>/smp_affinity_list
查看某个IRQ当前允许在哪些CPU上处理。
例如:
cat /proc/irq/35/smp_affinity_list
可能返回:
0-7
意味着这个IRQ可以在所有CPU上处理。
如果希望它避开CPU 6,可以根据系统实际情况调整IRQ affinity。
在一些系统中可以通过:
echo 0-5,7 > /proc/irq/35/smp_affinity_list
进行调整。
当然,实际操作时不能机械地把所有IRQ都搬走。
因为某些IRQ与特定CPU、设备拓扑、驱动以及系统服务存在关系。
更合理的思路应该是:
普通设备IRQ
↓
普通CPU
实时相关设备IRQ
↓
指定实时CPU
非关键高流量设备IRQ
↓
尽可能避开实时CPU
也就是说:
IRQ不是越少越好,而是要有规划。
对于工业控制系统,这个问题会更加明显。
例如:
EtherCAT网卡
↓
网络IRQ
↓
SoftIRQ
↓
控制线程
↓
计算控制量
↓
输出执行器
如果整个链路的CPU分配没有经过设计,那么即使控制线程本身使用了高实时优先级,也可能被网络处理链路中的其他工作影响。
所以实时性优化实际上不是:
提高线程优先级
这么简单。
而是:
CPU
↓
IRQ
↓
SoftIRQ
↓
调度器
↓
实时线程
↓
内存
↓
网络/设备
整条链路都要考虑。
四、现在才轮到cyclictest:建立一个真正有意义的对比实验
当CPU和IRQ的基本情况已经明确以后,我们再运行:
cyclictest
cyclictest来自Linux实时测试工具集 rt-tests,核心用途是测量线程按照周期唤醒时,从“理论应该被唤醒”的时间点到“实际开始执行”之间产生了多少延迟。
一个常见测试形式可以是:
cyclictest -p 80 -t 4 -m
其中:
-p 80
设置实时优先级。
-t 4
启动4个测试线程。
-m
锁定内存,避免测试过程受到部分内存分页行为影响。
实际参数需要根据测试目的和系统环境调整。
如果希望进一步指定CPU,可以结合CPU affinity,例如:
taskset -c 6,7 cyclictest -p 80 -t 2 -m
这里的含义可以理解为:
cyclictest
↓
只在CPU 6、7运行
↓
两个测试线程
↓
实时优先级
↓
观察唤醒延迟
测试结果可能类似:
T: 0 ... Max: 12
T: 1 ... Max: 15
T: 2 ... Max: 11
T: 3 ... Max: 18
不同版本工具输出格式会有所区别,但通常会关注:
Min
Avg
Max
也就是:
最小延迟
平均延迟
最大观测延迟
这时候千万不要只盯着平均值。
例如:
平均延迟:3 μs
最大延迟:8 μs
和:
平均延迟:3 μs
最大延迟:280 μs
对于实时控制系统来说,意义完全不同。
第一种情况下,延迟分布相对集中。
第二种情况下,虽然平均延迟看起来同样很好,但偶发的大延迟可能直接影响控制周期。
所以实时系统更关注:
长尾。
也就是那些偶尔出现、但可能影响系统确定性的异常延迟。
接下来,我们故意制造压力。
例如可以使用:
stress-ng --cpu 6 --vm 2 --io 2 --timeout 5m
制造CPU、内存和I/O压力。
然后同时运行:
cyclictest
这时候可能出现一个非常有价值的现象:
系统空闲:
Max = 10 μs
系统高负载:
Max = 150 μs
甚至可能出现更大的峰值。
这时候真正值得问的问题不是:
“为什么Linux变慢了?”
而是:
到底是什么因素造成了这次150μs的延迟?
这就是实时Linux测试从“跑工具”进入“做工程分析”的分界线。
五、从“测出来”到“解释清楚”:用trace把异常延迟抓出来
假设我们观察到:
平均延迟:4 μs
最大延迟:180 μs
但是我们不知道180μs到底发生了什么。
这时候继续跑100次cyclictest,并不能直接告诉我们原因。
因为:
cyclictest告诉你“发生了延迟”,但不一定告诉你“为什么发生”。
这时候就需要进入trace阶段。
Linux本身提供了非常丰富的内核跟踪能力,其中最常用的工具之一就是:
ftrace
以及围绕它构建的:
trace-cmd
例如可以先查看系统是否安装:
trace-cmd --version
然后针对调度行为进行跟踪。
一个典型思路是:
trace-cmd record \
-e sched_switch \
-e sched_wakeup \
-e irq_handler_entry \
-e irq_handler_exit \
-e softirq_entry \
-e softirq_exit \
cyclictest ...
具体事件和命令参数需要根据实际内核和trace-cmd版本调整。
测试结束后:
trace-cmd report
就可以进一步分析任务切换、中断以及SoftIRQ等事件。
例如我们可能发现:
实时线程应该在:
10:00:01.000000
实际开始:
10:00:01.000180
中间的180μs发生了:
实时线程
↓
等待运行
↓
IRQ进入
↓
SoftIRQ处理
↓
其他线程运行
↓
实时线程重新获得CPU
那么我们就终于从:
“cyclictest测到了180μs”
进一步走到了:
“180μs是由什么造成的”
这两者是完全不同的工程能力。
如果继续使用:
perf
还可以从CPU性能角度观察:
context switch
CPU migration
cache miss
page fault
CPU cycles
instructions
等信息。
于是整个测试体系就可以逐渐建立起来:
实时性测试
│
┌────────────┼────────────┐
↓ ↓ ↓
cyclictest ftrace perf
│ │ │
延迟多少? 为什么延迟? CPU发生了什么?
│ │ │
└────────────┼────────────┘
↓
定位干扰源
↓
调整系统架构
↓
再测试
这才是一套完整的实时性验证流程。
最终形成:
测试
↓
发现异常
↓
定位原因
↓
调整CPU/IRQ/调度/内存/网络
↓
重新测试
↓
压力测试
↓
再次测试
而不是:
跑一次cyclictest
↓
看到一个漂亮数字
↓
宣布系统实时
后者在工程上是远远不够的。
六、从测试环境回到真实系统:为什么望获OS更强调“整体实时性”
到了这里,我们可以重新理解一个问题:
实时Linux究竟应该优化什么?
答案已经不是单纯的:
让调度器更快
而是建立一个完整的确定性执行环境。
一个典型的实时控制任务,实际上可能经过:
传感器
↓
设备中断
↓
IRQ
↓
SoftIRQ/驱动
↓
实时线程唤醒
↓
调度器
↓
CPU执行
↓
内存访问
↓
控制算法
↓
网络/总线
↓
执行器
其中任何一环出现不可控延迟,都可能影响最终的控制周期。
所以真正意义上的实时Linux,需要关注的不只是:
实时调度。
还包括:
CPU核心隔离、IRQ隔离、任务亲和性、内存确定性、网络确定性、锁竞争、I/O干扰以及共享硬件资源。
这也是为什么“实时Linux”不能简单等同于“给Linux加一个实时补丁”。
实时补丁解决的是实时性问题中的一部分。
而一个面向工业控制、机器人、智能制造等场景的实时操作系统,还需要进一步考虑:
实时任务在哪里运行?
谁可以干扰它?
中断在哪里处理?
内存是否确定?
网络是否确定?
多个任务如何协调?
AI和控制任务如何共存?
异常情况下系统如何保持可控?
这实际上已经从一个“内核参数问题”,变成了一个完整的系统架构问题。
对于望获OS这类面向嵌入式硬实时场景的操作系统来说,核心隔离的价值也正在这里体现。
真正需要隔离的并不是简单意义上的“一个CPU核心”。
而是围绕关键任务建立一个相对独立、低干扰、可预测的执行环境:
系统资源
│
┌───────────────┼───────────────┐
↓ ↓ ↓
普通业务 AI/计算 实时控制
│ │ │
CPU 0-3 CPU 4-5 CPU 6-7
│ │ │
普通IRQ 高吞吐任务 RT IRQ
│ │ │
└───────────────┴───────────────┘
↓
统一系统管理
这样做的意义,是让实时任务不再和所有系统任务“抢同一块资源”。
这也是从普通Linux走向实时Linux,再进一步走向硬实时系统时,一个非常关键的思维变化:
实时性不是单个参数,而是一种系统级资源组织方式。
七、写在最后:一套真正有价值的实时Linux测试应该是什么样?
如果把今天的实验浓缩成一张流程图,大概可以归纳为:
① 查看CPU拓扑
↓
② 查看系统任务
↓
③ 选择实时CPU
↓
④ 配置CPU亲和性/隔离
↓
⑤ 检查IRQ分布
↓
⑥ 调整IRQ亲和性
↓
⑦ 运行cyclictest
↓
⑧ 加入CPU/内存/I/O/网络压力
↓
⑨ 观察最大延迟
↓
⑩ 使用ftrace/trace-cmd定位异常
↓
⑪ 使用perf辅助分析
↓
⑫ 修改系统配置
↓
⑬ 再次测试
↓
⑭ 比较优化前后结果
这套方法最大的价值,并不是得到一个“多少微秒”的漂亮数字。
而是建立一种可以重复验证的工程方法。
例如我们可以把测试条件完整记录下来:
CPU型号:
内核版本:
是否PREEMPT_RT:
CPU数量:
实时CPU:
IRQ配置:
调度策略:
实时优先级:
测试周期:
压力类型:
测试时长:
最大延迟:
平均延迟:
异常事件:
优化措施:
优化后最大延迟:
这样得到的数据才具有比较价值。
例如以后测试:
普通Linux
vs
实时Linux
或者:
优化前
vs
优化后
甚至:
不同CPU平台
不同内核版本
不同实时配置
都可以建立相对一致的测试方法。
而这也正是实时系统工程中非常重要的一件事情:
不是证明“我的系统很快”,而是证明“在明确的测试条件和干扰条件下,我的系统能够稳定地完成实时任务”。
从这个角度看,cyclictest只是起点。
真正的实时性验证,最终一定会走向:
调度 + CPU隔离 + IRQ管理 + 内存管理 + 网络确定性 + 硬件资源隔离 + 压力测试 + 内核跟踪。
当这些因素被统一起来考虑时,我们才真正开始理解“硬实时”这三个字背后的技术含义。
对于工业控制、机器人、实时仿真以及智能制造等系统来说,这种确定性的价值往往比单纯追求更高的平均吞吐量更加重要。
因为控制系统真正关心的从来不是:
大多数时候能不能及时完成?
而是:
在最需要它的时候,它能不能按时完成。
这也是实时Linux从“能运行”走向“可验证、可预测、可控制”的关键一步。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)