# Cobalt抢占式和优先级调度

### Cobalt 抢占式和优先级调度

`xkernel` 项目是一个开源的 RTOS-to-Linux 可移植框架，遵循 Creative Commons BY-SA 3.0 和 GPLv2 许可证，有两种形式：

- 作为经过补丁处理的 `Linux` 的共核/实时扩展（RTE），代号为 `Cobalt`。
- 作为本地 `Linux`（包括 `PREEMPT-RT`）的库，代号为 `Mercury`。它旨在 3.0 分支中既能作为共核使用，也能在 `PREEMPT_RT` 上运行。

`xkernel` 项目将一个名为 `Cobalt` 核心的实时内核合并到了 `Linux` 内核中，并在内核空间中共存。`Cobalt` 核心中的中断和线程优先级比 `Linux` 内核中的中断和线程更高。由于 `Cobalt` 核心执行的指令较 `Linux` 内核少，因此可以减少调用路径中的不必要延迟和历史负担。

使用这种实时目标设计，`xkernel`修补的`Linux`内核可以实现良好的实时多线程性能。

### 双域中断管道 - [Head] 和 [Root]

两阶段中断管道是实现 `Cobalt` 实时框架的底层机制。

`Cobalt` 补丁 `Linux` 内核在硬件中断上占据主导地位，这些中断原本属于 `Linux` 内核。`Cobalt` 将首先处理它感兴趣的中断，然后将其他中断路由到 `Linux` 内核。前者称为 head stage，后者称为 root stage：

- [Head] 阶段 对应于 Cobalt 核心（实时域）
- [Root] 阶段 对应于 Linux 内核（非实时域）

[Head] 阶段的优先级高于 [Root] 阶段，通过压缩硬件和软件的处理时间提供最短的响应时间。

### 设置POSIX 线程在双域切换

在 `Linux` 上，`taskset` 命令允许你更改进程的 CPU 亲和性。通常，它与内核命令行中确定的 CPU 隔离一起使用。以下示例脚本演示了将实时进程的亲和性更改为核心 1 的操作：

    cpu="1"
    taskset -c $cpu latency -c1 -t0 -p 100 -P 99 -h -g result.txt

如果作为systemd服务作为系统守护进程，可以参考下面的设置：

    [Unit]
    Description=cobalt latency service
    Wants=network-online.target
    [Service]
    CPUAffinity=1
    ExecStart=/usr/local/bin/latency -c1 -t0 -p 100 -P 99 -h -g result.txt
    Type=exec
    [Install]
    WantedBy=multi-user.target
    Alias=latency.service

`Cobalt` 核心的线程并不是完全与 `Linux` 内核的线程隔离。相反，它重用了普通的 `kthread` 并添加了特殊功能；`kthread` 可以在 `Cobalt` 的实时上下文（out-of-band 实时域）和普通的 `Linux` 内核上下文（in-band 非实时域）之间切换。其优势在于，当处于实时域上下文时，线程可以利用 `Linux` 内核的基础设施。在典型场景中，`Cobalt` 线程会以普通 `kthread` 的形式启动，调用 `Linux` 内核的 API 进行准备工作，然后切换到实时域上下文，作为 `Cobalt` 线程执行实时工作。其劣势在于，处于实时域上下文时，`Cobalt` 线程很容易因为误调用 `Linux` 内核 API 而迁移到非实时上下文。在这种情况下，问题很难被发现；开发者通常误认为他们的线程正在 `Cobalt` 核心下运行，直到检查 `ftrace` 输出或任务超出其截止期限时才注意到问题。

基于 `Cobalt` 的用户空间应用程序可以在抢占式调度和通用分时（SCHED_OTHER）调度策略之间影射同一线程：

非实时域：此模式下可以访问 Linux GPOS 服务和 Linux [ROOT] 域设备驱动程序（例如，可以使用 ps -x 或 top 等 Linux 命令）。 实时域：此模式下可以访问所有 xkernel RTOS 服务和 RTDM [HEAD] 域设备驱动程序。

可以通过以下命令查看进程状态：

    cat /proc/xenomai/sched/stat

### 使用Cobalt POSIX API 封装

当使用 `libcobalt.so` 链接时，通过 `pthread_create()` 和 `pthread_setschedparam()` 设置的 Linux pthread，任何对标准(S) POSIX 调度策略的更改，例如 `SCHED_FIFO` 和 `SCHED_RR`，都会通过一种称为影射 (shadowing) 的机制，跳转到 `Cobalt` 任务。

此外，xkernel/Cobalt 提供了补充的调度策略（X）：

- SCHED_TP 实现了线程组的时间分区调度策略（一个组可以是一个或多个线程）。
- SCHED_SPORADIC 实现了任务服务器调度器，用于运行分散活动，并使用配额避免周期性 (在 `SCHED_RR` 或 `SCHED_FIFO` 任务) 干扰。
- SCHED_QUOTA 实现了基于预算的调度策略。线程组在预算超出后暂停。预算将在每个配额周期内重新填充。

Scheduling Policies | 基础Linux | 基础Linux + PREEMPT_RT | 基础Linux + xkernel/COBALT
--- | --- | --- | ---
SCHED_TP, SCHED_BATCH, SCHED_IDLE | S | S | S
SCHED_FIFO, SCHED_RR | N | N | N
SCHED_FIFO | N | P | P
SCHED_TP, SCHED_SPORADIC, and SCHED_QUOTA | - | - | X

注释：

- S = 标准调度策略
- N = PREEMPT 补充调度策略
- P = PREEMPT_RT 补充调度策略
- X = XENOMAI 补充调度策略

### 在Cobalt中设置高分辨率计时器线程

MetaOS 支持使用X86 硬件定时器，以基于高优先级 实时任务中断创建时间间隔：

- [host-timer/x] 和 [watchdog] 被多路复用为多个软件可编程定时器，并暴露给 Cobalt
- `timerfd_handler` POSIX 定时器 API 从用户空间调用
- `clock_nanosleep()` 通过高精度定时器硬件卸载实现线程精确唤醒
- `Linux` 用户空间文件系统接口允许报告 `Cobalt` 定时器信息：

通过下面的命令查看Cobalt计时器信息：

    cat /proc/xenomai/timer/coreclk

### 深入解析 Cobalt 的补充调度算法

在实时系统开发中，调度算法的选择和应用对系统的性能和实时性至关重要。`xkernel` 的 `Cobalt` 实时内核为开发者提供了多种调度策略，以满足不同应用场景的需求。将深入探讨 `Cobalt` 的补充调度算法，包括 `SCHED_QUOTA`、`SCHED_TP`、`SCHED_WEAK` 和 `SCHED_SPORADIC`，并详细解释它们的工作原理、应用场景以及如何在实际开发中有效地利用这些策略。

在实时系统中，任务的调度需要考虑到多种因素，如任务的优先级、执行时间、资源使用等。传统的调度策略，如 `SCHED_FIFO` 和 `SCHED_RR`，在某些场景下可能无法满足复杂的调度需求。为此，`Cobalt` 提供了多种补充调度策略，帮助开发者更灵活地管理系统中的线程和资源。

#### SCHED_QUOTA 调度策略

`SCHED_QUOTA` 是一种基于配额（Quota）的调度策略，它通过限制线程在固定的时间周期内的 CPU 使用量，来实现对线程组的资源控制。在该策略下，线程被分配到不同的线程组（Thread Group），每个组在全局配额周期内被分配一定的运行时间配额（以百分比表示）。在组内，所有线程遵循 `SCHED_FIFO` 策略。

##### 工作原理

全局配额周期（Global Quota Period）：这是一个固定的时间周期，例如 1 秒（默认），整个系统的配额管理都基于这个周期。

线程组配额：每个线程组被分配一定比例的运行时间，例如 35%、25% 等。

线程执行：

- 当一个属于 `SCHED_QUOTA` 策略的线程获得 CPU 资源时，其消耗的时间会被计入所属线程组的配额。
- 当线程组的总消耗时间达到其配额上限时，该组内的所有线程将被暂停，直到下一个全局配额周期开始。
- 在新的周期开始时，每个线程组的配额会被重置，线程可以继续运行。

##### 示例说明

假设系统中有 5 个线程组，每个组的配额和全局周期如下：

SCHED_QUOTA 调度策略示例图

- 全局配额周期：1 秒
- 线程组配额：
  - 组 1：35%（350 毫秒）
  - 组 2：25%（250 毫秒）
  - 组 3：15%（150 毫秒）
  - 组 4：10%（100 毫秒）
  - 组 5：5%（50 毫秒）

在每个全局周期内，各线程组可以在其配额内运行线程。当组内所有线程的总运行时间达到配额后，该组将被暂停，等待下一个周期。

运行时间预算和峰值配额

运行时间预算：在每个全局周期开始时，线程组获得其完整的配额。如果该组在当前周期未完全消耗其配额，剩余的预算会被累积到下一个周期，直到达到定义的峰值配额（Peak Quota）。

峰值配额：这是线程组可累积的最大配额。当累积的预算超过峰值配额时，超出的部分会在后续多个周期内逐步消耗。

应用场景

- 资源限制：需要限制某些线程组对 CPU 资源的使用，以防止资源被少数线程过度占用。
- 服务质量保障：为关键任务分配更高的配额，确保其在系统中获得足够的 CPU 时间。

#### SCHED_TP 调度策略

SCHED_TP 策略实现了一种称为**时间分区（Temporal Partitioning）**的机制，它通过将 CPU 时间划分为固定的时间窗口，确保不同线程组在时间上不发生重叠地执行。

##### 工作原理

主时间帧（Global Time Frame）：一个固定的、重复的全局时间周期。

次要帧（Secondary Frames）：主时间帧被划分为多个次要帧（时间窗口），每个次要帧具有固定的持续时间和相对于主时间帧的偏移量。

分区（Partitions）：每个次要帧分配给一个特定的线程组（分区）。在次要帧的时间窗口内，只有该分区内的线程被允许执行。

线程执行：

- 当一个次要帧开始时，其对应分区内的线程可以开始执行。
- 当次要帧结束时，该分区内的线程被暂停，切换到下一个分区的线程执行。
- 当所有次要帧执行完毕后，主时间帧重新开始，循环往复。

##### 示例说明

假设主时间帧为 100 毫秒，被划分为 5 个次要帧：

- 次要帧 1：持续时间 30 毫秒，分区 A
- 次要帧 2：持续时间 20 毫秒，分区 B
- 次要帧 3：持续时间 10 毫秒，分区 C
- 次要帧 4：持续时间 20 毫秒，分区 D
- 次要帧 5：持续时间 10 毫秒，分区 D

在每个次要帧内，只有对应分区内的线程被调度执行。这样可以确保不同分区的线程在时间上严格隔离，防止相互干扰。

SCHED_TP 调度策略示例图

应用场景

- 安全关键系统：需要严格的时间隔离，防止不同任务之间的干扰。
- 实时性保障：确保高优先级任务在指定的时间窗口内得到执行。

#### SCHED_WEAK 调度策略

`SCHED_WEAK` 策略用于实现弱调度（Weak Scheduling），其主要目的是让线程能够与实时线程进行同步，但不与实时线程竞争 CPU 资源。

工作原理

- 线程属性：`SCHED_WEAK` 类的线程具有较低的优先级，不会抢占实时线程。
- 模式切换：当 `SCHED_WEAK` 线程在 `Cobalt` 系统调用返回后，会自动离开实时域，回到非实时模式。
- 同步机制：`SCHED_WEAK` 线程可以使用 `Cobalt` 提供的同步原语（如互斥锁、条件变量）与实时线程进行同步。

应用场景

- 辅助线程：执行非关键的后台任务，但需要与实时线程共享数据或进行同步。
- 资源监控：收集系统状态、日志记录等，不影响实时任务的执行。

#### SCHED_SPORADIC 调度策略

`SCHED_SPORADIC`零星调度策略通常用于对给定时间段内的线程执行时间提供上限。在零星调度下，线程的优先级可以在前台(正常优先级)与后台(低优先级)之间动态振荡。与 FIFO 调度一样，使用零星调度的线程会继续执行，直到它被更高优先级的线程阻塞或抢占。与自适应调度一样，使用零星调度的线程的优先级会降低，但使用零星调度可以更精确地控制线程的行为。

##### 零星调度策略支持的不规则条件：

初始预算(C)：线程在被降级为低优先级 (L) 之前被允许以正常优先级 (N) 执行的时间量。

低优先级(L)：线程将降级到的优先级。线程在后台时以这个较低的优先级 (L) 执行，在前台时以正常优先级 (N) 运行。

补充期(T)：线程被允许消耗其执行预算的时间段。为了安排补充操作，`POSIX` 实现还使用此值作为线程变为 READY 的时间偏移量。

待处理补充的最大数量：此值限制可以进行的补充操作的数量，从而限制不规则调度策略消耗的系统开销。

执行过程：

1. 初始运行：线程以正常优先级（N）开始执行，预算为 10 毫秒。
2. 预算消耗：线程运行 3 毫秒后被阻塞，剩余预算为 7 毫秒。
   - 这 3 毫秒的运行时间被安排在下一个补充期结束时补充，即在 40 毫秒后。
3. 唤醒继续：线程在 6 毫秒时被唤醒，继续以正常优先级运行，消耗剩余的 7 毫秒预算。
   - 这 7 毫秒的运行时间被安排在第二个补充期结束时补充，即在 46 毫秒后。
4. 预算耗尽：预算耗尽后，线程的优先级降低为低优先级（L）。
5. 补充预算：在补充期（T）结束后，线程的预算被补充，优先级恢复为正常优先级（N）。
6. 循环执行：线程继续在两个优先级之间切换，按照预算和补充期的设定执行。

示例图解：

SCHED_SPORADIC 调度策略示例图

时间轴：

- 0 ms：线程开始执行，预算为 10 ms，优先级为 N。
- 3 ms：线程阻塞，消耗了 3 ms 预算，剩余 7 ms。3 ms 的预算将在 40 ms 后补充。
- 6 ms：线程被唤醒，继续执行，消耗剩余的 7 ms 预算。7 ms 的预算将在 46 ms 后补充。
- 13 ms：预算耗尽，线程优先级降为 L。
- 40 ms：第一个补充期结束，补充 3 ms，优先级升为 N。
- 43 ms：消耗补充的 3 ms 预算，优先级降为 L。
- 46 ms：第二个补充期结束，补充 7 ms，优先级升为 N。
- 53 ms：消耗补充的 7 ms 预算，优先级降为 L。

这样线程将继续在其两个优先级之间振荡，以一种受控的、可预测的方式为系统中的非周期性事件提供服务。

应用场景

- 非周期性事件处理：需要对突发事件进行处理，但又要限制线程对系统资源的占用。
- 资源保护：防止线程过度占用 CPU，影响其他实时任务的执行。