3. 任务控制块(TCB):任务调度的“身份证”
各位同学,咱们今天聊聊RTOS里一个核心中的核心——任务控制块,也就是TCB。你想想看,一个系统里跑着十几个、几十个任务,系统怎么知道谁是谁?谁该运行了?谁在等什么?靠的就是这个TCB。
我个人习惯把TCB比作任务的“身份证”。每个任务一创建,系统就给它发一张身份证,上面写满了这个任务的所有关键信息。这张身份证,就是TCB。
3.1 TCB数据结构:里面都装了啥?
TCB本质上就是一个结构体。不同的RTOS,这个结构体长得不太一样,但核心成员八九不离十。我最早接触uC/OS-II的时候,它的TCB结构体有几十个成员,看得我眼花缭乱。后来做项目多了,发现真正核心的,其实就那么几个。
一个典型的TCB,至少包含以下几部分:
- 任务栈指针:指向任务私有栈顶的指针。这是最重要的,没有它,任务一切换就找不到北了。
- 任务状态:当前任务处于什么状态?就绪、运行、阻塞、挂起还是休眠?系统调度器全靠这个字段做决策。
- 任务优先级:这个任务的“官阶”有多高?数字越小优先级越高,还是反过来?不同RTOS定义不同,但本质一样。
- 任务入口函数:任务从哪开始执行?说白了就是函数指针。
- 任务名称:给人看的,方便调试。我习惯用“Task_LED”、“Task_Key”这种命名,一看就懂。
- 延时/超时字段:任务要睡多久?或者等待某个事件最多等多久?
- 事件等待链表:任务在等什么信号量、消息队列?系统得知道它在等谁。
下面是一个简化版的TCB结构体,我当年在STM32上做消防报警主机时就是这么定义的:
typedef struct {
uint32_t *pStackTop; // 栈顶指针,切换任务时用
uint8_t ucPriority; // 任务优先级,0最高
uint8_t ucTaskState; // 任务状态:就绪、运行、阻塞等
void (*pvTaskFunc)(void *); // 任务入口函数
char cTaskName[16]; // 任务名字,调试用
uint32_t ulDelayTicks; // 延时节拍数
void *pvEventWait; // 等待的事件对象
} TCB_t;
嗯,这里要注意,实际产品中的TCB远比这个复杂。比如FreeRTOS的TCB里还有各种链表节点、临界区嵌套计数器等。但核心思想不变——TCB就是任务的“档案袋”。
重要提醒:TCB结构体的大小直接影响RAM占用。一个系统如果有100个任务,每个TCB占100字节,那就是10KB。在资源紧张的MCU上,这可不是小数目。我见过有人把任务名定义成64字节,结果RAM爆了……
3.2 任务栈分配:给任务一个“私人空间”
每个任务都需要自己的栈空间。为什么?因为任务切换时,CPU的寄存器、局部变量、函数调用返回地址等,都得找个地方存起来。这个“地方”,就是任务栈。
任务栈的分配,说白了就是给每个任务划一块内存。这块内存可以是静态分配的(编译时就定好了),也可以是动态分配的(运行时从堆里申请)。
静态分配:
// 静态定义一个栈空间
#define TASK_STACK_SIZE 128
static uint32_t taskStack[TASK_STACK_SIZE];
// 创建任务时传入栈地址
TCB_t xTask;
xTask.pStackTop = &taskStack[TASK_STACK_SIZE - 1];
动态分配:
// 动态申请栈空间
uint32_t *pStack = (uint32_t *)malloc(TASK_STACK_SIZE * sizeof(uint32_t));
if (pStack != NULL) {
xTask.pStackTop = &pStack[TASK_STACK_SIZE - 1];
}
我个人习惯用静态分配。为什么?因为消防报警设备对可靠性要求极高,动态分配可能产生内存碎片,万一分配失败,系统就挂了。我曾经在一个项目中吃过这个亏——系统跑了三天,突然一个任务创建失败,整个报警主机死机了。从那以后,凡是涉及安全的产品,我坚决用静态分配。
避坑指南:任务栈大小怎么定?我一般这样估算:任务函数里最深的函数调用嵌套层数 × 每层需要的寄存器数 + 中断嵌套需要的额外空间 + 20%的余量。比如一个任务最深调用5层函数,每层约20字节,中断嵌套预留100字节,那栈大小就是 5×20+100=200,再加20%余量,取240字节。当然,最靠谱的方法还是实际跑起来看栈使用率。
3.3 任务优先级存储:谁大谁小,一目了然
优先级是任务调度的核心依据。RTOS调度器每次切换任务时,都会从就绪队列里挑一个优先级最高的任务来运行。
优先级怎么存?很简单,就是一个整数。但不同RTOS的约定不同:
| RTOS | 优先级数值 | 数值越小优先级越? |
|---|---|---|
| FreeRTOS | 0 ~ configMAX_PRIORITIES-1 | 低 |
| uC/OS-II | 0 ~ 63 | 高 |
| RT-Thread | 0 ~ RT_THREAD_PRIORITY_MAX-1 | 高 |
你看,FreeRTOS是数值越大优先级越高,而uC/OS-II和RT-Thread是数值越小优先级越高。这个细节不注意,移植代码时很容易出bug。我记得有一次帮客户排查问题,发现一个高优先级任务老是被低优先级任务抢断,查了半天,原来是优先级数值搞反了……
在TCB里,优先级字段通常这样用:
// 创建任务时设置优先级
xTask.ucPriority = 5; // 假设0最高,数值越大优先级越低
// 调度器比较优先级
if (newTask.ucPriority < currentTask.ucPriority) {
// 新任务优先级更高,切换!
taskSwitch(&newTask);
}
优先级存储还有一个关键点——优先级位图。很多RTOS为了快速找到最高优先级的就绪任务,会用位图来记录哪些优先级上有任务就绪。比如一个32位的变量,每一位代表一个优先级,哪位是1,就说明那个优先级上有任务就绪。调度器只需要一条指令就能找到最高优先级,效率极高。
注意:优先级数量不是越多越好。优先级太多,调度器的查找开销会增大,而且容易导致优先级反转问题。我个人建议,一般MCU项目用8~16个优先级就足够了。消防报警系统里,我通常只用了10个优先级,从高到低分别是:紧急报警处理、通信管理、按键扫描、LED显示、蜂鸣器控制、自检任务、日志记录、空闲任务等。
小结
好了,这一章的内容就这些。TCB是RTOS的基石,理解了TCB,你就理解了任务调度的本质。下一章我们会讲任务状态与状态迁移,到时候你会看到,TCB里的状态字段是怎么配合调度器工作的。
记住一句话:没有TCB,就没有任务调度。设计TCB时,多想想你的应用场景,别盲目照搬别人的结构体。适合自己的,才是最好的。