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的根基,你把它搞透了,后面讲同步、通信、中断,就顺风顺水。下一章,咱们聊聊任务间的“悄悄话”——任务同步与通信。