ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Xenomai 4新架构:EVL与Dovetail重塑Linux硬实时

Xenomai 4新架构:EVL与Dovetail重塑Linux硬实时 Xenomai 4 这个名字圈内人确实等了不少时间。如果你用过 Xenomai 3 的 Cobalt 核会知道那套双内核思路在工业实时控制里有多能打如果你维护过它的工程也会知道维护 I-pipe中断管道内核补丁有多痛苦。Xenomai 4 把底层核心换成了 EVL core把中断虚拟化的实现从 I-pipe 换成了 Dovetail表面上还是跟 Linux 抢时间的老路线实际上从内核补丁、API 到调度模型都推倒重来了一遍。这篇文章不打算念官方文档我按自己从 Xenomai 3 迁移过来、把一个运动控制 demo 跑在 Xenomai 4 上的实际经历聊聊 EVL 和 Dovetail 到底解决了什么、开发环境怎么搭、以及最容易踩的坑。1. 从Cobalt/Mercury到EVLXenomai 4为什么敢推倒重来1.1 实时性的本质Linux 到底差在哪先说清楚一个前提Linux 不是不能做实时而是它的实时是统计意义上的软实时。普通内核里一个高优先级线程唤醒后到底多久能跑到取决于太多变量——中断优先级、spinlock 持有时间、调度器 tick、甚至同一个 CPU 上的 cache 竞争。系统可能平均 50 微秒就响应了但最坏情况下几百微秒甚至毫秒级延迟也不是没可能。硬实时系统看的恰恰是最坏情况worst-case不是平均值。运动控制里一个 1kHz 的伺服周期如果某一拍延迟超过 200 微秒可能直接导致轨迹偏差工业总线的错误帧倒计时更狠。所以才有两类主流方案一类是给 Linux 打 PREEMPT_RT 补丁另一类就是 Xenomai 这类双内核方案。PREEMPT_RT 的思路是让内核处处可抢占、把不可延迟路径尽量缩短。它仍然是Linux 独占整机实时性上限完全取决于内核代码里还剩多少坑。双内核方案则更霸道在 Linux 旁边放一个实时核硬件中断先经过实时核实时核判定这个中断是自己的还是 Linux 的只有 Linux 的中断才被注入给 Linux。普通 Linux 程序该怎么跑怎么跑但实时任务跑在实时核上拥有最高优先级的中断入口和调度权。1.2 Xenomai 3 的遗产Cobalt 与 MercuryXenomai 3 提供两种运行模式。Cobalt 是真正的双内核依赖 I-pipe 把 Linux 中断截流在上面实现了一个功能很完整的实时内核而且通过 skin 层提供 POSIX、VxWorks、pSOS、uITRON 等一堆 API。Mercury 则不做双内核直接运行在带 PREEMPT_RT 的普通 Linux 上API 走的是普通 syscall。Cobalt 的能力没得说但问题也很明显。第一是 API 太重skin 层为了兼容老实时系统保留了大量历史包袱学习成本高。第二是 I-pipe 这个补丁太大、太侵入它几乎改到了内核的每个角落内核迭代一次I-pipe 就要跟着大改一次。第三是调试困难双内核模式下实时核和 Linux 共用一套硬件资源出问题的时候很难判断是哪一侧的锅。Mercury 虽然不用 I-pipe但它的实时上限就是 PREEMPT_RT 的上限等于放弃了双内核带来的最坏情况保障。所以 Xenomai 3 其实是要么强大但难维护要么好维护但不强大的二选一。1.3 Xenomai 4 的选择EVL DovetailXenomai 4 的定位一句话概括保留双内核实时能力但把整个实现做薄。EVL core 是新一代实时内核核心它不再搞 Xn skin 那套多 API 兼容层而是提供一套紧凑的、偏 POSIX 风格的 C API叫 libevl。Dovetail 则是替代 I-pipe 的中断流水线补丁目标是让 Linux 内核同时具备 in-band带内和 out-of-band带外两级执行环境EVL 的实时线程跑在带外普通 Linux 跑在带内。说白了Xenomai 4 想解决的不是实时性不够——Cobalt 早就证明了实时性够——而是这套东西能不能活得更久、更容易跟上 Linux 内核的演进、更容易被新项目接受。Dovetail 的补丁量比 I-pipe 小一个数量级这是它的核心卖点。2. Dovetail中断流水线从总机接线员到快递分拣线2.1 I-pipe 为什么难用I-pipe 的名字是 Interrupt Pipeline思路是在硬件中断和 Linux 内核之间加一条管道。所有中断必须先进入管道头部由管道决定这个中断要发给哪个客户。这个客户可以是 Linux也可以是 Cobalt 的实时处理程序。问题在于I-pipe 的管道是串行结构而且为了维护优先级在实时处理程序运行期间整个管道都会被冻结——所有其他中断都被挡在管道外面。在实时任务密集的单核系统上这会拖慢 Linux 侧的中断响应在多核系统上跨 CPU 的中断和 IPI 也会挤在这条管道里。更麻烦的是I-pipe 对内核的改动近乎无孔不入。它需要拦截中断、syscall、page fault、信号等几乎所有内核入口点而且这些拦截点散布在各个架构的汇编代码里。每次 Linux 发布新内核I-pipe 的移植团队都要花费大量精力重新适配。这也直接导致 Xenomai 能跟踪的内核版本总是滞后。2.2 Dovetail 的两级流水out-of-band 与 in-bandDovetail 保留了管道这个概念但设计思路完全变了。如果让我用一个更直观的比喻它更像一条快递分拣线每个 CPU 上有一条中断流水线流水线上有两个分拣口第一个分拣口是 out-of-band带外第二个是 in-band带内。硬件中断到达后先经过带外分拣口。如果 EVL core 在这个中断号上注册了带外处理函数那就直接在这里处理完Linux 完全不知道这件事。如果没有带外处理函数中断就被放行到带内分拣口进入 Linux 正常的 request_irq 处理流程。这里最关键的一点是Dovetail 不是把 Linux 的中断处理包起来而是把 Linux 的内核入口分层化。带外代码运行的时候Linux 侧的中断、调度、软中断都可以被合理地推迟但 Dovetail 不会粗暴地屏蔽整个 CPU 的中断。它用一套经过精心设计的同步原语比如 per-CPU 的 pipeline state、oob stall 标志来保证两侧不会互相踩踏。2.3 Dovetail 不止管中断这是很多人忽略的一点Dovetail 拦截的不仅仅是 IRQ。为了使 EVL 线程能够真正带外运行它还必须处理 syscall 入口、page fault、信号、RCU 读锁等一连串内核事件。EVL 线程在带外运行时如果要访问用户态内存触发了缺页Dovetail 需要让这个缺页事件从带外降级到带内处理否则实时线程就可能带着锁去缺页造成不可控延迟。换句话说Dovetail 是一条横贯内核事件的流水线中断只是其中最频繁、最关键的乘客。也正因如此它的补丁虽然比 I-pipe 小但依然会触及 arch 底层的 entry 代码只是触达方式更集中、更模块化维护起来轻松很多。2.4 补丁量变化带来的实际收益我印象中I-pipe 针对一个内核版本的补丁经常是几万行级别而 Dovetail 的 core 补丁大概只有几千行。数字不用纠结重点是维护模型的改变I-pipe 是外科手术式地把一个完整子系统塞进内核Dovetail 则是加一层薄薄的分流器。这个差异直接影响你对内核版本的选择——Xenomai 4 跟进新内核的速度明显比 Xenomai 3 时代快这对要上新产品的人来说是实打实的利好。3. EVL核心的架构与编程模型从引经据典到够用就好3.1 EVL 在系统里的位置EVL core 是一个运行在内核态的实时核它的代码量比 Cobalt 小很多但职责非常明确管理带外线程的调度、时钟、定时器以及提供进程间通信原语。它不是一个独立操作系统它的存在高度依赖 LinuxEVL 线程的完整上下文虚拟内存、地址空间、打开的文件等依然是 Linux 的只是它的执行时机和调度由 EVL 的调度器接管。用一句直白的话讲Linux 提供身体EVL 接管大脑里跟时间最敏感的那部分决策。EVL core 本身是作为内核模块还是编译进内核取决于你用的构建方式。它对外暴露一个主设备节点我记得是 /dev/evl用户态程序通过 ioctl 与它通信。当然libevl 把这些细节都封装好了绝大多数时候你不需要直接碰 ioctl。3.2 线程模型与调度策略EVL 线程的创建方式有两种一种是从普通 Linux 线程升级上来调用 evl_attach_self()让当前线程进入 EVL 的调度域另一种是直接调用 evl_create_thread() 创建新的 EVL 线程。无论哪种方式线程创建后都拥有一个名字这个名字在 /dev/evl 下会有对应的资源项方便调试。调度策略上EVL 提供几种SCHED_FIFO经典优先级抢占调度实时线程最常用的模式。SCHED_TP时间分区调度可以在多个实时任务之间固定分配 CPU 时间比例适合有混合关键性需求的项目。SCHED_QUOTA配额调度给一组任务设定带宽上限防止某个失控任务把 CPU 吃满。3.3 一个最小例子周期线程我最初跑通 Xenomai 4 是在 x86_64 的 NUC 上libevl 的版本大概是 0.9 左右。下面是我整理过的一个最小周期线程代码不追求绝对完整重点看模型的差别#include evl/evl.h #include evl/thread.h #include evl/timer.h #include errno.h #include stdio.h #include time.h static void rt_loop(void *arg) { struct timespec next; int ret; /* 让当前线程进入 EVL 调度域 */ evl_attach_self(rt-cycle); /* 读取 EVL 维护的时钟 */ evl_read_clock(CLOCK_MONOTONIC, next); for (;;) { next.tv_nsec 1000000; /* 1ms 周期 */ if (next.tv_nsec 1000000000L) { next.tv_sec 1; next.tv_nsec - 1000000000L; } /* 这里放真正的实时任务逻辑 */ evl_printf(tick at %ld.%06ld\n, next.tv_sec, next.tv_nsec); /* 睡眠到绝对时间点保证周期不漂移 */ ret evl_sleep_until(CLOCK_MONOTONIC, next); if (ret errno ! EINTR) break; } } int main(int argc, char *argv[]) { struct evl_thread *thread; int ret; ret evl_create_thread(thread, rt-cycle, rt_loop, NULL, 90, 0); if (ret 0) { perror(evl_create_thread); return 1; } pause(); return 0; }这里有一个和普通 pthread 编程很不一样的点周期线程使用绝对时间点来安排下一次唤醒所以即使某个周期内处理逻辑偶尔多花了一点时间调度器也会按照绝对时间线把线程推到下一个理论上应该执行的时刻不会像相对延时那样越漂越远。3.4 IPC 原语的实际用法周期线程只是第一步真实项目里线程之间必须交换数据、同步状态。EVL 提供的 IPC 对象虽然数量不多但都踩在实时系统的痛点上semaphore经典计数信号量用于任务间发信号的场景。mutex优先级继承互斥锁。注意这里不带条件变量因为条件变量在实时场景下容易被误用。monitorEVL 特有的事件化互斥量可以理解成 mutex condition variable broadcast 的组合用于一对多唤醒。event flag事件标志组等待某个位掩码组合适合状态机同步。xbuf带外无锁环形缓冲区专门用来在实时线程和非实时线程之间搬大数据是采集数据交给 Linux 去分析这个场景的主力。我自己的经验是EVL 的 API 名称和 POSIX 很像但语义会对齐到实时场景。比如 mutex 的等待方可以指定超时避免实时任务被锁死了不报错。这种少而精准的 IPC 集合第一眼可能觉得不够用实际用了以后发现比 Xenomai 3 的一大堆 API 更容易设计出清晰的结构。4. 搭建Xenomai 4环境从内核补丁到跑通延迟测试4.1 选内核版本与拿补丁想跑 Xenomai 4首先得有一个打上 Dovetail 补丁的 Linux 内核。Dovetail 会针对特定内核版本发布补丁不要拿任意版本硬套。我当时选择的是项目维护的 dovetail 分支比如基于 5.15 或 6.x 的长期稳定分支用 git 直接检出比手动打 patch 省心太多。步骤大致是准备好一个支持 Dovetail 的 Linux 源码树。把 EVL core 的代码编进去或编成模块。编译内核启动后确认 /proc 下能看到 Dovetail 和 EVL 相关节点。编译安装 libevl编译测试工具。libevl 的构建方式很常规我用的版本是 meson 工程configure 之后 make install 就可以测试工具会在 build 目录下生成。如果你只是想快速评估官方仓库里有现成的容器或脚本模板但我的建议是自己在目标板子上完整走一遍编译流程因为交叉编译和容器里碰到的工具链问题迟早会暴露。4.2 关键内核配置项这里放一张我自己写进项目文档的配置对照表省得你踩我踩过的重复配置配置项推荐值说明CONFIG_DOVETAILy启用 Dovetail 中断流水线Xenomai 4 的前提CONFIG_EVLy编译 EVL core如果做模块则后续 modprobeCONFIG_HZ_PERIODICn周期 tick 会干扰带外实时线程尽量关闭CONFIG_HZ_1000y在部分内核上可提高内带调度精度视版本而定CONFIG_NO_HZ_FULLy配合启动参数 nohz_full 使用减少无谓 tickCONFIG_RCU_NOCB_CPUy将 RCU 回调转移到非实时 CPUCONFIG_PREEMPTy普通 Linux 侧保持低延迟CONFIG_SMPy多核情况下 EVL 也可以多核并行注意 CONFIG_EVL 如果编成模块启动后要记得 modprobe evl否则 /dev/evl 不会出现。我一开始就栽在这上面模块没加载程序一直报找不到设备节点。4.3 启动参数与 CPU 隔离实时延迟最关键的因素不在内核编译选项而在启动参数。你要把实时任务所在的 CPU 从 Linux 的日常事务里摘出去。我常用的启动参数组合isolcpus2,3 nohz_full2,3 rcu_nocbs2,3 irqaffinity0,1含义是CPU 2、3 留给 EVL 实时线程Linux 的普通进程和调度器 tick 尽量别上去RCU 回调和设备中断都赶去 CPU 0、1。IRQ 是最后一个要点检查你实时线程要用的外设中断落在哪个 CPU 上把中断亲和性手动设置到非实时 CPU。我见过很多人在这一步翻车内核编译全对了latency 测试却总是会有几十微秒的尖刺最后一查网卡的中断和实时线程挤在同一个核上网卡一忙就是一片尖峰。4.4 跑通第一个延迟测试环境起来后先跑 libevl 自带的 latency 工具。以我手头版本为例命令类似cd build/tests ./latency -p 1000 --cpu 2-p 1000 表示 1ms 周期--cpu 2 把测试线程绑到 CPU 2。程序会周期性触发一个任务记录实际触发时刻相对理论时刻的偏差最后打印最小值、平均值和最大值。第一次跑通的时候我的 NUC 上平均延迟大概是 1 到 2 微秒最大延迟 6 微秒左右。不要急着和网上那些 0.x 微秒的数据比硬件的体质、BIOS 设置、中断负载不同结果差一个数量级都正常。关键看两个指标一是最大值是否稳定不漂移二是是否频繁出现尖刺。5. 从Cobalt迁移到EVL的实测经验三个坑与一个建议5.1 坑一EVL 线程里调用 Linux 服务会破功双内核方案有个冷酷的物理事实EVL 线程在带外运行它的身体还是用户态程序一旦调用 open、printf、malloc 触页、futex 这类普通 Linux 服务就会经历一次 in-band 迁移也就是从带外掉进带内。我最初跑周期线程习惯性用 printf 打印调试信息结果延迟表上立刻出现几百微秒的毛刺。后来改成 evl_printf它直接走带外输出缓冲才恢复正常。malloc 也一样第一次访问新分配内存会触发缺页这个缺页要被 Dovetail 转给 Linux 处理延迟不可控。所以实时任务里如果要开缓冲区务必在进入实时循环前提前分配并且触碰一遍prefault。5.2 坑二CPU 与中断不隔离平均值好看、最大值爆炸我第二次搭环境时偷懒没有配置 isolcpus只开了内核选项latency 的平均值居然也还行但最大值动不动就 50 微秒以上。后来用 perf 查了一下是 kworker 和调度器的 housekeeping 扰动了 CPU。把 CPU 隔离配置加上后最大延迟直接回到个位数微秒。还有一次问题是板卡上某个驱动的轮询线程频繁产生 IPIIPI 会穿过 Dovetail 流水线如果在实时 CPU 上被处理也会造成抖动。这种情况下要把 IRQ affinity 和相关线程的 CPU 亲和性都调到非实时核。所以我的固定流程是先看 /proc/interrupts 统计每个中断都跑到哪个 CPU再决定隔离哪些 CPU最后才跑延迟测试。跳过这一步的人和当年 I-pipe 时代一样依然会翻车。5.3 坑三Dovetail 和 PREEMPT_RT 不是同一个东西很多刚接触 Xenomai 4 的朋友会问Dovetail 是不是就是 PREEMPT_RT 的替代品真不是。PREEMPT_RT 是让 Linux 内核自身变为可抢占Dovetail 是给 Linux 加一条带外执行通道。两者面向的问题域不一样。如果你的项目是绝大多数时候延迟不错、偶尔一次 1ms 也能忍那是软实时PREEMPT_RT 完全够用没必要上双内核。如果你的项目要求硬化的最坏情况边界比如抓拍系统、工业总线主站、飞行控制器那才需要 EVL 这种带外通道。我不建议为了拉低一个内部延迟指标而无脑上 Xenomai 4双内核带来的调试复杂度是实打实的。先量化需求再做技术选型这比任何内核补丁都重要。5.4 我的建议用最小实时核的视角重新设计程序最后说一点方法论。Cobalt 时代很多人习惯了把整个应用塞进实时核因为 API 齐全、什么都能干。EVL 的设计哲学明显是反过来的实时核只管最敏感的那一小段逻辑其他事情全部交给普通 Linux 线程。我现在的做法是一个高优先级周期线程做采样和控制通过 xbuf 把原始数据发给普通线程做分析、显示、日志实时线程里不碰锁、不做 IO、不调用任何可能缺页的函数所有配置在启动时一次性初始化好。这样写出来的程序不仅延迟稳定而且排查问题的时候目标非常小——要么是实时线程的问题要么是 Linux 侧的接口问题不需要像以前那样在两套 API 之间来回猜。另外还有一个小技巧调试阶段给实时线程加上 deadline 工具或者在时间戳上打上序列号看最大延迟对应的代码位置比直接看平均值有用得多。我自己就是靠这个在驱动中断亲和性配错的时候快速锁定了是网卡 IRQ 而不是调度器的问题。
返回列表