1. 先说结论:这套双内核组合解决的是"最坏情况延迟"
刚接手产线项目那会儿,看到 xenomai 3.0.5/3.0.9 + linux 4.9.38/4.9.90 这套组合,第一反应也是:都什么年代了还在用 4.9 内核?等我把 EtherCAT 主站跑起来、把伺服周期从 1ms 压到 500us,才明白这套老组合能一直在工控设备上存活是有理由的。这篇文章就是把环境搭建到稳定运行的配置细节,以及过程中踩过的一堆 BUG 原原本本记下来,给后面接手类似项目的人省点时间。
Xenomai 的 Cobalt 模式,本质是在 Linux 内核里插入一条 IRQ pipeline(中断流水线),让实时任务跑在一个更"霸道"的执行域里。普通 Linux 任务照常使用各种生态,实时任务走专用通道,两者共享 CPU 但优先级完全不对等。它真正的价值不是平均延迟,而是最坏情况延迟。PREEMPT_RT 方案能把平均延迟做到几十微秒,但 worst case 偶尔给你来一下几百微秒,运动控制里这一下可能就是一次急停报警。Xenomai 要做的就是把 max 值钉死在个位数的微秒量级,这是双内核方案到今天仍被工控行业继续采用的核心原因。
这篇文章适合两类人:一类是在 x86_64 工控机或 ARM 板子上做运动控制、数据采集,需要硬实时但还没碰过 Xenomai 的开发者;另一类是已经能编译内核、但被各种延迟毛刺折腾得睡不着觉的老手。我会把版本搭配、打补丁、内核配置、用户态编译运行、以及故障排查的思路全部摊开讲,每个 BUG 都会说明现象、根因和最终的处理手段,尽量让你能照着抄作业。
1.1 Cobalt 双内核到底怎么个工作法
理解 Xenomai 不用想得太玄。你可以把 Linux 内核想象成一栋写字楼,普通任务就是楼里的普通上班族,按电梯排队上楼。Xenomai 做的是在这栋楼里加了一条 VIP 专用电梯通道,所有外部中断先经过这条通道的保安亭,实时任务要处理的中断直接走 VIP 电梯,剩下 Linux 不关心的中断才放行给普通电梯。这个保安亭就是 I-pipe/IRQ pipeline,实时任务的运行环境就是 Cobalt 核。
所以 Xenomai 的实时任务和 Linux 任务并不是"两个操作系统各跑各的",而是共享同一个 CPU 资源池、由中断优先级强行分出了先后。Cobalt 域的实时线程永远先于 Linux 域的进程调度,Linux 域密集计算再厉害,也干扰不到实时任务的关键路径。这个设计带来的代价是要打内核补丁,而且补丁版本必须和内核版本严格对齐,这正是本文后半部分大量 BUG 的源头。
1.2 适合谁、不适合谁
这套方案适合交互频繁、对确定性要求苛刻的场景:EtherCAT 主站、CNC 插补、多轴协调运动控制、高速数据采集、以及需要和机器视觉联动的硬实时环节。这些场景的共同特点是"迟到一个周期就出事",而不是"平均速度够快就行"。
不适合的场景也明确:纯算力密集任务、图形界面为主的产品、或者团队里没人愿意维护一个打了私有补丁的内核。Xenomai 这套东西一旦跑起来,你就得一直维护这个打了 ipipe 补丁的内核,升级内核等于重新做一遍全套测试,这个维护成本在立项前就必须想清楚。
2. 版本矩阵:3.0.5/3.0.9 与 4.9.38/4.9.90 怎么对上号
2.1 版本搭配总表
先说这次涉及的组合,实际用过两套配置,一套是量产设备的基础版本,一套是后来升级修订的版本。
| Xenomai 版本 | Linux 内核 | ipipe 补丁示例 | 说明 |
|---|---|---|---|
| 3.0.5 | 4.9.38 | ipipe-core-4.9.38-x86-3.patch | 早期量产组合,稳定但有一些 SMP 下的延迟抖动 |
| 3.0.9 | 4.9.90 | ipipe-core-4.9.90-x86-2.patch | 升级后的组合,高负载下调度更稳 |
| 3.0.9 | 4.9.90 | ipipe-core-4.9.90-arm-*.patch | ARM 平台对应版本,注意架构不能混用 |
上面补丁文件名末尾的修订号以官网实际发布为准,不同时期会有 -2、-3、-4 这类后缀差别,但命名规则就是这样:ipipe-core-内核版本-架构-修订号。
这里最要命的一条规则是:ipipe 补丁必须精确匹配内核的小版本号。4.9.38 的补丁打不到 4.9.90 上,4.9.90 的补丁也打不到 4.9.38 上,一个点版本不对就是一片 hunk failed。因为 ipipe 补丁会改动内核的kernel/irq/、arch/x86/kernel/这类核心目录,上下文对不上直接拒绝。
2.2 3.0.5 到 3.0.9:升级到底赚了什么
两个 Xenomai 版本的用户态 API 基本兼容,所以从 3.0.5 升到 3.0.9 的成本不高,但收益是实打实的。3.0.9 修了一批 Cobalt 核在 SMP 环境下的中断亲和性问题,在我这边的现象是:原来 3.0.5 在高负载下偶尔冒出来的 50us 左右的抖动,升级之后基本消失。另外 3.0.9 对 4.9 系列较新的点版本支持更完整,所以如果内核要升到 4.9.90,Xenomai 这边也建议同步升到 3.0.9。
内核为什么选 4.9 LTS 系列也很简单:4.9 是长期维护版本,ipipe 补丁覆盖全,工控行业当时的主流物料就是它在支撑。4.9.38 和 4.9.90 都属于这个分支的不同维护点,后者修掉了很多前者时期的驱动和调度问题。如果项目从零开始,直接上 3.0.9 + 4.9.90 就好,别再用 3.0.5 折腾自己。
3. 关键配置实录:打上 ipipe 补丁本身就有一堆坑
3.1 补丁获取与打补丁
ipipe 补丁可以从 Xenomai 官网的 ipipe 下载目录拿,也可以用git拉取对应仓库。下载时先确认两件事:架构对不对、内核点版本对不对。x86_64 和 ARM 的补丁文件不是同一个,在 x86 机器上下载 ARM 补丁,打上去大概率直接报错。
拿到补丁后的标准操作如下:
# 假设已经下载好 linux-4.9.90.tar.xz 和 ipipe-core-4.9.90-x86-2.patch tar xf linux-4.9.90.tar.xz cd linux-4.9.90 patch -p1 < ../ipipe-core-4.9.90-x86-2.patch打完补丁马上检查有没有.rej文件残留,有就说明部分补丁没打进去:
find . -name "*.rej" -o -name "*.orig"如果这条命令有输出,千万不要继续往下编译,先把补丁问题解决再说。常见情况是下载到了网页 HTML 而不是原始文件,file命令看一眼就能识别。
3.2 内核配置的关键项
打完补丁之后,内核配置菜单里会多出一个 IRQ pipeline 相关菜单。用make menuconfig打开,搜索IPIPE能看到对应选项。我这边实际用下来的配置项推荐如下:
| 配置项 | 推荐值 | 原因 |
|---|---|---|
| CONFIG_IPIPE | y | 打开中断流水线,这是 Xenomai 运行的前提 |
| CONFIG_HZ_1000 | y | 时钟频率提到 1000Hz,Linux 域响应更及时 |
| CONFIG_PREEMPT | y | Linux 域任务抢占粒度更细,减少对实时的干扰 |
| CONFIG_CPU_FREQ | n | 关闭 CPU 调频,避免频率切换造成延迟抖动 |
| CONFIG_CPU_IDLE | n | 关闭 CPU 深度睡眠,省得唤醒时卡顿 |
| CONFIG_SUSPEND / HIBERNATION | n | 关闭睡眠唤醒路径 |
| CONFIG_IPIPE_DEBUG | n | 调试版会拖慢实时路径,量产必须关 |
| CONFIG_LOCKDEP | n | 锁检测开销极大,会让延迟数据完全失真 |
配置的时候建议从发行版自带的 config 出发,而不是make defconfig从头开始:
cp /boot/config-$(uname -r) .config make olddefconfig make menuconfig这样能少踩很多驱动缺失的坑。改完配置后编译:
make -j$(nproc) bzImage modules sudo make modules_install sudo cp arch/x86/boot/bzImage /boot/vmlinuz-4.9.90-xenomai装完内核记得更新引导配置,然后重启确认内核版本里带-ipipe后缀。如果重启后uname -r还是普通版本号,先查引导配置有没有生效。
3.3 工具链提醒
4.9 内核配太新的 GCC 容易编译报错,比如 GCC 11 之后常见各种隐含声明、头文件不兼容问题。这不是 Xenomai 的问题,是老内核和新编译器的兼容性问题。我这边用的是 GCC 7/8 附近的工具链,一次通过。如果你只有新 GCC,可以先试试在旧内核源码里补一些兼容性头文件,但最省事的方案还是换工具链。
4. 从编译到跑通 latency:用户态安装实操
4.1 Xenomai 用户态编译
内核搞定后,接着编译 Xenomai 用户态。下载 xenomai-3.0.9 源码包后:
tar xf xenomai-3.0.9.tar.bz2 cd xenomai-3.0.9 ./scripts/bootstrap ./configure --with-core=cobalt --enable-smp --prefix=/usr/xenomai make -j$(nproc) sudo make installbootstrap这步很多人会漏掉。直接从官网下载的 release 包一般带 configure 脚本,但如果是从 git 仓库拉的最新代码,必须先跑 bootstrap 生成 configure。--with-core=cobalt是选择双内核模式,这也是 Xenomai 实时性最强的工作方式。
编译期间一个常见问题是缺 autoconf、automake、libtool 这类工具,发行版里装一下即可。别小看这一步,bootstrap 阶段报错基本都是这些基础工具版本太老或者没装全。
4.2 环境变量与模块加载
安装完成后,把环境变量写进当前会话或用户的脚本里:
export XENO_BASE=/usr/xenomai export PATH=$XENO_BASE/bin:$PATH export XENO_CONFIG=$XENO_BASE/bin/xeno-config加载内核模块和启动 Xenomai 服务:
sudo $XENO_BASE/sbin/start.sh dmesg | grep -i xenomai正常情况下 dmesg 里能看到 Xenomai/Cobalt 相关初始化信息。如果 start.sh 报模块加载失败,直接看下一章的 BUG 记录,这恰恰是最容易卡住的环节。
4.3 第一次跑 latency
用户态跑通后,第一个要测的就是自带 latency 示例,这是判断环境是否干净的黄金标准:
sudo /usr/xenomai/demos/latency -T 60 -p 1000 -s -i-p 1000表示测试周期 1000us,-T 60表示跑 60 秒,-s是同步模式,更接近真实控制循环的触发方式,-i会在测试结束时输出更多统计信息。运行后屏幕会滚动类似这样的数据:
RTT| 00:00:10 (periodic, 1000 us, 10000 samples, avg 2 us, max 9 us)判断标准很简单:x86_64 平台上 max 值应该在 10us 以内,大部分时候是个位数;ARM 平台看主频和中断控制器质量,但 max 原则上也不该超过 50us。如果你的 max 数值在这个量级以上,直接跳到后面的排查章节,你的环境肯定有地方没配置干净。
5. BUG 记实(上):补丁打不上、模块加载失败、启动即卡死
5.1 现象:patch 报 hunk FAILED
这是新手必踩的第一个坑,我也没躲过去。现象是执行patch -p1后屏幕一堆Hunk #1 FAILED at 1042这类输出,或者干脆一路 reject。排查链如下:
- 第一步,
file查看补丁文件类型,如果是 "HTML document" 说明下载到了网页而不是原始文件,重新下载 raw 版本。 - 第二步,核对架构。x86 的补丁里会看到 arch/x86 的改动,ARM 补丁则涉及 arch/arm,看 20 行就能区分。
- 第三步,核对内核点版本。
tar xf出来的目录名必须和补丁文件名里的版本完全一致,比如 linux-4.9.90 的源码树,就只能打 ipipe-core-4.9.90 的补丁。
这个问题的根因就一个:版本或来源不对。补丁不是"通用的",它是针对特定内核版本生成的上下文差异。所以别去第三方博客下载补丁,一定要从官方渠道拿,并且和内核版本逐字符核对。
5.2 现象:模块 version magic 对不上
内核和用户态都编译完了,跑 start.sh 加载模块时报错,大意是内核模块的 version magic 不匹配,说当前内核是4.9.90-ipipe,但模块期望的是4.9.90,或者反过来。
根因在 ipipe 补丁会给内核版本自动追加-ipipe后缀。外部模块必须用打过补丁的内核源码树编译,它的 version magic 才会带上-ipipe。最常见的错误是/lib/modules/$(uname -r)/build这个软链接指向了没打补丁的干净内核源码,导致外部模块编译时用了错误的头文件。
处理办法是重新确认软链接指向打过补丁的那棵源码树,然后重新make modules_install,再重新编译安装 Xenomai 用户态。这一步如果不重做,模块加载的报错会反复出现。
5.3 现象:启动到 I-pipe 附近卡死
另一个让人头皮发麻的问题是:内核引导到刚初始化中断流水线的地方就没了输出,反复重启都一样。如果手头是 x86_64 工控机,常见原因是串口 console 驱动与其他驱动在中断接管上冲突,或者配置里开了某些调试选项导致早期初始化路径变得极慢。
我当时的排查过程是:先在引导参数里加earlyprintk,把启动输出提前打出来,确认卡死的位置;再把可能抢占早期中断的设备驱动从内核里摘掉,比如串口 DMA、某些 GPIO 控制器;最终定位到是设备树里一个中断属性与 ipipe 补丁的预期不一致。这个问题的通用排查路径是:先确认卡死函数,再逐个禁用可疑驱动,最后对比设备树中断配置。ARM 平台尤其常见,x86 平台相对少,但排查思路一样。
6. BUG 记实(下):延迟毛刺、外设中断和用户态段错误
6.1 固定周期毛刺:是内核在"做家务"
latency 测试最大的敌人之一,是周期性出现的尖峰。现象非常典型:平时 max 只有 5us,但每隔固定时间就冒出来一个 100us 左右的尖峰,像时钟一样准。
这种固定周期的毛刺,优先怀疑三个东西:NMI watchdog、RCU 回调、tick 处理。NMI watchdog 会定期在每个 CPU 上触发不可屏蔽中断,这个中断在实时域看来就是天外来客;RCU 的回调如果落在实时任务所在的核心上,也会周期性制造延迟。处理手段如下:
# 关闭 NMI watchdog sysctl -w kernel.nmi_watchdog=0 # 引导参数里把 CPU1 从调度和 RCU 家务里摘出来 # isolcpus=1 rcu_nocbs=1 # 然后实时任务固定绑在 CPU1 上 taskset -c 1 /usr/xenomai/demos/latency -T 60 -p 1000 -s -i当时我加了这几个参数之后,那个如钟表般准时的尖峰就消失了。这个问题的本质是Linux 域的周期性家务打扰了 Cobalt 域,方向比具体参数更重要。
6.2 外设一跑实时就崩:中断亲和性
另一种高频场景:安静环境下 latency 一切正常,一开网卡流量,max 值从个位数直接跳到几百微秒甚至毫秒级。我遇到过一跑 iperf 实时就报警的项目,排查到后面发现是网卡中断和实时任务挤在了同一个 CPU 核心上。
x86 平台的irqbalance会动态轮转中断到各核心,但它的调度逻辑不会考虑你的实时任务绑在哪。解决办法是把实时任务和中断分别固定在两个核上:
# 实时任务绑 CPU1 taskset -c 1 /usr/xenomai/demos/latency -T 60 -p 1000 -s -i # 把网卡中断固定到 CPU0 # 先 /proc/interrupts 找到网卡的 IRQ 号,假设是 16 echo 1 > /proc/irq/16/smp_affinityUSB 控制器也有同样的问题,插拔 U 盘、移动鼠标时如果延迟出现毛刺,把对应 IRQ 挪走即可。这个排查方向对任何"外设一动作实时就崩"的问题都适用,先看/proc/interrupts确认中断落在哪个核上,再调整亲和性。
6.3 十几毫秒巨坑:查电源管理
最阴间的 BUG 往往在电源管理这里。现象是:测 30 秒没事,跑 10 分钟突然冒出一个 10ms 级别的巨坑,而且间隔非常不规则。如果你先入为主地去查中断,大概率一无所获,因为问题根本不在中断,而在CPU 频率和 C-state 深度睡眠。
x86 平台默认的 CPU 调频策略是 powersave,负载降下来就会自动降频,实时任务被唤醒时 CPU 正处在低频或者深睡眠状态,唤醒延迟直接拉满。处理方式分三层:
- 第一层,BIOS 里关闭 C3/C6 等深度休眠状态。
- 第二层,内核配置里关掉 CONFIG_CPU_FREQ 和 CONFIG_CPU_IDLE。
- 第三层,运行时强制 performance governor:
cpupower frequency-set -g performance我在这上面耗时最久,一度以为是网卡驱动问题。后来把电源管理全部关掉,所有不规则大坑全部消失。x86 平台做实时,先关电源管理再谈别的,这句话我现在逢人就讲。
6.4 用户态一碰实时 API 就段错误
还有一个用户态的高频问题:用 xeno-config 编译出来的程序,一调用rt_task_create就段错误,普通 Linux 程序完全正常。这个现象的根因几乎都在编译环境或权限。
编译环境问题:xeno-config --cflags给出的编译参数必须完整带进你的构建过程。很多人只抄了-I/usr/xenomai/include,漏了-D_GNU_SOURCE这类宏定义,结果头文件里的声明和库实现对不上,运行直接崩。正确的做法是用命令替换:
export XENO_USER_CFLAGS=$(/usr/xenomai/bin/xeno-config --cflags) export XENO_USER_LDFLAGS=$(/usr/xenomai/bin/xeno-config --ldflags) # Makefile 里通过 $(shell ...) 引入,不要手抄权限问题:非 root 用户跑实时程序,需要把用户加入 Xenomai 的允许组,否则内核会拒绝你创建实时任务。方法是在引导参数里加:
xenomai.allowed_group=1000或者运行时把组 ID 写进/proc/xenomai/group。这个问题别看简单,现场项目里因为漏了这一步导致程序段错误、然后怀疑整了半天内核配置的案例非常普遍。
7. 延迟乱飞的系统化排查思路
7.1 先分域:Linux 域问题 vs Cobalt 域问题
踩过一圈 BUG 之后,我最大的体会是:延迟问题排查前,先分清是 Linux 域的问题还是 Cobalt 域的问题。实时任务里如果调用了printf、open、malloc这类 Linux 系统服务,就会发生域切换,切换本身就有开销,次数多了延迟自然难看。这不是内核的错,是代码写得不对。
Cobalt 域里做日志应该用rt_printk这类实时安全接口,数据交换用预分配的内存池,文件系统操作全部放到普通 Linux 线程里做。很多"延迟莫名其妙变高"的案例,最后定位到就是实时任务里多了一行printf。
7.2 症状-怀疑对象对照表
现场排查时我习惯拿一张表对着看,效率比瞎猜高得多:
| 症状 | 优先怀疑对象 | 验证手段 |
|---|---|---|
| 固定周期尖峰 | NMI watchdog、RCU、tick | 关闭 watchdog,isolcpus/rcu_nocbs 隔离 |
| 外设一动作就崩 | 中断亲和性 | /proc/interrupts 查核,调 smp_affinity |
| 不规则十几 ms 大坑 | 电源管理、C-state | 关 CPUFreq、关 C-state,performance governor |
| 用户态段错误 | 编译参数、运行权限 | 检查 xeno-config cflags/ldflags、allowed_group |
| 一跑负载就抖 | SMP 调度、高优先级任务干扰 | 绑核、调整任务优先级、隔离核 |
这张表就是前面所有 BUG 的浓缩版。遇到新问题先对表,对不上再往下挖。
7.3 压测环境"干净化"清单
做延迟测试前,先把环境搞干净,否则你测出来的数据全是干扰,没法定位。我自己的固定流程如下:
- SSH 登录而不是本地图形界面,把桌面、合成器全部停掉。
- 关闭 NMI watchdog:
sysctl -w kernel.nmi_watchdog=0。 - 关闭电源管理:BIOS 层面和内核层面同时处理。
- 临时断开网卡,或把网卡中断固定到非实时核。
- 实时任务用
taskset绑核,并确认没有其他高优先级进程落在同一个核上。 - 每次测试记录
dmesg、/proc/interrupts和latency -h直方图,存成带日期的文件备查。
这个流程跑下来,如果还有异常,那基本可以确定是硬件或者驱动层面的问题,而不是配置层面的问题。
8. 一点经验小结
后来我把新项目切到了更新的内核与 Xenomai 组合,但每次新设备出现莫名的延迟上涨,还是会翻出当年这套 3.0.5/3.0.9 + 4.9.38/4.9.90 的排查笔记,思路永远是同一个顺序:先看电源管理、再看中断亲和、再看 RCU。最后分享一个让我少走很多弯路的小习惯——每套内核验证稳定后,把dmesg、/proc/interrupts、latency直方图连同内核的.config一起打包备份,命名里带上日期和内核版本。这套"证据链"在你三个月后回来处理现场问题时,比任何排查工具都管用。