
做运动控制的人应该都有过这种经历用户态程序跑得好好的一上 Linux 就时不时卡一下。就那一下伺服可能已经丢了脉冲轨迹上多了一个毛刺。我最早以为是自己代码写得不够好后面把进程优先级、中断绑核、内存锁全试了一圈才发现问题不在应用层而在 Linux 调度器本身。于是我开始认真考虑双内核架构 Xenomai 4 这条路线。Xenomai 4 不是传统意义上的补丁式实时方案它是在 Linux 旁边再放一个真正的实时内核让控制循环跑在另一个核心里。这篇安装教程不讲官方文档里那些已经写清楚的东西而是把从源码准备、内核编译、用户态安装到实测 latency 的完整过程以及我中间翻车的几次经历一次讲完。1. 为什么还要装一套双内核Xenomai 4 解决的是哪类痛点1.1 单内核实时方案的天花板先聊一个很现实的问题PREEMPT_RT 已经让 Linux 在绝大多数场景下能做到几十微秒的调度延迟为什么还要折腾双内核PREEMPT_RT 的思路是把 Linux 内核里几乎所有不可抢占的区域都改成可抢占把自旋锁尽量替换成可睡眠的互斥锁。这个方案在日常负载下表现确实不错我在 x86 工控机上实测空闲状态下 wakeup latency 可以稳定在 20 到 50 微秒。但问题在于它始终跑在 Linux 内核这个大系统里调度器、锁竞争、中断下半部、RCU 回调、内存管理这些路径总会在某些极端时刻产生一次延迟尖峰。你要做 1ms 周期的位置环控制偶尔一次 200 微秒的抖动可能还能忍但要是把周期压到 250 微秒这种不确定性就完全不可接受了。单内核方案像是把主干道所有红绿灯都调成感应式平时很顺但高峰期仍然可能突然出现一次全员红灯。双内核则完全不同它直接给实时任务单独修了一条不经过主干道的专用通道。1.2 双内核到底“双”在哪Xenomai 4 的双内核架构简单说就是在 Linux 内核旁边维护了一个独立的实时核心在 Xenomai 4 里这个核心叫 EVL core。Linux 本身照常运行负责文件系统、网络协议栈、驱动、图形界面这些非实时任务实时任务则交给 EVL core 管理走完全独立的一套调度器和时钟机制。关键点在于中断的拆分普通中断继续走 Linux 的 irq 子系统而实时任务关心的中断比如伺服驱动器的位置反馈信号、编码器索引信号会被 EVL core 直接接管响应路径比 Linux 原生的中断线程化要短得多。Xenomai 4 的这套机制叫 out-of-band 执行实时线程运行在带外上下文里不受 Linux 内核锁、调度器 tick、RCU 这些机制的影响。和 Xenomai 3 相比Xenomai 4 把用户态 API 重新梳理了一遍libevl 比老的 libcobalt 更干净性能上也有优化。更重要的是Xenomai 4 允许你在 Linux 侧同时启用 PREEMPT_RT也就是说两边都有实时能力但硬实时路径只走 EVL core。1.3 什么项目才值得上双内核我的判断标准很简单如果你的控制周期在 1ms 以上、延迟要求 100 微秒级别先用 PREEMPT_RT 优化一轮成本低得多。当你需要把控制周期压到 500 微秒以内同时又必须在同一台设备上跑 Linux 协议栈、文件系统、人机界面双内核才开始真正划算。适合的场景包括机器人控制器、CNC 插补、高速数据采集、实时仿真、飞控半实物测试。不适合的场景也很明确普通 Web 服务、桌面系统、只需要毫秒级响应的 IoT 设备这些场景上 Xenomai 只是徒增维护成本。2. 安装前的底子内核源码、Dovetail 补丁与编译环境2.1 硬件与发行版选择先说硬件。Xenomai 4 本身没有苛刻的硬件要求x86-64 和 arm64 都可以跑树莓派 4 这种开发板也能跑起来。但有一点必须提前想清楚做实时性验证一定要用真实物理机不要在 VMware、VirtualBox 这类虚拟化环境里做最终性能验收。虚拟机的定时器、中断控制器都是模拟出来的latency 数字会非常难看而且完全没有参考价值。虚拟机里可以验证安装流程是否走通仅此而已。CPU 数量上至少双核推荐四核以上。因为双内核架构通常需要专门隔离一个核心给实时任务剩下的核心跑 Linux 以及应用程序。如果你只有双核隔离一个核心后 Linux 侧就只剩下一个核心跑起来会很难受。发行版我推荐 Ubuntu 22.04 LTS 或 Debian 12原因没什么高深的内核编译工具链成熟遇到问题搜得到解决方案官方文档里的示例也基本上以 Debian 系为主。滚动发行版不是不行但内核版本更新太快Dovetail 补丁往往跟不上容易踩版本匹配的坑。磁盘空间方面内核源码解压加编译产物建议预留至少 20GB。/boot分区如果是独立分区至少保证 500MB 以上空间否则安装新内核时可能因为空间不足失败。2.2 把编译工具链一次装齐这一步看着简单但只要少装一个包后面编译到一半就会卡住。我用的是 Ubuntu 22.04完整依赖如下sudo apt update sudo apt install -y build-essential git libncurses-dev flex bison \ libelf-dev libssl-dev bc rsync kmod dwarves cpupower fakeroot逐个解释一下libncurses-dev是 make menuconfig 图形化配置界面的依赖不装的话配置内核时会提示缺少 ncursesflex和bison是内核解析配置文件和设备树时的工具libelf-dev用于内核模块的 ELF 信息处理libssl-dev是内核模块签名和某些加密功能编译时的依赖新内核基本是必须的bc用于内核编译脚本里的数学计算dwarves提供pahole工具新版内核生成 BTF 信息时要用它cpupower后面调 CPU 频率调速器会用到。这里有个容易忽略的点如果你用 Ubuntu 20.04 这类旧发行版默认的 GCC 版本可能偏老编译 6.x 内核时会出现奇奇怪怪的报错。建议至少 GCC 11 以上。Ubuntu 22.04 默认 GCC 11问题不大。2.3 拿到版本匹配的内核与补丁这是整个安装过程里最容易翻车的一步。Xenomai 4 的 EVL core 不是独立内核而是以补丁形式合入 Linux 内核源码树中的一个实时核心同时需要配合 Dovetail 补丁来改造中断管道机制。这里的核心铁律是Linux 内核版本、Dovetail 补丁版本、Xenomai 用户态版本三者必须严格对应。操作步骤是这样的打开 Xenomai 官网的下载页面找到当前推荐的内核版本号和对应的 Dovetail 补丁下载链接。从 kernel.org 或镜像站下载对应小版本的 Linux 内核源码比如官网推荐 linux-6.1.51你就必须下载 6.1.51不能拿 6.1.50 凑合。克隆 Xenomai 用户态源码git clone https://git.xenomai.org/xenomai.git -b master如果官网提供的是 tarball 包直接下载解压也可以。另外建议把下载到的 patch 文件放到和内核源码同级的目录后面方便操作。我在第一次装的时候发现文件名字写着 6.1.51但下载下来的内核目录是 6.1.50patch 的时候一堆 hunk failed白白浪费了一个下午。所以解压完内核请务必确认目录名和Makefile顶部的版本号。3. 内核侧的四步打补丁、配选项、编内核、装引导3.1 打 Dovetail 补丁进入内核源码目录后先用 dry-run 模式测试补丁是否能干净地打上去tar xf linux-6.1.51.tar.xz cd linux-6.1.51 xzcat ../dovetail-patch-*.patch.xz | patch -p1 --dry-run如果输出全是patching file ...并且没有FAILED说明版本匹配正确。然后去掉--dry-run实际打补丁xzcat ../dovetail-patch-*.patch.xz | patch -p1打完以后检查有没有残留的.rej文件find . -name *.rej如果有.rej文件说明有补丁冲突这时候千万别用--force强行打否则编译出来的内核大概率起不来。正确做法是回到版本匹配的起点确认内核版本和补丁版本完全一致。3.2 配置内核关键选项开始配置之前先加载一个默认配置作为基础避免从零开始漏掉基础驱动make x86_64_defconfig make menuconfig在 menuconfig 里按/搜索EVL你会看到一批以 CONFIG_EVL 开头的配置项。最关键的是CONFIG_EVL这个总开关必须设置成y不能用m因为 EVL core 需要静态编入内核不能作为可加载模块。其他 EVL 子选项在调试阶段建议全部打开尤其是CONFIG_EVL_DEBUG虽然会带来少量性能开销但出问题时日志信息要完整得多。等你要发布正式固件时再关掉。另外几个和实时性强相关的选项也值得注意CPU 调频相关把默认的 CPU frequency governor 设成 performance或者至少在后面通过引导参数和 cpupower 强制设成 performance。CPU 频率动态调整会让定时器的精度飘忽不定latency 测试时数据会很难看。HZ 值在 Kernel Features 里把 HZ 设为 1000。HZ 越高Linux 侧的时间片粒度越细对实时核的干扰越小。如果你打算在 Linux 侧同时启用 PREEMPT_RT需要提前准备好 PREEMPT_RT 补丁并在 General setup → Preemption Model 里选择 Fully Preemptible Kernel。不过第一次安装不建议同时上两者先把纯双内核跑通后续再叠加。如果这一步你想用make localmodconfig来裁剪内核千万注意这个命令会按当前系统加载的模块来裁剪很容易把 EVL 相关代码也裁掉。新手第一次建议老老实实defconfig加手动调整不要追求最小化裁剪。3.3 编译并生成安装包配置保存后在 Ubuntu/Debian 系发行版上最省事的做法是直接生成 deb 包make -j$(nproc) deb-pkg这个命令会生成linux-image-*.deb、linux-headers-*.deb等文件生成的文件在当前目录的上一级目录也就是../。如果你用的是其他没有 dpkg 机制的发行版也可以走传统三条命令make -j$(nproc) sudo make modules_install sudo make install编译时间取决于机器性能i7 级别的 CPU 大概 20 到 40 分钟树莓派这种 arm64 板子可能要两三个小时。编译过程中报错的话优先看报错信息里提示的头文件或工具版本绝大多数情况是前面依赖装得不全。3.4 安装内核与调整 Grub 引导参数deb 包生成后安装并更新引导sudo dpkg -i ../linux-image-*.deb ../linux-headers-*.deb sudo update-grub sudo reboot重启后确认内核版本和 EVL 是否加载uname -r sudo dmesg | grep -i evl如果dmesg里能看到 EVL core 初始化的相关输出说明内核侧已经成功了。如果什么都搜不到先别急着装用户态回头检查内核配置里 CONFIG_EVL 是否真的编进去了。接下来是引导参数的调优。编辑/etc/default/grub在GRUB_CMDLINE_LINUX_DEFAULT里加参数GRUB_CMDLINE_LINUX_DEFAULTquiet splash isolcpus1 nohz_full1 rcu_nocbs1 intel_pstatedisable processor.max_cstate1解释一下这串参数isolcpus1把 CPU1 从 Linux 调度器的普通任务池里隔离出去专门留给实时任务。nohz_full1对 CPU1 关闭调度器时钟中断减少对实时任务的周期性打扰。rcu_nocbs1把 CPU1 上的 RCU 回调转移到其他 CPU避免 RCU 处理打断实时线程。intel_pstatedisable和processor.max_cstate1关闭 CPU 频率动态调节和深度 C-state从根源上减少定时器抖动。这两项对性能影响不小我实测在没加 C-state 限制之前latency 偶尔会出现 50 微秒以上的尖峰加上之后最大值稳定在 10 微秒以内。修改后更新引导并确认隔离生效sudo update-grub sudo reboot cat /sys/devices/system/cpu/isolated输出里应该包含1。4. 用户态环境与第一段实时程序4.1 编译安装 Xenomai 用户态库内核起来之后回到之前克隆的 Xenomai 源码目录编译用户态库cd xenomai ./configure --prefix/usr/xenomai make -j$(nproc) sudo make install不同小版本的 configure 参数可能有差异第一次运行之前先执行./configure --help扫一眼具体选项。安装完成后把路径写进 shell 配置文件export PATH/usr/xenomai/bin:$PATH export LD_LIBRARY_PATH/usr/xenomai/lib:$LD_LIBRARY_PATH export PKG_CONFIG_PATH/usr/xenomai/lib/pkgconfig:$PKG_CONFIG_PATH这三行建议追加到~/.bashrc末尾因为后续所有实时测试程序都要用到这个动态库路径。这里有个很容易踩的坑如果只设了 PATH 没设 LD_LIBRARY_PATH运行/usr/xenomai/bin/latency时会报error while loading shared libraries: libevl.so.0。更稳妥的做法是把库路径写进系统的 ld 配置echo /usr/xenomai/lib | sudo tee /etc/ld.so.conf.d/xenomai.conf sudo ldconfig之后不管在哪个终端下运行工具都不会再报找不到库。获取编译自己程序需要的参数可以用xeno-configxeno-config --cflags --ldflags输出类似-I/usr/xenomai/include -L/usr/xenomai/lib -levl写 Makefile 的时候直接引用即可。4.2 设备节点与实时权限Xenomai 4 的 EVL core 初始化后会在/dev下创建设备节点/dev/evl用户态程序通过这个节点和 EVL core 通信。默认情况下这个节点只有 root 可访问需要配置 udev 规则把权限放开。创建一个 udev 规则文件/etc/udev/rules.d/99-evl.rulesKERNELevl, MODE0660, GROUPrealtime然后创建 realtime 用户组并把当前用户加进去sudo groupadd --system realtime sudo usermod -aG realtime $USER接着修改/etc/security/limits.conf给 realtime 组放开实时调度和内存锁限制realtime soft rtprio 99 realtime hard rtprio 99 realtime soft memlock unlimited realtime hard memlock unlimitedrtprio限制决定了普通用户能否创建 SCHED_FIFO 实时线程memlock限制决定能否锁定内存页防止换出。这两个不放开实时程序要么报Operation not permitted要么在运行中出现页面换入换出导致的延迟尖峰。改完以后必须重新登录一次终端让用户组和 limits 生效。这一步特别容易忽略我见过不少人卡在权限问题上最后发现只是没重新登录。4.3 写一个实时周期任务验证环境环境就绪后写一个最简单的实时周期任务来验证整条链路。新建demo.c#include evl/evl.h #include evl/thread.h #include evl/clock.h #include stdio.h int main(int argc, char *argv[]) { struct timespec period { .tv_sec 0, .tv_nsec 1000000 }; struct timespec now; int ret; ret evl_attach_self(demo-rt-task); if (ret 0) { perror(evl_attach_self); return 1; } while (1) { clock_gettime(CLOCK_REALTIME, now); printf(rt tick: %ld.%09ld\n, now.tv_sec, now.tv_nsec); fflush(stdout); evl_sleep(period); } return 0; }这个程序做的事情很简单把当前线程挂到 EVL core 上然后以 1ms 为周期循环打印时间戳。evl_attach_self是关键调用它把普通 Linux 线程切换到 EVL 的带外执行上下文之后这个线程的调度就完全由 EVL core 接管了不再受 Linux 调度器影响。evl_sleep的声明在不同版本头文件里可能略有差异如果编译报错打开安装目录下的include/evl/clock.h看一眼实际签名把参数形式调整一致即可。编译运行gcc -o demo demo.c $(xeno-config --cflags --ldflags) ./demo正常运行的话终端里会以接近 1ms 的间隔稳定输出rt tick。如果能看到这个输出说明双内核全部打通了内核侧 EVL 正常、用户态库正常、设备节点权限正常、实时调度生效。这一步是后续做任何工业控制项目的基础。5. 实测数据latency 输出到底怎么看5.1 跑一个标准 latency 测试环境通了之后下一步就是测实时性。Xenomai 4 安装完成后会带上标准测试工具latency直接运行/usr/xenomai/bin/latency -t 2 -p 1000参数含义-t 2表示测试 2 分钟-p 1000表示周期 1000 微秒。如果这个工具没生成可以到 Xenomai 源码的examples目录下单独编译。输出大致长这样 Sampling period: 1000 us Test mode: periodic user-mode task All results in microseconds warming up... RTT| 00:00:01 (periodic user-mode task, 1000 us period, priority 99) RTH|----lat min|----lat avg|----lat max|-overrun|--msw|---lat best|--lat worst RTD| 0.823| 1.437| 4.912| 0| 0| 0.811| 4.912逐列解释lat min / avg / max整个测试过程中每次唤醒延迟的最小值、平均值、最大值单位微秒这是最核心的指标。overrun超过周期上限的次数意味着某个周期内实时任务没有在截止时间内完成工业控制里这个数字一次都不能有。mswmode switch 次数表示实时线程被迫退出带外模式退回 Linux 普通模式。正常情况下应该一直是 0如果这个数字持续增长说明实时线程里调用了不允许的服务实时性已经失效。lat best / worst到目前为止看到过的最好和最坏延迟。5.2 不同条件下双内核的预期表现我在一台 i5-8500 工控机上实测的数据大致是这个水平系统状态max latency备注空闲CPU 锁定 performance2~10 us正常水平千兆网卡持续打流 磁盘写入10~30 us偶尔出现小尖峰同一台机器单独跑 PREEMPT_RT30~200 us对比参考VMware 虚拟机内100 us 以上且波动极大只适合验证流程需要特别说明的是这里的 max latency 是全程最大值不是平均值。实时系统评价的不是平均表现而是最坏情况。双内核之所以能做到个位数微秒级别是因为实时任务的唤醒不依赖 Linux 调度器的 tick而由 EVL core 的独立时钟直接触发中断响应路径也短得多。5.3 测试时注意事项跑 latency 的时候有几点经验测试期间锁屏、关掉桌面特效图形环境本身会产生大量中断和调度活动影响数据纯净度。至少跑 24 小时再下结论半小时的数据只能暴露高频问题低频的偶发尖峰需要足够长的样本才能看到。把测试线程的 CPU 亲和性绑到隔离出的实时核上。latency 工具一般自带绑核参数没有的话用taskset -c 1包一层。每次测试前先确认 CPU 频率调速器是 performance用cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor检查。6. 踩坑记录从启动 panic 到权限不足的完整排查链路6.1 内核没起来版本不匹配和 Secure Boot先说我自己遇到过的最严重的问题打完补丁编译出来的内核重启后直接黑屏。排查链路是这样的重启后在 Grub 菜单里手动选择新内核依然黑屏。进入旧内核后执行uname -r确认新内核没有正常引导。检查dmesg但黑屏的时候根本没机会看日志我是通过串口才拿到 panic 信息的。后来发现原因很简单我下载的 Dovetail 补丁是 6.1.51 的但内核源码实际是 6.1.50patch 时有一半文件打不进去我图省事用了--force硬打最终编译出了一个结构损坏的内核。教训是不管补丁冲突看起来多轻微都不要强行跳过。版本号对不上唯一正确的做法就是换内核版本或换补丁版本直到 dry-run 输出干净为止。另外一个新手容易忽略的问题是 Secure Boot。Ubuntu 默认开启 Secure Boot 后未签名的内核模块会被直接拒绝加载症状是 EVL 相关的初始化日志完全看不到。调试阶段最简单的处理方式是进 BIOS 关掉 Secure Boot后面如果要上产线再考虑做完整的模块签名流程。6.2 /dev/evl 节点不出现内核装好、用户态库也编译完了运行 demo 程序却报找不到/dev/evl。排查顺序ls -l /dev/evl看节点是否真的不存在。sudo dmesg | grep -i evl看 EVL core 有没有注册设备。如果 dmesg 里完全没有 EVL 相关输出大概率是内核配置里 CONFIG_EVL 总开关没打开或者编译时被裁剪了。如果 dmesg 有输出但节点不存在检查 udev 规则是否加载。除了重新加载规则还有一个更隐蔽的点重启后 Grub 默认启动项可能是旧内核uname -r确认一下当前跑的是不是新内核。如果上述都正常检查/dev目录的权限以及 udev 规则里 GROUP 对应的用户组名是否一致。我实际遇到过一次udev 规则文件名写得不对系统加载了别的规则节点权限完全没变。重命名为99-evl.rules才解决。6.3 latency 周期性尖峰网卡中断和 CPU 调频有一次 latency 测试整体平均值很漂亮在 2 微秒左右但每隔差不多一秒就会出现一次 50 到 100 微秒的尖峰而且非常有规律。排查链路cat /proc/interrupts观察各中断号的计数增长速度发现网卡中断增长最快eth0 对应的 IRQ 每秒触发上千次。把网卡中断的 SMP affinity 强行指定到非实时核。具体做法是查看/proc/irq/中断号/smp_affinity写入目标 CPU 的掩码。比如想只用 CPU0就写1。同时检查 CPU 频率调速器发现虽然是 performance但 CPU C-state 没有关闭ACPI 事件还是会周期性唤醒 CPU。在 grub 参数里加上processor.max_cstate1后尖峰彻底消失。这种偶发尖峰是最坑人的因为它不在平均值里体现只有 max 值能看出来。所以我的习惯是每次调完配置都把latency -t 600的 max 值记录下来跟之前的数据对比不能只看 avg。6.4 权限不足rtprio 和 memlock 限制还有一种特别常见的场景普通用户跑实时程序结果报sched_setscheduler: Operation not permitted。原因基本可以锁定在/etc/security/limits.conf没配好或者配置了但没重新登录。排查命令ulimit -r ulimit -l如果ulimit -r输出 0说明 rtprio 限制没生效。确认当前用户已经在 realtime 组里groups如果组信息里没有 realtime说明usermod之后没重新登录或者当前 shell 还是旧会话。另外注意 limits.conf 的写法通配符*不会自动包含实时优先级提升的权限必须按组或按用户显式配置realtime soft rtprio 99这种形式。6.5 用户态库安装完找不到动态库make install成功但运行/usr/xenomai/bin/latency时报找不到libevl.so.0。这是典型的动态库路径问题。Xenomai 4 的库默认装在/usr/xenomai/lib但系统默认的ld.so搜索路径里没有它。解决办法前面已经提到过写/etc/ld.so.conf.d/xenomai.conf然后ldconfig。这个坑在使用sudo运行时特别明显因为 sudo 会重置 LD_LIBRARY_PATH 环境变量之前设的 export 通通失效。写入 ld.so.conf 才是治本的方法。7. 个人经验收尾什么情况下我建议上 Xenomai 4最后聊点实际的选型和维护建议。如果你做的是 1ms 以上的周期控制延迟要求 100 微秒级别先把 PREEMPT_RT 调校到极致成本比上 Xenomai 低得多。只有当你要把控制周期压到 250 到 500 微秒同时又必须在同一台 Linux 设备上保留文件系统、网络协议栈、人机界面时双内核架构才开始体现真正的价值。不要在生产固件里用 master 分支。Xenomai 4 还在活跃迭代API 偶尔会有调整。我自己维护项目时会把内核源码、Dovetail 补丁、Xenomai 用户态源码连同编译脚本全部提交到版本管理里锁死一个稳定版本绝不随意升级。内核和补丁的版本匹配关系是双内核方案里最脆弱的环节不锁版本就是在给自己埋雷。最后一个小技巧把latency测试、CPU 频率状态检查、中断亲和性检查这几个命令写成一个固定脚本每次内核配置或引导参数有改动后跑一个 24 小时的样本并保存结果。实时系统的问题大多不是平均延迟不够好而是偶发尖峰有多高、多久出现一次。别信十分钟的测试数据实时性这东西测的时间越长越能看出真相。