第三章:任务创建与管理
任务管理,说白了就是RTOS的心脏。你想想看,一个实时系统里跑着十几个甚至几十个任务,怎么让它们井井有条地工作?这就得靠任务控制块、状态机、优先级这些核心机制来撑腰。
我个人习惯把任务管理比作一个公司的员工管理。每个员工(任务)都有自己的档案(任务控制块),有自己当前的状态(上班、休息、请假),还有自己的职级(优先级)。老板(内核)根据这些信息来调度大家干活。
3.1 任务控制块(TCB)—— 任务的身份证
任务控制块,简称TCB。每个任务创建时,内核都会给它分配一个TCB。这里面存了什么?我列给你看:
- 任务栈指针:指向任务自己的栈空间,保存局部变量和函数调用信息
- 任务状态:当前处于就绪、运行、阻塞还是挂起
- 优先级:数字越小优先级越高,还是反过来?各家RTOS定义不同
- 任务名称:调试时用的,方便你认出是哪个任务
- 任务入口函数:任务启动后执行的函数指针
- 延时计数器:任务要睡多久才醒
我在项目中遇到过一个问题:有个同事把TCB结构体定义得特别大,里面塞了一堆调试信息。结果呢?每个任务多占几十字节,系统里跑了50个任务,内存直接炸了。嗯,这里要注意——TCB要精简,够用就行。
下面是一个典型的TCB结构体定义:
typedef struct tcb {
uint32_t *stack_ptr; // 当前栈指针
uint32_t *stack_base; // 栈底地址
uint32_t stack_size; // 栈大小(字节)
uint8_t priority; // 任务优先级(0最高)
uint8_t state; // 任务状态
char name[16]; // 任务名称
void (*entry)(void*); // 入口函数
void *arg; // 入口参数
uint32_t delay_ticks; // 延时计数
struct tcb *next; // 链表指针
} tcb_t;
3.2 任务状态机 —— 任务的一生
任务不是一直在跑的。它会在几个状态之间跳来跳去。我画了一张图,你看一眼就明白了:
这张图里,四个状态之间的转换逻辑很清楚。我重点说一下几个容易搞混的地方:
- 就绪态 → 运行态:调度器选中了优先级最高的就绪任务
- 运行态 → 就绪态:时间片到了,或者有更高优先级的任务抢占了
- 运行态 → 阻塞态:任务主动调用延时、等待信号量、等待消息队列
- 阻塞态 → 就绪态:等待的条件满足了,比如延时结束、信号量释放
- 运行态 → 挂起态:被其他任务或中断强制挂起
- 挂起态 → 就绪态:被其他任务恢复
重要提醒:挂起态和阻塞态不一样。阻塞态是任务自己主动等的,挂起态是被别人强制暂停的。阻塞态的任务还在等条件,挂起态的任务直接不参与调度了。
3.3 任务优先级 —— 谁先谁后
优先级是RTOS调度决策的核心依据。我见过太多人在这上面栽跟头了。
先说两种常见的优先级策略:
| 策略类型 | 说明 | 典型RTOS |
|---|---|---|
| 固定优先级 | 每个任务创建时指定优先级,运行中不变 | FreeRTOS、uC/OS-II |
| 动态优先级 | 优先级可以运行时调整,防止优先级反转 | VxWorks、RT-Thread |
优先级设计有几个原则,我踩过的坑都列出来:
- 实时性要求高的任务给高优先级:比如电机控制、传感器采集
- 避免优先级反转:低优先级任务占着高优先级任务需要的资源,这事我遇到过好几次
- 不要所有任务都用同一个优先级:那跟裸机轮询有什么区别?
- 优先级数量要合理:32级够用,256级有点浪费
我曾经...在一个项目中把所有通信任务都设成了最高优先级。结果呢?低优先级的控制任务根本跑不起来,电机抖得像筛子。后来我把通信任务降了一级,控制任务升了一级,世界清净了。
3.4 任务创建与删除
任务创建,就是告诉内核:嘿,我要跑一个新任务了。内核会分配TCB、分配栈空间、把任务加入就绪队列。
创建任务的典型流程:
- 分配TCB内存(静态或动态)
- 分配任务栈空间
- 初始化栈帧(模拟一次中断返回的现场)
- 设置任务优先级、名称等属性
- 把任务加入就绪队列
代码示例:
// 任务入口函数
void led_task(void *arg) {
while(1) {
GPIO_Toggle(LED_PIN);
delay_ms(500);
}
}
// 创建任务
tcb_t *task_tcb;
uint32_t task_stack[512];
task_tcb = task_create(
"led_task", // 任务名称
led_task, // 入口函数
NULL, // 参数
task_stack, // 栈空间
sizeof(task_stack), // 栈大小
5 // 优先级
);
if (task_tcb == NULL) {
// 创建失败,处理错误
error_handler();
}
删除任务呢?就是把任务从所有队列中移除,回收TCB和栈空间。但要注意:
我个人习惯:不到万不得已不要动态删除任务。删除任务容易造成内存碎片,而且如果任务正持有某个资源,删除后资源就泄漏了。我一般用挂起代替删除,或者让任务自己退出。
3.5 任务挂起与恢复
挂起和恢复,是任务管理的两个重要操作。挂起让任务暂停运行,恢复让它重新参与调度。
挂起操作的内部流程:
- 检查任务是否在运行态或就绪态
- 如果是运行态,触发一次调度,让出CPU
- 把任务状态改为挂起态
- 从就绪队列中移除
恢复操作的内部流程:
- 检查任务是否在挂起态
- 把任务状态改为就绪态
- 加入就绪队列
- 如果恢复的任务优先级更高,触发调度
代码示例:
// 挂起任务
void task_suspend(tcb_t *tcb) {
if (tcb->state == STATE_RUNNING || tcb->state == STATE_READY) {
tcb->state = STATE_SUSPENDED;
remove_from_ready_queue(tcb);
// 如果挂起的是当前任务,触发调度
if (tcb == current_task) {
schedule();
}
}
}
// 恢复任务
void task_resume(tcb_t *tcb) {
if (tcb->state == STATE_SUSPENDED) {
tcb->state = STATE_READY;
add_to_ready_queue(tcb);
// 如果恢复的任务优先级更高,触发调度
if (tcb->priority < current_task->priority) {
schedule();
}
}
}
注意:挂起操作可以嵌套吗?有些RTOS支持多次挂起,需要恢复同样次数才能回到就绪态。我个人觉得这个设计有点多余,增加复杂度不说,还容易搞混。我建议一次挂起对应一次恢复,简单明了。
3.6 实战经验总结
讲了这么多,我总结几条实战中容易踩的坑:
- 栈空间别给太少:我见过一个任务栈溢出,把隔壁任务的TCB给覆盖了,查了两天才找到原因
- 优先级别设太高:中断服务程序里别创建任务,优先级反转要处理
- 删除任务要小心:确保任务没有持有互斥锁、信号量等资源
- 挂起任务别太久:挂起时间太长,其他任务等它的资源会饿死
嗯,任务创建与管理这块,说白了就是三板斧:TCB存信息、状态机管状态、优先级定顺序。把这三点吃透了,RTOS的任务管理你就掌握了八成。
最后分享一个小技巧:调试时把每个任务的TCB信息打印出来,看看栈使用率、状态变化、运行次数。这些数据能帮你快速定位问题。我每次做项目都会加这个调试功能,省了不少事。