任务与调度:任务状态机、优先级与调度策略

各位同学,今天我们聊聊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调度的基础。搞懂了它们,你就能设计出稳定、高效的水质监测系统。下次我们聊聊任务间通信——信号量和消息队列。