调度器核心设计:任务控制块(TCB)、任务状态与优先级
调度器说白了就是操作系统的“大脑”。它决定哪个任务该跑,哪个任务该等。而支撑这个决策的,就是三个核心概念:任务控制块(TCB)、任务状态和优先级。
我个人习惯把TCB比作任务的“身份证”。没有它,调度器根本不知道你是谁。咱们一步步拆开看。
任务控制块(TCB)——任务的身份证
每个任务在系统里都有一个专属的数据结构,这就是TCB。它记录了任务的所有关键信息。我早期做项目时,TCB设计得太简陋,结果调试时连任务栈溢出了都查不出来。嗯,后来我学乖了。
一个典型的TCB长这样:
typedef struct {
uint32_t *sp; // 栈指针(核心)
uint8_t priority; // 任务优先级
uint8_t state; // 任务状态
uint32_t stack_size; // 栈大小
uint32_t *stack_base; // 栈基地址
void (*task_func)(void *arg); // 任务函数指针
void *arg; // 任务参数
uint32_t delay_ticks; // 延时计数
uint32_t slice_ticks; // 时间片计数
char name[16]; // 任务名(调试用)
} TCB_t;
这里我重点说几个字段:
- sp(栈指针):这是调度器的命根子。任务切换时,CPU就是靠保存和恢复这个指针来切换上下文的。
- priority:决定了任务在就绪队列里的排队顺序。数值越小优先级越高,这是业界惯例。
- state:任务当前处于什么状态,后面会细说。
- delay_ticks:任务主动延时或等待事件时用的计数器。每次系统时钟节拍中断,调度器会遍历所有任务,把非0的delay_ticks减1。
我的经验:TCB里一定要留一个name字段。别省那十几个字节。我在项目里遇到过,任务跑飞了,靠打印任务名才定位到是哪个任务出了问题。没有名字,你只能对着地址发愣。
任务状态机——从生到死
任务不是一直都能跑的。它会在几个状态之间跳来跳去。我画了一张图,你看一眼就明白了。
四个状态,你记一下:
| 状态 | 含义 | 典型触发条件 |
|---|---|---|
| 就绪态 | 任务一切就绪,只等CPU | 创建任务、延时结束、事件到达 |
| 运行态 | 正在占用CPU执行 | 调度器选中该任务 |
| 阻塞态 | 等待某件事发生 | 调用延时、等待信号量、等待队列消息 |
| 挂起态 | 被强制暂停,不参与调度 | 被其他任务挂起、调试暂停 |
我曾经踩过的坑:任务在阻塞态时,千万别去操作它的栈空间。有一次我在中断里直接修改了一个阻塞任务的TCB栈指针,结果任务恢复后栈完全乱套了。记住,阻塞态的任务,它的栈是“冻结”的,碰都不要碰。
优先级——谁先谁后
优先级是调度器做决策的核心依据。我见过两种主流方案:
- 静态优先级:任务创建时定死,运行期间不变。简单可靠,适合确定性强的系统。
- 动态优先级:运行时可以调整。比如优先级继承、优先级反转避免等场景会用到。
你想想看,如果两个任务优先级相同怎么办?这时候就需要时间片轮转了。每个任务跑一个固定时间片,然后切换到下一个。
// 优先级相同的任务,采用时间片轮转调度
void scheduler_tick_handler(void) {
TCB_t *current = get_current_task();
// 如果当前任务优先级与下一个就绪任务相同
if (current->priority == next_ready_task->priority) {
current->slice_ticks--;
if (current->slice_ticks == 0) {
// 时间片用完,强制切换
current->slice_ticks = DEFAULT_SLICE_TICKS;
schedule(); // 触发调度
}
}
}
这里有个细节:优先级反转。低优先级任务占着高优先级任务需要的资源,导致高优先级任务被阻塞。我曾在电机控制项目里遇到过,一个高优先级的急停任务被一个低优先级的通信任务堵住了,差点出事故。解决方案是优先级继承——临时把低优先级任务的优先级提到和高优先级一样,等资源释放后再降回去。
核心原则:优先级设计要遵循“关键任务高优先级,非关键任务低优先级”。中断服务函数里尽量只做标记,实际处理交给高优先级任务。别在中断里做复杂运算,那是新手常犯的错。
调度器如何用TCB工作
调度器的工作流程,说白了就是三步:
- 遍历就绪队列:从所有就绪态任务中找出优先级最高的那个。
- 上下文切换:保存当前任务的寄存器到它的TCB.sp,然后从新任务的TCB.sp恢复寄存器。
- 更新状态:旧任务变就绪态,新任务变运行态。
我习惯用位图法来管理就绪队列。每个优先级对应一个位,有就绪任务就置1。查找最高优先级时,直接用CPU的指令(比如CLZ)找第一个1的位置。时间复杂度O(1),非常快。
// 位图法就绪队列示例
#define MAX_PRIORITY 32
uint32_t ready_bitmap; // 每一位代表一个优先级是否有就绪任务
TCB_t *ready_tasks[MAX_PRIORITY]; // 每个优先级指向一个任务链表
uint8_t get_highest_ready_priority(void) {
// 使用CPU指令查找最高优先级(假设优先级数值越小优先级越高)
return __CLZ(ready_bitmap); // 返回前导零个数
}
嗯,这里要注意:位图法虽然快,但每个优先级只能有一个任务。如果同优先级有多个任务,你需要用链表把它们串起来。
好了,TCB、任务状态、优先级这三个东西,是调度器的基石。你把这些搞透了,后面讲上下文切换和时钟节拍协同的时候,就会轻松很多。