Xenomai Cobalt 调度全貌:从中断到线程切换的微秒级之旅

Xenomai Cobalt + Linux 双内核调度深度解析

面向有 Linux 内核调度基础的 Xenomai 初学者。目标:建立系统性认知框架,遇到问题时能按图索骥定位。


一、架构全景:为什么需要双内核

Linux 内核即使打了 PREEMPT_RT 补丁,仍无法保证微秒级的确定性延迟------因为内核中存在不可抢占的临界区、中断延迟不确定、softirq/workqueue 排队等问题。

对此,Xenomai 的解决方案是在 Linux 之上插入一个独立的实时内核 Cobalt,通过 I-pipe(Interrupt Pipeline)对 Linux 拥有绝对的中断优先权:

复制代码
硬件中断
    │
    ▼
┌──────────────────────────────────────┐
│          I-pipe 中断管道              │
│                                      │
│  ┌────────────────────────────────┐  │
│  │ Cobalt 实时域(Head Domain)    │  │  ← 高优先级,先看到所有中断
│  │ xnsched_realtime_domain        │  │
│  └────────────────────────────────┘  │
│                │                     │
│                │ 无实时任务时放行      │
│                ▼                     │
│  ┌────────────────────────────────┐  │
│  │ Linux 根域(Root Domain)       │  │  ← 低优先级,只能看到被放行的中断
│  │ ipipe_root                     │  │
│  └────────────────────────────────┘  │
└──────────────────────────────────────┘

核心认知 :"域"不是两个独立的 CPU 执行流,而是同一个 CPU 上的两个中断优先级层。Cobalt 不是从 Linux"借"CPU 时间,而是在每次中断到来时优先"插队"几微秒处理,处理完后 CPU 自然回到 Linux,但在有 Cobalt 任务需要执行的时候,要等到 Cobalt 任务执行完成后,才会返回 Linux,设计不良的 Cobalt 任务会导致 Linux 任务长期得不到执行。


二、Cobalt 调度器内部机制

2.1 线程状态机

Cobalt 用位掩码(thread->state)表示线程状态,多个阻塞位可叠加:

复制代码
XNDORMANT ──(start)──→ XNREADY ──(pick_next)──→ running
                          ↑                        │
                  resume (所有阻塞位清除)            │ suspend(mask)
                          ↑                        ▼
                        blocked ←──────────────── (BLOCK_BITS)

关键规则:

  • XNTHREAD_BLOCK_BITS = XNSUSP|XNPEND|XNDELAY|XNDORMANT|XNRELAX|...
  • 任意一位被置位,线程就不可运行
  • 所有位清除后,线程才回到 XNREADY

常见的阻塞位含义:

标志 含义 设置时机 清除时机
XNPEND 等待同步对象 mutex_lock/sem_wait 对象被释放
XNDELAY 延时等待 sleep/带超时的等待 定时器到期
XNSUSP 显式挂起 suspend() resume()
XNRELAX 在 Linux 域中 relax(迁移到 Linux) harden(迁移回 Cobalt)
XNDORMANT 未启动 init() start()

2.2 调度类与线程选择

调度策略通过调度类链表实现多态(按权重从高到低):

复制代码
rt(1024) → quota → sporadic → tp → weak → idle(0)

xnsched_pick_next() 从链表头开始调用各类的 sched_pick(),第一个返回非 NULL 的线程即为选中者。idle 类始终返回 rootcb(Linux 域代理),保证不会返回 NULL。

c 复制代码
// kernel/xenomai/sched.c
struct xnthread *xnsched_pick_next(struct xnsched *sched)
{
    // 当前线程持调度锁 → 拒绝抢占
    if (curr->lock_count > 0) {
        xnsched_set_self_resched(sched);
        return curr;
    }
    // 当前线程未阻塞 → 放回运行队列
    if (!xnthread_test_state(curr, XNREADY)) {
        xnsched_requeue(curr);  // 放回同优先级组头部(LIFO)
        xnthread_set_state(curr, XNREADY);
    }
    // 遍历调度类,选最高优先级线程
    for_each_xnsched_class(p) {
        thread = p->sched_pick(sched);
        if (thread) {
            set_thread_running(sched, thread);
            return thread;
        }
    }
    ...
}

2.3 懒惰重调度(Lazy Rescheduling)

Cobalt 不在每次状态变更时立即切换,而是累积标记,统一处理

复制代码
┌─────────── 生产端(只标记)───────────┐      ┌────── 消费端(真正切换)──────┐
│ xnthread_resume()                   │     │ 中断退出(inesting→0)        │
│ xnsched_set_policy()                │────→│ 线程主动阻塞(suspend)         │
│ xnsched_putback() (RR到期)           │  ↑  │ 调度锁释放(unlock)            │
│ xnsched_migrate()                   │  │  │ SMP IPI                      │
└─────────────────────────────────────┘  │  └──────────────────────────────┘
         设置 XNRESCHED 标志 ─────────────┘     检查并清除 XNRESCHED → pick_next → switch

懒惰重调度带来的好处是:一次 xnsynch_flush 唤醒 5 个线程,只做一次 pick_next 和一次上下文切换。

2.4 调度锁

c 复制代码
curr->lock_count++;   // xnsched_lock() --- 禁止被抢占
curr->lock_count--;   // xnsched_unlock() --- lock_count→0 时立即 xnsched_run()

语义:禁止被抢占,但允许主动阻塞。持锁线程阻塞时 lock_count 保留,恢复后继续有效。


三、Shadow Thread 模型

3.1 核心概念

每个用户态 RT 线程同时拥有两个身份:

复制代码
┌─────────── 一个用户态 RT 线程 ──────────────┐
│                                           │
│  Cobalt 身份            Linux 身份         │
│  ┌──────────────┐      ┌───────────────┐  │
│  │ xnthread     │◄────►│ task_struct   │  │
│  │ - state      │      │ - mm (地址空间)│  │
│  │ - cprio      │      │ - files       │  │
│  │ - tcb        │      │ - signal      │  │
│  └──────────────┘      └───────────────┘  │
│         共享同一套 CPU 寄存器上下文          │
└───────────────────────────────────────────┘

3.2 双域运行模式

同一时刻只有一个调度器管辖该线程:

复制代码
┌──── Primary Mode(Cobalt 调度)────┐
│  由 xnsched_run()/pick_next 调度   │
│  享受微秒级确定性延迟                │
│  不能调用 Linux 阻塞服务            │
└──────────┬────────────────┬──────┘
           │ relax           │ harden
           │ (阻塞Linux调用)  │ (Xenomai syscall)
           ▼                 │
┌──── Secondary Mode(Linux 调度)───┐
│  由 Linux schedule() 调度          │
│  可以调用所有 Linux 服务            │
│  不享受实时保证                     │
└──────────────────────────────────┘

切换原理

  • Harden :清除 XNRELAX → xnthread 变为可运行 → Cobalt 可调度它
  • Relax :设置 XNRELAX + post_wakeup(task) → Cobalt 侧阻塞,Linux 侧唤醒

为什么要这么设计呢?为什么不直接用内核线程?有以下一些原因:

  1. 地址空间:用户态 RT 线程需要自己的 mm_struct,只有 Linux task 能提供
  2. 信号:POSIX 信号语义需要 Linux 基础设施
  3. 文件/网络:open()/read()/write() 都是 Linux 服务
  4. 调试:ptrace、/proc/pid/、gdb 都依赖 Linux task

Shadow 线程让 RT 线程在需要实时性时跑在 Cobalt 域,需要 Linux 服务时透明地切回 Linux 域,对应用程序完全透明。

3.3 创建过程

复制代码
用户:pthread_create(&tid, SCHED_FIFO/80, worker, arg)
    │
    ▼ libcobalt 拦截(--wrap 链接器机制)
    │
    ├─ ① Linux clone() → 创建普通 Linux 线程(有 mm、PID)
    │
    └─ ② 新线程启动后,发起 Cobalt 系统调用:
         ├─ xnthread_init() → 创建 Cobalt 身份
         ├─ cobalt_map_user() → 绑定 shadow 关系
         └─ xnthread_harden() → 迁移到 Cobalt 域
              → 线程现在由 Cobalt 调度,用户代码开始执行

四、中断与定时器如何驱动调度

4.1 时钟中断路径

复制代码
硬件定时器中断
    │
    ▼ I-pipe → dispatch_irq_head()
    ▼ xnintr_core_clock_handler()        ← Cobalt 的时钟中断处理
    │
    ├─ xnclock_tick(&nkclock)             ← 扫描到期定时器
    │    ├─ htimer 到期 → 设 XNHTICK      (有 host tick 待转发给 Linux)
    │    ├─ rtimer 到期 → timeout_handler → xnthread_resume (清 XNDELAY)
    │    ├─ ptimer 到期 → periodic_handler → xnthread_resume
    │    └─ rrbtimer 到期 → roundrobin_handler → xnsched_putback (RR 轮转)
    │
    └─ 中断退出(inesting→0) → xnsched_run()
         └─ pick_next → 选中被唤醒的最高优先级线程 → switch_context

4.2 设备中断路径

复制代码
硬件设备中断(如 CAN/SPI/GPIO)
    │
    ▼ I-pipe → dispatch_irq_head()
    ▼ xnintr_irq_handler()
    │
    ├─ intr->isr(intr)                   ← RTDM 驱动的 ISR
    │    └─ rtdm_event_signal(&event)    → xnthread_resume(thread, XNPEND)
    │
    └─ 中断退出 → xnsched_run()
         └─ 立即切到被唤醒的 RT 线程

延迟:设备中断 → RT 线程恢复执行 ≈ 几微秒(确定性)

4.3 Host Tick 转发

Cobalt 接管硬件定时器后,Linux 不再直接收到时钟中断。Cobalt 通过仿真转发:

c 复制代码
// htimer 到期时:
sched->lflags |= XNHTICK;  // 标记有 tick 待转发

// 切到 rootcb 时或中断退出时 curr==rootcb:
xnintr_host_tick(sched):
    ipipe_post_irq_root(HOST_TICK_IRQ);  // 放入 Linux pending 队列

// Linux 在下一个 local_irq_enable() 时消费:
__ipipe_sync_stage() → Linux tick handler → Linux schedule()

4.4 Oneshot 定时器模型

Cobalt 不使用周期性 tick,而是 oneshot 模式------每次只编程到最近的下一个到期时间:

复制代码
定时器队列:[100ns后] [500ns后] [2ms后]
                ↑
       硬件定时器编程为 100ns

100ns 后中断触发 → 处理第一个 → 重编程为 500ns → ...

好处:空闲时不产生无意义的 tick 中断,减少功耗和 jitter。


五、核心调度循环 ___xnsched_run()

这是整个双内核调度的心脏:

c 复制代码
// kernel/xenomai/sched.c
int ___xnsched_run(struct xnsched *sched)
{
    xnlock_get_irqsave(&nklock, s);

    curr = sched->curr;

reschedule:
    if (!test_resched(sched))   // 检查 XNRESCHED,发 SMP IPI
        goto out;               // 无需重调度 → 直接返回

    next = xnsched_pick_next(sched);  // 选择最高优先级线程
    if (next == curr) {
        // 仍是当前线程(可能是 rootcb)
        if (XNROOT && XNHTICK)
            xnintr_host_tick(sched);  // 转发 host tick
        goto out;
    }

    // ---- 需要切换 ----
    prev = curr;
    WRITE_ONCE(sched->curr, next);

    if (prev == rootcb)
        leave_root(prev);       // 保存 Linux 上下文,启动看门狗
    else if (next == rootcb)
        enter_root(next);       // 转发 host tick,停止看门狗

    switch_context(sched, prev, next);  // 真正的寄存器切换

    // ---- 以下代码在 next 的上下文中执行 ----
    if (shadow && ipipe_root_p)
        goto shadow_epilogue;   // relax 导致的域迁移收尾

    // 正常路径:完成切换后清理
    sched = xnsched_finish_unlocked_switch(sched);
    curr = sched->curr;

out:
    xnlock_put_irqrestore(&nklock, s);
    return switched;
}

六、典型场景走读

场景 A:RT 线程周期性唤醒

复制代码
clock_nanosleep(1ms)
    → suspend(XNDELAY) + 启动 rtimer
    → pick_next → rootcb → Linux 运行
    ...
[1ms 后] 定时器中断
    → xnclock_tick → rtimer 到期 → resume(XNDELAY) → set_resched
    → 中断退出 → xnsched_run → pick_next → RT 线程
    → leave_root + switch_context → RT 线程恢复执行(延迟 ≈ 几μs)

场景 B:设备中断唤醒 RT 线程

复制代码
RT 线程阻塞在 rtdm_event_wait(XNPEND)
    ...
[设备中断]
    → ISR: rtdm_event_signal → resume(XNPEND) → set_resched
    → 中断退出 → xnsched_run → pick_next → RT 线程
    → leave_root + switch_context → RT 线程处理数据(延迟 ≈ 几μs)

场景 C:RT 线程阻塞,Linux 恢复

复制代码
RT 线程调用 mutex_lock(锁被占)
    → suspend(XNPEND) → set_resched
    → xnsched_run → pick_next → rootcb
    → enter_root + switch_context → Linux 恢复执行
    → __ipipe_sync_stage → Linux 消费 pending tick → Linux schedule()

七、关键数据结构速查

per-CPU 调度器(struct xnsched

c 复制代码
struct xnsched {
    unsigned long status;        // XNRESCHED / XNINSW
    unsigned long lflags;        // XNHTICK / XNINIRQ / XNIDLE
    struct xnthread *curr;       // 当前运行线程
    struct xnsched_rt rt;        // RT 类运行队列
    struct xnthread rootcb;      // Linux 域代理(idle 优先级)
    struct xntimer htimer;       // host tick 仿真定时器
    struct xntimer rrbtimer;     // round-robin 时间片定时器
    struct xntimer wdtimer;      // 看门狗定时器
    volatile unsigned inesting;  // 中断嵌套计数
};

线程控制块(struct xnthread

c 复制代码
struct xnthread {
    __u32 state;                 // 线程状态位掩码
    struct xnsched *sched;       // 所属 per-CPU 调度器
    struct xnsched_class *sched_class;  // 当前调度类
    int bprio;                   // 基础优先级
    int cprio;                   // 当前有效优先级(含 PI/PP 提升)
    int lock_count;              // 调度锁嵌套计数
    struct xntimer rtimer;       // 等待超时定时器
    struct xntimer ptimer;       // 周期定时器
    xnticks_t rrperiod;          // RR 时间片(纳秒)
    struct xnsynch *wchan;       // 当前等待的同步对象
};

八、调试与问题定位指南

常见问题定位思路

现象 可能原因 排查方向
RT 线程响应延迟偶尔飙高 线程误入 secondary mode 检查是否调用了 Linux 阻塞服务;开启 XNWARN
RT 线程永远不运行 更高优先级线程未阻塞 检查所有高优先级线程的 sleep/wait 是否正确
Linux 卡死/无响应 RT 线程独占 CPU 开启看门狗;检查 RT 线程是否有 sleep 路径
看门狗频繁触发 RT 任务执行时间超预期 分析任务执行时间,排查死循环或优先级反转
系统启动后 Cobalt 不工作 命令行 xenomai.state=disabled 检查 /proc/xenomai 是否存在

关键调试文件

复制代码
/proc/xenomai/sched/threads  --- 所有 Cobalt 线程状态
/proc/xenomai/sched/stat     --- 线程调度统计(上下文切换次数、模式切换次数)
/proc/xenomai/sched/acct     --- 执行时间统计
/proc/xenomai/irq            --- Cobalt 域中断统计
/proc/xenomai/clock/coreclk  --- 时钟信息

SIGDEBUG 信号

当线程设置了 XNWARN 标志时,以下异常情况会触发 SIGDEBUG 信号:

  • SIGDEBUG_MIGRATE_SIGNAL:因收到信号被迫 relax
  • SIGDEBUG_MIGRATE_SYSCALL:调用了非实时系统调用
  • SIGDEBUG_WATCHDOG:看门狗超时
  • SIGDEBUG_LOCK_BREAK:持调度锁时尝试阻塞

在应用中注册 SIGDEBUG handler 可以帮助发现意外的模式切换。


九、关键源码文件索引

文件 内容
kernel/xenomai/init.c Cobalt 启动入口 xenomai_init()
kernel/xenomai/sched.c 调度核心:___xnsched_run()、类注册、pick_next
kernel/xenomai/thread.c 线程生命周期:harden、relax、suspend、resume
kernel/xenomai/intr.c 中断处理:时钟中断、设备中断、host tick 转发
kernel/xenomai/clock.c xnclock_tick():定时器扫描与触发
kernel/xenomai/timer.c 定时器管理:grab_hardware、start/stop
kernel/xenomai/synch.c 同步对象:mutex、sem 的等待/唤醒、PI 协议
kernel/ipipe/core.c I-pipe:中断分发、域管理、__ipipe_dispatch_irq
kernel/ipipe/timer.c I-pipe 定时器:ipipe_timer_startgrab_timer
include/xenomai/cobalt/kernel/sched.h struct xnschedstruct xnsched_class
include/xenomai/cobalt/kernel/thread.h struct xnthread
include/xenomai/cobalt/uapi/kernel/thread.h 线程状态标志位定义

十、总结

Cobalt 通过 I-pipe 劫持硬件中断获得绝对优先权,用 oneshot 定时器和事件驱动模型实现微秒级确定性调度。每个 RT 线程是 Linux task 的"影子"(shadow),在 Cobalt 域(实时)和 Linux 域(通用服务)之间透明切换。整套机制的核心就是:中断到来 → Cobalt 先看 → 唤醒 RT 线程 → 中断退出时立即切换------从中断到线程恢复,全程确定性,不超过几微秒。