2. 任务管理基础:任务的概念、任务状态、任务优先级与任务控制块

大家好,我是你们的嵌入式系统讲师。今天我们来聊聊RTOS里最核心的东西——任务管理。

说实话,我刚入行那会儿,对“任务”的理解就是“一个死循环”。后来踩了不少坑才明白,任务远不止这么简单。你想想看,一个单片机里跑着十几个任务,它们怎么切换?怎么知道谁该运行、谁该等着?这些问题的答案,就在我们今天要讲的内容里。

2.1 任务到底是什么?

任务,在RTOS里,本质上是一段独立的程序代码。它有自己的栈空间,有自己的执行逻辑。

我习惯把任务比作“一个独立的小人”。每个小人都有自己的工作要做。有的负责读传感器,有的负责刷屏幕,有的负责处理网络数据。这些小人共享一个CPU,但同一时刻只能有一个小人真正在干活。

从代码角度看,一个任务通常长这样:

void task_led_blink(void *parameter)
{
    while(1)
    {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

嗯,这里要注意:任务函数绝对不能返回。一旦返回,RTOS会认为这个任务结束了,会把它清理掉。我在项目中遇到过有人不小心在任务里写了return,结果任务莫名其妙消失了,排查了半天。

2.2 任务的五种状态

一个任务在它的生命周期里,会经历几种不同的状态。说白了,就是“小人”在不同时刻的处境。

状态 含义 典型场景
就绪(Ready) 任务已准备好运行,等待CPU 刚创建的任务,或从阻塞中恢复
运行(Running) 任务正在占用CPU执行 当前正在执行的任务
阻塞(Blocked) 任务在等待某个事件或时间 调用vTaskDelay(),或等待信号量
挂起(Suspended) 任务被暂停,不参与调度 调用vTaskSuspend()
删除(Deleted) 任务已被销毁 调用vTaskDelete()

这里我特别想强调一下阻塞和挂起的区别。很多初学者容易搞混。

阻塞:任务在等某个条件。比如等延时结束、等队列有数据。条件满足了,它自动回到就绪态。

挂起:任务被强制暂停。除非有人调用vTaskResume(),否则它永远不会自己醒来。

我曾经在一个项目里,用挂起来实现“暂停所有任务”的功能。结果忘了恢复,整个系统卡死了。嗯,从那以后我每次用挂起都会再三检查恢复逻辑。

2.3 任务优先级——谁先跑?

优先级决定了任务在就绪队列里的“插队权”。数值越小,优先级越低(这是FreeRTOS的约定,其他RTOS可能相反)。

举个例子:

// 创建两个任务
xTaskCreate(task_high, "High", 128, NULL, 2, NULL);  // 优先级2
xTaskCreate(task_low,  "Low",  128, NULL, 1, NULL);  // 优先级1

当高优先级任务就绪时,它会立即抢占低优先级任务的CPU使用权。这就是所谓的“抢占式调度”。

核心原则:RTOS总是运行就绪态中优先级最高的那个任务。如果多个任务优先级相同,则采用时间片轮转。

我个人习惯把关键任务(比如电机控制、通信处理)设为高优先级,把非关键任务(比如日志打印、LED闪烁)设为低优先级。但要注意,优先级不是越高越好。优先级太高,可能导致低优先级任务永远得不到CPU——这就是“优先级反转”和“饥饿”问题。

避坑指南:我曾经在一个多传感器采集项目中,把所有采集任务都设成了相同的高优先级。结果它们互相抢占,数据采集时序全乱了。后来我改用“优先级+事件驱动”的方式才解决。记住:优先级设计要结合任务的实际时序要求。

2.4 任务控制块(TCB)——任务的身份证

每个任务在RTOS内部都有一个对应的数据结构,叫任务控制块(Task Control Block,TCB)。它记录了任务的所有信息。

说白了,TCB就是任务的“档案袋”。里面装着:

  • 任务栈指针(SP)——任务当前执行到哪里
  • 任务状态——就绪、阻塞、挂起等
  • 任务优先级——当前优先级和原始优先级
  • 任务名称——调试用的字符串
  • 任务句柄——用来操作任务的ID
  • 事件列表项——用于等待事件
  • ……

在FreeRTOS里,TCB的结构大致如下(简化版):

typedef struct tskTaskControlBlock
{
    volatile StackType_t *pxTopOfStack;    // 栈顶指针
    ListItem_t xStateListItem;             // 状态列表项
    ListItem_t xEventListItem;             // 事件列表项
    UBaseType_t uxPriority;               // 当前优先级
    StackType_t *pxStack;                  // 栈起始地址
    char pcTaskName[configMAX_TASK_NAME_LEN]; // 任务名
    // ... 还有其他字段
} tskTCB;

你想想看,当RTOS进行任务切换时,它实际上就是在做一件事:保存当前任务的TCB,恢复下一个任务的TCB。这就是上下文切换的本质。

小技巧:调试时,我经常通过打印TCB里的栈指针来检查任务栈是否溢出。如果栈指针离栈底太近,说明栈空间不够了。这个习惯帮我避免了好几次栈溢出导致的死机。

2.5 任务状态转换图

理解了上面这些,我们来看一张经典的状态转换图(文字描述版):

  1. 创建任务 → 进入就绪态
  2. 就绪态 → 被调度器选中 → 运行态
  3. 运行态 → 调用延时/等待 → 阻塞态
  4. 阻塞态 → 条件满足 → 就绪态
  5. 运行态 → 被更高优先级任务抢占 → 就绪态
  6. 运行态/就绪态 → 调用vTaskSuspend() → 挂起态
  7. 挂起态 → 调用vTaskResume() → 就绪态
  8. 运行态 → 调用vTaskDelete() → 删除态

这个转换过程,RTOS内部每时每刻都在发生。你写的每个vTaskDelay、每个信号量获取,背后都是这些状态的切换。

2.6 小结

今天的内容是RTOS的基石。任务的概念、五种状态、优先级设计、TCB结构,这些东西搞不清楚,后面学信号量、队列、互斥锁都会很吃力。

我建议你:

  • 动手创建一个简单的任务,观察它的状态变化
  • 试着调整优先级,看看调度行为怎么变
  • 打印TCB里的信息,加深理解

下一章我们会讲任务的创建、删除和常用API。到时候我会带大家手写一个简单的任务调度demo。今天就到这里,有问题随时交流。


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