4、任务管理与调度:任务创建与删除、任务优先级、调度策略
好,咱们进入第四章。这一章可以说是RTOS的心脏——任务管理与调度。说白了,就是操作系统怎么管好你那一堆任务,谁先跑、谁后跑、谁该睡觉、谁该起床。
我在医疗贴片项目里吃过不少亏。有一次,心率采集任务和无线发送任务抢CPU,结果心率数据丢了几个包,差点没把认证给搞黄了。从那以后,我对任务调度就特别较真。
4.1 任务创建与删除
任务是什么?在RTOS里,任务就是一个无限循环的函数,加上它自己的栈空间和优先级。创建任务,就是给这个函数分配一个“身份证”。
拿FreeRTOS举例,创建任务就一行代码:
TaskHandle_t xHeartTaskHandle = NULL;
void vHeartRateTask(void *pvParameters) {
while(1) {
// 读取心率传感器
read_heart_rate();
// 等100ms再读
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 在初始化里创建
xTaskCreate(
vHeartRateTask, // 任务函数
"HeartRate", // 任务名字,调试用
256, // 栈深度,单位是字
NULL, // 参数
3, // 优先级,数字越大优先级越高
&xHeartTaskHandle // 任务句柄
);
嗯,这里要注意。栈深度给多少?我见过有人给32个字,结果任务一跑就崩。医疗贴片里,我一般给256到512个字。为什么?因为传感器库函数调用栈深,再加上中断嵌套,小了真不行。
删除任务呢?更简单:
vTaskDelete(xHeartTaskHandle);
但我建议你,非必要不删除。创建和删除本身有开销,而且容易造成内存碎片。医疗设备讲究稳定,任务一旦创建,就让它一直活着。我习惯的做法是:系统启动时把所有任务都建好,后面只挂起和恢复,不删。
核心原则:医疗贴片中,任务创建放在系统初始化阶段完成。运行时动态创建任务,风险高,不推荐。
4.2 任务优先级
优先级,就是任务的“官大官小”。数字越大,官越大,越先抢到CPU。
在医疗贴片里,优先级怎么分?我有个经验法则:
| 任务类型 | 优先级 | 理由 |
|---|---|---|
| 安全监控(看门狗、电压检测) | 最高(如5) | 出问题要立刻响应,否则设备可能失效 |
| 实时数据采集(心率、血氧) | 高(如4) | 数据不能丢,采样周期要严格保证 |
| 数据处理与算法 | 中(如3) | 可以稍微等一等,但不能太久 |
| 无线通信 | 中低(如2) | 偶尔延迟一下,问题不大 |
| 显示/日志 | 低(如1) | 可有可无,CPU闲了再干 |
你想想看,如果心率采集任务优先级设低了,被通信任务抢了CPU,那心率数据就断了。病人心跳骤停的那一瞬间,你正好在发数据包——这后果,谁敢担?
警告:优先级不要超过RTOS支持的最大值。FreeRTOS一般支持0到31级,但实际用5到8级就够了。级数太多,调度开销反而大。
4.3 调度策略
调度策略,就是RTOS怎么决定“下一个该谁跑”。主要有两种:抢占式和时间片轮转。
4.3.1 抢占式调度
抢占式,说白了就是“高优先级任务一来,低优先级任务立马让位”。
举个例子:
// 低优先级任务正在跑
void vLowPriorityTask(void *pvParameters) {
while(1) {
// 正在执行到这里...
// 突然,高优先级任务就绪了!
// RTOS立刻暂停我,把CPU让给高优先级任务
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 高优先级任务就绪
void vHighPriorityTask(void *pvParameters) {
while(1) {
// 传感器中断触发了,我来了
process_sensor_data();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
我在项目中遇到过一个问题:低优先级任务正在写一个全局变量,写到一半,高优先级任务抢进来,也读这个变量。结果数据就乱了。这就是经典的“资源共享问题”。解决办法?用互斥锁或者关中断保护临界区。
小技巧:抢占式调度下,临界区代码要短。我一般控制在10条指令以内。太长会影响实时性。
4.3.2 时间片轮转
时间片轮转,就是“同优先级的任务,一人跑一小会儿,轮流来”。每个任务分到一个时间片,比如1毫秒。时间到了,就换下一个。
什么时候用?当你有多个同优先级的任务,而且它们都不太急。比如两个日志记录任务,一个写SD卡,一个写串口。它们优先级一样,那就轮着来。
// 两个任务优先级相同,都是2
xTaskCreate(vLogToSD, "LogSD", 256, NULL, 2, NULL);
xTaskCreate(vLogToUART, "LogUART", 256, NULL, 2, NULL);
// 调度器会这样跑:
// LogSD 跑 1ms -> LogUART 跑 1ms -> LogSD 跑 1ms -> ...
但说实话,医疗贴片里我很少用时间片轮转。为什么?因为医疗任务大多有明确的实时性要求,我更倾向于用优先级把任务排得清清楚楚。时间片轮转反而增加了不确定性。
4.4 任务状态转换
任务在RTOS里有四种状态:运行、就绪、阻塞、挂起。我画个图你感受一下:
创建
|
v
就绪态 <---------> 运行态
| |
| v
| 阻塞态
| |
v v
挂起态 <----------- 挂起态
简单解释一下:
- 运行态:正在使用CPU。同一时刻只有一个任务在运行。
- 就绪态:啥都准备好了,就等CPU。优先级最高的就绪任务会变成运行态。
- 阻塞态:在等某个事件,比如等延时结束、等信号量、等队列消息。这是最常见的状态。
- 挂起态:被vTaskSuspend()强制暂停了。只有调用vTaskResume()才能恢复。
我举个例子,你跟着走一遍:
void vHeartTask(void *pvParameters) {
while(1) {
// 1. 一开始,任务在就绪态
// 2. 调度器选中我,变成运行态
read_sensor();
// 3. 调用vTaskDelay,进入阻塞态
vTaskDelay(pdMS_TO_TICKS(100));
// 4. 100ms后,定时器唤醒我,回到就绪态
// 5. 如果我是最高优先级,立刻又变成运行态
}
}
我曾经犯过一个低级错误:在阻塞态的任务里调用了vTaskSuspend(NULL),结果把自己挂起了。然后没有其他任务来唤醒它,整个系统就卡死了。嗯,从那以后,我每次挂起任务都会再三确认“谁来恢复它”。
避坑指南:我曾经在医疗贴片项目里,把一个高优先级任务设成了阻塞态,但忘了给它设置超时。结果它一直等一个永远不会来的信号量,CPU被低优先级任务占着,高优先级任务永远跑不了。这叫“优先级反转”。解决办法:阻塞时加超时,或者用互斥量带优先级继承。
好了,这一章就到这里。任务管理是RTOS的根基,你把它搞透了,后面讲同步、通信、中断,就顺风顺水。下一章,咱们聊聊任务间的“悄悄话”——任务同步与通信。