任务与调度:任务状态机、优先级与调度策略
各位同学,今天我们聊聊RTOS调度里最核心的几个概念。说白了,就是任务在系统里到底是怎么“活着”的,以及系统怎么决定“先干谁”。
我在做水质监测项目时,遇到过这样一个场景:传感器采集任务和串口发送任务同时就绪,结果串口发送把CPU占满了,传感器数据采集被饿死。嗯,这就是调度没设计好。今天我们就来彻底搞明白这件事。
一、任务状态机:任务的五种“活法”
一个任务在RTOS里,不是一直跑着的。它会在几个状态之间跳来跳去。我习惯把这五个状态画成一个圈,方便理解。
| 状态 | 含义 | 典型场景 |
|---|---|---|
| 就绪(Ready) | 任务已准备好运行,只等CPU | 等待调度器选中它 |
| 运行(Running) | 任务正在占用CPU执行 | 当前正在执行的任务 |
| 阻塞(Blocked) | 任务在等待某个事件或资源 | 等待传感器数据、等待信号量 |
| 挂起(Suspended) | 任务被暂停,不参与调度 | 调试时手动暂停、低功耗模式 |
| 终止(Terminated) | 任务执行完毕或被删除 | 一次性任务完成后 |
你想想看,一个任务从创建开始,通常先进入就绪态。调度器选中它,就变成运行态。如果它调用了延迟函数或等待信号量,就进入阻塞态。等条件满足了,又回到就绪态。
核心要点:只有就绪态的任务才能被调度器选中。阻塞和挂起的任务,调度器根本不会看它们一眼。
我在项目中遇到过一个问题:一个任务调用了vTaskDelay(10),结果它一直没回来。后来发现是延时单位搞错了,FreeRTOS里是tick,不是毫秒。嗯,这种坑踩过一次就记住了。
二、任务优先级:谁更重要谁先跑
RTOS里每个任务都有一个优先级。数值越小优先级越低,还是越高?不同RTOS不一样。FreeRTOS里数值越大优先级越高,uC/OS里数值越小优先级越高。我个人习惯用FreeRTOS,所以下面都以它为例。
优先级的设计,说白了就是回答一个问题:当多个任务同时就绪时,CPU先干谁?
我的建议:水质监测系统里,数据采集任务的优先级通常高于显示任务。因为数据丢了就丢了,显示慢一点没关系。我曾经把一个显示任务的优先级设得比采集还高,结果数据采集被频繁打断,采样间隔变得不稳定。后来调回来就好了。
优先级分配有几个原则:
- 实时性要求高的任务给高优先级(如报警处理、传感器读取)
- 计算量大但不紧急的任务给低优先级(如数据统计、日志记录)
- 避免优先级反转——这个后面会细讲
三、调度策略:抢占式 vs 时间片轮转
调度策略决定了系统怎么分配CPU时间。RTOS里最常见的就是两种:抢占式调度和时间片轮转调度。
3.1 抢占式调度
抢占式调度的核心思想是:高优先级的任务一旦就绪,立刻抢走CPU。不管低优先级任务跑了一半还是刚跑了一行代码。
举个例子:
// 任务A:优先级5,负责传感器采集
void TaskA(void *pvParameters) {
while(1) {
read_sensor(); // 正在执行这里
// 此时任务B就绪了,优先级更高
// 任务A被立刻打断,CPU交给任务B
process_data();
}
}
// 任务B:优先级10,负责报警处理
void TaskB(void *pvParameters) {
while(1) {
if(alarm_triggered()) {
send_alarm(); // 任务B抢到CPU
}
}
}
抢占式调度的好处是实时性好。高优先级任务能及时响应。坏处是低优先级任务可能被饿死——如果高优先级任务一直不阻塞,低优先级任务永远拿不到CPU。
注意:我曾经在一个项目中,把串口发送任务的优先级设得比主控任务还高。结果串口发送任务一直在发数据,主控任务根本跑不起来。这就是典型的“高优先级任务霸占CPU”问题。解决办法是让高优先级任务适当阻塞,或者用时间片轮转。
3.2 时间片轮转调度
时间片轮转调度,说白了就是大家轮流用CPU。每个任务分到一小段时间片,时间到了就换下一个。不管优先级高低,大家都有机会跑。
FreeRTOS里,如果多个任务优先级相同,就会自动使用时间片轮转。每个任务跑一个tick的时间,然后切换到下一个。
// 任务A和任务B优先级相同,都是5
// 系统会轮流执行它们
void TaskA(void *pvParameters) {
while(1) {
// 跑一个tick的时间
do_something();
// 时间片到了,切换到任务B
}
}
void TaskB(void *pvParameters) {
while(1) {
// 跑一个tick的时间
do_something_else();
// 时间片到了,切换到任务A
}
}
时间片轮转的好处是公平。每个任务都能分到CPU时间。坏处是实时性不如抢占式——高优先级任务不能立刻响应。
四、实际应用:水质监测系统里的调度设计
好了,理论讲完了。我们来看看实际的水质监测系统里怎么用。
我设计过一套系统,任务分配如下:
| 任务名称 | 优先级 | 调度策略 | 说明 |
|---|---|---|---|
| 传感器采集任务 | 高(8) | 抢占式 | 每100ms采集一次,不能丢 |
| 报警处理任务 | 高(7) | 抢占式 | 水质超标时立即响应 |
| 数据发送任务 | 中(5) | 时间片轮转 | 通过串口发送数据,可以等 |
| 显示刷新任务 | 低(3) | 时间片轮转 | 更新LCD屏幕,慢一点没关系 |
| 日志记录任务 | 低(2) | 时间片轮转 | 写SD卡,不紧急 |
你想想看,为什么传感器采集任务优先级最高?因为水质数据是实时变化的,如果采集被延迟,数据就不准了。报警处理紧随其后,因为一旦超标,必须立刻通知。
而显示和日志任务,优先级最低。它们用时间片轮转,大家轮流跑。显示慢个几百毫秒,用户根本感觉不到。日志写慢一点,大不了缓冲区大一点。
设计原则:实时性要求高的任务用抢占式、高优先级。非实时任务用时间片轮转、低优先级。这样既保证了关键任务的响应速度,又避免了低优先级任务被饿死。
五、避坑指南
最后,分享几个我踩过的坑:
- 优先级不要超过系统限制——FreeRTOS默认最大优先级是configMAX_PRIORITIES,设大了会出问题
- 高优先级任务要适当阻塞——否则低优先级任务永远跑不了
- 注意优先级反转——低优先级任务持有高优先级任务需要的资源,会导致高优先级任务被阻塞。解决办法是用优先级继承或互斥信号量
- 时间片长度要合理——太短会导致频繁切换,浪费CPU;太长会导致响应变慢
我曾经在一个项目里,把时间片设成了1个tick。结果系统大部分时间都在做任务切换,实际干活的时间反而少了。后来改成5个tick,效果好了很多。
好了,今天的内容就到这里。任务状态机、优先级、调度策略,这三个东西是RTOS调度的基础。搞懂了它们,你就能设计出稳定、高效的水质监测系统。下次我们聊聊任务间通信——信号量和消息队列。