任务与任务调度:任务的定义、任务状态与调度策略
各位同学,大家好。今天我们来聊聊RTOS里最核心的概念——任务与任务调度。说实话,我刚开始接触RTOS时,觉得任务不就是个函数吗?后来踩了不少坑才明白,任务远不止这么简单。
任务的定义:它到底是什么?
任务,在RTOS里,本质上是一段无限循环的代码。它有自己的栈空间、优先级和状态。你可以把它想象成一个独立的小工人,专门负责干某一件事。
比如在心电监护仪里,我们会把采集心电信号、计算心率、驱动屏幕显示、处理按键输入,分别做成不同的任务。每个任务各司其职,互不干扰。
任务的核心要素:
- 任务函数:实际执行的代码,通常是个死循环
- 任务栈:保存局部变量和函数调用信息
- 任务控制块(TCB):记录任务的状态、优先级、栈指针等
- 任务优先级:决定谁先获得CPU使用权
我习惯把任务函数写成这样:
void heart_rate_task(void *param)
{
while(1)
{
// 采集心电信号
ecg_sample();
// 计算心率
calculate_hr();
// 等待下一次采集周期
vTaskDelay(pdMS_TO_TICKS(10));
}
}
注意看,这里用了vTaskDelay。为什么?因为不能让任务一直占着CPU不放。你想想看,如果采集任务一直跑,那显示任务就没机会刷新屏幕了。
任务状态:就绪、运行、阻塞、挂起
任务有四种基本状态。我在项目中见过不少新手搞混,这里我用自己的理解讲清楚。
| 状态 | 含义 | 实际场景 |
|---|---|---|
| 就绪(Ready) | 任务已准备好运行,等待CPU | 就像排队买饭,轮到你了就能吃 |
| 运行(Running) | 任务正在使用CPU | 正在吃饭,别人得等着 |
| 阻塞(Blocked) | 任务在等待某个事件或资源 | 等外卖送到,暂时不吃饭 |
| 挂起(Suspended) | 任务被暂停,不参与调度 | 被叫去开会,饭先放一边 |
嗯,这里要注意:阻塞和挂起看起来有点像,但本质不同。阻塞是任务主动等待,比如等信号量、等延时结束。挂起则是被外部强制暂停,比如被另一个任务调用vTaskSuspend()。
我的经验:在调试心电监护系统时,我经常用挂起状态来临时停止某个任务。比如做自检时,先把采集任务挂起,避免干扰。自检完再恢复,非常方便。
任务优先级:谁更重要?
优先级决定了任务获得CPU的先后顺序。数值越小优先级越高,还是越大越高?这取决于RTOS的实现。FreeRTOS里,数值越大优先级越高。
我建议优先级不要超过5级。为什么?优先级太多,调度逻辑会变得复杂,而且容易出问题。我在一个项目中见过有人设了20级优先级,结果调试时自己都搞不清哪个任务在跑。
我曾经踩过的坑:有一次我把心电采集任务设成了最高优先级,显示任务设成了最低。结果采集任务一直占着CPU,显示任务几乎没机会运行,屏幕卡得要命。后来我把采集任务里加了个延时,让出CPU给显示任务,问题才解决。
调度策略:抢占式 vs 时间片轮转
调度策略,说白了就是CPU怎么决定下一个该谁干活。主要有两种:
抢占式调度
高优先级任务就绪后,立即打断当前运行的低优先级任务。这就像领导来了,普通员工得先让位。
在心电监护里,报警任务通常设成最高优先级。一旦检测到心律失常,必须立刻响应,哪怕正在刷新屏幕也得停。
// 高优先级任务就绪,抢占低优先级任务
void alarm_task(void *param)
{
while(1)
{
if(detect_arrhythmia())
{
// 立即触发报警
trigger_alarm();
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
时间片轮转调度
同优先级的任务,轮流使用CPU。每个任务运行一个时间片,时间到了就换下一个。这就像大家轮流发言,每人说一分钟。
我习惯把同优先级的任务数量控制在3个以内。太多了,切换开销会变大,实时性反而下降。
实际项目中的选择:
- 对实时性要求高的任务(如心电采集),用高优先级 + 抢占式
- 对实时性要求不高的任务(如日志记录),用低优先级 + 时间片轮转
- 关键任务(如报警),用最高优先级 + 抢占式
避坑指南:我的一些教训
最后,分享几个我亲身经历的教训:
- 优先级反转:低优先级任务持有资源,高优先级任务等资源,结果中优先级任务抢了CPU。解决办法是使用优先级继承协议。
- 任务栈溢出:栈空间设太小,任务跑着跑着就崩了。我一般会留30%的余量,并在调试时开启栈溢出检测。
- 阻塞超时:等待信号量或队列时,一定要设超时。否则任务可能永远阻塞,系统就卡死了。
好了,关于任务和任务调度,今天就讲到这里。记住一句话:任务不是越多越好,优先级不是越高越好。合理设计,才能让系统跑得稳、跑得快。