任务与任务调度:任务的定义、任务状态与调度策略

各位同学,大家好。今天我们来聊聊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%的余量,并在调试时开启栈溢出检测。
  • 阻塞超时:等待信号量或队列时,一定要设超时。否则任务可能永远阻塞,系统就卡死了。

好了,关于任务和任务调度,今天就讲到这里。记住一句话:任务不是越多越好,优先级不是越高越好。合理设计,才能让系统跑得稳、跑得快。