第三章:任务与调度 — 任务状态机、优先级设计与调度策略
好,咱们进入第三章。这一章可以说是RTOS的核心中的核心。你想想看,裸机编程里,CPU就是一条道走到黑,要么在主循环里轮询,要么靠中断插队。但到了RTOS世界,事情变得有意思了——我们要让多个任务“同时”跑起来。
怎么做到的?靠的就是任务状态机、优先级设计和调度策略这三板斧。我当年从裸机转RTOS时,第一个卡住的地方就是任务状态切换。明明创建了任务,它怎么不跑?后来才明白,哦,原来它还在“就绪态”排队呢。
3.1 任务状态机:任务的生老病死
每个任务在RTOS里都不是一直运行的。它有生命周期。就像人一样,有出生、有等待、有干活、有睡觉、有死亡。
典型的RTOS任务状态包括:
- 就绪态(Ready):任务已经准备好,随时可以运行,就等CPU分配时间
- 运行态(Running):任务正在占用CPU,真正在执行代码
- 阻塞态(Blocked):任务在等某个事件,比如等信号量、等消息队列、等延时结束
- 挂起态(Suspended):任务被强制暂停,不参与调度
- 终止态(Terminated):任务执行完毕或被删除
核心要点:一个任务同一时刻只能处于一种状态。状态之间的转换由RTOS内核控制,不是你想切就能切的。
我在项目中遇到过一个问题:一个传感器采集任务,明明优先级很高,但就是偶尔不响应。查了半天,发现是它在等一个永远不会来的信号量——任务直接卡死在阻塞态了。嗯,这就是典型的“死等”问题。
状态转换的典型路径是这样的:
- 任务创建 → 进入就绪态
- 调度器选中 → 进入运行态
- 调用延时或等待资源 → 进入阻塞态
- 延时结束或资源可用 → 回到就绪态
- 被更高优先级任务抢占 → 回到就绪态
- 任务删除 → 进入终止态
我的习惯:在调试阶段,我会在每个任务里加一个状态打印钩子。每隔几秒输出一下当前所有任务的状态。这样一眼就能看出哪个任务“卡”住了。这个习惯帮我省了无数排查时间。
3.2 任务优先级设计:谁先谁后?
优先级设计,说白了就是给任务排个座次。谁更重要,谁就先跑。
RTOS里优先级通常用数字表示。有的系统数字越小优先级越高(如FreeRTOS),有的相反(如uC/OS)。我个人习惯用FreeRTOS那套——0是最低优先级,数值越大优先级越高。
设计优先级时,我一般遵循这几个原则:
- 硬实时任务最高:比如电机控制、刹车系统,必须按时响应
- 软实时任务次之:比如按键扫描、数据显示,可以偶尔延迟
- 后台任务最低:比如日志记录、系统自检,有空再干
举个例子,一个智能家居控制器的优先级设计:
| 任务名称 | 优先级 | 说明 |
|---|---|---|
| 紧急报警处理 | 最高(5) | 火灾、入侵检测,必须立即响应 |
| 传感器采集 | 高(4) | 温度、湿度,需要周期性读取 |
| 通信处理 | 中(3) | WiFi/蓝牙数据收发 |
| 用户界面 | 低(2) | 屏幕刷新、按键响应 |
| 日志记录 | 最低(1) | 写Flash,不着急 |
避坑指南:我曾经在一个项目里把两个任务设成了相同优先级,结果发现其中一个任务几乎不运行。为什么?因为另一个任务在while(1)里疯狂循环,根本不释放CPU。相同优先级下,如果没有时间片轮转,一个任务可能把CPU“饿死”。
3.3 调度策略:抢占式 vs 时间片
调度策略,就是RTOS决定“下一个该谁跑”的规则。主流RTOS支持两种策略:抢占式调度和时间片轮转调度。
3.3.1 抢占式调度
这是最常用的策略。说白了就是:高优先级任务随时可以打断低优先级任务。
你想想看,一个低优先级任务正在跑,突然来了一个高优先级的中断或事件。RTOS会立刻保存当前任务的上下文,把CPU交给高优先级任务。等高优先级任务跑完了,再回来继续跑低优先级任务。
代码示例(FreeRTOS风格):
// 高优先级任务:紧急报警
void vEmergencyTask(void *pvParameters) {
for (;;) {
// 等待报警信号
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 一旦收到信号,立刻执行
vTurnOnAlarm();
vSendAlertMessage();
// 处理完继续等待
}
}
// 低优先级任务:日志记录
void vLogTask(void *pvParameters) {
for (;;) {
vWriteLogToFlash();
vTaskDelay(pdMS_TO_TICKS(1000)); // 每秒写一次
}
}
这里要注意:抢占式调度下,低优先级任务可能被“饿死”。如果高优先级任务一直不阻塞,低优先级任务永远没机会跑。
我的建议:高优先级任务里不要做耗时操作。比如报警任务,只负责发信号和简单处理,别在里面写Flash或做复杂计算。把这些活丢给低优先级任务去做。
3.3.2 时间片轮转调度
时间片轮转,就是让相同优先级的任务轮流跑。每个任务分到一个时间片(比如10ms),时间到了就换下一个。
这解决了“相同优先级任务互相饿死”的问题。你想想看,如果没有时间片,两个相同优先级的任务,一个在死循环里,另一个永远没机会跑。
时间片轮转的典型配置:
// FreeRTOS中配置时间片
// 在FreeRTOSConfig.h中
#define configUSE_TIME_SLICING 1 // 启用时间片
#define configTICK_RATE_HZ 100 // 系统时钟节拍,100Hz = 10ms
// 两个相同优先级的任务
void vTaskA(void *pvParameters) {
for (;;) {
// 运行10ms后自动切换到TaskB
vProcessSensorData();
}
}
void vTaskB(void *pvParameters) {
for (;;) {
// 运行10ms后自动切换到TaskA
vUpdateDisplay();
}
}
时间片的大小由系统时钟节拍决定。节拍越快,时间片越短,切换越频繁,但CPU开销也越大。
核心对比:
- 抢占式:高优先级任务可以随时打断低优先级任务。适合硬实时场景。
- 时间片:相同优先级任务轮流执行。适合软实时或非实时场景。
- 两者可以共存:不同优先级之间用抢占式,相同优先级之间用时间片。
3.4 实际项目中的调度策略选择
说了这么多理论,咱们聊聊实际怎么选。
我做过一个无人机飞控项目。飞控对实时性要求极高——姿态控制必须在1ms内响应。这种情况下,我用了纯抢占式调度:
- 姿态控制任务:最高优先级,1ms周期
- GPS数据处理:中等优先级,10ms周期
- 日志记录:最低优先级,有空再写
姿态控制任务每次运行时间很短(不到0.1ms),跑完就阻塞等下一个周期。这样低优先级任务也能得到CPU时间。
但另一个项目——智能手表,情况就不同了。界面刷新、传感器采集、蓝牙通信,这些任务优先级差不多,而且都不算特别紧急。我用了时间片轮转,每个任务分20ms时间片。这样用户操作界面时不会感觉卡顿。
避坑指南:我曾经在一个项目里把时间片设得太小(1ms)。结果CPU大部分时间都在做任务切换,真正干活的时间反而少了。任务切换是有开销的——保存寄存器、恢复寄存器、查就绪列表。时间片太小,得不偿失。一般建议10-50ms。
3.5 总结与思考
好,这一章的内容就到这里。咱们回顾一下:
- 任务状态机:就绪、运行、阻塞、挂起、终止。理解状态转换是理解RTOS的第一步。
- 优先级设计:硬实时最高,软实时次之,后台最低。别把所有任务都设成高优先级。
- 调度策略:抢占式适合硬实时,时间片适合软实时。两者可以结合使用。
最后留个思考题:如果一个系统里,所有任务都是最高优先级,会发生什么?嗯,其实就等于没有优先级——大家抢着跑,谁抢到算谁的。这种情况下,系统行为变得不可预测。所以,优先级设计一定要有层次感。
下一章,咱们聊聊任务间通信——信号量、消息队列、事件标志组。这些东西是让任务之间“说话”的工具。到时候见。