3、任务管理核心:任务控制块(TCB)设计、任务状态机、任务创建与删除、任务优先级管理
好,咱们今天聊聊任务管理。这是RTOS的心脏,说白了,操作系统就是靠这个来“管人”的。你想想看,一个系统里跑着几十个任务,谁先跑、谁后跑、谁被暂停了、谁在等资源——这些信息总得有个地方记下来吧?这个地方,就是任务控制块(TCB)。
3.1 任务控制块(TCB)设计
TCB是什么?每个任务在系统里都有一个“身份证”。我习惯把它叫做任务的“户口本”。里面记录了这个任务的所有家底:栈指针、状态、优先级、名字,等等。
我在项目中遇到过一个问题:有个任务跑着跑着就飞了,查了半天,发现是TCB里的栈指针被意外改写了。从那以后,我对TCB的布局就特别敏感。
一个典型的TCB结构体长这样:
typedef struct tcb {
uint32_t *sp; // 栈指针,保存CPU现场
uint32_t priority; // 任务优先级
uint32_t state; // 任务状态
void (*entry)(void *arg); // 任务入口函数
void *arg; // 入口参数
char name[16]; // 任务名字,调试用
struct tcb *next; // 链表指针
uint32_t stack_size; // 栈大小
uint32_t *stack_base; // 栈底地址
} tcb_t;
嗯,这里要注意:sp 是第一个成员。为什么?因为上下文切换时,汇编代码直接通过TCB地址偏移来访问它。放第一个,偏移就是0,省一条指令。这种细节,性能敏感的地方特别重要。
核心要点:TCB是任务管理的“元数据”。设计时,把频繁访问的成员(如sp、priority)放在前面,能提升上下文切换效率。
3.2 任务状态机
任务不是一直在跑的。它会在几个状态之间跳来跳去。这就是任务状态机。我刚开始做RTOS时,状态机画得乱七八糟,结果任务调度经常出bug。后来我总结了一个原则:状态转换的路径必须清晰,不能有“歧义”。
常见的任务状态有四种:
- 就绪(Ready):任务准备好了,随时可以跑,就等CPU分配时间片。
- 运行(Running):正在占用CPU,真正在执行代码。
- 阻塞(Blocked):在等某个事件,比如信号量、消息队列、延时。说白了,就是“我卡住了,你先跑”。
- 挂起(Suspended):被人为暂停了,不参与调度。这个状态我用的不多,但调试时很有用。
状态转换的规则,我列个表:
| 当前状态 | 触发事件 | 下一状态 |
|---|---|---|
| 就绪 | 调度器选中 | 运行 |
| 运行 | 时间片用完 | 就绪 |
| 运行 | 等待资源 | 阻塞 |
| 阻塞 | 资源就绪 | 就绪 |
| 任意 | 调用suspend | 挂起 |
| 挂起 | 调用resume | 就绪 |
我的经验:状态机里最容易出bug的地方是“阻塞→就绪”这一步。我曾经在信号量释放时忘了把任务从阻塞队列移到就绪队列,结果任务永远醒不过来。调试了一整天,最后发现是链表操作漏了一行。
3.3 任务创建与删除
任务创建,说白了就是给新任务“上户口”。系统得给它分配TCB、分配栈空间,然后把它挂到就绪链表里。
创建任务的典型流程:
- 从TCB池里拿一个空闲的TCB结构体。
- 分配栈空间(可以是静态数组,也可以是动态内存)。
- 初始化栈帧:把入口函数地址、参数、返回地址等压到栈里,模拟成“刚被中断打断”的样子。
- 设置优先级、状态为就绪。
- 把TCB插入就绪优先级链表。
代码示例:
tcb_t *task_create(void (*entry)(void *), void *arg, uint32_t prio) {
tcb_t *tcb = tcb_alloc(); // 从池中分配
if (!tcb) return NULL;
uint32_t *stack = stack_alloc(tcb->stack_size);
if (!stack) {
tcb_free(tcb);
return NULL;
}
tcb->sp = stack_init(stack, tcb->stack_size, entry, arg);
tcb->priority = prio;
tcb->state = TASK_READY;
ready_list_insert(tcb); // 插入就绪队列
return tcb;
}
删除任务呢?反过来:从就绪链表移除、释放栈、释放TCB。但有个坑:如果删除的是当前正在运行的任务怎么办?我建议的做法是:标记这个任务为“待删除”,等调度器下次切换时再真正清理。否则,你删着删着,CPU就不知道跑哪去了。
警告:千万不要在中断服务函数里直接删除任务!我曾经这么干过,结果中断返回后,系统直接崩溃。原因很简单:中断上下文和任务上下文不是一套栈,乱删会导致栈指针错乱。
3.4 任务优先级管理
优先级,就是任务的“级别”。级别高的先跑,级别低的后跑。RTOS里最常用的调度策略就是“基于优先级的抢占式调度”。
我一般把优先级分成几个档次:
- 0-31:实时任务,优先级最高。比如电机控制、传感器采集。
- 32-127:普通任务,比如数据处理、协议栈。
- 128-255:后台任务,比如日志输出、自检。
优先级管理的关键是“位图调度”。说白了,就是用一张位图来记录“当前有哪些优先级有就绪任务”。查找最高优先级时,直接查位图,O(1)复杂度。我见过有人用链表遍历来找最高优先级,任务一多,性能就崩了。
位图调度的核心代码:
uint32_t prio_bitmap; // 每一位代表一个优先级
void ready_list_insert(tcb_t *tcb) {
prio_bitmap |= (1 << tcb->priority);
// 插入对应优先级的链表
}
uint32_t get_highest_prio(void) {
// 用CLZ指令(Count Leading Zeros)找最高位
return __builtin_clz(prio_bitmap);
}
嗯,这里要注意:__builtin_clz 是GCC的内建函数,ARM Cortex-M系列有对应的硬件指令。如果你的编译器不支持,可以用查表法代替,但速度会慢一些。
避坑指南:我曾经在优先级位图里用了32位整数,但系统有64个优先级。结果高32个优先级永远调度不到。后来改成64位位图才解决。记住:位图的位数必须大于等于优先级总数。
优先级反转也是个经典问题。低优先级任务占着资源,高优先级任务等资源,中间优先级任务抢了CPU——结果高优先级任务反而跑不过中间的。解决办法是“优先级继承”或“优先级天花板协议”。这个后面讲互斥量时会细说,这里先提一嘴。
好了,任务管理的核心就这些。TCB是基础,状态机是骨架,创建删除是操作,优先级是灵魂。把这四个点吃透了,RTOS的任务管理你就掌握了八成。下一章,咱们聊聊调度器——那个真正决定“谁先跑”的家伙。