RTOS内核基础:任务与调度

各位同学好,我是老张。今天咱们聊聊RTOS最核心的东西——任务与调度。说实话,我做了十几年嵌入式,见过太多人在任务调度上栽跟头。有的项目跑着跑着就卡死了,有的任务优先级设得乱七八糟,还有的堆栈配得太大浪费内存,配得太小直接崩掉。

嗯,咱们今天就把这些坑一个个填上。

任务到底是什么?

说白了,任务就是一个无限循环的函数。它有自己的栈空间,有自己的优先级,还有自己的状态。我习惯把任务想象成一个独立的小人——他要么在干活(运行态),要么在等活干(就绪态),要么在睡觉(阻塞态),要么被强行打断(挂起态)。

你想想看,一个单片机里同时跑十几个任务,每个任务都觉得自己独占CPU。这就是RTOS的魔法。

核心要点:任务 = 代码 + 栈 + 优先级 + 状态

任务状态机——任务的一生

每个任务都有四种基本状态。我在项目中遇到过新手把任务状态搞混,结果任务永远跑不起来。

  • 运行态(Running):CPU正在执行这个任务的代码。同一时刻只有一个任务能处于运行态。
  • 就绪态(Ready):任务啥都准备好了,就等CPU来执行。说白了就是排队等调度。
  • 阻塞态(Blocked):任务在等某个事件,比如等信号量、等消息队列、等延时结束。这时候它不消耗CPU。
  • 挂起态(Suspended):被强行暂停了。我一般调试的时候才用这个状态。

为什么会这样设计?你想想,如果所有任务都在抢CPU,那系统就乱套了。状态机让每个任务都知道自己该干嘛。

运行态 就绪态 阻塞态 挂起态 调度器选择 时间片用完/被抢占 等待事件 事件到达 调用挂起API 调用恢复API RTOS任务状态机

抢占式调度 vs 时间片调度

这两种调度方式,我当年刚接触时也迷糊过。咱们用大白话讲清楚。

抢占式调度

高优先级的任务来了,低优先级的任务立马让路。不管低优先级任务干到哪了,都得停下来。我做过一个电机控制项目,电流环任务优先级最高,它一来,其他任务全部靠边站。这样才能保证控制周期稳定。

我的经验:抢占式调度适合实时性要求高的场景。但要注意优先级反转问题——我曾经在一个项目中,低优先级任务拿着高优先级任务需要的资源,结果系统卡了整整两天才排查出来。

时间片调度

同优先级的任务轮流用CPU。每个任务分到一个时间片,用完了就换下一个。说白了就是大家轮着来,谁也别想独占。

我记得有个项目,三个传感器采集任务优先级相同,用时间片调度刚刚好。每个任务跑5ms,轮流采集,谁也不耽误谁。

特性 抢占式调度 时间片调度
触发条件 高优先级任务就绪 时间片用完
适用场景 实时性要求高 同优先级任务公平共享CPU
潜在问题 优先级反转、死锁 上下文切换开销大
典型应用 工业控制、汽车电子 数据采集、人机交互

任务优先级——别乱设!

优先级设置是个技术活。我见过有人把10个任务设成10个不同的优先级,结果系统调度开销大得吓人。也见过有人所有任务设成同一个优先级,结果关键任务被耽误。

我的建议是:

  1. 关键实时任务:最高优先级。比如电机控制、通信协议栈。
  2. 普通周期性任务:中等优先级。比如传感器采集、状态监测。
  3. 后台任务:最低优先级。比如日志记录、统计计算。

注意:优先级不是越多越好。FreeRTOS支持几十个优先级,但实际项目中我一般只用3-5个。优先级太多,调度器光比较优先级就够忙的了。

堆栈配置——配大了浪费,配小了崩

堆栈大小是RTOS里最让人头疼的问题之一。配大了,RAM不够用;配小了,任务跑着跑着就栈溢出,系统直接挂掉。

我曾经在一个项目中,一个任务需要处理JSON解析,堆栈配了512字节。结果一解析大文件就死机。查了两天才发现是栈溢出了,改成1024字节就没事了。

怎么估算堆栈大小?我一般这么干:

  • 先给个保守值,比如256字节或512字节
  • 跑一下任务,用调试工具看实际栈使用量
  • 加上20%的余量,作为最终配置
// FreeRTOS任务创建示例
// 堆栈大小以字为单位(1字 = 4字节)
#define TASK_STACK_SIZE   128  // 128字 = 512字节

TaskHandle_t xTaskHandle = NULL;

void vTaskFunction(void *pvParameters)
{
    // 任务代码
    for(;;)
    {
        // 干活...
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 创建任务
xTaskCreate(
    vTaskFunction,          // 任务函数
    "MyTask",               // 任务名
    TASK_STACK_SIZE,        // 堆栈大小(字)
    NULL,                   // 参数
    3,                      // 优先级(0-31,数值越大优先级越高)
    &xTaskHandle            // 任务句柄
);

调试技巧:FreeRTOS提供了uxTaskGetStackHighWaterMark()函数,可以查看任务栈还剩多少空间。我每次调试新任务都会调用这个函数,确保栈不会溢出。

避坑指南

最后分享几个我踩过的坑:

  • 优先级反转:低优先级任务拿着高优先级任务需要的互斥锁。解决办法是用优先级继承协议。
  • 栈溢出检测:一定要开启栈溢出检测功能。FreeRTOS里配置configCHECK_FOR_STACK_OVERFLOW为1或2。
  • 任务优先级不要动态改变:除非你非常清楚自己在做什么。动态改优先级容易引发不可预知的行为。
  • 空闲任务别闲着:可以在空闲任务里做低功耗处理或者系统统计。

嗯,今天的内容就到这。任务调度是RTOS的基石,搞懂了这些,后面学消息队列、信号量、互斥锁就轻松多了。


公众号:蓝海资料掘金营,微信deep3321