任务调度基础:任务状态机、TCB与调度策略
各位同学,今天我们来聊聊任务调度的地基。说实话,这部分内容看起来有点枯燥,但它是整个RTOS的命脉。我在做第一代金融POS机系统时,就因为状态机设计有漏洞,导致交易任务卡死,差点酿成生产事故。嗯,咱们从最核心的状态机开始。
一、任务状态机:就绪/运行/阻塞/挂起
任务在RTOS里不是一直跑着的。它会在几个状态之间跳来跳去。我习惯把这四个状态想象成人的一天:
- 就绪(Ready):任务已经准备好,就等CPU分配时间片。好比你在排队等公交,车来了就能上。
- 运行(Running):任务正在占用CPU。就是你正在刷卡消费的那一刻。
- 阻塞(Blocked):任务在等某个事件,比如等I/O完成、等信号量。就像你等公交车,车没来就只能干等。
- 挂起(Suspended):任务被强制暂停,不参与调度。好比司机临时停车,乘客不能上下车。
为什么会这样设计?你想想看,如果所有任务都在抢CPU,系统就乱套了。我在项目中遇到过一个问题:某个读卡器任务在等待硬件中断时,如果状态没切到阻塞,它会一直空转,白白浪费CPU。这就是状态机设计不严谨的后果。
核心要点:状态切换必须由内核控制,不允许任务自己乱改状态。我曾经见过新手直接修改任务状态位,结果调度器直接崩溃。
二、任务控制块(TCB)结构设计
TCB就是任务的身份证。每个任务都有一个TCB,内核靠它来管理任务。说白了,TCB就是一块内存区域,里面存着任务的所有关键信息。
我建议TCB至少包含以下字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| task_id | uint32_t | 任务唯一标识,我习惯从0开始编号 |
| state | uint8_t | 当前状态:就绪/运行/阻塞/挂起 |
| priority | uint8_t | 优先级,数值越小优先级越高 |
| stack_ptr | uint32_t* | 栈指针,保存任务上下文 |
| stack_size | uint16_t | 栈大小,单位字节 |
| delay_ticks | uint32_t | 阻塞延时计数,用于延时阻塞 |
| next | TCB_t* | 链表指针,用于就绪队列 |
来看一个简单的TCB结构体定义:
typedef struct tcb {
uint32_t task_id; // 任务ID
uint8_t state; // 任务状态
uint8_t priority; // 任务优先级
uint32_t* stack_ptr; // 栈指针
uint16_t stack_size; // 栈大小
uint32_t delay_ticks; // 延时计数
struct tcb* next; // 链表指针
} TCB_t;
个人经验:TCB的栈大小一定要预留足够。我曾经在POS机项目里,因为栈空间只给了256字节,结果任务嵌套调用时栈溢出,数据被覆盖,查了三天才找到原因。建议至少512字节起步。
三、任务优先级与调度策略
优先级是调度器的核心。我习惯用0-31级,0最高,31最低。为什么是这个范围?因为32位机上一个整型就能表示所有优先级,位运算效率极高。
调度策略主要有两种:
- 抢占式调度:高优先级任务就绪时,立即抢占当前运行的低优先级任务。POS机里按键响应任务必须用最高优先级,否则用户按了键没反应,体验极差。
- 时间片轮转:同优先级任务轮流执行。我一般给每个任务分配10ms时间片,太长影响实时性,太短上下文切换开销大。
嗯,这里要注意:优先级不要设太多。我见过有人设了256级,结果调度器光比较优先级就花了不少时间。其实POS机场景下,8-16级完全够用。
避坑指南:我曾经在项目中遇到优先级反转问题——低优先级任务持有锁,高优先级任务等锁,结果中优先级任务抢占了低优先级任务,导致高优先级任务迟迟拿不到锁。解决方案是优先级继承协议,这个后面章节会细讲。
四、调度策略的选择建议
我个人习惯这样选:
- 硬实时任务(如刷卡中断处理):抢占式,最高优先级
- 软实时任务(如显示刷新):抢占式,中等优先级
- 后台任务(如日志记录):时间片轮转,最低优先级
你想想看,如果所有任务都用抢占式,低优先级任务可能永远得不到CPU。反过来,如果都用时间片轮转,紧急任务可能被耽误。所以,混合策略才是王道。
最后说一句:调度策略没有银弹。我在不同POS机项目里用过uC/OS、FreeRTOS、甚至自己写过调度器,每个场景都要根据硬件资源和实时性要求来权衡。记住,好的调度策略能让系统跑得稳,差的调度策略能让系统跑得崩。