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 任务状态转换图
理解了上面这些,我们来看一张经典的状态转换图(文字描述版):
- 创建任务 → 进入就绪态
- 就绪态 → 被调度器选中 → 运行态
- 运行态 → 调用延时/等待 → 阻塞态
- 阻塞态 → 条件满足 → 就绪态
- 运行态 → 被更高优先级任务抢占 → 就绪态
- 运行态/就绪态 → 调用vTaskSuspend() → 挂起态
- 挂起态 → 调用vTaskResume() → 就绪态
- 运行态 → 调用vTaskDelete() → 删除态
这个转换过程,RTOS内部每时每刻都在发生。你写的每个vTaskDelay、每个信号量获取,背后都是这些状态的切换。
2.6 小结
今天的内容是RTOS的基石。任务的概念、五种状态、优先级设计、TCB结构,这些东西搞不清楚,后面学信号量、队列、互斥锁都会很吃力。
我建议你:
- 动手创建一个简单的任务,观察它的状态变化
- 试着调整优先级,看看调度行为怎么变
- 打印TCB里的信息,加深理解
下一章我们会讲任务的创建、删除和常用API。到时候我会带大家手写一个简单的任务调度demo。今天就到这里,有问题随时交流。
公众号:蓝海资料掘金营,微信deep3321