任务调度基础:调度器的工作原理、调度点与上下文切换
调度器,说白了就是 RTOS 的大脑。它决定「下一个该谁跑」。很多初学者觉得调度很神秘,其实拆开来看,核心就三件事:什么时候切、怎么切、切给谁。
我早年在一个工业控制项目里,就因为调度点没设计好,导致电机控制任务频繁丢步。那次排查了整整三天,最后发现是 Tick 中断频率和任务挂起逻辑打架了。嗯,从那以后我对调度器的敬畏心就上来了。
调度器的工作原理
调度器本质上是一个优先级驱动的状态机。它维护一张「就绪任务表」,每次调度发生时,从表中挑出优先级最高的任务,把 CPU 交给它。
核心调度逻辑(伪代码):
void Scheduler(void) {
// 1. 关中断,保护就绪表
EnterCritical();
// 2. 从就绪表中找到最高优先级任务
task_t *next = GetHighestReadyTask();
// 3. 如果 next 不是当前任务,就切换
if (next != current_task) {
// 保存当前任务上下文
// 恢复 next 任务上下文
ContextSwitch(current_task, next);
current_task = next;
}
// 4. 开中断
ExitCritical();
}
你想想看,这个函数每次执行的时间必须极短——微秒级。我在一个航空电子项目中,调度器本身的开销被严格限制在 5 微秒以内,超过就算故障。
| 调度器组件 | 作用 | 典型实现 |
|---|---|---|
| 就绪表 | 记录所有可运行的任务 | 位图 + 链表(uC/OS-III 风格) |
| 优先级决策 | 选出最高优先级任务 | 查找前导零指令(CLZ) |
| 上下文切换 | 保存/恢复寄存器现场 | PendSV 异常(Cortex-M) |
调度点:什么时候触发调度?
调度不会凭空发生。必须有「事件」触发调度器去查就绪表。这些事件就是调度点。常见的调度点有三种:
1. Tick 中断
系统定时器周期性产生中断,每次 Tick 中断里,内核会:
- 更新时间片计数
- 检查是否有任务延时到期
- 如果到期任务优先级更高,触发调度
我个人习惯把 Tick 频率设在 100-1000 Hz 之间。频率太低,任务响应慢;频率太高,中断开销吃掉 CPU。我曾经在一个项目中把 Tick 设到 10 kHz,结果调度器本身占了 30% 的 CPU——得不偿失。
经验值参考:
- 低功耗 IoT 设备:100 Hz(省电优先)
- 工业控制:500-1000 Hz(响应优先)
- 音频处理:1000-2000 Hz(低抖动需求)
2. 任务主动挂起
任务调用 delay()、wait()、pend() 等 API 时,会主动让出 CPU。这是最干净的调度点——任务自己知道现在不需要跑了。
void Task_ReadSensor(void *arg) {
while (1) {
sensor_data = ReadADC();
Queue_Send(&data_queue, sensor_data);
// 主动挂起,让出 CPU
Task_Delay(10); // 挂起 10 个 Tick
}
}
注意:主动挂起时,任务状态从「就绪」变为「阻塞」。调度器会立刻重新选一个任务来跑。
3. 中断退出时
中断服务程序(ISR)执行完毕后,如果它唤醒了一个更高优先级的任务,调度器会在中断返回前做一次调度。这就是「中断级调度」。
我曾经踩过的坑: 在 ISR 里调用 Task_Delay()。这是绝对禁止的!ISR 里不能阻塞,只能发信号或消息。否则调度器会崩溃,系统直接 HardFault。
上下文切换机制
上下文切换,就是「保存现场 + 恢复现场」。听起来简单,但实现细节极其敏感。
在 Cortex-M 内核上,上下文切换通常借助 PendSV 异常 来完成。为什么不用 SVC?因为 PendSV 优先级最低,可以等所有高优先级中断处理完再执行,避免抢占冲突。
上下文切换的典型步骤:
- 触发 PendSV 异常(写 ICSR 寄存器)
- 硬件自动压栈:xPSR、PC、LR、R12、R3-R0
- 软件手动压栈:R4-R11、LR(EXC_RETURN)
- 更新当前任务的 SP 指针到 TCB
- 从 TCB 中恢复下一个任务的 SP 指针
- 软件手动出栈:R4-R11、LR
- 硬件自动出栈:R0-R3、R12、LR、PC、xPSR
- 返回任务模式,继续执行
我建议你记住一个数字:上下文切换时间。在 72 MHz 的 STM32F103 上,一次完整的切换大约 1-2 微秒。如果超过 5 微秒,说明你的压栈/出栈代码有优化空间。
调度点与上下文切换的关系图
下面这张图展示了调度点如何触发上下文切换的完整流程:
调度延迟:一个容易被忽视的指标
调度延迟 = 从调度点触发到新任务开始执行的时间。它包括:
- 中断响应时间(硬件延迟)
- 调度器决策时间
- 上下文切换时间
我见过一个项目,调度延迟高达 50 微秒,原因是 Tick 中断里做了大量浮点运算。后来把浮点运算挪到任务里,延迟降到 3 微秒。所以,中断里只做最必要的事,这是铁律。
快速自查清单:
- ☐ Tick 中断处理时间 < 10% 的 Tick 周期
- ☐ 上下文切换时间 < 5 µs
- ☐ 没有在 ISR 里调用阻塞 API
- ☐ 就绪表查找使用位图或硬件指令
调度器的工作原理,说白了就是「事件驱动 + 优先级裁决」。调度点决定了系统何时响应,上下文切换决定了响应有多快。这两者配合好了,你的 RTOS 才能跑得稳、跑得快。