第三章:任务与调度 — 任务状态机、优先级设计与调度策略

好,咱们进入第三章。这一章可以说是RTOS的核心中的核心。你想想看,裸机编程里,CPU就是一条道走到黑,要么在主循环里轮询,要么靠中断插队。但到了RTOS世界,事情变得有意思了——我们要让多个任务“同时”跑起来。

怎么做到的?靠的就是任务状态机、优先级设计和调度策略这三板斧。我当年从裸机转RTOS时,第一个卡住的地方就是任务状态切换。明明创建了任务,它怎么不跑?后来才明白,哦,原来它还在“就绪态”排队呢。

3.1 任务状态机:任务的生老病死

每个任务在RTOS里都不是一直运行的。它有生命周期。就像人一样,有出生、有等待、有干活、有睡觉、有死亡。

典型的RTOS任务状态包括:

  • 就绪态(Ready):任务已经准备好,随时可以运行,就等CPU分配时间
  • 运行态(Running):任务正在占用CPU,真正在执行代码
  • 阻塞态(Blocked):任务在等某个事件,比如等信号量、等消息队列、等延时结束
  • 挂起态(Suspended):任务被强制暂停,不参与调度
  • 终止态(Terminated):任务执行完毕或被删除

核心要点:一个任务同一时刻只能处于一种状态。状态之间的转换由RTOS内核控制,不是你想切就能切的。

我在项目中遇到过一个问题:一个传感器采集任务,明明优先级很高,但就是偶尔不响应。查了半天,发现是它在等一个永远不会来的信号量——任务直接卡死在阻塞态了。嗯,这就是典型的“死等”问题。

状态转换的典型路径是这样的:

  1. 任务创建 → 进入就绪态
  2. 调度器选中 → 进入运行态
  3. 调用延时或等待资源 → 进入阻塞态
  4. 延时结束或资源可用 → 回到就绪态
  5. 被更高优先级任务抢占 → 回到就绪态
  6. 任务删除 → 进入终止态

我的习惯:在调试阶段,我会在每个任务里加一个状态打印钩子。每隔几秒输出一下当前所有任务的状态。这样一眼就能看出哪个任务“卡”住了。这个习惯帮我省了无数排查时间。

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的第一步。
  • 优先级设计:硬实时最高,软实时次之,后台最低。别把所有任务都设成高优先级。
  • 调度策略:抢占式适合硬实时,时间片适合软实时。两者可以结合使用。

最后留个思考题:如果一个系统里,所有任务都是最高优先级,会发生什么?嗯,其实就等于没有优先级——大家抢着跑,谁抢到算谁的。这种情况下,系统行为变得不可预测。所以,优先级设计一定要有层次感。

下一章,咱们聊聊任务间通信——信号量、消息队列、事件标志组。这些东西是让任务之间“说话”的工具。到时候见。