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 我在项目中如何选择?
说实话,没有银弹。我一般按这个思路来选:
- 任务类型单一,且对延迟不敏感 → 用 FCFS 或 RR 就够了。比如嵌入式设备上的数据采集,所有传感器数据同等重要。
- 交互式系统,需要快速响应 → 用 MFQ。比如智能音箱,语音识别必须优先,音乐播放可以稍等。
- 实时性要求极高(毫秒级) → 别用通用调度了,上实时操作系统(RTOS)的优先级抢占调度。
- 混合负载,既有交互又有批处理 → MFQ 是首选。Linux 的完全公平调度器(CFS)其实也借鉴了 MFQ 的思想。
我个人习惯是:先画任务优先级矩阵。把每个任务的紧急程度和计算量画在二维图上。紧急且短的任务 → 最高优先级;不紧急且长的任务 → 最低优先级。然后根据这个矩阵来设计 MFQ 的队列数和时间片。
一个小技巧:如果你不确定时间片设多少,先设 20ms。然后跑压力测试,观察上下文切换次数和 CPU 利用率。如果上下文切换超过 CPU 时间的 5%,说明时间片太小了。如果 CPU 利用率低于 70%,说明时间片可能太大了。
4.4 知识体系:单机调度算法全景图
下面这张图,是我自己总结的。它把单机调度算法的分类和适用场景串起来了。你保存下来,面试或者做方案时拿出来看看,思路会清晰很多。
说白了,调度算法的选择,就是一场「公平」与「效率」的博弈。RR 追求公平,但可能让短任务等太久。MFQ 追求效率,但实现复杂,还要防饥饿。我个人的经验是:先跑起来,再优化。别一开始就想着完美调度,先让系统能跑,然后用性能监控工具(比如 perf、ftrace)看瓶颈在哪,再针对性调整。
嗯,这一节就到这里。记住:调度算法没有最好,只有最合适。你的业务场景,才是最终的裁判。
最后补一句:如果你在项目中遇到调度相关的坑,欢迎交流。想要更多资料,联系可可