3. 任务调度算法:优先级抢占式调度、时间片轮转调度、协作式调度、调度点分析

调度算法,说白了就是操作系统决定「下一个该谁跑」的规则。我做了十几年嵌入式,见过太多因为调度选型翻车的项目。有的系统跑着跑着就卡死,有的任务优先级设得乱七八糟,还有的明明CPU负载不高,响应就是慢半拍。嗯,这些坑我基本都踩过。

今天咱们就把几种主流调度算法掰开揉碎讲清楚。你想想看,一个RTOS的核心竞争力,其实就体现在调度器上。

3.1 优先级抢占式调度

这是RTOS里最常用的调度方式。它的核心逻辑很简单:谁优先级高,谁先跑。而且高优先级任务一旦就绪,可以立刻打断正在运行的低优先级任务。

我个人习惯把这种调度比作「急诊室机制」。普通门诊按号排队,但急诊病人来了,不管前面排了多少人,都得先处理。抢占式调度就是这个道理。

核心规则:

  • 每个任务分配一个优先级数值(通常数值越小优先级越高)
  • 调度器永远选择就绪态中优先级最高的任务运行
  • 高优先级任务就绪时,立即抢占当前运行的低优先级任务
  • 被抢占的任务进入就绪态,等待下次被调度

我在项目中遇到过一个问题:一个数据采集任务优先级设得特别高,结果它一启动就把所有低优先级任务都饿死了。后来我加了个「同优先级时间片轮转」,才把问题解决。这里要记住:优先级不是越高越好,够用就行

// 伪代码:优先级抢占式调度核心逻辑
void scheduler(void) {
    task_t *current = get_current_task();
    task_t *highest = find_highest_ready_task();
    
    if (highest->priority < current->priority) {
        // 高优先级任务就绪,执行抢占
        save_context(current);
        restore_context(highest);
    }
}

避坑指南:我曾经在一个项目中把所有任务优先级都设成0(最高优先级),结果系统表现跟裸机轮询差不多。优先级要有层次感,一般建议保留3-5个有效优先级层级就够了。

3.2 时间片轮转调度

时间片轮转,说白了就是「一人跑一会儿」。每个任务分配一个固定时间片,时间到了就切换到下一个同优先级的任务。它解决的是同优先级任务之间的公平性问题。

你想想看,如果没有时间片轮转,两个同优先级的任务,一个跑死循环,另一个永远没机会执行。这显然不合理。

参数 典型值 说明
时间片长度 1-10ms 取决于系统滴答时钟精度
切换开销 几微秒到几十微秒 上下文保存+恢复的时间
适用场景 同优先级任务较多 需要公平分配CPU时间

我记得有一次做工业控制器,有4个同优先级的通信任务。一开始没开时间片轮转,结果某个任务一直占用CPU,其他三个任务的数据包全丢了。开了时间片轮转后,每个任务都能按时收发数据,问题迎刃而解。

小技巧:时间片不宜设得太短。太短会导致频繁切换,上下文开销占比过高。我一般建议时间片长度是任务典型执行时间的3-5倍。

3.3 协作式调度

协作式调度,也叫非抢占式调度。它的特点是:任务主动让出CPU,调度器才能切换。任务不主动调用yield或阻塞,就一直跑下去。

这种调度方式在小型MCU上很常见,比如早期的uC/OS就有协作模式。它的优点是:没有抢占,就没有竞态问题。你不需要考虑锁、信号量这些同步机制,因为任务切换只发生在你主动让出的时刻。

但缺点也很明显:一个任务死循环,整个系统就挂了。我在一个智能家居项目里用过协作式调度,结果某个传感器的while循环忘了加延时,整个系统卡死。从那以后,我对协作式调度就格外谨慎。

// 协作式调度:任务主动让出CPU
void task_a(void) {
    while (1) {
        do_something();
        task_yield();  // 主动让出CPU
    }
}

void task_b(void) {
    while (1) {
        do_other_thing();
        task_yield();  // 主动让出CPU
    }
}

避坑指南:我曾经在一个协作式调度系统里,某个任务调用了阻塞式API(比如等待消息队列),结果它阻塞了,但调度器不知道要切换,整个系统就卡在那里。记住:协作式调度下,所有阻塞操作都必须是非阻塞的轮询版本

3.4 调度点分析

调度点,就是调度器被触发执行任务切换的时刻。理解调度点,是理解RTOS行为的关键。我见过不少开发者,代码写得挺好,但就是搞不清任务什么时候会被切换,结果调试起来一头雾水。

常见的调度点有以下几个:

  1. 时间中断:系统滴答时钟触发,检查是否有更高优先级任务就绪
  2. 任务主动阻塞:任务调用delay、等待信号量、等待消息队列等
  3. 任务主动让出:调用yield或sleep(0)
  4. 中断退出时:中断服务程序结束后,检查是否需要重新调度
  5. 创建/删除任务时:任务状态变化,可能触发调度

关键理解:调度点决定了系统的实时性上限。如果调度点只发生在时间中断,那么任务切换的最大延迟就是一个时间片。如果调度点还包含中断退出,那么高优先级任务的响应时间可以缩短到微秒级。

我个人习惯把调度点分为两类:被动调度点主动调度点。被动调度点由外部事件触发(如中断),主动调度点由任务自身行为触发(如阻塞)。理解这个分类,能帮你更好地设计系统的实时响应策略。

举个例子:一个紧急报警任务,优先级最高。如果调度点只包含时间中断,那么报警信号来了,最快也要等到下一个时间片才能响应。但如果调度点包含中断退出,报警中断一结束,调度器立刻切换,响应时间可以做到微秒级。

// 调度点示例:中断退出时触发调度
void SysTick_Handler(void) {
    // 系统滴答中断
    tick_increment();
    
    // 调度点:检查是否需要切换任务
    if (need_reschedule()) {
        scheduler();
    }
}

void EXTI_Handler(void) {
    // 外部中断,比如按键
    set_event_flag();
    
    // 调度点:中断退出前检查
    if (need_reschedule()) {
        scheduler();
    }
}

小技巧:调试调度问题时,我习惯在每个调度点加一个GPIO翻转。用示波器一看,就知道任务切换发生在哪里,频率是多少。这个方法比打印日志高效得多。

最后总结一下:优先级抢占式调度保证实时性,时间片轮转保证公平性,协作式调度保证简单性。实际项目中,往往是混合使用。比如高优先级任务用抢占式,同优先级任务用时间片轮转。调度点的设计,则决定了系统的响应极限。

嗯,调度算法这块内容比较多,但核心就这些。下一章咱们聊聊任务间通信和同步,那又是另一片天地了。