前面的文章,我们讨论了一个问题:

很多人第一反应是运行一个 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从“能运行”走向“可验证、可预测、可控制”的关键一步。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐