本节摘要:FreeRTOS 调度器只认两条法条——固定优先级抢占(谁优先级高谁跑,高的来了低的立刻让位)与同优先级时间片轮转(同级任务按滴答轮流坐庄)。本节讲清抢占发生的全部时机、时间片的生效条件、空闲任务的隐藏职责,以及四种调度配置组合各自适合什么系统;再用一个三任务实验把规则变成可预测的现象,最后给出把"截止时间"翻译成"优先级"的设计方法。读完你能对任何任务组合画出执行时间线。
想象一个急诊室:分诊台按病情危急程度叫号,危重病人插队时轻症立即让出诊室——这是抢占;两个同样不危重的病人轮流候诊——这是时间片。调度器的全部智慧就这两条,但"什么时候检查插队条件"这个细节,决定了你对系统行为的预测能力。本节把这两条法条连同它们的执行时机一次讲透。
法条一:固定优先级抢占。每个任务一个优先级,数值越大越优先(空闲任务固定为最低的零);只要更高优先级的任务进入就绪态,调度器立刻切换——不等当前任务"把话说完"。抢占不是持续监控,而是发生在明确的检查点上:高优先级任务被某个事件唤醒时(中断送来数据、别的任务释放了信号量)、当前任务主动阻塞或让出时、每个滴答中断里。检查点之外,低优先级任务尽管跑——这正是"固定优先级"的可分析性所在:最坏响应时间的账(1.1 节)能算,就是因为抢占时机有限且可枚举。
法条二:同优先级时间片轮转。同级的就绪任务按滴答轮流执行,每个滴答中断里轮到下一个。注意它的生效范围:只影响"同一优先级上有多个就绪任务"的场景,一个级别只有一个任务时时间片完全无感。两个配置开关组合出四种行为:
| 抢占 | 时间片 | 行为 | 适用场景 |
|---|---|---|---|
| 开 | 开 | 默认:高优先级即抢,同级轮流 | 绝大多数实时系统 |
| 开 | 关 | 高优先级即抢,同级"先到先得"直到它阻塞 | 同级任务少且希望减少切换开销 |
| 关 | 开 | 协作式:任务阻塞或让出才切换,同级轮流 | 极简单系统或调试期隔离问题 |
| 关 | 关 | 纯协作式,接近前后台 | 几乎只用于教学对比 |
协作模式值得多说一句:它可以让"任何一段代码执行期间绝不被打断"(只要不阻塞),新手拿它规避竞态很诱人——但代价是高优先级任务的响应时间等于最长任务段,实时性名存实亡。正确做法是第 3 章的临界区工具:只保护真正需要原子性的那几行,而不是整个任务。
优先级还有两个工程参数要说清。最大优先级数决定就绪链表数组长度,用多少声明多少;开启"移植层优化选择"后(利用处理器的前导零计数指令)查找最高优先级任务只需数条指令,但优先级被限制在 32 级以内——对绝大多数系统绰绰有余,它默认就是开的。
优先级零上住着一个你没写却永远存在的任务:空闲任务。调度器启动时自动创建,别人都在睡时它值班。别小看这个"值班员",它有三项正经职责:回收遗体——被删除任务的栈与控制块由它在空闲态释放(2.1 节的两段式删除在此闭环);跑空闲钩子——低功耗入口、喂狗、后台统计都常挂在这里;配合无滴答休眠——第 6 章低功耗的判定前提就是"空闲任务在跑"。
空闲任务带来的一个微妙问题:如果你把业务任务也放在优先级零(和空闲同级),时间片会让空闲任务与业务任务轮流——空闲钩子被业务拖着周期性执行,行为看似正常实则浪费。配置里为此准备了一个开关:空闲任务在钩子里主动让位,把滴答让给同级的业务任务。更干净的做法是业务任务至少站在优先级一以上,别和管家同层。
⚠️ 空闲钩子里的禁令比别处都严:不许阻塞(空闲任务一卡,删除回收与低功耗全停),不许干重活(它偷的每毫秒都是全系统的空转)。喂狗、翻转观测引脚、进入休眠指令,才是它的本职。
规则背十遍不如跑一次。三个任务:高速采集(优先级三)、显示刷新(优先级二)、日志上报(优先级一),采集与日志周期性阻塞,显示是长计算。
static void vAdc(void *a) { for (;;) { read_adc(); vTaskDelay(pdMS_TO_TICKS(10)); } } static void vLcd(void *a) { busy_computation_8ms(); } /* 从不阻塞 */ static void vLog(void *a) { for (;;) { send_log(); vTaskDelay(pdMS_TO_TICKS(100)); } }
预测一下执行时间线。采集任务每 10 毫秒醒一次,干几微秒又睡;日志每 100 毫秒醒一次;显示任务永不阻塞,醒来就霸场 8 毫秒。现象:显示任务抢到处理器后一跑就是 8 毫秒——期间采集任务若到期醒来,会立刻把它打下场(抢占),采集干完睡去,显示从中断点继续。日志任务最惨:它的 100 毫秒周期被显示的 8 毫秒段和采集的频繁抢占拉长,但依然能保证"最坏晚 8 毫秒左右",账算得出来。

把采集任务的周期从 10 毫秒改到 2 毫秒再看:显示任务几乎再也凑不出连续的 8 毫秒,切换开销占比上升——这演示了"最高优先级任务的唤醒频率是全系统切换开销的旋钮"。再把显示拆成状态机(每次只算一小段就主动让出),开销立刻回落。这个实验链条建议完整做一遍,它把两条法条、抢占开销、任务拆分价值全部串了起来。
调度器只认优先级,而你的需求说的是截止时间——中间的翻译质量决定系统成败。四条翻译规则,按可信度排序:
规则一:截止越紧,优先级越高。 硬实时任务站在顶端,软实时居中,尽力而为垫底。这是响应时间分析的直接推论:高优先级任务的响应上界里包含的是"更高优先级任务的打扰",越往上越干净。
规则二:周期越短,优先级越高。 同为周期任务时,这条经典规则(按周期分配优先级)在理论上被证明是最优的静态分配之一,且实现简单——周期 1 毫秒的任务排在周期 10 毫秒前面,多数场景直接可用。
规则三:执行越短,越配上高位。 长任务霸场是响应时间的毒药;一个 5 毫秒的长计算若站上最高优先级,全系统的截止时间都要为它让路。长活要么拆碎,要么降级。
规则四:宁可层级稀疏,不要蠕变。 优先级"蠕变"指每次加需求都顺手把新任务调高半级,半年后所有任务挤在顶部两三级,时间片轮转偷偷接管了全部调度——设计者以为的优先级系统退化成了轮转系统。防御办法是在项目初期就规划优先级表:每一级写明"什么性质的任务住这里",新任务对号入座;评审时优先级数字的变更与代码一样要走说明。
💡 一个反直觉但重要的推论:优先级不是任务"重要性"的排名,而是"截止紧急度"的排名。性命攸关但截止宽松的后台自检任务,优先级应该低于无足轻重但必须微秒级响应的按键消抖——如果排反了,自检任务会亲手把按键响应拖垮。
两条法条之上还有一层隐藏机制:调度器凭什么"立刻"就能切换?保存现场、恢复现场的几十纳秒里发生了什么?下一章走进调度器底层,看滴答中断、就绪链表与上下文切换的微观世界,以及把这套机制搬到一颗新芯片上要写哪些代码。