3. 实时调度类(RT)基本原理

实时调度类(RT)是 Linux 内核中为硬实时和软实时任务提供确定性调度的核心机制。其根本思想是通过优先级来确保关键任务在截止时间前获得 CPU 资源。本章深入剖析基于优先级的调度策略,详细对比 SCHED_FIFO 与 SCHED_RR 的差异,并阐明静态优先级与动态优先级的工作机制。

核心要点:RT 调度类保证最高优先级的就绪任务总是被优先执行,且执行权只能被更高优先级的任务或中断抢占。

3.1 基于优先级的调度策略

在 Linux 实时调度中,每个实时任务都被赋予一个唯一的优先级数值(数值越小,优先级越高)。调度器在每次调度点(如任务阻塞、时间片耗尽、新任务唤醒)发生时,遍历所有处于 TASK_RUNNING 状态的实时任务,选择优先级最高的任务投入运行。

这种策略的数学本质是:∀ t ∈ T_ready, priority(t) = min(priority(t_i)),即调度器总是选择优先级数值最小的任务。

优先级范围:Linux 实时优先级范围为 0~99(对应内核的 SCHED_FIFO 和 SCHED_RR),数值越小优先级越高。普通进程(SCHED_OTHER)的 nice 值映射到 100~139,始终低于任何实时任务。
基于优先级的实时调度决策流程 调度事件触发 遍历 RT 就绪队列 选择最高优先级任务 上下文切换并执行 触发条件 任务阻塞/唤醒 时间片耗尽 优先级变更 选择依据 优先级数值最小者胜出

3.2 SCHED_FIFO 与 SCHED_RR 的区别

Linux 内核提供了两种实时调度策略:SCHED_FIFO(先进先出)和 SCHED_RR(轮转)。两者都基于静态优先级,但在时间片管理上存在根本差异。

特性 SCHED_FIFO SCHED_RR
时间片 无时间片限制,可无限运行直到主动让出或阻塞 有固定时间片(默认 100ms),耗尽后自动让出
同优先级调度 按队列顺序执行,后唤醒的任务排在队尾 时间片轮转,每个任务轮流获得 CPU
抢占条件 仅被更高优先级任务或中断抢占 同优先级任务时间片到期后自动切换
适用场景 计算密集型、需独占 CPU 的关键任务 需要公平共享 CPU 的多个同优先级实时任务
饥饿风险 高优先级 FIFO 任务可能饿死低优先级任务 同优先级任务间不会饥饿,但低优先级仍可能被饿死
重要警告:SCHED_FIFO 任务如果进入无限循环且不主动让出 CPU,将永久阻塞所有同优先级或更低优先级的任务。这是实时系统设计中最常见的错误之一,必须通过 watchdog 或优先级继承协议来防范。

3.2.1 SCHED_FIFO 详细行为

当使用 SCHED_FIFO 时,调度器维护一个按优先级分组的就绪队列。每个优先级对应一个 FIFO 队列。任务被唤醒时,插入到其优先级队列的尾部。调度器总是从最高优先级的非空队列头部取出任务执行。

关键行为规则:

  • 任务一旦获得 CPU,将一直运行直到:主动调用 sched_yield()、被更高优先级任务抢占、进入阻塞状态(如等待 I/O、睡眠)、被信号杀死。
  • 如果任务被更高优先级任务抢占,它会被放回原优先级队列的头部,以便抢占结束后立即恢复执行。
  • 多个同优先级 FIFO 任务之间不会自动切换,除非主动让出。
// 设置 SCHED_FIFO 策略的示例代码
#include <sched.h>
#include <stdio.h>

int main() {
    struct sched_param param;
    param.sched_priority = 80;  // 实时优先级 80

    // 设置当前进程为 SCHED_FIFO
    if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
        perror("sched_setscheduler failed");
        return 1;
    }

    // 确认当前调度策略
    int policy = sched_getscheduler(0);
    printf("Current policy: %d (SCHED_FIFO=%d)\n", policy, SCHED_FIFO);
    return 0;
}

3.2.2 SCHED_RR 详细行为

SCHED_RR 在 SCHED_FIFO 的基础上增加了时间片轮转机制。每个 RR 任务拥有一个固定的时间片(默认 100ms,可通过 sched_rr_get_interval() 查询)。

关键行为规则:

  • 任务运行直到时间片耗尽、主动让出、被抢占或阻塞。
  • 时间片耗尽后,任务被放回同优先级队列的尾部,调度器选择队列头部的下一个任务运行。
  • 如果任务被更高优先级任务抢占,时间片保留,抢占结束后继续消耗剩余时间片。
  • 同优先级任务之间实现公平的 CPU 时间分配。
// 查询 SCHED_RR 时间片大小的示例
#include <sched.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    struct sched_param param;
    param.sched_priority = 50;
    sched_setscheduler(0, SCHED_RR, &param);

    struct timespec tp;
    if (sched_rr_get_interval(0, &tp) == 0) {
        printf("SCHED_RR time slice: %ld sec %ld ns\n",
               tp.tv_sec, tp.tv_nsec);
    }
    return 0;
}
SCHED_FIFO vs SCHED_RR 调度行为对比 SCHED_FIFO 队列(优先级 50) 任务A 任务B 任务C 执行顺序:A → A → A ...(直到A主动让出) B和C永远得不到CPU(如果A不阻塞) SCHED_RR 队列(优先级 50) 任务A 任务B 任务C 执行顺序:A → B → C → A → B → C ... 每个任务运行一个时间片后轮转 核心差异总结 SCHED_FIFO:无时间片,同优先级任务按序独占 → 适合单任务关键控制 SCHED_RR:有时间片,同优先级任务轮转 → 适合多任务公平调度 时间轴 → t0 t1 t2 t3 t4

3.3 静态优先级与动态优先级机制

Linux 实时调度类采用静态优先级机制,即任务的优先级在创建时设定,除非显式调用 sched_setparam() 或 sched_setscheduler(),否则不会改变。这与普通进程(SCHED_OTHER)使用的动态优先级(基于 nice 值和运行历史调整)形成鲜明对比。

特性 静态优先级(RT 任务) 动态优先级(普通任务)
修改方式 仅通过系统调用显式修改 内核根据运行行为自动调整
优先级范围 0~99(实时优先级) 100~139(对应 nice -20~19)
老化机制 无老化,优先级永不衰减 有老化,长时间运行的任务优先级会降低
确定性 高,行为完全可预测 低,受负载和历史影响
适用场景 硬实时、确定性要求高的任务 交互式、批处理等非实时任务

3.3.1 静态优先级的工作原理

静态优先级存储在任务描述符 task_struct 的 rt_priority 字段中。调度器在每次调度时直接读取该字段,不进行任何计算或调整。这种设计的优势在于:

  • 确定性:任务的响应时间只取决于其优先级和系统中更高优先级任务的行为,不受运行历史影响。
  • 低开销:调度决策仅需比较整数,无需计算动态权重。
  • 可分析性:系统设计者可以通过优先级分配精确预测最坏情况响应时间(WCRT)。
内核实现关键点:在 kernel/sched/rt.c 中,pick_next_task_rt() 函数通过 sched_find_first_bit() 位图操作快速找到最高优先级的非空队列,时间复杂度为 O(1)。

3.3.2 动态优先级在 RT 中的角色

虽然 RT 任务本身使用静态优先级,但 Linux 内核在以下场景中引入了动态优先级机制来辅助实时调度:

  • 优先级继承(Priority Inheritance):当低优先级 RT 任务持有高优先级任务需要的锁时,低优先级任务临时继承高优先级,防止优先级反转。
  • 优先级提升(Priority Boost):在特定事件(如唤醒等待队列)中,内核临时提升任务的优先级以避免活锁。
  • RT 节流(RT Throttling):当 RT 任务占用 CPU 超过 /proc/sys/kernel/sched_rt_runtime_us 限制时,内核强制降低其优先级以保障普通任务的运行。
// 查看和修改 RT 任务优先级的系统调用
#include <sched.h>
#include <stdio.h>

void print_rt_priority(pid_t pid) {
    struct sched_param param;
    int policy;

    // 获取当前调度参数
    if (sched_getparam(pid, &param) == -1) {
        perror("sched_getparam");
        return;
    }
    policy = sched_getscheduler(pid);

    printf("PID %d: policy=%d, static priority=%d\n",
           pid, policy, param.sched_priority);
}

// 修改优先级(需要 root 权限)
void set_rt_priority(pid_t pid, int new_prio) {
    struct sched_param param;
    param.sched_priority = new_prio;

    if (sched_setparam(pid, &param) == -1) {
        perror("sched_setparam");
    }
}
静态优先级 vs 动态优先级机制对比 静态优先级(RT 任务) 创建时设定,永不改变 调度决策:直接比较 rt_priority 优点:确定性高,可分析性强 缺点:需要人工精细分配优先级 动态优先级(普通任务) 内核根据运行历史自动调整 调度决策:计算动态权重 + 老化 优点:自动平衡交互与批处理 缺点:行为不可预测,不适合实时

3.4 优先级反转与继承协议

在基于静态优先级的实时系统中,优先级反转是一个经典问题:低优先级任务持有高优先级任务需要的共享资源,导致高优先级任务被阻塞,而中等优先级任务(不依赖该资源)却可以抢占低优先级任务运行,造成高优先级任务无限期等待。

Linux 内核通过 优先级继承协议(Priority Inheritance Protocol, PIP) 来解决此问题:

  • 当高优先级任务被低优先级任务持有的锁阻塞时,低优先级任务临时继承高优先级任务的优先级。
  • 继承的优先级确保低优先级任务不会被中等优先级任务抢占,从而快速释放锁。
  • 锁释放后,低优先级任务恢复其原始优先级。
实现位置:优先级继承逻辑实现在内核的 kernel/locking/rtmutex.c 中,通过 rt_mutex 机制支持。这是 PREEMPT_RT 内核的关键组成部分。
// 优先级继承的伪代码逻辑
// 当任务 T1(优先级 80)尝试获取 T2(优先级 30)持有的锁时:
if (lock->owner != NULL && lock->owner->prio < current->prio) {
    // 临时提升锁持有者的优先级
    lock->owner->prio = current->prio;  // 继承优先级
    // 锁持有者现在以优先级 80 运行,不会被中等优先级任务抢占
}
// 当锁释放时:
lock->owner->prio = lock->owner->original_prio;  // 恢复原始优先级

通过优先级继承,系统保证了实时任务的响应时间可预测性,这是静态优先级调度能够用于硬实时系统的关键保障机制。