4. embOS任务调度机制

embOS 的调度机制可以概括为一句话:调度器只在“安全点”运行,并在安全点从就绪任务中选出优先级最高的那个任务投入运行。理解这句话,需要把抢占式调度、时间片轮转、调度器启停、任务切换时机和调度性能测量这五个问题串起来看。它们不是五个独立知识点,而是同一套调度逻辑在不同侧面的表现。

4.1 抢占式调度:优先级决定一切

embOS 默认采用严格优先级抢占式调度。每个任务在创建时被赋予一个优先级,数值越小优先级越高,例如优先级 1 高于优先级 10。调度器维护一张就绪表,任何时刻只让“当前最高优先级的就绪任务”运行。

抢占发生在任务状态变化的瞬间。假设任务 A 优先级 5 正在运行,任务 B 优先级 3 因为等待的信号量被释放而进入就绪态,那么调度器会立即把 CPU 从 A 切换到 B,A 被放回就绪表。这就是“抢占”:高优先级任务不需要等待低优先级任务主动让出 CPU。

需要特别注意的是,embOS 的抢占不是“随时发生”,而是发生在调度器被调用的安全点。中断服务程序(ISR)中如果释放了信号量,通常不会在 ISR 内部直接切换任务,而是设置一个“切换请求”标志,等退出中断、进入调度器后才真正切换。这样做的目的是保证任务栈和内核数据结构在切换时处于一致状态。

4.2 时间片轮转:同优先级任务的公平分享

当多个任务具有相同优先级时,embOS 使用时间片轮转(Round Robin)来分配 CPU。每个任务有一个时间片计数器,单位是系统节拍(Tick)。任务运行过程中,每个 Tick 中断会递减当前任务的时间片;当时间片减到 0 时,调度器把该任务移到同优先级就绪队列的末尾,并让下一个同优先级任务运行。

时间片长度通过 OS_SetTimeSlice() 设置,也可以在创建任务时指定。例如把时间片设为 5 个 Tick,系统节拍为 1 ms,则每个同优先级任务最多连续运行 5 ms。时间片只对同优先级任务有意义:如果存在更高优先级就绪任务,抢占会先发生,时间片轮转根本轮不到。

一个常见的误区是把时间片当成“任务必须运行这么久”。实际上,如果任务在时间片用完之前主动阻塞(等待信号量、延时等),它会立刻让出 CPU,剩余时间片不会保留到下次运行。时间片轮转解决的是“同优先级任务谁先运行、运行多久”的问题,而不是“任务必须运行多久”的问题。

embOS 调度器:就绪表选择与任务切换时机 就绪表(按优先级) P1:Task_High P5:Task_Mid_A P5:Task_Mid_B P10:Task_Low 数值越小优先级越高 调度器 选最高优先级 同优先级轮转 安全点切换 CPU 运行 当前任务 Task_High 切换时机 高优先级就绪 时间片耗尽 任务主动阻塞 中断退出调度 调度器恢复 调度器只在安全点运行:任务状态变化、Tick 中断退出、显式调用 OS_Switch() 等

4.3 调度器启动与停止

embOS 程序启动后,main() 中通常先调用 OS_Init() 初始化内核,再创建若干任务,最后调用 OS_Start() 启动调度器。OS_Start() 不会返回,它从就绪表中选出最高优先级任务并开始运行。在此之前,所有任务都处于“已创建但未运行”的状态。

调度器可以被停止和恢复。调用 OS_Suspend() 会挂起调度器,此后即使有更高优先级任务就绪,也不会发生任务切换,当前任务继续运行。这通常用于临界区保护:在修改多个任务共享的数据结构时,暂时关闭调度可以避免被抢占。与之对应,OS_Resume() 恢复调度,并立即检查是否有更高优先级任务需要运行。

需要区分“挂起调度器”和“关闭中断”。挂起调度器只阻止任务切换,中断仍然可以响应;关闭中断则连中断都不响应。embOS 还提供 OS_EnterRegion() / OS_ExitRegion() 用于更细粒度的临界区保护,它们内部会处理调度器挂起和中断状态的嵌套计数。

4.4 任务切换时机

任务切换不是随时随地发生的,它只发生在调度器运行的安全点。典型的安全点包括:

  • 任务调用阻塞 API,如 OS_TaskDelay()、OS_WaitEvent()、OS_Semaphore_Wait();
  • 任务释放了某个同步对象,使更高优先级任务就绪;
  • 系统节拍中断(Tick)退出时,检查到时间片耗尽或延时到期;
  • 任务显式调用 OS_Switch() 主动让出 CPU;
  • 调度器从挂起状态恢复(OS_Resume())时。

在中断服务程序中,如果唤醒了更高优先级任务,embOS 通常只设置一个“切换待处理”标志。等 ISR 退出、进入内核调度路径后,才真正执行上下文切换。这样设计的好处是:切换发生在已知的、栈状态完整的时刻,避免在中断嵌套中直接切换带来的复杂性。

4.5 调度性能测量

调度性能通常关注两个指标:任务切换时间和调度延迟。任务切换时间指从触发切换条件到新任务第一条指令执行之间的时间;调度延迟指从事件发生(如信号量释放)到高优先级任务真正运行之间的时间。

在 embOS 中,可以利用系统节拍计数器或硬件定时器来测量。一种常见做法是:在任务 A 中翻转一个 GPIO,然后调用 OS_TaskDelay(1) 或释放信号量唤醒任务 B;任务 B 被调度后立即翻转另一个 GPIO。用示波器测量两个 GPIO 边沿之间的时间,即可得到包含调度器开销在内的切换时间。示例中若测得约 2.5 μs,这只是一个合理假设的示例值,实际结果取决于 MCU 主频、Flash 等待周期、是否使用 FPU 以及编译优化等级。

更精细的测量可以使用 Cortex-M 的 DWT 周期计数器:在切换前后读取 DWT->CYCCNT,差值除以主频即为切换耗时。需要注意,测量本身会引入少量开销,且应关闭编译器优化对测量代码的重排影响。

实践建议:测量调度性能时,先固定 CPU 主频、编译优化等级和中断优先级配置,再改变单一变量(如时间片长度、任务数量),否则测得的差异无法归因。同时应分别测量“同优先级时间片轮转切换”和“高优先级抢占切换”,两者路径不同,耗时也可能不同。

4.6 小结与配置检查

把上述机制落到配置上,可以检查以下几点:任务优先级是否按“越小越高”正确分配;同优先级任务是否设置了合理的时间片;临界区是否用 OS_Suspend() 或 OS_EnterRegion() 保护;中断中唤醒任务后是否依赖调度器在退出时完成切换;是否用 GPIO 或 DWT 对切换时间做过实际测量。这些检查点直接决定调度行为是否符合设计预期。

机制 触发条件 结果 典型 API
抢占式调度 更高优先级任务就绪 立即切换到高优先级任务 OS_Semaphore_Post 等
时间片轮转 同优先级任务时间片耗尽 轮到同优先级下一任务 OS_SetTimeSlice
调度器挂起 调用 OS_Suspend 禁止任务切换,中断仍响应 OS_Suspend / OS_Resume
主动让出 调用 OS_Switch 立即重新调度 OS_Switch

掌握这些机制后,再回头看 embOS 的调度行为,就不会把它当成黑盒:优先级决定谁先运行,时间片决定同优先级任务如何轮流,安全点决定切换何时发生,而测量则让你知道这套机制在目标硬件上的真实开销。