4、任务调度机制:抢占式调度、时间片轮转、任务优先级
调度机制,说白了就是操作系统决定「下一个该谁跑」的规则。FreeRTOS 的调度策略并不复杂,但理解透了,你写出来的代码才稳。我刚开始接触 RTOS 时,总觉得调度是系统的事,跟我没关系——直到有一次产品在现场死机,排查了三天,最后发现是优先级设错了。
嗯,咱们今天就把这块彻底讲清楚。
4.1 抢占式调度:高优先级任务「插队」
抢占式调度是 FreeRTOS 默认的调度方式。它的核心规则很简单:只要有一个优先级更高的任务就绪,当前任务立刻被挂起,CPU 让给高优先级任务。
你想想看,这就像排队买饭。你正排着队,突然来了个 VIP,他可以直接走到窗口前面。你只能停下来,等他买完再继续排。
关键点:抢占发生在任何时刻——中断里、延时里、甚至你正在执行一条指令的中间。只要调度器发现更高优先级的任务就绪,立马切换。
我在项目中遇到过一个问题:两个任务共享一个全局变量,低优先级任务刚写到一半,被高优先级任务抢占了。高优先级任务读取了这个半成品的变量,结果数据全乱套了。这就是典型的「抢占式调度带来的竞态问题」。
避坑指南:我曾经因为没注意抢占时机,导致一个电机控制任务读取了错误的 PWM 占空比,电机直接抖了起来。从那以后,凡是共享数据,我必加临界区或互斥信号量。
4.2 时间片轮转:同优先级任务的「轮流坐庄」
如果多个任务优先级相同,抢占式调度就不起作用了——因为谁也不比谁高。这时候,FreeRTOS 会采用时间片轮转。
说白了,就是每个任务分到一小段 CPU 时间(一个 tick,通常 1ms),时间到了就换下一个。大家轮流跑,谁也不饿着。
| 调度方式 | 触发条件 | 适用场景 |
|---|---|---|
| 抢占式 | 高优先级任务就绪 | 实时性要求高的任务 |
| 时间片轮转 | 同优先级任务时间片耗尽 | 多个同等重要的后台任务 |
我记得有一次调试一个数据采集系统,三个传感器任务优先级相同。一开始没开时间片轮转,结果第一个任务一直占着 CPU,后面两个根本跑不了。后来在 FreeRTOSConfig.h 里把 configUSE_TIME_SLICING 设为 1,问题就解决了。
小技巧:时间片轮转默认是开启的。如果你希望某个任务独占 CPU,可以把它优先级设高,或者用 taskYIELD() 主动让出 CPU。
4.3 任务优先级:数字越小越「卑微」
FreeRTOS 中,优先级是一个整数。数字越大,优先级越高。这一点和 uC/OS 正好相反,我刚从 uC/OS 转过来时经常搞混。
优先级范围由 configMAX_PRIORITIES 定义,一般设为 5 到 32 之间。我个人习惯设成 8,够用又不浪费内存。
// 定义优先级
#define TASK_LED_PRIORITY 2
#define TASK_UART_PRIORITY 3
#define TASK_CONTROL_PRIORITY 5
// 创建任务
xTaskCreate(
vTaskLed, // 任务函数
"LED Task", // 任务名
128, // 栈大小(字)
NULL, // 参数
TASK_LED_PRIORITY, // 优先级
NULL // 句柄
);
重要原则:优先级不要设太多层级。我见过有人设了 16 级,结果调试时自己都搞不清哪个任务该先跑。一般来说,3~5 级就够用了:
- 最高级:中断处理、紧急控制
- 中级:通信、数据采集
- 低级:界面刷新、日志打印
4.4 优先级反转:一个经典陷阱
优先级反转,说白了就是低优先级任务「拖住」了高优先级任务。为什么会这样?
假设有三个任务:高优先级 H、中优先级 M、低优先级 L。L 占着一个信号量,H 想要这个信号量,只能等 L 释放。但这时候 M 抢占了 L,L 一直没机会运行,H 就只能干等。结果 M 反而比 H 先跑完了——优先级完全反过来了。
我曾经踩过这个坑:一个温控系统,温度采样任务(高优先级)被一个日志打印任务(低优先级)拖住,导致温度控制延迟了 200ms,产品直接过温保护。后来用了优先级继承机制才解决。
FreeRTOS 支持优先级继承,可以在使用互斥信号量时自动启用。当高优先级任务等待一个被低优先级任务持有的互斥量时,低优先级任务会临时「继承」高优先级,直到释放互斥量。
// 使用互斥信号量(带优先级继承)
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 任务中获取
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
xSemaphoreGive(xMutex);
}
4.5 实际项目中的调度策略选择
说了这么多理论,实际项目中怎么选?我一般遵循这几个原则:
- 硬实时任务(比如电机控制、PWM 输出):用最高优先级,抢占式调度
- 软实时任务(比如按键扫描、数据显示):中等优先级,允许被抢占
- 后台任务(比如日志存储、看门狗喂狗):最低优先级,时间片轮转
你想想看,如果所有任务都用同一个优先级,那 FreeRTOS 就退化成前后台系统了。如果所有任务都用不同优先级,那低优先级的任务可能永远跑不了——这叫「优先级饥饿」。
我的建议:刚开始做项目时,先画一张任务优先级表。把每个任务的实时性要求、执行频率、最大允许延迟都列出来。优先级分配不是拍脑袋,是算出来的。
嗯,调度机制这块就讲到这里。记住一句话:抢占式调度保证实时性,时间片轮转保证公平性,优先级设置体现你的设计思路。下一章咱们聊聊任务间通信,那才是真正体现功力的地方。