4. 任务调度算法:时间片轮转、优先级抢占调度、协作式调度、调度点与上下文切换
调度算法,说白了就是RTOS的“大脑”。它决定哪个任务能跑,哪个任务得等着。我见过不少初学者,一上来就纠结“哪种调度算法最好”。其实没有最好的,只有最合适的。选错了,轻则CPU空转,重则系统崩溃。
这一章,咱们把四种核心调度机制掰开揉碎。时间片轮转、优先级抢占、协作式调度,还有调度点和上下文切换。嗯,都是硬骨头,但啃下来,你就掌握了RTOS调度的命脉。
4.1 时间片轮转调度
时间片轮转,也叫Round-Robin。它的逻辑很简单:每个任务轮流跑,每人分一个固定时间片。时间一到,强制切换。
我在一个多路数据采集项目里用过这个方案。当时有6个传感器,每个都要定期采样。用时间片轮转,每个传感器任务分10ms,一圈下来60ms,刚好满足所有采样周期。简单粗暴,效果不错。
| 参数 | 典型值 | 说明 |
|---|---|---|
| 时间片长度 | 1~100 ms | 取决于系统滴答时钟精度 |
| 任务数量 | N个 | 所有任务优先级相同 |
| 切换时机 | 时间片耗尽 | 由SysTick中断触发 |
4.2 优先级抢占调度
这是RTOS里最常用的调度方式。每个任务有个优先级,高优先级的任务随时可以打断低优先级的任务。你想想看,紧急的事情先处理,这不就是我们日常工作的逻辑吗?
优先级抢占的核心是:只要高优先级任务就绪,低优先级任务立刻被挂起。这个“立刻”很关键,它意味着响应时间是确定的。
优先级抢占的典型场景:
- 紧急报警任务:优先级最高,一旦触发立即响应
- 通信接收任务:优先级较高,防止数据丢失
- 普通计算任务:优先级较低,有空再跑
但这里有个坑——优先级反转。我记得有一次调试一个电机控制程序,高优先级的控制任务总是被莫名其妙地延迟。查了两天才发现,是低优先级的任务占着共享资源不放。嗯,这就是经典的优先级反转问题。解决办法?优先级继承或者优先级天花板协议。
4.3 协作式调度
协作式调度,也叫非抢占式调度。任务自己决定什么时候让出CPU。说白了,就是“自觉”。
这种调度方式在早期的嵌入式系统里很常见。任务跑完一段逻辑后,主动调用task_yield()或者等待某个事件。你不让,CPU就不换人。
协作式调度的优点也很明显:没有抢占,就没有复杂的同步问题。共享数据不需要加锁,因为任务切换是可控的。适合那些任务数量少、逻辑简单的系统。
4.4 调度点与上下文切换
调度点,就是RTOS决定“要不要换人”的时刻。上下文切换,就是“换人”的具体操作。这两个概念是调度的底层机制。
常见的调度点有:
- 时间片耗尽:SysTick中断里检查,该换人了
- 任务主动挂起:调用
task_delay()或等待信号量 - 中断退出:中断服务程序结束后,检查是否有更高优先级的任务就绪
- 任务创建/删除:任务状态变化,触发重新调度
上下文切换,说白了就是保存当前任务的“现场”,再恢复下一个任务的“现场”。这个“现场”包括:
- CPU寄存器(R0-R15, PC, LR, xPSR等)
- 栈指针(SP)
- 浮点寄存器(如果有FPU)
- MPU配置(如果有内存保护)
下面这张图,展示了四种调度算法的核心逻辑对比:
4.5 实际项目中的调度策略选择
说了这么多理论,咱们聊聊实际怎么选。我个人的习惯是:
- 硬实时任务(比如电机控制、刹车系统):必须用优先级抢占,而且优先级要固定,不能动态调整
- 软实时任务(比如数据采集、通信处理):优先级抢占为主,配合时间片轮转处理同优先级任务
- 非实时任务(比如日志输出、状态显示):用协作式调度或者低优先级的时间片轮转
一个典型的混合调度配置:
// 任务优先级定义
#define TASK_EMERGENCY_PRIO 5 // 最高优先级,抢占式
#define TASK_CONTROL_PRIO 4 // 高优先级,抢占式
#define TASK_COMM_PRIO 3 // 中优先级,抢占式
#define TASK_SENSOR_PRIO 2 // 低优先级,时间片轮转
#define TASK_LOG_PRIO 1 // 最低优先级,协作式
// 时间片配置(仅对同优先级任务有效)
#define TIME_SLICE_MS 20 // 每个任务20ms时间片
你想想看,如果所有任务都用抢占式,那低优先级的任务可能永远跑不了。如果都用协作式,高优先级的任务又可能被堵住。所以,混合调度才是工程实践中的常态。
调度算法的选择,没有标准答案。它取决于你的硬件资源、实时性要求、任务数量、以及开发团队的维护能力。记住一句话:简单够用就好,别为了炫技把系统搞复杂了。