本节摘要:上下文切换是「暂停一个任务、恢复另一个任务」的完整现场保护动作,它的开销由硬件入栈、内核记账、汇编搬运三段构成。本节拆解动作序列、给出周期级的开销账本,并列出减少无谓切换的工程清单——这些开销直接进入每个任务的截止期预算。
嵌入式口语里管寄存器组叫「现场」。中断进门时硬件要保现场,任务切换时内核要换现场——两个「现场」动作长得很像,开销也常被混为一谈,但它们发生在不同时机、由不同机制执行。本节把任务切换这件事从触发到收尾完整拆开,因为 3.2 节算出的每一笔响应时间预算,最终都要靠这条路径兑现。
以 Cortex-M 上的 FreeRTOS 为参照,一次由中断引起的抢占切换按时间顺序经历七步。第一步,外设事件触发中断,硬件自动把八个核心寄存器压入当前任务的栈——这一步是芯片行为,无论有没有 RTOS 都会发生。第二步,中断服务程序执行,末尾调用 FromISR 类 API 通知内核「有更高优先级任务就绪了」,此时内核并不切换,只记录待切换任务的优先级。第三步,中断出口处内核比较优先级,确认需要切换,置起 PendSV 挂起位。第四步,PendSV 异常(被刻意设为最低优先级,确保所有中断处理完才轮到它)获得执行,进入内核的切换汇编。第五步,汇编把其余寄存器压入旧任务栈,把栈顶指针存回旧任务的 TCB。第六步,从新任务的 TCB 取出栈顶指针装入处理器栈指针,弹出新任务压栈的寄存器。第七步,执行异常返回指令,硬件自动弹回八个核心寄存器,新任务从上次中断的地方继续跑。
整个过程对任务完全透明:任务醒来时并不知道处理器已被借走又还回,它看到的栈与寄存器和让出时分毫不差。

按上面的分解,168MHz 的 Cortex-M4 上一次完整切换大致在两百到五百个周期之间,折合一到三微秒;开了浮点上下文保存则接近翻倍。这个数字本身不可怕,可怕的是乘法:一个每毫秒醒来轮询一次的「看门」任务,每秒制造上千次切换,仅切换开销就吃掉百分之几的处理器时间,还向所有低优先级任务注入了周期性扰动。切换开销的正确用法是当作每任务每秒的预算项:估算各任务的唤醒频率,乘以单次开销,进不了预算就改设计。
想实测自己的系统,可以用数据观察点的周期计数器夹住切换点:
/* 用 DWT 周期计数器量一次任务切换的毛开销 */ DWT->CYCCNT = 0; /* 清零周期计数器 */ vTaskDelay(1); /* 让出:一次阻塞到唤醒的完整往返 */ uint32_t cycles = DWT->CYCCNT; /* 含延时节拍误差,仅作量级参考 */
更精确的做法是打开内核的运行时统计(configGENERATE_RUN_TIME_STATS),用统计任务读出各任务的切换次数与占用时间——它同时回答「切了多少次」与「每次切多久」两个问题。
设计意图值得单独一问:为什么不让中断服务程序直接切换,而要绕道最低优先级的 PendSV。答案是处理中断嵌套。设中断 A 处理中途又来了更高优先级的中断 B,若 A 在出口处直接切换到新任务,B 返回后还得再切换回来,多付一轮现场搬运;把切换统一推迟到 PendSV,则所有嵌套中断彻底退干净后才切一次,切换次数被压到最少。「先记账、后切换」这个模式贯穿所有 RTOS 的移植层,第二章的两条链路里它已出现过。
切换无法消灭,但可以少付。按收益排序的清单:其一,事件任务一律改阻塞等待,消灭轮询唤醒(3.1 节的反模式)。其二,中断到任务的轻量通知优先用任务通知(直接写入 TCB 的一个字),比信号量省一次内核对象的排队入队与出队。其三,合并事件:把「每个字节一个中断」改成空闲帧中断,把多次队列写入合并后一次唤醒。其四,同优先级不开时间片轮转,少一种强制切换来源。其五,节拍频率按需设置——1 千赫兹节拍下系统每秒至少一千次时钟中断,没有毫秒级截止期就别付这个钱;低功耗场景还有更彻底的 tickless 方案,6.2 节展开。
问:切换开销与节拍开销哪个大? 量级相近且都随配置变化,正确的算法是把两者都摊到秒:切换开销等于单次开销乘每秒切换次数,节拍开销等于单次开销乘节拍频率。轮询任务被消灭后,切换项通常骤降一个数量级,而节拍项要等降频或上 tickless 才明显缩小。
问:任务数多会让切换变慢吗? 切换本身的路径长度与任务数无关(找最高优先级靠位图指令),变慢的是同优先级链的处理与统计类记账。让每个关键任务独占档位,切多快依然切多快。
问:协程式的协作调度还值得用吗? 在任务数量少、行为完全可控的小系统里,协作调度消除了被动抢占,时间行为简单到透明,代价是任何一处不主动让出就拖垮全局。它适合代码全部自研、评审严格的场景;团队规模一大,主动权交给固定优先级抢占更稳。
问:怎么向项目经理解释「切换不是免费的」? 用乘法表说话:每秒唤醒次数乘以单次开销,直接给出处理器占比与电池损耗,再给一行「改为阻塞等待后」的对照数——数字对比比原理讲解有说服力得多。
问:中断会打断切换汇编吗? 会,而且这正是设计使然——切换异常是最低优先级,切换中途被中断打断后,中断退完再继续切换,现场不会乱。理解这一点就明白为什么切换汇编要写成可重入的。
在开发板上做两个小实验,切换开销会从数字变成手感。实验一:两个任务各翻转一路 GPIO,示波器量两路波形的间隔,它就是「延时到期加切换加恢复」的真实周期,对照理论值算偏差,偏差大就回头查节拍与时钟树。实验二:把其中一个任务改成轮询式(延时改成一毫秒的短循环),再看运行时统计里切换次数的暴涨与处理器占用的攀升——污染源的形态会给你留下比任何文档都深的印象。两个实验合计半小时,值得每个团队新人亲手做一遍。