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 侧唤醒
为什么要这么设计呢?为什么不直接用内核线程?有以下一些原因:
- 地址空间:用户态 RT 线程需要自己的 mm_struct,只有 Linux task 能提供
- 信号:POSIX 信号语义需要 Linux 基础设施
- 文件/网络:open()/read()/write() 都是 Linux 服务
- 调试: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:因收到信号被迫 relaxSIGDEBUG_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_start、grab_timer |
include/xenomai/cobalt/kernel/sched.h |
struct xnsched、struct 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 线程 → 中断退出时立即切换------从中断到线程恢复,全程确定性,不超过几微秒。