3. 调度算法(上):FCFS、SJF、RR 详解与对比

调度算法,说白了就是决定「下一个该谁跑」的规则。我做了这么多年 RTOS,见过不少因为选错调度算法导致系统崩溃的案例。今天咱们就把三个最基础的算法掰开揉碎讲清楚。

核心观点:没有最好的调度算法,只有最适合场景的调度算法。理解这三个基础算法,是设计复杂调度器的第一步。
三种基础调度算法核心逻辑 先来先服务 (FCFS) 按就绪队列顺序执行 非抢占式 短任务优先 (SJF) 按执行时间排序 可抢占/非抢占 时间片轮转 (RR) 固定时间片轮流执行 抢占式 关键区别: • FCFS:谁先来谁先跑,公平但可能「长任务堵车」 • SJF:谁短谁先跑,平均等待时间最短但「饿死长任务」 • RR:每人跑一段,响应时间均匀但「上下文切换开销大」

3.1 先来先服务(FCFS)

FCFS 是最直观的算法。任务按到达顺序排成一队,CPU 一个一个处理。就像银行柜台排队,先到先办。

我记得第一次在 8051 上写调度器时,用的就是 FCFS。当时觉得这算法太简单了,肯定不会出问题。结果呢?一个长任务占着 CPU 不放,其他任务全部超时。嗯,这就是 FCFS 的致命伤—— convoy effect(护航效应)。

避坑指南:我曾经在一个数据采集项目里用 FCFS,一个传感器校准任务跑了 200ms,导致按键响应延迟到 300ms 以上。用户按一下按钮,要等半天才有反应。后来改成时间片轮转才解决。

FCFS 的数学特性

  • 平均等待时间: 取决于任务到达顺序。短任务排在长任务后面时,等待时间会很长。
  • 吞吐量: 低。长任务阻塞 CPU,短任务无法及时执行。
  • 响应时间: 不可预测。最坏情况下,一个任务可能等所有前面的任务执行完。
指标 FCFS 表现 原因
平均等待时间 高(长任务在前时) 短任务被长任务阻塞
CPU 利用率 高(无切换开销) 非抢占式,无上下文切换
公平性 按到达顺序公平 先到先服务,逻辑简单
适用场景 批处理系统 任务执行时间相近时效果较好

3.2 短任务优先(SJF)

SJF 的思路是:谁执行时间短,谁先跑。你想想看,如果有一个 1ms 的任务和一个 100ms 的任务同时就绪,让短任务先跑,长任务等一会儿,整体等待时间是不是更短?

我个人习惯把 SJF 看作「最优平均等待时间算法」。从数学上讲,它确实能最小化平均等待时间。但实际项目中,我很少直接用纯 SJF。为什么?因为你根本不知道任务要跑多久。

注意:SJF 需要预先知道任务的执行时间。在嵌入式系统中,这通常做不到。除非你的任务是固定周期的、执行时间确定的(比如传感器采样、PID 计算)。

非抢占式 vs 抢占式 SJF

  • 非抢占式 SJF: 任务一旦开始执行,就跑到完。新来的短任务必须等当前任务结束。
  • 抢占式 SJF(SRTF): 新来的短任务如果比当前剩余时间还短,就抢占 CPU。这更灵活,但实现复杂。

我在一个电机控制项目里试过抢占式 SJF。当时想,让短任务优先处理急停信号,应该没问题。结果呢?一个长任务被频繁抢占,永远跑不完。这就是 starvation(饥饿)问题——长任务可能永远得不到 CPU。

实战建议:SJF 适合用在任务执行时间已知且差异不大的场景。如果任务时间差异很大,一定要加老化机制(aging),防止长任务饿死。

3.3 时间片轮转(RR)

RR 算法把 CPU 时间切成固定大小的「时间片」,每个任务轮流跑一个时间片。跑完就换下一个。说白了,就是「每人吃一口,吃完换人」。

我最早接触 RR 是在学习 uC/OS-II 的时候。当时觉得这算法太优雅了——每个任务都能得到响应,没有任务会被饿死。但后来发现,时间片大小选不好,系统性能会崩。

时间片大小的选择

时间片大小 优点 缺点
太小(< 1ms) 响应快,交互性好 上下文切换开销大,CPU 利用率低
适中(1-10ms) 平衡响应和效率 需要根据任务特性调整
太大(> 100ms) 切换开销小 响应慢,接近 FCFS
经验之谈:我曾经在一个 72MHz 的 STM32 上做 RR 调度,时间片设成 5ms。结果上下文切换占了 15% 的 CPU 时间。后来改成 20ms,切换开销降到 3%,但按键响应变慢了。最后折中到 10ms,两边都能接受。

RR 的上下文切换开销

每次切换都要保存当前任务的寄存器、堆栈指针,再恢复下一个任务的上下文。这个开销是固定的。如果时间片太小,切换开销占比会很高。

// 伪代码:RR 调度器的核心逻辑
void RR_Scheduler(void) {
    while (1) {
        Task *current = get_next_task_from_ready_queue();
        if (current == NULL) {
            idle(); // 没有任务就空闲
            continue;
        }
        
        // 设置定时器为时间片长度
        set_timer(TIME_SLICE_MS);
        
        // 切换到当前任务
        context_switch_to(current);
        
        // 任务执行直到时间片用完或任务主动让出 CPU
        // 定时器中断会触发下一次调度
    }
}
注意:RR 算法假设所有任务优先级相同。如果任务有优先级差异,需要结合优先级调度(比如优先级 + 时间片轮转)。

3.4 三种算法对比总结

特性 FCFS SJF RR
抢占性 非抢占 可抢占/非抢占 抢占式
平均等待时间 高 最低 中等
响应时间 差 中等 好
公平性 按到达顺序 偏向短任务 均匀分配
饥饿问题 无 长任务可能饿死 无
实现复杂度 最低 中等 中等
适用场景 批处理、简单系统 任务时间已知的系统 交互式、实时系统

选哪个算法?我的建议是:先看你的任务特性。如果任务执行时间都差不多,FCFS 就够用。如果任务时间差异大但你知道每个任务的时间,SJF 能优化平均等待时间。如果需要每个任务都响应及时,RR 是最稳妥的选择。

一句话总结:FCFS 简单但可能堵车,SJF 高效但可能饿死人,RR 公平但有切换开销。没有银弹,只有最适合你项目的算法。