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 抢占触发条件
- 任务创建:新创建的任务优先级高于当前运行任务。
- 任务唤醒:等待事件的任务被唤醒,且其优先级高于当前任务。
- 任务优先级变更:当前运行任务的优先级被降低,或另一个任务的优先级被提升。
- 中断退出:中断服务程序退出后,调度器检查是否有更高优先级的任务就绪。
4.2.2 抢占式调度流程图
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 时间片轮转调度示意图
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 协作式调度流程图
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 支持同时启用抢占式调度和时间片轮转,高优先级任务抢占,同优先级任务轮转。
最佳实践:在项目初期,优先使用抢占式调度 + 时间片轮转的组合策略。只有在明确评估了协作式调度的风险后,才考虑在特定场景中使用它。