4、任务调度算法(上):基于优先级的抢占式调度,时间片轮转调度,如何选择调度策略。

各位同学,咱们今天聊点硬核的。任务调度,说白了就是RTOS的“大脑”。它决定哪个任务先跑,哪个任务后跑。你想想看,一个MCU就一个核,同一时间只能干一件事。那怎么让几十个任务看起来像在“同时”运行?全靠调度算法在背后翻牌子。

我个人习惯把调度算法分成两大类:优先级驱动时间驱动。今天咱们先啃下这两块硬骨头。

4.1 基于优先级的抢占式调度

这是工业控制领域最常用的调度方式。没有之一。它的核心思想就一句话:谁优先级高,谁先跑;高优先级的来了,低优先级的立刻让路。

我在项目中遇到过好几次这种情况:一个电机控制任务正在跑,突然来了个急停信号。如果用的是非抢占式调度,那得等电机控制任务跑完才能响应急停。电机都转飞了,黄花菜都凉了。所以,抢占式调度在实时性要求高的场合,是必须的。

抢占式调度的核心规则:

  • 每个任务都有一个唯一的优先级(数值越小,优先级越高,这是uC/OS和FreeRTOS的惯例)。
  • 就绪队列中,优先级最高的任务获得CPU使用权。
  • 如果一个更高优先级的任务就绪了,当前任务会被立刻挂起,让出CPU。
  • 调度发生在:任务创建、任务阻塞、任务唤醒、中断退出时。

嗯,这里要注意。抢占式调度虽然实时性好,但有个大坑——优先级反转。我后面会专门讲,这里先提一嘴。简单说就是:低优先级任务拿着锁,高优先级任务等着用,结果中优先级任务插进来把低优先级任务抢了,高优先级任务反而被“饿死”。

咱们看个简单的代码示例,模拟一下抢占式调度的逻辑:

// 伪代码:抢占式调度核心
void scheduler(void) {
    task_t *current_task = get_current_task();
    task_t *highest_ready_task = get_highest_ready_task();

    // 如果当前任务不是最高优先级的,就换人
    if (highest_ready_task->priority < current_task->priority) {
        // 保存当前任务上下文
        save_context(current_task);
        // 恢复新任务上下文
        restore_context(highest_ready_task);
        // 切换任务
        switch_to(highest_ready_task);
    }
}

你看,逻辑其实不复杂。但真正实现的时候,临界区保护中断嵌套才是考验功力的地方。我刚开始写RTOS内核时,就因为在调度器里忘了关中断,结果任务切换时被中断打断,栈指针乱飞,查了两天bug。

避坑指南:我曾经在抢占式调度中犯过一个低级错误——在调度器里直接调用了阻塞函数。结果调度器自己把自己挂起了,整个系统死锁。记住:调度器本身不能阻塞,它必须是“原子”的。

4.2 时间片轮转调度

说完抢占式,咱们聊聊时间片轮转。这个算法,说白了就是“轮流坐庄”。每个任务分到一小段固定的CPU时间,时间到了就换下一个。不管优先级高低,大家机会均等。

你可能会问:那还要优先级干嘛?其实,时间片轮转通常和优先级配合使用。比如:相同优先级的任务之间,用时间片轮转;不同优先级之间,用抢占式。FreeRTOS就是这么干的。

我记得有个做智能家居网关的项目,里面有几个网络协议栈任务,优先级都一样。如果不用时间片轮转,一个任务可能一直霸占CPU,其他任务就饿死了。开了时间片轮转后,每个任务都能分到一点时间,系统响应就平滑多了。

时间片轮转的关键参数:

参数 说明 典型值
时间片长度 每个任务一次能跑的最大时间 1ms ~ 100ms
系统时钟节拍 RTOS的心跳,通常由定时器中断产生 1ms ~ 10ms
时间片计数 每个任务剩余的时间片数 通常为1

时间片轮转的实现也很直观:

// 伪代码:时间片轮转调度
void tick_handler(void) {
    task_t *current = get_current_task();

    // 当前任务的时间片减一
    current->time_slice--;

    // 时间片用完了,换下一个同优先级的任务
    if (current->time_slice == 0) {
        current->time_slice = DEFAULT_TIME_SLICE;
        task_t *next = get_next_ready_task(current->priority);
        if (next != NULL && next != current) {
            save_context(current);
            restore_context(next);
            switch_to(next);
        }
    }
}

这里有个细节:时间片轮转只在相同优先级的任务之间生效。如果来了个更高优先级的任务,那时间片轮转立刻被打断,高优先级任务先跑。等它跑完了,再回来继续轮转。

小技巧:时间片别设得太短。我见过有人设成100us,结果系统大部分时间都在做任务切换,真正干活的时间反而少了。一般来说,时间片是系统时钟节拍的整数倍,1ms到10ms比较合适。

4.3 如何选择调度策略?

好,两种调度算法都讲完了。那问题来了:我到底该用哪一种?

这个问题没有标准答案。但我可以给你几个实用的判断标准:

  1. 看任务的实时性要求:如果有任务必须在某个时间内响应(比如电机控制、传感器采集),那就必须用抢占式。时间片轮转没法保证响应时间。
  2. 看任务的优先级分布:如果所有任务优先级都一样,那用时间片轮转就够了。如果优先级差异很大,那抢占式更合适。
  3. 看任务的CPU占用率:如果有个任务特别耗CPU(比如复杂的数学计算),用时间片轮转可以防止它饿死其他任务。
  4. 看系统的复杂度:简单的系统,比如就两三个任务,用时间片轮转就挺好。复杂的系统,任务多、优先级多,那必须上抢占式。

我个人的经验是:工业控制领域,90%的场景用抢占式就够了。剩下的10%,比如一些数据采集、日志打印之类的非实时任务,可以放在一个低优先级任务里,然后用时间片轮转让它们公平竞争。

举个例子。我之前做的一个PLC项目:

  • 任务1:IO扫描(最高优先级,抢占式)
  • 任务2:运动控制(高优先级,抢占式)
  • 任务3:通信协议栈(中优先级,抢占式)
  • 任务4:人机界面刷新(低优先级,时间片轮转)
  • 任务5:日志记录(低优先级,时间片轮转)

你看,高优先级的任务用抢占式保证实时性,低优先级的任务用时间片轮转保证公平性。这就是典型的混合调度策略。

总结一下:

  • 抢占式调度:实时性强,适合有严格时间要求的任务。但要注意优先级反转。
  • 时间片轮转:公平性好,适合同优先级任务。但实时性无法保证。
  • 混合策略:大多数RTOS的默认选择。高优先级抢占,同优先级轮转。

好了,今天的内容就到这儿。下节课咱们接着聊调度算法的下半部分——EDF(最早截止时间优先)RMS(单调速率调度)。这两种算法在硬实时系统中非常关键,到时候我会结合一个导弹舵机控制的案例来讲。嗯,敬请期待。