Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
Python系列文章目录
Go语言系列文章目录
Docker系列文章目录
操作系统系列文章目录
01-【操作系统-Day 1】万物之基:我们为何离不开操作系统(OS)?
02-【操作系统-Day 2】一部计算机的进化史诗:操作系统的发展历程全解析
03-【操作系统-Day 3】新手必看:操作系统的核心组件是什么?进程、内存、文件管理一文搞定
04-【操作系统-Day 4】揭秘CPU的两种工作模式:为何要有内核态与用户态之分?
05-【操作系统-Day 5】通往内核的唯一桥梁:系统调用 (System Call)
06-【操作系统-Day 6】一文搞懂中断与异常:从硬件信号到内核响应的全流程解析
07-【操作系统-Day 7】程序的“分身”:一文彻底搞懂什么是进程 (Process)?
08-【操作系统-Day 8】解密进程的“身份证”:深入剖析进程控制块 (PCB)
09-【操作系统-Day 9】揭秘进程状态变迁:深入理解就绪、运行与阻塞
10-【操作系统-Day 10】CPU的时间管理者:深入解析进程调度核心原理
11-【操作系统-Day 11】进程调度算法揭秘(一):简单公平的先来先服务 (FCFS) 与追求高效的短作业优先 (SJF)
文章目录
摘要
在操作系统的世界里,CPU 是最宝贵的资源之一。如何公平、高效地将 CPU 时间分配给嗷嗷待哺的众多进程,是进程调度模块的核心使命。调度算法作为其灵魂,直接决定了系统的性能和用户体验。本文作为进程调度算法系列的第一篇,将深入剖析两种最基础、也最具代表性的调度策略:先来先服务(FCFS)和短作业优先(SJF)。我们将从它们的核心原理出发,通过实例演算,直观感受其工作流程,并系统性地分析各自的优缺点、潜在问题及适用场景,为理解更复杂的现代调度算法打下坚实的基础。
一、温故知新:为何需要调度算法?
在上一篇文章《【操作系统-Day 10】CPU 的时间管理者:进程调度》中,我们已经了解了调度的时机、类型以及评价调度好坏的核心指标。在深入具体的算法之前,让我们简要回顾一下这些关键概念。
1.1 调度的核心目标与评价指标
进程调度的根本目标是在多个可运行的进程之间,选择一个来占用 CPU,以实现系统资源的优化配置。评价一个调度算法优劣的维度通常包括:
- CPU 利用率 (CPU Utilization): 确保 CPU 尽可能处于“忙碌”状态,而不是空闲等待。
- 吞吐量 (Throughput): 单位时间内完成的进程数量。
- 周转时间 (Turnaround Time): 从进程提交到进程完成所花费的总时间。
周转时间 = 等待时间 + 运行时间
。 - 等待时间 (Waiting Time): 进程在就绪队列中等待被调度所花费的时间总和。
- 响应时间 (Response Time): 从用户发出请求到系统首次产生响应所花费的时间。
一个优秀的调度算法,需要在这些往往相互制约的指标之间做出精妙的权衡 (Trade-off)。
1.2 算法的基本分类
根据调度时机,调度算法可以分为两大类:
- 非抢占式 (Non-preemptive): 一旦 CPU 分配给某个进程,该进程会一直持有 CPU,直到它自己主动释放(例如,执行完毕或等待 I/O)。
- 抢占式 (Preemptive): 操作系统可以强制剥夺当前正在运行进程的 CPU 使用权,将其分配给另一个优先级更高的进程。
我们接下来要讨论的 FCFS 是典型的非抢占式算法,而 SJF 则可以有非抢占式和抢占式两种版本。
二、先来先服务 (FCFS):最直观的公平
先来先服务 (First-Come, First-Served, FCFS) 是最简单的调度算法,其思想如同我们在日常生活中排队办事一样:谁先来,谁就先得到服务。
2.1 核心原理
FCFS 算法的实现非常简单,它维护一个就绪队列,新到达的进程被追加到队尾。当 CPU 空闲时,调度程序会从队头选择一个进程,让其投入运行。这是一种典型的非抢占式策略。
- 数据结构: 通常使用一个先进先出 (FIFO) 队列来管理就绪进程。
- 核心思想: 公平,简单,易于实现。
2.2 实例演算:FCFS 的工作流程
假设我们有以下四个进程,它们在同一时间(时刻 0)到达,需要的 CPU 运行时间(Burst Time)也已知。
进程 (Process) | 运行时间 (Burst Time) |
---|---|
P1 | 24 ms |
P2 | 3 ms |
P3 | 3 ms |
如果进程的到达顺序是 P1, P2, P3,那么 FCFS 的调度过程如下:
- 时刻 0: P1, P2, P3 同时到达。按 FCFS 规则,P1 首先获得 CPU。
- P1 开始运行,P2 和 P3 在就绪队列中等待。
- 时刻 24: P1 运行结束。P2 从就绪队列中被选中,开始运行。P3 继续等待。
- 时刻 27 (24+3): P2 运行结束。P3 从就绪队列中被选中,开始运行。
- 时刻 30 (27+3): P3 运行结束。所有进程完成。
我们可以用甘特图 (Gantt Chart) 来直观地表示这个过程:
| P1 (24ms) | P2 (3ms) | P3 (3ms) |
0 24 27 30
现在,我们来计算关键性能指标:
-
等待时间 (Waiting Time):
- P1 的等待时间 = 0 ms (它立刻就运行了)
- P2 的等待时间 = 24 ms (它必须等 P1 运行完)
- P3 的等待时间 = 27 ms (它必须等 P1 和 P2 都运行完)
- 平均等待时间 = (0 + 24 + 27) / 3 = 17 ms
-
周转时间 (Turnaround Time):
- P1 的周转时间 = 0 (等待) + 24 (运行) = 24 ms
- P2 的周转时间 = 24 (等待) + 3 (运行) = 27 ms
- P3 的周转时间 = 27 (等待) + 3 (运行) = 30 ms
- 平均周转时间 = (24 + 27 + 30) / 3 = 27 ms
2.3 优缺点分析
2.3.1 优点:公平直观,实现简单
FCFS 最大的优点在于其公平性和简单性。它保证了每个进程最终都会得到执行,不会产生“饥饿”现象(即某个进程永远得不到 CPU)。算法逻辑清晰,代码实现开销小。
2.3.2 致命弱点:“护航效应” (Convoy Effect)
FCFS 的性能受进程到达顺序的影响极大,并且存在一个致命的问题——护航效应。当一个需要很长 CPU 时间的进程(“重型卡车”)先于许多需要很短 CPU 时间的进程(“小轿车”)到达时,这些短进程将被迫长时间排队等待,即使 CPU 只需要为它们服务一小会儿。
让我们回到刚才的例子,如果进程到达顺序变为 P2, P3, P1,情况会如何?
甘特图如下:
| P2 (3ms) | P3 (3ms) | P1 (24ms) |
0 3 6 30
- 等待时间:
- P2 等待时间 = 0 ms
- P3 等待时间 = 3 ms
- P1 等待时间 = 6 ms
- 平均等待时间 = (0 + 3 + 6) / 3 = 3 ms
可以看到,仅仅改变了到达顺序,平均等待时间从 17ms 骤降至 3ms!这个例子生动地展示了护航效应的巨大负面影响。由于这个原因,纯粹的 FCFS 算法在现代分时操作系统中很少被用作主要的调度策略。
2.4 适用场景
尽管有明显缺点,FCFS 在某些场景下仍然有用。例如,在一些批处理系统中,任务的公平性是首要考虑的,且任务通常是长作业,护航效应不那么突出。此外,它也常常作为其他复杂调度算法(如多级反馈队列)中某一级的调度策略。
三、短作业优先 (SJF):追求极致效率
如果我们的目标是获得最短的平均等待时间,那么短作业优先 (Shortest-Job-First, SJF) 算法是理论上的最优选择。
3.1 核心原理
SJF 算法的核心思想非常直接:当 CPU 空闲时,从就绪队列中选择下一次 CPU 运行时间最短的进程来执行。这个“作业”在这里指的就是进程的下一次 CPU 执行周期 (CPU Burst)。
3.2 SJF 的两种模式
SJF 算法根据其是否允许抢占,可以分为两种模式。
3.2.1 非抢占式 SJF
当一个进程获得 CPU 后,它会一直运行,直到其当前的 CPU 执行周期结束。在此期间,即使有更短的作业到达就绪队列,调度器也不会打断当前正在运行的进程。
3.2.2 抢占式 SJF
抢占式 SJF 也被称为最短剩余时间优先 (Shortest-Remaining-Time-First, SRTF) 算法。在这种模式下,如果一个新到达的进程,其所需的 CPU 运行时间比当前正在运行进程的剩余运行时间还要短,那么调度器会立即中断当前进程(即抢占),将 CPU 分配给这个新来的、更短的进程。
3.3 实例演算:SJF/SRTF 的威力
为了更好地对比,我们引入进程的到达时间,并使用一个新的例子:
进程 | 到达时间 (Arrival Time) | 运行时间 (Burst Time) |
---|---|---|
P1 | 0 | 8 |
P2 | 1 | 4 |
P3 | 2 | 9 |
P4 | 3 | 5 |
(1) 非抢占式 SJF
- 时刻 0: P1 到达,就绪队列中只有 P1。P1 开始运行。
- 时刻 1: P2 到达,加入就绪队列。但 P1 是非抢占的,继续运行。
- 时刻 2: P3 到达,加入就绪队列。P1 继续运行。
- 时刻 3: P4 到达,加入就绪队列。P1 继续运行。
- 时刻 8: P1 运行结束。此时就绪队列中有 {P2(4), P3(9), P4(5)}。根据 SJF,选择最短的 P2。P2 开始运行。
- 时刻 12 (8+4): P2 运行结束。就绪队列中有 {P3(9), P4(5)}。选择最短的 P4。P4 开始运行。
- 时刻 17 (12+5): P4 运行结束。只剩 P3,P3 开始运行。
- 时刻 26 (17+9): P3 运行结束。
甘特图:
| P1 (8) | P2 (4) | P4 (5) | P3 (9) |
0 8 12 17 26
平均等待时间 = ((8-1) + (12-3) + (17-2) + (0-0)) / 4 = (7 + 9 + 15 + 0) / 4 = 7.75
(2) 抢占式 SJF (SRTF)
- 时刻 0: P1 到达,开始运行。剩余时间 8。
- 时刻 1: P2 到达 (运行时间 4)。P1 此时剩余时间为 7。因为 4 < 7,P2 抢占 CPU。P1 被放回就绪队列。
- 时刻 2: P3 到达 (运行时间 9)。P2 正在运行,剩余时间为 3。因为 9 > 3,P2 继续运行。
- 时刻 3: P4 到达 (运行时间 5)。P2 仍在运行,剩余时间为 2。因为 5 > 2,P2 继续运行。
- 时刻 5 (1+4): P2 运行结束。此时就绪队列中有 {P1(剩余7), P3(9), P4(5)}。选择剩余时间最短的 P4。P4 开始运行。
- 时刻 10 (5+5): P4 运行结束。就绪队列中有 {P1(剩余7), P3(9)}。选择剩余时间最短的 P1。P1 继续运行。
- 时刻 17 (10+7): P1 运行结束。只剩 P3,P3 开始运行。
- 时刻 26 (17+9): P3 运行结束。
甘特图:
| P1(1) | P2 (4) | P4 (5) | P1 (7) | P3 (9) |
0 1 5 10 17 26
平均等待时间 = ((10-1) + (1-1) + (17-2) + (5-3)) / 4 = (9 + 0 + 15 + 2) / 4 = 6.5
可以看到,抢占式的 SRTF 算法获得了更低的平均等待时间。事实上,可以证明 SRTF 算法在所有调度算法中能获得最优的平均等待时间。
3.4 优缺点分析
3.4.1 优点:理论最优的平均等待时间
SJF 及其抢占式版本 SRTF 的最大优点是,它们能够最小化进程的平均等待时间和平均周转时间,从而极大地提高了系统的吞吐量。
3.4.2 现实的骨感:无法精准预测
SJF 算法有一个致命的、在现实中几乎无法克服的缺点:我们无法在进程执行前,精确地知道它下一次需要运行多久。这就像让售票员预测每位乘客需要办理多久业务一样困难。
虽然无法精确预测,但可以进行估算。一种常见的技术是指数平均法 (Exponential Averaging)。它根据进程过去的历史运行时间来预测下一次的运行时间。公式如下:
τ
n
+
1
=
α
⋅
t
n
+
(
1
−
α
)
⋅
τ
n
\tau_{n+1} = \alpha \cdot t_n + (1 - \alpha) \cdot \tau_n
τn+1=α⋅tn+(1−α)⋅τn
其中:
- t a u _ n + 1 \\tau\_{n+1} tau_n+1 是下一次的预测值。
- t _ n t\_n t_n 是第 n n n 次(即最近一次)的实际 CPU 运行时间。
- t a u _ n \\tau\_n tau_n 是第 n n n 次的预测值(即上一次的预测结果)。
- a l p h a \\alpha alpha 是一个权重因子 ( 0 l e a l p h a l e 1 0 \\le \\alpha \\le 1 0lealphale1),决定了历史和当前观测值在预测中的重要性。
这种预测机制增加了系统的复杂性,且预测结果的准确性直接影响调度性能。
3.4.3 潜在风险:“饥饿”问题
与 FCFS 不同,SJF 算法可能会导致饥饿 (Starvation) 问题。如果系统中不断有新的短作业到达,那么一个需要较长运行时间的作业可能永远被插队,始终无法获得 CPU 资源,从而“饿死”。这个问题在负载很高的系统中尤其突出。
四、总结
本文详细探讨了两种基础的进程调度算法:先来先服务 (FCFS) 和短作业优先 (SJF)。它们是后续更复杂调度策略的重要思想来源。
-
先来先服务 (FCFS):
- 核心思想: 按照进程到达的先后顺序进行调度,是一种非抢占式算法。
- 优点: 公平、实现简单,不会产生饥饿。
- 缺点: 平均等待时间波动大,存在严重的“护航效应”,导致系统效率低下。
-
短作业优先 (SJF):
- 核心思想: 优先调度下一次 CPU 运行时间最短的进程。有非抢占式和抢占式 (SRTF) 两种模式。
- 优点: 理论上可以获得最低的平均等待时间,系统吞吐量高。
- 缺点: 无法精确预知作业的运行时间,是其在现实中应用的主要障碍。此外,可能导致长作业“饥饿”。
-
算法对比:
特性 | 先来先服务 (FCFS) | 短作业优先 (SJF / SRTF) |
---|---|---|
调度策略 | 非抢占式 | 非抢占式或抢占式 (SRTF) |
核心依据 | 进程到达时间 | 进程下一次 CPU 运行时间(或剩余时间)的预测 |
优点 | 公平,简单,无饥饿 | 平均等待时间最短,吞吐量高 |
缺点 | 护航效应,平均等待时间长 | 难以预测运行时间,可能导致长作业饥饿 |
目标 | 公平性 | 效率 (最小化平均等待时间) |
FCFS 和 SJF 向我们展示了调度算法设计中最初的两种价值取向:绝对的公平与极致的效率。然而,它们各自的明显缺陷也促使操作系统设计者们去探索更为均衡和实用的策略。在下一篇文章中,我们将继续探讨兼顾公平与响应的优先级调度和时间片轮转算法,看它们是如何解决 FCFS 和 SJF 所面临的挑战的。