任务基础:TCB、状态、优先级与栈空间
好,咱们正式开始聊任务。很多初学者觉得任务调度很玄乎,其实说白了,任务就是一段可以独立运行的代码。但RTOS怎么管理这段代码?靠的就是任务控制块(TCB)。
我个人习惯把TCB想象成任务的「身份证」。系统不关心你的函数里写了什么,它只关心这张身份证上的信息——你叫什么(任务名)、你排第几(优先级)、你现在什么状态(就绪/运行/阻塞/挂起)、你的私人物品放哪(栈指针)。
核心观点:没有TCB,任务就是一段裸代码;有了TCB,任务才成为RTOS中的一等公民。
2.1 任务控制块(TCB)——任务的身份证
每个任务在创建时,内核都会分配一个TCB。TCB里存了什么?我列个表,你一看就明白。
| 字段 | 含义 | 我见过的坑 |
|---|---|---|
| 任务栈指针 | 指向任务私有栈顶 | 栈溢出时这个指针会乱飞 |
| 任务状态 | 就绪/运行/阻塞/挂起 | 状态切换漏了,任务就「死」了 |
| 优先级 | 0~255,数值越小优先级越高 | 优先级反转?我吃过亏 |
| 任务入口函数 | 任务启动时执行的函数地址 | 别把函数地址写错了 |
| 任务名称 | 调试用的字符串 | 命名规范很重要 |
| 延时/超时计数 | 用于阻塞状态计时 | 时间片轮转时容易算错 |
你看,TCB其实就是个结构体。我最早用uC/OS-II时,它的TCB大概有30多个字段。当时觉得太多了,后来自己写迷你内核才发现——每个字段都有用,少一个就出bug。
我的习惯:在项目初期就把TCB结构体定义好,哪怕有些字段暂时不用。预留总比后面加字段要安全,因为TCB是全局数据结构,改一次要重新编译所有任务代码。
2.2 任务状态机——四种状态与切换
任务不是一直在跑的。它会在四种状态之间跳来跳去:就绪、运行、阻塞、挂起。
为什么会这样?你想想看,单核CPU同一时刻只能跑一个任务。其他任务要么等着(就绪),要么在等某个事件(阻塞),要么被人为暂停(挂起)。
这张图我建议你多看几遍。我曾经在一个项目中,把阻塞和挂起搞混了——任务在等串口数据,我用了挂起而不是阻塞。结果数据来了,任务醒不过来。排查了两天才找到原因。
避坑指南:阻塞是任务主动等资源,资源到了自动恢复;挂起是被别人暂停,必须由别人恢复。这两个状态千万别搞混。
2.3 任务优先级——谁先跑谁后跑
优先级是RTOS调度的心脏。数值越小优先级越高,这是大多数RTOS的约定(FreeRTOS反过来,数值越大优先级越高,但原理一样)。
优先级怎么定?我一般遵循几个原则:
- 硬实时任务(比如电机控制)给最高优先级
- 软实时任务(比如按键扫描)给中等优先级
- 后台任务(比如日志输出)给最低优先级
但这里有个经典问题——优先级反转。低优先级任务占着资源不放,高优先级任务等资源,中优先级任务趁机抢跑。结果高优先级任务被中优先级任务「踩」了。
我记得第一次遇到这个问题是在一个工业控制器项目里。一个高优先级的报警任务被一个中优先级的UI刷新任务堵住了,报警延迟了200ms。客户差点退货。
解决方案:使用优先级继承协议或优先级天花板协议。FreeRTOS的互斥量自带优先级继承,这也是我推荐用互斥量而不是二值信号量的原因之一。
2.4 任务栈空间分配——给任务一个「私人房间」
每个任务都有自己的栈。栈里存什么?局部变量、函数调用返回地址、中断现场。如果栈不够大,任务就会踩到别人的内存——这叫栈溢出。
栈溢出是嵌入式开发中最隐蔽的bug之一。它不会立刻崩溃,而是跑着跑着突然死机,或者出现诡异的数据错误。
| 栈大小 | 适用场景 | 我的建议 |
|---|---|---|
| 64~128字节 | 极简任务,无局部数组 | 太危险,不推荐 |
| 256~512字节 | 简单控制任务 | 适合按键扫描、LED控制 |
| 1KB~2KB | 中等复杂度任务 | 适合通信协议解析 |
| 4KB+ | 复杂任务,有递归或大数组 | 注意内存总量,别超了 |
栈大小怎么估算?我有个笨办法:先把栈设大一点(比如1KB),跑一段时间后用内核提供的栈使用统计API查看峰值。然后根据峰值加30%余量。
经验之谈:我曾经在一个项目里把栈设成刚刚好,结果产品发布后客户现场偶发死机。最后发现是某个极端路径下多嵌套了两层函数调用。从那以后,我每个任务至少留50%的栈余量。
2.5 代码示例:创建一个任务
说了这么多,咱们看个实际代码。以FreeRTOS为例:
// 任务函数
void vTask1(void *pvParameters)
{
while(1)
{
// 任务逻辑
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 任务句柄(本质是指向TCB的指针)
TaskHandle_t xTask1Handle = NULL;
// 创建任务
BaseType_t xReturn = xTaskCreate(
vTask1, // 任务函数
"Task 1", // 任务名称(调试用)
256, // 栈大小,单位是字(4字节)
NULL, // 任务参数
2, // 优先级,数值越大优先级越高(FreeRTOS约定)
&xTask1Handle // 返回的任务句柄
);
if (xReturn == pdPASS)
{
// 创建成功
// 可以在这里用xTask1Handle操作任务
}
else
{
// 创建失败,通常是内存不足
// 检查堆大小或栈大小
}
注意看,xTaskCreate内部会做三件事:分配TCB、分配栈空间、把任务加入就绪列表。如果堆不够,返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。
常见错误:栈大小单位是「字」不是「字节」。在32位处理器上,256字=1024字节。我见过有人把栈设成256字节,结果任务一跑就崩。
2.6 本章小结
任务基础这块,说白了就四个东西:TCB是身份证,状态是当前处境,优先级是排队规则,栈是私人空间。这四个概念贯穿整个RTOS学习过程,后面讲调度算法、内存管理、任务间通信,全都离不开它们。
嗯,我建议你动手写一个最简单的任务,然后用调试器看看TCB里的字段值。亲眼看到状态从就绪变成运行,再变成阻塞,比看十遍书都管用。