4. 单机调度算法(下)

上一节我们聊了 FCFS 和 SJF,说白了都是「非抢占式」的调度。但现实中的操作系统,比如 Linux、Windows,它们可不会让一个进程一直霸占 CPU。你想想看,要是你打开浏览器,结果一个后台任务把 CPU 占死了,鼠标都动不了,这谁受得了?

所以,这一节我们来聊聊真正的「抢占式」调度。我重点讲两个经典算法:时间片轮转(RR)和多级反馈队列(MFQ)。嗯,这两个算法,我在做嵌入式实时系统时,都亲手调过参。

4.1 时间片轮转(RR):公平,但未必高效

时间片轮转,说白了就是「一人一票,轮流上」。每个进程分到一个时间片,比如 10 毫秒。时间一到,不管任务有没有做完,CPU 都得让给下一个进程。

这个算法的核心参数就一个:时间片大小。

核心要点:时间片太大,RR 退化成 FCFS;时间片太小,上下文切换开销会吃掉 CPU 性能。

我在项目中遇到过一个问题:一个数据采集系统,需要同时处理 8 个传感器数据流。我一开始把时间片设成 50ms,结果发现传感器 A 的数据总是丢包。为什么?因为 A 的数据量最大,50ms 根本处理不完,每次都被打断,下次轮回来又要重新加载上下文。后来我把时间片改成 100ms,问题就解决了。

这里有个经验公式:时间片 ≈ 80% 的进程能在一个时间片内完成其典型计算量。当然,这是经验值,具体还得看业务。

// 伪代码:时间片轮转调度核心逻辑
while (ready_queue not empty) {
    process = dequeue(ready_queue);
    set_timer(time_slice);  // 设置时间片定时器
    dispatch(process);      // 调度执行
    
    // 时间片到,触发中断
    if (process not finished) {
        save_context(process);
        enqueue(ready_queue, process);  // 放回队尾
    } else {
        destroy(process);
    }
}

我的建议:如果你做的是交互式系统(比如 GUI 应用),时间片别超过 50ms。人眼对 50ms 以上的延迟就有感知了。如果是纯后台批处理,可以放宽到 200ms 甚至更高。

4.2 多级反馈队列(MFQ):兼顾公平与优先级

RR 有个硬伤:它不区分任务优先级。你想想看,一个后台日志写入任务,和一个前台用户点击任务,如果都用同样的时间片,那用户体验肯定完蛋。

多级反馈队列(MFQ)就是来解决这个问题的。它的核心思想是:设置多个队列,每个队列有不同的优先级和时间片大小。新进程先进入最高优先级队列,时间片最短。如果进程在时间片内没完成,就降级到下一级队列,时间片变长。

为什么会这样设计?因为大多数进程都是「短作业」——它们很快就能完成。MFQ 让短作业优先得到响应,长作业虽然被降级,但最终也能得到 CPU 时间。

关键机制:

  • 高优先级队列:时间片短(比如 10ms),优先调度
  • 低优先级队列:时间片长(比如 100ms),轮转调度
  • 进程用完时间片未完成 → 降级到下一级队列
  • 进程主动让出 CPU(如 I/O 等待)→ 保持当前优先级

我曾经在一个网关设备上实现过 MFQ。设备要同时处理网络包转发(高优先级)和日志记录(低优先级)。我设计了三级队列:

队列级别 优先级 时间片 典型任务
Q0 最高 10ms 网络包处理、中断响应
Q1 中等 50ms 协议解析、状态机
Q2 最低 200ms 日志写入、统计上报

嗯,这里要注意:优先级反转。如果高优先级任务依赖低优先级任务的数据,而低优先级任务被降级了,高优先级任务反而要等。我当时的解决办法是:如果高优先级队列等待超过 100ms,临时提升依赖任务的优先级。

避坑指南:我曾经在 MFQ 中忘了处理「饥饿」问题。一个长计算任务被不断降级,最后在最低优先级队列里永远轮不到。后来我加了一个机制:如果某个进程在最低队列等待超过 1 秒,强制提升一级。这叫「老化」(Aging)。

4.3 我在项目中如何选择?

说实话,没有银弹。我一般按这个思路来选:

  1. 任务类型单一,且对延迟不敏感 → 用 FCFS 或 RR 就够了。比如嵌入式设备上的数据采集,所有传感器数据同等重要。
  2. 交互式系统,需要快速响应 → 用 MFQ。比如智能音箱,语音识别必须优先,音乐播放可以稍等。
  3. 实时性要求极高(毫秒级) → 别用通用调度了,上实时操作系统(RTOS)的优先级抢占调度。
  4. 混合负载,既有交互又有批处理 → MFQ 是首选。Linux 的完全公平调度器(CFS)其实也借鉴了 MFQ 的思想。

我个人习惯是:先画任务优先级矩阵。把每个任务的紧急程度和计算量画在二维图上。紧急且短的任务 → 最高优先级;不紧急且长的任务 → 最低优先级。然后根据这个矩阵来设计 MFQ 的队列数和时间片。

一个小技巧:如果你不确定时间片设多少,先设 20ms。然后跑压力测试,观察上下文切换次数和 CPU 利用率。如果上下文切换超过 CPU 时间的 5%,说明时间片太小了。如果 CPU 利用率低于 70%,说明时间片可能太大了。

4.4 知识体系:单机调度算法全景图

下面这张图,是我自己总结的。它把单机调度算法的分类和适用场景串起来了。你保存下来,面试或者做方案时拿出来看看,思路会清晰很多。

单机调度算法全景图 单机调度算法 非抢占式 抢占式 FCFS(先来先服务) SJF(短作业优先) RR(时间片轮转) MFQ(多级反馈队列) 适用场景 批处理系统、任务长度已知 对响应时间不敏感 适用场景 交互式系统、实时响应要求高 混合负载(长短任务共存) 选择建议 任务单一 → RR | 混合负载 → MFQ | 实时要求高 → RTOS

说白了,调度算法的选择,就是一场「公平」与「效率」的博弈。RR 追求公平,但可能让短任务等太久。MFQ 追求效率,但实现复杂,还要防饥饿。我个人的经验是:先跑起来,再优化。别一开始就想着完美调度,先让系统能跑,然后用性能监控工具(比如 perf、ftrace)看瓶颈在哪,再针对性调整。

嗯,这一节就到这里。记住:调度算法没有最好,只有最合适。你的业务场景,才是最终的裁判。

最后补一句:如果你在项目中遇到调度相关的坑,欢迎交流。想要更多资料,联系可可