3. 任务管理基础:任务控制块(TCB)、任务状态机、任务创建与删除API
任务管理,说白了就是RTOS的“心脏”。你想想看,一个系统里跑着好几个任务,谁先跑、谁后跑、谁在等什么——这些都得有个规矩。而这个规矩的核心,就是任务控制块(TCB)、任务状态机,以及创建/删除的API。
我个人习惯,讲任务管理一定先从TCB入手。为什么?因为TCB就是每个任务的“身份证”。没有它,调度器根本不知道你是谁。
3.1 任务控制块(TCB)—— 任务的“身份证”
每个任务在RTOS里都有一个对应的数据结构,这就是TCB。它记录了任务的所有关键信息。我见过不少初学者,写任务时只关注函数体,却忽略了TCB的初始化——结果系统一跑就崩。
核心观点:TCB是任务存在的唯一凭证。调度器不认函数名,只认TCB指针。
一个典型的TCB结构体长什么样?我们来看一下:
typedef struct tcb {
uint32_t *sp; // 栈指针(保存CPU上下文)
uint8_t priority; // 任务优先级
uint32_t *stack_base; // 栈底地址
uint32_t stack_size; // 栈大小(单位:字)
uint32_t state; // 任务状态
char name[16]; // 任务名(调试用)
struct tcb *next; // 链表指针
uint32_t delay_ticks; // 延时计数
void *msg_queue; // 消息队列指针(可选)
} TCB_t;
这里我重点说几个字段:
- sp(栈指针)—— 这是最重要的字段。任务切换时,CPU寄存器全部压栈,sp就指向栈顶。调度器恢复任务时,第一件事就是恢复sp。
- priority—— 优先级决定了任务被调度的顺序。我在项目中遇到过一个问题:两个任务优先级设反了,结果高优先级任务一直占着CPU,低优先级任务饿死了。嗯,调了一整天才发现。
- state—— 任务当前处于什么状态,后面会细讲。
- stack_base / stack_size—— 栈空间的管理。我曾经犯过一个错:栈大小给少了,任务一跑就栈溢出,踩坏了相邻的TCB数据。从那以后,我每个任务都留出20%的栈余量。
经验之谈:TCB通常放在一个全局数组里静态分配,或者从内存池动态分配。我个人偏好静态分配——嵌入式系统里,动态分配容易产生碎片,你懂的。
3.2 任务状态机 —— 任务的一生
任务不是一直在跑的。它有多种状态,状态之间可以切换。这就是任务状态机。
RTOS里常见的状态有四种:
| 状态 | 含义 | 典型触发条件 |
|---|---|---|
| 就绪(Ready) | 任务可以运行,但CPU正被别的任务占用 | 创建完成、被唤醒、时间片用完 |
| 运行(Running) | 任务正在占用CPU执行 | 调度器选中该任务 |
| 阻塞(Blocked) | 任务在等待某个事件(延时、信号量、队列等) | 调用延时函数、等待资源 |
| 挂起(Suspended) | 任务被强制暂停,不参与调度 | 调用挂起API |
状态切换的流程,我画了一张图:
为什么会这样设计?因为任务不能一直占着CPU。你想想看,如果一个任务在等串口数据,它没必要空转——进入阻塞状态,把CPU让给别人,这才是RTOS的优雅之处。
避坑指南:我曾经在一个项目中,所有任务都只用了就绪和运行两个状态,没有用阻塞。结果CPU空转率高达70%,功耗根本压不下去。后来改成事件驱动+阻塞,CPU利用率直接降到15%。记住:阻塞不是坏事,它是RTOS的灵魂。
3.3 任务创建与删除API
有了TCB和状态机,接下来就是怎么创建和删除任务了。这两个API是RTOS的入口和出口。
3.3.1 任务创建
任务创建的典型API长这样:
// 函数原型
int task_create(
TCB_t *tcb, // 指向TCB的指针
const char *name, // 任务名
void (*entry)(void*), // 任务入口函数
void *param, // 入口函数参数
uint8_t priority, // 优先级
uint32_t *stack_base, // 栈底
uint32_t stack_size // 栈大小
);
创建任务时,RTOS内部做了三件事:
- 初始化TCB—— 填充优先级、状态设为就绪、清空链表指针。
- 初始化栈—— 在栈里预置好CPU上下文(模拟一次“刚被中断”的状态)。这样调度器第一次切换时,就能直接恢复上下文,开始执行任务函数。
- 加入就绪队列—— 把TCB挂到就绪链表中,等待调度器选中。
我建议你在创建任务时,注意以下几点:
- 栈大小要合理—— 太小会溢出,太大浪费内存。一般任务512字节起步,带复杂局部变量的给1KB。
- 优先级不要重复—— 同优先级任务用时间片轮转,但如果你想要确定性的调度行为,最好每个任务优先级不同。
- 入口函数不能返回—— 任务函数通常是一个无限循环。如果返回了,RTOS会调用一个空处理函数,任务就“死”了。
个人习惯:我写任务入口函数时,一定在函数末尾加一句 while(1){} 或者调用 task_delete(NULL) 来显式结束。这样即使逻辑出错,也不会让任务“裸奔”。
3.3.2 任务删除
删除任务比创建更危险。为什么?因为你要确保任务当前没有持有任何资源。
// 函数原型
int task_delete(TCB_t *tcb);
删除操作的核心步骤:
- 从所有队列中移除—— 就绪队列、阻塞队列、挂起队列,都得清掉。
- 释放栈空间—— 如果栈是动态分配的,这里要free。静态分配的,标记为“空闲”。
- 清空TCB—— 把TCB里的关键字段清零,防止野指针。
避坑指南:我曾经在一个项目中,直接删除了一个正在持有互斥锁的任务。结果其他任务永远等不到锁,整个系统死锁了。从那以后,我定了一条铁律:删除任务前,必须确保它不持有任何资源。如果非要删,先让任务自己释放资源,再调用删除API。
3.4 实战要点总结
任务管理这块,说难不难,说简单也不简单。我总结几个关键点:
- TCB是核心—— 所有调度操作都围绕TCB展开。调试时,先看TCB里的state和sp,基本能定位90%的问题。
- 状态切换要闭环—— 每个状态都要有明确的进入和退出条件。不要出现“任务既在就绪队列又在阻塞队列”这种荒唐事。
- 创建和删除要配对—— 动态创建的任务一定要有对应的删除逻辑,否则内存泄漏在嵌入式系统里是致命的。
嗯,任务管理的基础就这些。你把这些搞懂了,后面学调度算法、同步机制,就会轻松很多。