4、短作业优先(SJF):非抢占式与抢占式、最短剩余时间优先(SRTF)、饥饿问题

短作业优先,简称 SJF。这名字听着挺直白——谁活少谁先干。但实际用起来,坑不少。

我刚开始做调度器的时候,觉得这算法太简单了。不就是排个序嘛。后来被生产环境狠狠教育了一顿。嗯,咱们今天就把这个算法掰开揉碎了讲。

4.1 核心思想:谁短谁先跑

SJF 的目标很明确:最小化平均等待时间。你想想看,一个 2ms 的进程和一个 20ms 的进程同时等着,让短的先跑,长的后跑,平均等待时间肯定更低。

数学上可以证明,SJF 在「平均周转时间」这个指标上是最优的。但注意,我说的是「理论上」。实际中你根本不知道一个进程到底要跑多久。

关键点:SJF 需要预知进程的 CPU 执行时间。这是它最大的软肋。

4.2 非抢占式 SJF

非抢占式,说白了就是:一旦进程开始运行,就让它跑到完。中途不打断。

举个例子:

进程  到达时间  执行时间
P1    0         7
P2    2         4
P3    4         1
P4    5         4

非抢占式 SJF 的执行顺序是这样的:

  1. 时间 0,只有 P1 到达,P1 开始运行
  2. 时间 7,P1 结束。此时 P2、P3、P4 都已到达。选执行时间最短的 P3(1ms)
  3. 时间 8,P3 结束。剩下 P2(4ms)和 P4(4ms),随便选一个,比如 P2
  4. 时间 12,P2 结束,P4 开始
  5. 时间 16,全部结束

平均等待时间 = (0 + (7-2) + (7-4) + (8-5)) / 4 = (0 + 5 + 3 + 3) / 4 = 2.75ms

我在项目中遇到过一个问题:非抢占式 SJF 对短进程的响应时间其实并不友好。因为如果有一个长进程先到了,它会把 CPU 占住,后面来的短进程只能干等着。这在交互式系统里是灾难。

4.3 抢占式 SJF:最短剩余时间优先(SRTF)

抢占式版本,也叫 SRTF。核心逻辑变了:每次新进程到达时,比较当前运行进程的剩余时间和新进程的执行时间。如果新来的更短,就抢过来。

还是刚才的例子:

进程  到达时间  执行时间
P1    0         7
P2    2         4
P3    4         1
P4    5         4

SRTF 的执行过程:

  1. 时间 0,P1 到达,开始运行
  2. 时间 2,P2 到达。P1 剩余 5ms,P2 需要 4ms。P2 更短,P1 被抢占,P2 开始运行
  3. 时间 4,P3 到达。P2 剩余 2ms,P3 需要 1ms。P3 更短,P2 被抢占,P3 开始运行
  4. 时间 5,P3 结束。此时 P4 到达。P2 剩余 2ms,P4 需要 4ms。P2 继续运行
  5. 时间 7,P2 结束。P1 剩余 5ms,P4 需要 4ms。P4 更短,P4 开始运行
  6. 时间 11,P4 结束。P1 继续运行
  7. 时间 16,P1 结束

平均等待时间 = ((0-0) + (2-2) + (4-4) + (5-5) + (7-7) + (11-11)) ... 等等,我算一下:

P1 等待时间 = 0 + (7-2) + (11-7) = 9ms
P2 等待时间 = (2-2) + (4-4) = 0ms
P3 等待时间 = (4-4) = 0ms
P4 等待时间 = (5-5) = 0ms
平均 = (9 + 0 + 0 + 0) / 4 = 2.25ms

你看,比非抢占式的 2.75ms 更低了。但代价是——上下文切换次数增加了。每次抢占都要保存和恢复寄存器状态,这本身就有开销。

我的建议:SRTF 适合批处理系统,不适合交互式系统。因为频繁的抢占会导致响应时间抖动,用户体验很差。

4.4 饥饿问题:长进程的噩梦

这是 SJF 家族最严重的缺陷。什么叫饥饿?就是一个进程永远得不到 CPU

为什么会这样?你想想看:

  • 系统里不断有短进程到达
  • 每次调度,短进程都比长进程优先
  • 长进程永远排不上队

我曾经在一个实时系统中遇到过这个问题。有个后台日志处理进程,执行时间大约 500ms。结果系统里不断有几十微秒的键盘中断处理任务进来。那个日志进程等了整整 3 分钟才被调度到。用户以为系统死机了。

注意:饥饿问题在 SRTF 中比非抢占式 SJF 更严重。因为非抢占式至少保证长进程一旦开始就能跑完,而 SRTF 可能让长进程永远被抢占。

4.5 解决方案:老化技术

解决饥饿问题,最常用的方法是老化(Aging)。说白了就是:随着等待时间增长,动态提升进程的优先级

具体做法:

  1. 每个进程维护一个「等待时间计数器」
  2. 每次调度时,计算「有效优先级 = 原始优先级 + 等待时间 × 系数」
  3. 等待越久,优先级越高,最终一定能被调度到

举个例子:

假设系数为 1,每等待 1 秒优先级 +1。
一个长进程初始优先级为 10。
等待 5 秒后,优先级变成 15。
等待 10 秒后,优先级变成 20。
最终它会超过所有短进程。

核心原则:老化技术保证了「有界等待」——每个进程都能在有限时间内得到 CPU。

4.6 实际应用中的取舍

说实话,纯 SJF 在现代操作系统中很少直接使用。原因有三:

  • 无法预知执行时间——除非你用历史数据做预测,但预测总有误差
  • 饥饿问题——需要配合老化,但老化又增加了复杂度
  • 不适用于交互式系统——用户希望每次操作都能快速响应,而不是等一个长进程跑完

我个人的习惯是:在嵌入式系统中,如果任务集是固定的、已知的,可以用 SJF 做离线调度。但在通用操作系统中,我更倾向于使用多级反馈队列(MLFQ),它结合了 SJF 的效率和轮转法的公平性。

一个小技巧:如果你非要用 SJF,可以结合「执行时间预估」。比如用指数平均法:预估时间 = α × 上次实际时间 + (1-α) × 上次预估时间。α 通常取 0.5。这样能一定程度上缓解「不知道执行时间」的问题。

4.7 本章小结

SJF 的核心是「谁短谁先跑」,理论上平均等待时间最优。但实际中:

  • 非抢占式适合批处理,但短进程可能被长进程阻塞
  • 抢占式(SRTF)进一步降低平均等待时间,但上下文切换开销大
  • 饥饿问题是最大隐患,必须用老化技术解决
  • 现代系统中很少单独使用,更多是作为 MLFQ 等算法的组成部分

嗯,关于 SJF 就讲这么多。下一节咱们聊聊优先级调度——那个看似简单实则暗藏杀机的算法。

短作业优先(SJF)知识体系 SJF 核心思想 非抢占式 SJF 抢占式 SRTF 饥饿问题 进程一旦开始,运行到结束 适合批处理系统 新进程更短则抢占当前进程 平均等待时间更优 长进程永远得不到 CPU SRTF 中更严重 解决方案:老化技术(Aging) 等待时间提升优先级 保证有界等待

专注资料整理