4、任务管理:任务创建、删除、挂起、恢复与任务控制块(TCB)详解
各位好,欢迎来到第四章。
任务管理,说白了就是操作系统怎么管好你手底下的这些“干活的人”——任务。我做了这么多年嵌入式,见过太多系统崩溃,十有八九都是任务管理没搞明白。你想想看,一个实时系统里几十个任务跑来跑去,谁先跑、谁后跑、谁该歇着、谁该干活,这要是乱了套,系统不崩才怪。
这一章,咱们就把任务管理的几个核心操作掰开揉碎了讲。任务创建、删除、挂起、恢复,还有那个藏在背后的数据结构——任务控制块(TCB)。嗯,这些都是基本功,但基本功往往最见功力。
4.1 任务控制块(TCB)——任务的“身份证”
每个任务在系统里都有一个唯一的身份标识,这就是TCB。你可以把它理解成任务的“档案袋”,里面装着这个任务的所有关键信息。
我个人习惯把TCB比作一个“任务户口本”。系统调度器不看任务代码长啥样,它只看TCB。谁在TCB里登记了,谁就是合法任务;谁没登记,系统根本不认你。
一个典型的TCB结构体长这样:
typedef struct tcb {
uint32_t *stack_ptr; // 栈指针,指向当前栈顶
uint32_t *stack_base; // 栈基地址
uint32_t stack_size; // 栈大小(字节)
uint8_t priority; // 任务优先级
uint8_t state; // 任务状态:就绪、运行、挂起等
char name[16]; // 任务名称,调试用
void *msg_queue; // 消息队列指针
uint32_t delay_ticks; // 延时计数
struct tcb *next; // 链表指针
} TCB_t;
这里我要多说一句。我在项目中遇到过有人把stack_size设得特别小,结果任务一跑就栈溢出,系统直接死机。排查了两天才找到原因。所以,栈大小一定要留够余量,别抠门。
核心要点:TCB是任务管理的基石。系统调度器不直接操作任务函数,它只操作TCB。你创建任务,本质就是初始化一个TCB;你删除任务,本质就是回收这个TCB。
4.2 任务创建——让任务“活”起来
任务创建,就是把一段代码和一个TCB绑定起来,告诉系统:“嘿,这个任务可以跑了。”
创建任务时,系统会做三件事:
- 分配并初始化TCB
- 分配任务栈空间
- 初始化栈帧,让任务看起来像是“刚被切换出去”的状态
代码示例:
// 任务函数
void task_led(void *arg) {
while(1) {
GPIO_Toggle(LED_PIN);
os_delay(500); // 延时500ms
}
}
// 创建任务
TCB_t *tcb_led;
tcb_led = os_task_create(task_led, // 任务函数指针
NULL, // 参数
1024, // 栈大小
5); // 优先级5
if (tcb_led == NULL) {
// 创建失败,处理错误
error_handler();
}
注意看,os_task_create返回的是TCB指针。这个指针很重要,后续所有操作——删除、挂起、恢复——都要靠它来定位任务。
我的经验:创建任务时,优先级别设太高。我曾经一上来就把所有任务优先级设成0(最高),结果高优先级任务把CPU吃死了,低优先级任务永远得不到执行。优先级设计要像分蛋糕,得有个层次。
4.3 任务删除——让任务“安息”
任务删除,就是把任务从系统里移除。系统会回收TCB和栈空间。
但这里有个坑:你不能删除一个正在运行的任务。为什么?你想想看,任务正在执行,突然把自己删了,那栈指针、寄存器状态全乱套了。系统根本不知道下一步该干啥。
// 删除任务
os_task_delete(tcb_led);
就这么一行?对,就这么一行。但背后系统做了很多事:
- 从就绪队列中移除该任务
- 释放栈空间
- 将TCB标记为“空闲”
警告:千万不要在中断服务函数里调用任务删除!我曾经犯过这个错,中断里删了一个任务,结果系统调度器直接崩溃。中断服务函数应该只做最轻量级的工作,任务管理这种重活交给任务自己干。
4.4 任务挂起与恢复——让任务“休眠”和“唤醒”
挂起和恢复,是任务管理中我用得最多的两个操作。挂起不是删除,任务还在,只是暂时不参与调度。恢复就是让它重新回到就绪队列。
挂起一个任务:
os_task_suspend(tcb_led);
恢复一个任务:
os_task_resume(tcb_led);
就这么简单?嗯,接口确实简单,但用法有讲究。
我举个例子。我在做一个数据采集系统时,有个传感器任务只在特定条件下才需要运行。平时挂起它,条件满足时恢复它。这样既省CPU,又不用反复创建删除任务。创建删除是有开销的,挂起恢复几乎没有。
关键区别:
| 操作 | TCB是否保留 | 栈空间是否保留 | 是否可恢复 | 开销 |
|---|---|---|---|---|
| 挂起 | 是 | 是 | 是 | 小 |
| 删除 | 否 | 否 | 否 | 大 |
4.5 任务状态机——任务的一生
一个任务从创建到删除,会经历多个状态。我画个简单的状态图给你看:
- 就绪态:任务准备好了,等调度器选它
- 运行态:任务正在执行
- 挂起态:任务被挂起,不参与调度
- 阻塞态:任务在等某个事件(比如延时、信号量)
- 终止态:任务被删除,等待资源回收
任务创建后进入就绪态。调度器选中它,就变成运行态。运行中调用os_delay,就进入阻塞态。延时结束,回到就绪态。被挂起,进入挂起态。恢复后,回到就绪态。被删除,进入终止态。
这个状态流转,说白了就是任务的一生。你把这个搞清楚了,任务管理就掌握了一半。
避坑指南:我曾经在调试一个多任务系统时,发现有个任务莫名其妙不跑了。查了半天,原来是另一个任务不小心把它挂起了,而且忘了恢复。所以,挂起和恢复一定要成对使用,最好在代码里加个计数器,防止重复挂起。
4.6 实战建议——任务管理的最佳实践
最后,我总结几条实战经验,供你参考:
- 任务栈大小要留余量。我一般留30%的余量。栈溢出是嵌入式系统最难排查的问题之一。
- 优先级不要超过32级。优先级太多,调度器开销会变大。32级以内,调度效率最高。
- 挂起比删除好。如果任务需要反复启停,用挂起恢复,别用创建删除。创建删除有内存碎片风险。
- TCB指针要保护好。别让多个任务同时操作同一个TCB,加个互斥锁更安全。
- 调试时打印任务状态。我习惯在系统里加一个“任务监控”任务,定期打印所有任务的状态、栈使用率。这对排查问题帮助极大。
嗯,这一章就到这里。任务管理是实时操作系统的核心,你把这些操作练熟了,后面学信号量、消息队列、互斥锁就会轻松很多。下一章,咱们聊聊任务间的通信——信号量与互斥锁。