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

调度算法,说白了就是操作系统决定「下一个该谁跑」的规则。我做了这么多年嵌入式,见过不少系统因为调度策略选错而翻车。今天咱们就把三种最经典的调度算法掰开揉碎讲清楚。

4.1 时间片轮转调度(Round-Robin)

时间片轮转,我习惯叫它「大家轮流玩」策略。每个任务分一个固定时间片,时间一到就换人。公平,但未必高效。

核心思想:所有就绪任务排成队列,每个任务运行一个时间片(通常1-100ms),然后挂到队尾,让下一个任务运行。

我在一个智能家居项目里用过这种调度。当时有6个传感器采集任务,每个任务跑10ms时间片。嗯,效果还行,但有个温度传感器偶尔会超时——时间片不够它完成一次完整的数据处理。

// 时间片轮转调度伪代码
void RoundRobin_Scheduler(void) {
    while (1) {
        Task_t *current = GetNextTaskFromReadyQueue();
        if (current != NULL) {
            current->remaining_ticks = TIME_SLICE;
            // 切换到这个任务
            TaskSwitch(current);
            // 时间片用完或任务主动让出CPU
            if (current->remaining_ticks == 0) {
                // 挂到队尾
                AppendToReadyQueue(current);
            }
        }
    }
}
优点 缺点
公平性好,每个任务都能得到CPU 实时性差,紧急任务可能被延迟
实现简单,代码量小 上下文切换开销大(时间片越小越明显)
适合同优先级任务 不适合有严格截止时间的任务

我的经验:时间片大小选系统心跳的整数倍。我一般设10-50ms,太短切换开销大,太长响应变慢。你想想看,如果时间片设1ms,系统每秒光切换就要花掉不少CPU。

4.2 优先级抢占调度(Priority Preemptive)

这个就厉害了。每个任务有优先级,高优先级的任务随时可以打断低优先级的任务。说白了就是「官大一级压死人」。

我曾经在一个无人机飞控项目里用过这种调度。姿态控制任务优先级最高,必须每1ms响应一次。GPS数据采集优先级低一些,晚几十毫秒没关系。如果不用抢占式,飞机早就摔了。

// 优先级抢占调度伪代码
void Preemptive_Scheduler(void) {
    while (1) {
        Task_t *highest = FindHighestPriorityTask();
        if (highest != NULL) {
            // 如果当前运行的任务优先级更低,就抢占
            if (current_task != NULL && 
                current_task->priority < highest->priority) {
                // 保存当前任务上下文
                SaveContext(current_task);
                // 切换到高优先级任务
                TaskSwitch(highest);
            }
        }
    }
}

注意:优先级反转是个大坑。我曾经在一个项目中,低优先级任务拿了锁,中优先级任务一直跑,高优先级任务等锁等到超时。最后加了优先级继承协议才解决。

优先级抢占调度有两种常见策略:

  • 固定优先级:优先级在创建时定死,不改。适合任务角色明确的系统。
  • 动态优先级:优先级可以调整,比如根据任务剩余时间动态提升。更灵活,但实现复杂。
特性 固定优先级 动态优先级
可预测性
实现复杂度
适用场景 硬实时系统 软实时系统

4.3 协作式调度(Cooperative)

协作式调度,也叫非抢占式调度。任务自己决定什么时候让出CPU。说白了就是「自觉排队,不插队」。

我记得早期做的一个8位MCU项目,RAM只有256字节,跑不了复杂的抢占式内核。就用协作式调度,每个任务主动调用task_yield()让出CPU。虽然简单,但够用。

// 协作式调度伪代码
void Cooperative_Scheduler(void) {
    while (1) {
        Task_t *current = GetNextTaskFromReadyQueue();
        if (current != NULL) {
            // 运行当前任务,直到它主动让出CPU
            TaskSwitch(current);
            // 任务主动调用 task_yield() 后回到这里
            // 把任务挂到队尾
            AppendToReadyQueue(current);
        }
    }
}

关键区别:协作式调度中,任务不主动让出CPU,其他任务就别想跑。所以每个任务里不能有死循环或长时间阻塞操作。

协作式调度的优缺点很明显:

  • 优点:实现极简单,不需要定时器中断,不需要保存/恢复上下文。RAM占用极小。
  • 缺点:一个任务写崩了,整个系统就挂了。实时性完全靠任务自觉。

避坑指南:我曾经在一个协作式系统里,有个同事在任务里写了个while(1)等待外设就绪。结果其他任务全卡死了。后来我规定:所有等待操作必须用状态机实现,不能阻塞。

4.4 三种调度算法对比

咱们用一张表总结一下:

特性 时间片轮转 优先级抢占 协作式
实时性
公平性 低(高优先级任务霸占CPU) 中(依赖任务自觉)
实现复杂度
RAM占用 高(每个任务需要独立栈)
典型应用 多用户系统、简单控制 实时控制、通信协议栈 超小资源MCU、简单循环

4.5 实际项目中的选择策略

说实话,实际项目中很少只用一种调度算法。我常用的做法是:

  1. 关键实时任务用优先级抢占,保证响应时间
  2. 同优先级批量任务用时间片轮转,保证公平
  3. 后台低功耗任务用协作式,减少切换开销

比如我之前做的一个工业控制器:

  • 电机控制任务:优先级最高,抢占式,每100μs响应一次
  • 通信协议栈任务:中等优先级,时间片轮转,每个10ms
  • 日志记录任务:最低优先级,协作式,有空就跑

你想想看,如果所有任务都用抢占式,电机控制倒是爽了,但日志任务可能永远跑不了。如果全用协作式,电机控制又可能被日志任务拖死。混合使用才是王道。

我的建议:选调度算法前,先列清楚每个任务的实时性要求、执行时间、周期。然后画个时序图,看看最坏情况下能不能满足。别凭感觉选,要算清楚。

任务调度算法知识体系 任务调度算法 时间片轮转 优先级抢占 协作式 • 公平性好 • 实现简单 • 实时性中等 • 适合同优先级任务 • 实时性高 • 实现复杂 • 可能有优先级反转 • 适合硬实时系统 • 实现最简单 • RAM占用极小 • 实时性差 • 适合超小资源MCU

嗯,三种调度算法各有千秋。选哪个,取决于你的项目需求、硬件资源、还有你的胆量。我个人建议:新手先从协作式入手,理解任务切换的本质;然后尝试时间片轮转,感受公平调度;最后再挑战优先级抢占,处理各种竞态问题。

记住一点:没有完美的调度算法,只有适合的调度算法。你想想看,一个跑在8位MCU上的温控器,非要上抢占式RTOS,那不是杀鸡用牛刀吗?


公众号:蓝海资料掘金营,微信deep3321