Xenomai 实时系统系列(二):实时核心如何接进 Linux?

上一章说到两条路线:PREEMPT_RT 改造 Linux,Xenomai 3 Cobalt 增加实时协同机制。本章沿着 Cobalt 的路径往下走,看看一个硬件事件如何被分流、一个实时线程如何与 Linux 交接。

1. 先看全局:一次中断要经过哪些人?

把 CPU 想象成一个有两个入口的车站。

  • 带内(In-band)是 Linux 的普通站台:文件系统、网络协议栈、普通进程都在这里工作。
  • 带外(Out-of-band,OOB)是实时站台:实时核心优先处理时间敏感的事件。

Xenomai 3 Cobalt 是双内核配置里的实时协同核心。它处理实时中断、实时线程调度和其他时间关键活动,并拥有高于 Linux 原生活动的优先级。Cobalt 官方说明

这里的“双内核”说的是两套协同的软件机制,不是两颗 CPU。一个 CPU 也可以有两个执行域;多个 CPU 也可以同时运行带内和带外任务。

一次硬件中断在带内与带外之间的分流

图 1:Dovetail 提供管线,Cobalt 和 Linux 位于不同执行域。不是每个设备中断都能完全在带外完成。

当硬件发出中断时,事件先进入中断管线。实时核心注册的带外处理有机会先运行;如果事件属于 Linux 或需要 Linux 服务,再沿着管线交给带内处理。

这就是第一条重要规律:

带外优先描述处理顺序,不能把它理解成“所有中断都绕过 Linux”。

2. Dovetail 到底是什么?

Dovetail 不是又一个和 Linux 抢 CPU 的调度器。它更像一条精心设计的“接缝”,把 Linux 内核中的中断入口、调度点和上下文切换,与实时协同核心需要的执行路径接起来。

Xenomai 3 的构建说明把 Dovetail 称为 interrupt pipeline,并通过 prepare-kernel.sh 将对应补丁应用到 Linux 树中。Xenomai 安装说明

Linux、Dovetail 与 Cobalt 的分层关系

图 2:Dovetail 是中间的管线和协同机制,不是实时调度器本身。

从职责上拆开看:

  1. Linux仍然是完整的通用操作系统,拥有普通任务、文件系统、网络协议栈和大量驱动。
  2. Dovetail提供执行域之间的连接、事件分流和交接所需的内核机制。
  3. Cobalt使用这条连接建立实时调度、实时定时器和实时服务。

较早的 Xenomai 3 平台可能使用 I-pipe。资料中看到 Dovetail 还是 I-pipe,先核对内核和 Xenomai 版本,不要把两者画成永远同时存在的两层。

3. 一次中断是怎样把 CPU 交给实时任务的?

你可能会问:Linux 线程正在临界区里改数据,难道也能把 CPU 抢走?

答案是:普通 Linux 临界区通常挡不住 Cobalt;真正屏蔽硬件中断的区间,会让依赖这些中断的实时响应一起等待。 “保护区”要看它用什么机制保护、保护谁,不能只凭“临界区”三个字下结论。

以下以 Xenomai 3 Cobalt + Dovetail、同一个 CPU 为例,并假定相应 IRQ 已配置为带外处理。I-pipe 有类似设计,但接口细节要按对应版本核对。本文引用 Dovetail 文档解释共同的管线机制,不把其中的 EVL API 当作 Cobalt API。

3.1 从硬件事件到线程运行,中间还有几步

  1. Linux 正在执行。 当前可能是用户进程进入内核后的代码、内核线程,也可能是带内中断处理程序。
  2. CPU 接收中断。 前提是该中断没有被设备、中断控制器或 CPU 的硬件屏蔽机制挡住;管线将可投递的带外事件交给带外处理程序。
  3. 处理程序唤醒实时线程。 例如确认定时器事件,把等待下一周期的线程 R 置为就绪。此时“就绪”还不等于“已经开始执行”。
  4. Cobalt 做调度选择。 在退出相应中断上下文、允许调度时,选择该 CPU 上应当运行的实时线程并切换。这个决定不需要普通 Linux 调度器先允许带内任务抢占。Dovetail 中断进入/退出机制
  5. 带内执行暂时停在原处。 执行现场和持锁状态保留。实时线程可运行,也可等待实时同步对象、等待下一周期,或被更高优先级的实时工作抢占。
  6. 带外工作让出后,带内继续。 当该 CPU 上没有需要优先执行的带外工作时,恢复之前的 Linux 执行;后续普通 Linux 任务之间怎么切换,仍由 Linux 调度器决定。Dovetail 协同调度

Cobalt 从就绪任务中选择要运行的实时线程

图 3:关键动作不是“Linux 主动让出 CPU”,而是中断管线暂停带内阶段并优先运行带外工作。图中省略了嵌套中断、跨核 IPI 和驱动细节。

3.2 “保护区”究竟保护了什么

锁保证约定参与者之间的互斥,并不天然保证“持锁期间 CPU 一刻也不能离开”。例如,普通 mutex 的持有者本来就可以被 Linux 调度出去,锁仍由它持有。

当前保护机制 保护的范围 对当前 CPU 上带外响应的影响
持有普通 Linux mutex 使用该锁的线程之间互斥 持锁本身不阻止 Cobalt 执行
preempt_disable() 禁止当前任务被普通 Linux 调度抢占 本身不屏蔽带外中断
普通 Linux spinlock_t / raw_spinlock_t 按其配置语义保护带内代码 不因此自动获得跨域保护
local_irq_disable() / local_irq_save() 在 Dovetail 下暂停本 CPU 的带内 IRQ 投递 带外 IRQ 仍可投递
普通 Linux 锁的 spin_lock_irqsave() 普通自旋锁语义加上相应带内 IRQ 保护 不能据此认为带外也被关掉
hard_local_irq_disable() / hard_local_irq_save() 真正修改本 CPU 的硬件中断屏蔽状态 被屏蔽的可屏蔽 IRQ 必须等待,带外响应也受影响

表中前提是没有额外的硬件屏蔽或带外停滞。常规 local_irq_*()hard_local_irq_*() 的区别是 Dovetail 明确规定的接口语义。Dovetail 中断控制 API

还要记住:raw_spinlock_t 的 “raw” 不等于 Dovetail 的 hard_spinlock_t 后者用于需要跨带内/带外串行化的底层路径;使用相应硬关中断保护时,也会把临界区时间带进 IRQ 延迟。这样的区间必须短且不能睡眠,不能把普通锁全部换成 hard 锁来“解决问题”。Dovetail 锁类型

3.3 Linux 已经关中断了,Cobalt 怎么还能运行

关键在于 Dovetail 把“CPU 接收硬件 IRQ”和“Linux 执行这个 IRQ 的处理程序”分开了。

Linux 调用普通 local_irq_disable() 后,带内投递被暂停;CPU 仍可以接收未被硬件屏蔽的 IRQ。带外事件可以先处理,属于 Linux 的事件则记入该 CPU 的待处理日志,等 Linux 恢复带内中断投递时再处理。Dovetail 虚拟关中断与延迟投递

假设 L 正在普通 spin_lock_irqsave() 保护的区域里:Cobalt 中断到来后暂停 L,唤醒并运行实时线程 R。等带外工作让出,L 接着改数据,最后自己解锁并恢复原来的中断状态。暂停期间,锁一直在 L 手里。

普通带内关中断与真正硬件关中断的 CPU 时间线对比

图 3A:上、下各表示一次独立的执行过程。上图可在 Linux 临界区中途运行带外工作;下图必须先等剩余的硬关中断区结束。图中长度不代表实测耗时。

举个纯示意的数字:假设某段真正的硬关中断区共持续 8 μs,中断在进入后第 3 μs 到来,那么光等这段代码结束,就还需要 5 μs;后面还有 IRQ 处理、唤醒和切换等开销。Cobalt 无法把这 5 μs“抢”回来。

也别把 disable_irq()、设备寄存器里的中断屏蔽,与 local_irq_disable() 混为一谈:如果事件在硬件端就被挡住,中断管线无法凭空处理它。带外阶段自身停滞、Cobalt 暂时禁止调度,也分别可能推迟带外处理或线程运行。“能响应中断”和“能马上切线程”是两个判断。

3.4 Linux 的数据改了一半,为什么不会乱

前提是:带外代码不擅自读取或修改那份尚未完成更新、只用普通 Linux 锁保护的数据。 Linux 的其他合规访问者仍受锁约束;Cobalt 暂停持锁者,并不会自动让锁失效。

反过来,如果 R 调用某个普通 Linux 内核函数,而这个函数要取 L 手里的锁,就可能出现下图的死锁。提高 R 的优先级没有帮助,因为真正能解锁的人正在被它挡着。Dovetail 带外调用约束与死锁示例

带外代码忙等同 CPU 上被暂停的 Linux 持锁者所形成的死锁

图 3B:这里特指带外代码忙等普通 Linux 锁的错误用法;不是说所有实时同步等待都会死锁。

跨域共享数据要单独设计同步方式:可以选经过验证的有界队列,或在适合的内核路径中使用极短的跨域硬锁保护;队列仍需处理并发、内存顺序和满队列策略。文件、日志和普通网络通常交给带内服务线程。仅仅给接口贴上“RTDM”或“无锁”的名字,还不能证明它有确定的完成时间。

多核也不是自动豁免:当前 CPU 的 Linux 持锁者被暂停后,其他 CPU 上等待同一把锁的带内代码仍可能等得更久。提高带外优先级,不会消除这些间接影响。

3.5 换成 PREEMPT_RT,临界区又会怎样

PREEMPT_RT 的实时线程由 Linux 自己调度。它把许多原来不可抢占的工作变得可调度,并让普通 spinlock_t 使用可睡眠、支持优先级继承的实现:高优先级线程需要同一把锁时,可以阻塞并提升锁主优先级,让锁主尽快完成。Linux 锁语义

但显式 preempt_disable()raw_spinlock_t 和真正关中断的底层区间仍可能延迟 Linux 实时线程。Cobalt 则通过带外执行跨过许多带内限制,同时要求实时路径遵守跨域访问规则;两者都要控制真正硬关中断区的长度。PREEMPT_RT 的剩余不可抢占路径

4. 带内和带外:一个线程可以换域吗?

可以,但换域应该是明确的工程决策。

在 Cobalt 双内核配置中,Xenomai 实时线程由 Cobalt 管理。Cobalt 的 POSIX 接口中,pthread_create() 可以创建由 Cobalt 核心管理的线程。Cobalt 线程管理

阻塞不等于切回 Linux。 等待 Cobalt 的实时信号量或下一周期,可以由 Cobalt 管理等待;普通 Linux 系统调用则可能把线程带到带内路径。官方标签 switch-primary 表示可能切到 primary(实时模式),switch-secondary 表示可能切到 secondary(Linux 模式)。某些可阻塞的 Cobalt 服务从 secondary 调用时,反而需要先切回 primary。具体行为以各 API 的标签和调用条件为准。API 服务标签

实时线程在带外和带内之间切换

图 4:不要把“带外”简单理解成“所有 API 都不能用”。关键是知道哪个调用可能阻塞、会切换到哪里。

这里有三个容易混淆的词:

  • 带外 / OOB:实时协同域,重点是时间敏感的处理;
  • 带内 / In-band:Linux 原生域,重点是通用能力;
  • primary / secondary mode:Xenomai 3 Cobalt 文档中描述线程执行状态的术语。阅读具体 API 时,以该 API 的服务标签为准。

所以,“实时线程调用了 write()”这句话还不够。要继续问:这是普通文件写入还是 RTDM 实时驱动调用?调用时线程在哪个执行域?路径上有没有不可控的阻塞?

5. 为什么要把文件、日志和普通网络搬出去?

严格实时循环最怕“看起来只是一行代码”的无限等待:磁盘设备没准备好、网络协议栈正在重试、内存页需要调入,或者普通锁被另一个任务长时间占用。

更稳妥的划分是:

  • 带外循环只做采样、控制计算和有界的设备交互;
  • 用固定容量的缓冲区交出结果;
  • 带内服务线程负责日志、文件、界面和普通网络;
  • 明确缓冲区满了以后是丢弃、覆盖、阻塞还是报警。

实时控制循环与 Linux 服务线程的边界

图 5:跨域通信本身也有开销,容量与满队列策略要写进设计,而不是留给“应该不会满”。

这条建议对 PREEMPT_RT 同样成立。PREEMPT_RT 可以让更多 Linux 内核路径可抢占、让许多中断在可调度线程中处理,但它不会给普通文件 I/O 自动附加硬实时上界。PREEMPT_RT 原理

6. 从源码到启动:每一层都要对齐

运行一个 Cobalt 应用,不是只把用户程序编译出来就结束了。至少要对齐以下五层:

  1. 实时应用使用的 API 和调度策略;
  2. libcobalt 等用户态支持库;
  3. 与内核版本匹配的 Xenomai Cobalt;
  4. 带 Dovetail(或对应 I-pipe)的 Linux 内核;
  5. 目标硬件、定时器和中断控制器。

Xenomai Cobalt 的构建与运行栈

图 6:任一层版本或配置不匹配,都可能表现为编译失败、启动失败或延迟异常。

Xenomai 安装脚本会根据选择的核心构建对应的用户空间库;Cobalt 和 Mercury 的库不是同一套运行模型。Xenomai 安装说明

因此排查问题时,先记录这些信息:

uname -a
cat /proc/xenomai/version
cat /proc/cmdline
cat /sys/devices/system/cpu/online

再确认应用实际链接到哪套库、线程使用哪种调度策略、驱动走的是哪条路径。不要只看“程序能启动”,启动成功只说明接口能连上,不说明端到端延迟已经满足预算。

7. 用一个最小任务理解“实时接口”

下面的伪代码只表达职责边界,不能直接当作某个 Xenomai 版本的完整示例:

/* 带外:固定周期、预分配内存、使用有界实时接口 */
for (;;) {
    wait_period();
    sample_sensor();
    control_output = run_controller();
    write_rtdm_output(control_output);
    publish_to_bounded_buffer(control_output);
}

/* 带内:日志、文件和普通网络 */
for (;;) {
    result = read_bounded_buffer();
    write_log_file(result);
    send_regular_network_message(result);
}

真实代码还需要补上错误处理、退出策略、优先级、CPU 亲和性、内存锁定和设备关闭流程。write_rtdm_output() 也只是表达“经过验证的实时设备接口”,并不意味着任意驱动的 write() 都具有相同语义。

8. 动手检查:先确认域,再看延迟

可以把检查分成三步:

第一步:确认构建模型

cat /proc/xenomai/version
uname -a
ldd ./your_rt_app | grep -E 'cobalt|xenomai'

确认是 Cobalt 还是 Mercury,以及用户程序是否链接到预期的库。

第二步:确认线程行为

检查线程的调度策略、优先级和 CPU 亲和性。Cobalt 用户态线程的 API 服务标签还应作为“调用是否可能换域”的线索,而不是把所有 POSIX 调用都默认当作实时安全。

第三步:分别测量唤醒和端到端路径

latency 可以先观察周期唤醒和调度偏差;实际项目还需要把传感器、驱动、算法和输出端串起来测量。一次测试的 Max 只是本轮观测到的最大值,不能直接当作数学意义上的长期上界。

本章小结

  • Dovetail 是连接 Linux 与实时协同核心的中断管线和协同机制。
  • Cobalt 负责双内核配置中的实时调度和时间关键活动。
  • Linux 处于普通临界区时,带外阶段通常仍可运行;Linux 保持锁和执行现场,恢复后再继续,但带外代码不能反过来等待这个 Linux 锁。
  • 带内与带外是执行域;primary / secondary 是具体 API 文档里的线程模式术语。
  • 实时循环要把不可控的文件、日志和普通网络操作隔离出去。
  • Xenomai 的版本、补丁、内核、用户库和驱动必须作为一个整体验证。

下一章继续往下:Cobalt 实时线程是怎样创建、唤醒、阻塞和切换的?我们会从调度策略和优先级队列开始。

posted @ 2026-09-19 17:53  大米饭1  阅读(7)  评论(0)    收藏  举报