4. 任务优先级与调度策略

任务调度是 RTOS 的核心职能,决定了哪个任务在何时获得 CPU 使用权。本章深入剖析优先级的定义方法以及三种经典调度策略:抢占式调度、时间片轮转调度和协作式调度。理解这些机制是设计高实时性嵌入式系统的基石。

4.1 任务优先级定义

优先级是调度器判断任务紧急程度和重要性的数值依据。在 RTOS 中,每个任务在创建时都被赋予一个优先级,数值越小(或越大,取决于具体实现)表示优先级越高。

4.1.1 优先级数值与方向

RTOS 类型 优先级方向 示例 最高优先级值
FreeRTOS 数值越大,优先级越高 优先级 5 > 优先级 2 configMAX_PRIORITIES - 1
μC/OS-III 数值越小,优先级越高 优先级 2 > 优先级 5 0
RT-Thread 数值越小,优先级越高 优先级 8 > 优先级 16 0
设计建议:优先级数量并非越多越好。过多的优先级会增加调度器的查找开销,并可能引入优先级反转等复杂问题。通常 8~32 级优先级足以满足大多数嵌入式场景。

4.1.2 优先级分配原则

  • 硬实时任务:如电机控制、刹车系统,分配最高优先级。
  • 软实时任务:如音频播放、传感器数据采集,分配中等优先级。
  • 后台任务:如日志记录、自检程序,分配最低优先级。
  • 空闲任务:系统默认的最低优先级任务,仅在无其他任务就绪时运行。
关键点:优先级是静态配置的,但可以通过优先级继承协议或优先级天花板协议在运行时动态调整,以解决优先级反转问题。

4.2 抢占式调度

抢占式调度是 RTOS 最常用的策略。调度器可以在任意时刻中断当前正在运行的任务,并将 CPU 分配给另一个优先级更高的就绪任务。这种机制保证了高优先级任务的实时响应。

4.2.1 抢占触发条件

  1. 任务创建:新创建的任务优先级高于当前运行任务。
  2. 任务唤醒:等待事件的任务被唤醒,且其优先级高于当前任务。
  3. 任务优先级变更:当前运行任务的优先级被降低,或另一个任务的优先级被提升。
  4. 中断退出:中断服务程序退出后,调度器检查是否有更高优先级的任务就绪。

4.2.2 抢占式调度流程图

抢占式调度流程图 抢占式调度流程图 任务 A 运行中 高优先级任务就绪? 否 任务 A 继续运行 是 保存任务 A 上下文 恢复任务 B 上下文 任务 B 运行 说明 抢占式调度中,高优先级任务就绪后会立即获得 CPU 使用权。 被抢占的任务 A 将在下次调度时从断点处继续执行。

4.2.3 抢占式调度代码示例

/* FreeRTOS 抢占式调度示例 */
void vHighPriorityTask(void *pvParameters) {
    for (;;) {
        /* 高优先级任务执行关键操作 */
        vTaskDelay(pdMS_TO_TICKS(10));  /* 阻塞等待,让低优先级任务运行 */
    }
}

void vLowPriorityTask(void *pvParameters) {
    for (;;) {
        /* 低优先级任务执行后台操作 */
        /* 当高优先级任务就绪时,此任务会被立即抢占 */
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void main(void) {
    xTaskCreate(vHighPriorityTask, "High", 256, NULL, 3, NULL);  /* 优先级 3 */
    xTaskCreate(vLowPriorityTask,  "Low",  256, NULL, 1, NULL);  /* 优先级 1 */
    vTaskStartScheduler();
}

4.3 时间片轮转调度

时间片轮转调度用于同优先级任务之间的 CPU 时间分配。每个任务被分配一个固定的时间片(Time Slice),当任务用完其时间片后,调度器将 CPU 切换给下一个同优先级的就绪任务。

4.3.1 时间片轮转调度机制

  • 时间片长度:通常由系统 Tick 中断周期决定,例如 1ms 或 10ms。
  • 任务切换点:任务用完时间片、任务主动阻塞、任务被更高优先级任务抢占。
  • 公平性:确保同优先级任务获得大致相等的 CPU 时间。

4.3.2 时间片轮转调度示意图

时间片轮转调度示意图 时间片轮转调度示意图 t0 t1 t2 t3 t4 任务 A 任务 B 任务 C 任务 A 时间片轮转调度规则 1. 每个任务获得固定长度的时间片(如 150 个 Tick)。 2. 时间片用完后,调度器切换到下一个同优先级任务。 3. 如果任务提前阻塞,剩余时间片被丢弃,调度器立即切换。

4.3.3 时间片轮转配置示例

/* FreeRTOS 时间片轮转配置 */
/* 在 FreeRTOSConfig.h 中配置 */
#define configUSE_TIME_SLICING      1   /* 启用时间片轮转 */
#define configTICK_RATE_HZ         1000 /* 系统 Tick 频率 1000Hz,时间片为 1ms */

/* 创建同优先级任务,它们将自动进行时间片轮转 */
void main(void) {
    xTaskCreate(vTaskA, "TaskA", 256, NULL, 2, NULL);
    xTaskCreate(vTaskB, "TaskB", 256, NULL, 2, NULL);
    xTaskCreate(vTaskC, "TaskC", 256, NULL, 2, NULL);
    vTaskStartScheduler();
}

4.4 协作式调度

协作式调度(Cooperative Scheduling)是一种非抢占的调度策略。任务必须主动释放 CPU 控制权,调度器才能切换到其他任务。这种策略避免了复杂的临界区保护问题,但要求每个任务不能长时间占用 CPU。

4.4.1 协作式调度特点

特性 协作式调度 抢占式调度
任务切换时机 任务主动调用 yield 或阻塞 任意时刻,由调度器决定
临界区保护 通常不需要,因为不会发生抢占 必须使用信号量、互斥量等机制
实时性 较差,依赖任务自觉 强,高优先级任务可立即响应
代码复杂度 较低,无需考虑重入问题 较高,需处理竞态条件
适用场景 简单系统、状态机、低功耗设备 复杂系统、硬实时控制

4.4.2 协作式调度流程图

协作式调度流程图 协作式调度流程图 任务 A 运行 主动让出? 否 任务 A 继续运行 是 任务 B 运行 主动让出? 是 任务 C 协作式调度关键规则 1. 任务必须主动调用 taskYIELD() 或阻塞函数才能让出 CPU。 2. 如果一个任务不主动让出,其他任务将永远无法运行。 3. 适用于任务执行时间短且可预测的简单系统。

4.4.3 协作式调度代码示例

/* FreeRTOS 协作式调度配置 */
/* 在 FreeRTOSConfig.h 中配置 */
#define configUSE_PREEMPTION        0   /* 禁用抢占式调度 */
#define configUSE_TIME_SLICING      0   /* 禁用时间片轮转 */

/* 任务必须主动让出 CPU */
void vTaskA(void *pvParameters) {
    for (;;) {
        /* 执行任务 A 的工作 */
        processSensorData();
        /* 主动让出 CPU,切换到下一个任务 */
        taskYIELD();
    }
}

void vTaskB(void *pvParameters) {
    for (;;) {
        /* 执行任务 B 的工作 */
        updateDisplay();
        /* 也可以使用阻塞函数让出 CPU */
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}
注意:在协作式调度中,如果某个任务进入死循环或执行时间过长,整个系统将失去响应。因此,必须确保每个任务都能及时主动让出 CPU。

4.5 三种调度策略对比总结

调度策略 CPU 分配依据 实时性 临界区保护 典型应用
抢占式调度 任务优先级 高 需要 工业控制、汽车电子
时间片轮转 时间片 + 同优先级 中 需要 人机交互、数据采集
协作式调度 任务主动让出 低 通常不需要 简单状态机、低功耗设备

4.6 调度策略选择指南

  • 硬实时系统:必须使用抢占式调度,确保关键任务在 deadline 前完成。
  • 同优先级任务较多:结合时间片轮转,避免某个任务长时间独占 CPU。
  • 资源极度受限:考虑协作式调度,减少上下文切换开销和内存占用。
  • 混合策略:大多数 RTOS 支持同时启用抢占式调度和时间片轮转,高优先级任务抢占,同优先级任务轮转。
最佳实践:在项目初期,优先使用抢占式调度 + 时间片轮转的组合策略。只有在明确评估了协作式调度的风险后,才考虑在特定场景中使用它。