3. 任务控制块(TCB):TCB数据结构、任务优先级、任务上下文、任务栈空间
好,咱们今天聊聊任务控制块。说实话,TCB 是整个 RTOS 调度系统的「户口本」。没有它,任务就是一群没有身份的野孩子,系统根本不知道谁是谁。
我刚开始做嵌入式那会儿,自己手写过一个简单的调度器。当时觉得 TCB 不就是存几个变量嘛,随便搞搞就行。结果呢?任务切换的时候栈指针丢了,系统直接跑飞。嗯,从那以后我再也不敢小看 TCB 的设计了。
3.1 TCB 数据结构
TCB,全称 Task Control Block。每个任务都对应一个 TCB。说白了,它就是任务在操作系统里的「身份证」。
一个典型的 TCB 长什么样?我直接给你看代码:
typedef struct tcb {
uint32_t *stack_ptr; // 当前栈指针(SP)
uint32_t *stack_base; // 栈底地址
uint32_t stack_size; // 栈大小(字节)
uint8_t priority; // 任务优先级
uint8_t state; // 任务状态:就绪、运行、阻塞、挂起
uint8_t task_id; // 任务ID
void (*entry)(void*); // 任务入口函数
void *param; // 入口参数
uint32_t delay_ticks; // 延时计数(用于延时阻塞)
uint32_t timeout; // 超时时间
struct tcb *next; // 链表指针(用于就绪队列等)
} TCB_t;
这个结构体,我建议你背下来。为什么?因为几乎所有 RTOS 的 TCB 都大同小异。你理解了这一个,FreeRTOS、uC/OS、RT-Thread 的 TCB 你一眼就能看懂。
我个人习惯把 stack_ptr 放在第一个字段。为什么?因为任务切换时,汇编代码要频繁访问栈指针。放在第一个,偏移量为 0,访问速度最快。这是个小技巧,但性能敏感的地方很管用。
核心要点:TCB 必须包含三样东西——栈信息、优先级、状态。缺一不可。
3.2 任务优先级
优先级,说白了就是任务的「官大官小」。优先级高的任务,CPU 先伺候它。
常见的优先级设计有两种:
| 类型 | 说明 | 典型值 |
|---|---|---|
| 静态优先级 | 创建任务时定死,运行中不改 | 0~255,0 最高 |
| 动态优先级 | 运行中可以调整 | 如优先级继承、老化机制 |
我在项目中遇到过一个问题:一个低优先级的任务偶尔需要紧急处理数据,但高优先级任务一直占着 CPU。怎么办?后来用了优先级继承协议——低优先级任务临时「借」用高优先级,处理完再还回去。这招很实用。
我的建议:优先级不要用太多级。8 级、16 级足够了。级数越多,调度算法越复杂,反而容易出 bug。
3.3 任务上下文
任务上下文,就是任务在 CPU 上运行时的「快照」。包括 CPU 寄存器、程序计数器 PC、状态寄存器等。
你想想看,任务 A 运行到一半,突然被切换出去。等它再回来时,CPU 寄存器里的值必须和走的时候一模一样。否则程序就乱了。
上下文切换的典型流程:
- 保存当前任务的寄存器到它的栈中
- 更新当前任务的 TCB 中的栈指针
- 从就绪队列中选出下一个要运行的任务
- 从新任务的 TCB 中取出栈指针
- 从栈中恢复新任务的寄存器
- 跳转到新任务的 PC 位置继续执行
我曾经在 Cortex-M3 上调试一个 bug,任务切换后总是莫名其妙地死机。查了两天,发现是保存上下文时漏掉了 LR 寄存器。嗯,这种错误一次就够了,再也不会犯。
注意:上下文切换是 RTOS 最核心、最危险的地方。任何寄存器保存/恢复的顺序错误,都会导致系统崩溃。建议用汇编实现,C 语言容易出问题。
3.4 任务栈空间
每个任务都有自己的栈。栈用来存局部变量、函数调用信息、中断现场等。
栈空间怎么分配?我见过三种方式:
- 静态数组:每个任务一个固定大小的数组。简单,但浪费内存。
- 动态分配:创建任务时 malloc。灵活,但容易产生碎片。
- 内存池:预先划分好固定大小的块。兼顾效率和灵活性。
我个人偏爱内存池的方式。在系统启动时,把内存切成 256 字节、512 字节的块。任务创建时按需分配。这样既不会碎片化,分配速度也快。
栈大小怎么定?这是个老大难问题。给大了浪费,给小了溢出。
我分享一个经验:先给一个保守值,比如 512 字节。跑一段时间后,用工具查看栈的使用峰值。然后根据峰值再加 20% 的余量。我在项目中用这个方法,从来没出过栈溢出。
避坑指南:我曾经在一个项目中,任务栈设得太小。系统跑了三天才崩溃一次,查得我头都大了。后来加了栈溢出检测钩子函数,一溢出就打印日志。这才定位到问题。所以,一定要加栈溢出检测。
最后说一句,TCB 的设计直接决定了 RTOS 的稳定性和性能。你花再多时间研究它都不为过。我每次做新项目,都会重新审视 TCB 的设计,看看有没有优化的空间。这习惯,我保持了十几年。