任务调度核心概念:任务、上下文、状态机、调度策略
各位同学,今天咱们来聊聊任务调度里最基础、也最绕不开的几个概念。说实话,我见过不少工程师,写了几年裸机程序,一提到「任务」就以为是 while(1) 里那个大循环。嗯,这么理解不能说全错,但到了多任务系统里,事情就没那么简单了。
我个人习惯把任务调度比作一个公司里的「项目管理」。每个任务就是一个项目组,上下文就是组里的资料和状态,状态机是项目当前在干啥,调度策略就是老板怎么分配时间。咱们一个一个拆开讲。
任务(Task)到底是什么?
任务,说白了就是一段独立的程序执行流。它有自己的栈、自己的寄存器副本、自己的优先级。你想想看,在裸机里,你只有一个 main 函数跑到底,顶多用中断插一脚。但在 RTOS 里,你可以同时创建十几个任务,它们看起来就像「同时」在跑。
核心要点:任务 = 代码段 + 栈空间 + 任务控制块(TCB)
我在项目中遇到过一个问题:有个同事创建了 20 个任务,每个任务栈都给了 1KB。结果一算,光栈就占了 20KB,MCU 总共才 64KB RAM。后来我建议他把任务栈压到 256 字节,再配合栈溢出检测,才把问题解决掉。所以啊,任务栈大小不是拍脑袋定的,得算。
任务通常有这几个属性:
- 任务名:调试时用的,方便你从任务列表里找到它
- 优先级:数字越小优先级越高,还是越大越高?看具体 RTOS 实现
- 栈大小:单位是字节,得考虑局部变量和函数调用深度
- 入口函数:任务启动后执行的第一个函数
- 参数:传给入口函数的 void 指针
上下文(Context)—— 任务的「记忆」
上下文切换,这是 RTOS 最核心的机制之一。为什么?因为 CPU 只有一个,但任务有多个。每次切换任务,你得把当前任务的「记忆」保存下来,再把下一个任务的「记忆」恢复回去。
这个「记忆」包括什么?
- 通用寄存器(R0-R12 等)
- 程序计数器(PC)
- 栈指针(SP)
- 状态寄存器(xPSR)
- 浮点寄存器(如果有 FPU)
我的经验:我曾经在 Cortex-M3 上自己写过上下文切换代码。第一次写的时候,忘了保存 LR 寄存器,结果任务切回来直接跑飞了。调试了整整一个下午才发现。所以,如果你要手写上下文切换,一定对照芯片手册把寄存器列表核对三遍。
上下文切换的时机通常有两个:
- 系统节拍中断:比如每 1ms 触发一次 SysTick,检查是否需要切换
- 任务主动挂起:任务调用 delay 或等待信号量时,主动让出 CPU
状态机(State Machine)—— 任务的生命周期
每个任务都不是从头跑到尾的。它会在几个状态之间跳来跳去。我习惯用五状态模型来理解:
| 状态 | 说明 | 典型触发条件 |
|---|---|---|
| 就绪(Ready) | 任务可以运行,但 CPU 正被别人占着 | 创建任务、从阻塞恢复 |
| 运行(Running) | CPU 正在执行这个任务的代码 | 调度器选中该任务 |
| 阻塞(Blocked) | 任务在等某个事件,比如延时、信号量、消息队列 | 调用 delay、pend 信号量 |
| 挂起(Suspended) | 任务被强制暂停,不参与调度 | 调用 vTaskSuspend |
| 终止(Terminated) | 任务执行完毕或被删除 | 函数返回、调用 vTaskDelete |
你想想看,如果任务一直在 Running 状态不下来,其他任务就永远没机会跑。这就是为什么我们要有调度策略。
注意:有些 RTOS 把 Suspended 和 Blocked 合并成一个状态,叫「非就绪」。但我觉得分开理解更清晰。阻塞是「被动等待」,挂起是「主动暂停」,两者本质不同。
调度策略:协作式 vs 抢占式
这是本章的重头戏。两种策略,两种哲学。
协作式调度(Cooperative Scheduling)
协作式,说白了就是「自觉」。每个任务自己决定什么时候让出 CPU。你不让,别人就等着。
- 优点:实现简单,没有临界区问题,资源竞争少
- 缺点:一个任务写了个死循环,整个系统就挂了
- 典型场景:超低功耗设备、状态机明确的简单系统
我记得早期做的一个温控器项目,用的就是协作式调度。每个任务都在关键位置调用 task_yield()。刚开始跑得好好的,后来加了个新功能,有个任务忘了 yield,结果温度采集任务一直没机会跑,温控直接失效了。嗯,从那以后我对协作式就多了一份警惕。
抢占式调度(Preemptive Scheduling)
抢占式就霸道多了。调度器说了算,时间到了你就得下来,不管你在干啥。
- 优点:响应快,高优先级任务不会被低优先级任务拖死
- 缺点:需要处理临界区、互斥、优先级反转等问题
- 典型场景:实时控制、通信协议栈、多媒体处理
关键区别:协作式是任务主动让出,抢占式是调度器强制切换。前者靠自觉,后者靠规则。
为什么会发生优先级反转?我举个例子:低优先级任务拿了锁,中优先级任务抢占了它,高优先级任务等锁却等不到。这就是经典问题。解决方式有优先级继承、优先级天花板协议,咱们后面章节会细讲。
一张图看懂任务调度核心概念
下面这张 SVG 图,我把任务、上下文、状态机和调度策略的关系画出来了。你看一眼就能明白它们是怎么配合的。
两种调度策略的对比
我整理了一张对比表,方便你快速对照:
| 对比维度 | 协作式 | 抢占式 |
|---|---|---|
| 切换触发 | 任务主动调用 yield | 系统节拍中断 + 优先级比较 |
| 实时性 | 差,取决于任务何时让出 | 好,高优先级任务可立即执行 |
| 临界区 | 基本不需要 | 必须用互斥量或关中断保护 |
| 代码复杂度 | 低,适合小系统 | 高,需要处理优先级反转等问题 |
| 典型应用 | 温控器、简单传感器节点 | 电机控制、通信协议栈 |
我的建议:如果你刚开始学 RTOS,先从抢占式入手。虽然复杂一点,但它是主流。协作式更适合你对系统有绝对掌控力的小项目。别一上来就搞协作式,容易踩坑。
小结一下
这一章咱们把任务调度的四个核心概念捋了一遍。任务就是执行单元,上下文是它的记忆,状态机描述它的生命周期,调度策略决定谁先跑。这四个东西,你搞懂了,后面学 RTOS 就顺了。
我曾经带过一个新人,他问我:「老师,我能不能不用 RTOS,自己写个调度器?」我说可以啊,但你先得把上下文切换和状态机搞明白。他花了两周写了个协作式调度器,跑起来没问题,但一加抢占就崩了。后来他跟我说:「原来上下文切换这么讲究。」嗯,这就是实践出真知。