Skip to content

操作系统调度的机制

🎬 一个小故事

想象你是一家咖啡馆的老板,每天顾客源源不断。有时候一个顾客点的是复杂的定制饮品(长作业),有时候只是要一杯黑咖啡(短作业)。关键问题来了:

  • 你是按照先到先服务的顺序?还是先做简单的,后做复杂的?
  • 一个 VIP 顾客来了,你是否中断当前的工作去服务他?
  • 如果总是有新的急客到来,那些耐心的慢客是否永远得不到服务?

操作系统的调度算法,就是这家咖啡馆的"工作流程"。

Tips:这里假设咖啡馆只有一位咖啡师(即单核 CPU)来讨论调度问题。现代多核处理器会并行运行多个进程,但核心的调度思想是相同的——每个核心都需要独立做出"谁先服务"的决策。所以这里的所有讨论都适用于多核系统中的每一个核心


1. 调度的层级结构

操作系统调度分为两个关键层级:

graph LR
    A[外存作业池] -->|作业调度| B[内存]
    B -->|进程调度| C[CPU]
    style A fill:#e1f5fe,stroke:#0288d1
    style B fill:#e8f5e9,stroke:#2e7d32
    style C fill:#ffe0b2,stroke:#ef6c00
  • 作业调度:决定哪些作业可以进入内存(准入控制)
  • 进程调度:决定内存中的哪个作业使用 CPU(执行控制)

💡 作业管理方式

  • 单道系统:内存只能容纳 1 个作业,简单但低效
  • 两道系统:内存最多容纳 2 个作业,本文以此为例
  • 多道系统:内存容纳多个作业,现代操作系统的标准配置

2. 两级调度:分工明确

2.1 作业调度:看“时间” —— 短作业优先(SJF)

  • 目标:提高系统整体效率
  • 规则:在等待进入内存的作业中,选择估计运行时间最短
  • 特点非抢占式 —— 一旦进入内存,就不会被踢出去

2.2 进程调度:看"权重" —— 抢占式优先级调度

  • 目标:保证重要任务优先执行
  • 规则:采用优先级编号系统,优先级数值越小优先级越高(如:优先级 1 > 优先级 2 > 优先级 3)
  • 特点抢占式 —— 高优先级作业进入内存后,可以立即打断正在运行的低优先级作业

💡 :不同系统可能采用不同的优先级定义(有些系统优先级数值越大优先级越高),本文统一采用"数值越小优先级越高"的标准定义

2.3 协同工作流程

flowchart TD
    Start[新作业到达] --> CheckMem{内存是否满?}
    CheckMem -- 否 --> JobSched[作业调度: 从等待队列选SJF进内存]
    JobSched --> ProcSched[进程调度: 选最高优先级作业运行]
    CheckMem -- 是 --> Wait[作业加入等待队列]
    Wait -->|内存有空位| JobSched
    ProcSched --> Run[作业运行]
    Run --> Complete{作业完成?}
    Complete -- 是 --> Release[释放内存]
    Release --> CheckMem
    Complete -- 否 --> Preempt{有更高优先级作业进入?}
    Preempt -- 是 --> ProcSched
    Preempt -- 否 --> Run

💡 关键点:作业调度遵循**短作业优先(SJF)**原则,每次选择等待队列中估计运行时间最短的作业进入内存


3. 抢占式 vs 非抢占式:直观对比

3.1 场景设定

  • 进程 A:低优先级,需要运行 10 个时间单位
  • 进程 B:高优先级,在时间 2 到达,需要运行 5 个时间单位

3.2 非抢占式调度

在这种模式下,一旦 A 开始运行,B 必须等待 A 完全结束:

plaintext
时间轴:0         10        15
        |----------|---------|
        |    A     |    B    |

进程到达:A(t=0)      B到达(t=2)但无法抢占
  • A 从 0 运行到 10
  • B 在 t=2 到达,但由于 A 正在运行且系统不支持抢占,B 必须等待
  • B 从 10 开始运行,15 结束
  • B 的等待时间 = 10 - 2 = 8 个时间单位

3.3 抢占式调度

在这种模式下,B 到达后立即获得 CPU,打断 A 的执行:

plaintext
时间轴:0    2         7        15
        |----|---------|---------|
        | A1 |    B    |   A2    |

             ↑ t=2时刻,B到达,立即抢占
  • A 从 0 运行到 2(被 B 打断 = 抢占点
  • B 从 2 运行到 7(立即执行,几乎无等待)
  • A 从 7 继续运行到 15(完成剩余 8 个单位)
  • B 的等待时间 ≈ 0(只需排队极短时间)

对比效果:抢占式调度使 B 的响应延迟从 8 个时间单位降低到接近 0

3.4 关键差异总结

方面非抢占式抢占式
响应性差(重要任务需等待)好(重要任务立即执行)
实现复杂度简单复杂(需保存/恢复进程状态)
适用场景批处理系统交互式/实时系统

4. 多级反馈队列调度(MLFQ)

4.1 为什么需要 MLFQ?

  • 现实中很难预知进程运行时间(SJF 的前提)
  • 需要自动区分不同类型的进程
  • 要同时兼顾短作业的响应性和长作业的完成

4.2 三级队列典型配置

队列优先级时间片大小适合的进程类型
Q1最高4短作业、交互式进程
Q2中等8中等长度作业
Q3最低16长作业、CPU密集型进程

4.3 调度规则与核心机制

基本规则

  1. 新进程总是从 Q1 开始
  2. 如果在时间片内完成,进程结束
  3. 如果时间片用完但未完成,降级到下一级队列
  4. 如果因为I/O 等待而放弃 CPU,保持在当前队列(不降级)
  5. CPU 总是优先运行最高优先级非空队列中的进程

防止饥饿:老化机制(Aging - 等待时间补偿)✨

标准 MLFQ 必须配备的关键机制,否则会导致严重的进程饥饿:

  • 问题:持续有新的短作业到达时,Q3 中的长作业可能永远得不到执行
  • 解决方案:每隔一段时间(如 100ms),将所有等待时间过长的进程提升到更高优先级队列
  • 规则
    • 等待 100ms 在 Q2 → 提升到 Q1
    • 等待 100ms 在 Q3 → 提升到 Q2
  • 参数范围
    • 短期系统(响应式):50-100ms
    • 一般交互系统:100-200ms
    • 批处理系统:可达 1-5s
  • 效果:保证即使是长作业也最终能获得执行机会
  • 原理:进程"变老"(等待时间增加)后,获得更高的优先级,避免被永久饿死

💡 本文简化:后续示例采用简化版 MLFQ(不涉及老化机制)演示基本流程,实际系统中必须配备老化机制

4.4 调度流程图

flowchart TD
    New[新进程] --> Q1[进入Q1队列]
    Q1 --> Run1[运行4单位]
    Run1 -->|完成| End[结束]
    Run1 -->|未完成| Q2[降级到Q2队列]
    Q2 --> Run2[运行8单位]
    Run2 -->|完成| End
    Run2 -->|未完成| Q3[降级到Q3队列]
    Q3 --> Run3[在Q3中运行<br/>每个时间片16单位]
    Run3 -->|完成| End
    Run3 -->|时间片用完<br/>未完成| Q3
    Run1 -->|I/O等待| Q1[保持在Q1<br/>等待I/O完成后继续]
    Run2 -->|I/O等待| Q2[保持在Q2<br/>等待I/O完成后继续]
    Run3 -->|I/O等待| Q3[保持在Q3<br/>等待I/O完成后继续]

💡 Q3 特性:进程在 Q3 中采用轮转调度,每次运行 16 个时间单位,若未完成则继续排队等待下一个时间片

4.5 实际执行示例

示例场景设定

三个进程同时到达(t=0):

  • P1(6单位):Q1 运行 4 单位 → 降级到 Q2 → 再运行 2 单位完成
  • P2(13单位):Q1 运行 4 单位 → 降级到 Q2 → 运行 8 单位 → 降级到 Q3 → 运行 1 单位完成
  • P3(10单位):Q1 运行 4 单位 → 降级到 Q2 → 在 Q2 中运行 6 单位完成

详细执行时间轴

时间Q1队列Q2队列Q3队列CPU执行备注
0-4P2,P3等待--P1运行P1在Q1执行4单位
4-8P3等待P1等待-P2运行P2在Q1执行4单位
8-12-P1,P3等待-P3运行P3在Q1执行4单位
12-16-P1,P3运行-P1运行P1在Q2执行2单位→完成
16-20-P3运行-P3运行P3在Q2执行4单位
20-24-P2等待P2,P3等待P3运行P3在Q2执行2单位→完成
24-32--P2运行P2运行P2在Q3执行8单位
32-33---P2运行P2在Q3执行1单位→完成

性能指标

进程开始时间结束时间等待时间周转时间
P1012612
P20332033
P30241424
平均--13.323

计算说明

  • 等待时间 = 进程首次运行前的等待时间 + 被打断后重新等待的总时间
    • P1:0-0(未等待)+ 8-12(等待2单位) + 12-16(等待2单位) = 6 单位
    • P2:0-2(等待2单位) + 8-24(等待16单位) + 24-32(等待2单位) = 20 单位
    • P3:0-8(等待8单位)+ 12-16(等待4单位) = 14 单位
  • 周转时间 = 结束时间 - 开始时间
    • P1:12 - 0 = 12 单位
    • P2:33 - 0 = 33 单位
    • P3:24 - 0 = 24 单位
  • 平均值:(6+20+14)/3 = 13.3;(12+33+24)/3 = 23

💡 观察:短进程(P1)获得快速完成,长进程(P2)虽然等待长,但不会被无限延迟(在 Q3 最终获得执行)


5. 三种调度模型综合评估

现在我们已经理解了三种主要的调度算法:

  • 两级调度(作业调度用 SJF,进程调度用优先级)
  • 多级反馈队列(MLFQ)
  • 纯 SJF(纯短作业优先)

以下通过详细的对比,帮助您理解各算法的适用场景和权衡。

5.1 三种调度模型对比

特性两级调度(SJF+优先级)多级反馈队列(MLFQ)纯SJF(非抢占)
是否需要预知运行时间
是否支持抢占进程调度层支持完全支持不支持
防止饥饿机制依赖作业调度策略需要老化机制无法防止
内存管理限制并发作业数通常不限制通常不限制
主要优势简单明确,效率高自适应,兼顾各类进程理论最优平均等待时间
主要缺点需要预知时间,可能导致进程饥饿实现复杂,参数配置困难需要预知时间,无抢占导致响应慢
适用场景批处理系统、时间预知清楚通用操作系统、交互式系统学术研究、理论分析

解读

  • 两级调度是传统批处理系统的选择,简单高效
  • MLFQ 是现代通用操作系统(Unix、Linux)的常用方案,平衡了各类进程的需求
  • 纯SJF 主要用于理论计算,实际系统很少采用

5.2 饥饿问题的深入分析

什么是进程饥饿(Starvation)?

定义:一个就绪的进程长时间得不到 CPU 时间,无限期地等待执行的现象。

各调度算法的饥饿风险

两级调度(SJF + 优先级)中的饥饿

风险场景

text
场景:不断有新的短作业到达系统
- 长作业 L:需要 1 小时运行时间
- 短作业流 S1, S2, S3...:每个 1 分钟

SJF 作业调度的选择过程:
时间 0-1分钟:选 S1 进内存执行
时间 1-2分钟:选 S2 进内存执行
...
问题:长作业 L 可能永远被推后!

防止机制

  • 设置最大等待时间阈值,超过则强制进入内存
  • 采用"年龄"概念:等待时间越长,优先级越高
多级反馈队列(MLFQ)中的饥饿

风险场景

text
场景:持续有高优先级(交互式)进程到达
- 长作业进程在 Q3,但频繁有新进程进 Q1
- Q1 和 Q2 总是有进程等待执行
- Q3 中的长作业永远得不到 CPU

这就是 MLFQ 必须配备"老化机制"的原因!

防止机制

  • 老化机制:每隔一定时间,所有等待过久的进程自动提升到更高优先级队列
  • 典型参数:等待 100ms 以上自动提升一级

纯 SJF(非抢占)中的饥饿

风险等级:⚠️ 最严重

text
场景:有两个进程
- 长进程 L:10 个时间单位(首先到达)
- 短进程 S:1 个时间单位(后到达)

执行顺序:
- L 运行 10 个时间单位
- 最后 S 才能运行

如果源源不断有新短进程到达,L 会被无限延迟!

无法有效防止的原因:

  • 非抢占特性决定了一旦进程开始运行就不会被中断
  • 没有优先级机制可以打断长进程

总结表

算法饥饿风险防止方案
两级调度中等最大等待时间、年龄机制
MLFQ低(配老化机制)必需老化机制否则高风险
纯SJF很高改用抢占式 SJF 或其他算法

6. 实战应用场景

现代操作系统的实际选择

了解了三种算法的特点后,让我们看看真实的操作系统是如何选择的:

Linux/Unix 系统:MLFQ 变体(CFS)✨

采用算法:完全公平调度器(Completely Fair Scheduler, CFS)

  • 基础:多级队列的思想
  • 特色:使用红黑树代替固定队列,动态调整优先级
  • 老化机制:完整实现,防止任何进程长期饥饿
  • 适配性:自动识别 I/O 密集型 vs CPU 密集型进程

典型场景

text
- 文本编辑器:高优先级,快速响应用户输入
- 后台下载:低优先级,不影响前台任务
- 数据库:中等优先级,平衡吞吐和响应

实时操作系统(RTOS):两级调度

采用算法:优先级 + SJF 变体

  • 特点:预测性强,行为可预测
  • 保证:不同优先级任务的响应时间有上界
  • 应用:航空航天、医疗设备、工业控制

批处理系统(大数据处理):优化的 SJF

采用算法:带老化机制的 SJF

  • 目标:最大化系统吞吐量
  • 特点:长作业最终必定执行
  • 应用:Hadoop、Spark 等分布式计算框架

7. 关键要点总结

  • 两级调度:准入看效率(运行时间),执行看重要性(优先级)
  • 抢占性:决定了高优先级任务能否"插队",直接影响系统响应性
  • 多级反馈队列:通过多级试探自动分类进程,是 SJF 思想的实用化实现
  • I/O密集型进程:在 MLFQ 中天然获得高响应性(频繁阻塞不降级)
  • 老化机制:是现代调度系统防止饥饿的必要机制
  • 设计哲学:调度的本质是在系统效率用户体验之间找到最佳平衡点

上次更新于: