本文还有配套的精品资源,点击获取
简介:进程调度是操作系统核心功能之一,直接影响系统效率与资源利用率。本文通过Java编程模拟实现四种经典调度算法:先来先服务(FCFS)、短作业优先(SJF)、时间片轮转(RR)和高响应比优先(HRN),深入解析其调度机制与优缺点。项目包含进程类、调度器类和主控程序,结构清晰,注释完整,适用于操作系统课程实验与学习实践。通过该实验,学生可掌握调度算法的核心逻辑,提升对多任务环境CPU资源分配机制的理解。
1. 进程调度算法概述与操作系统核心作用
在现代计算机系统中,操作系统作为硬件与应用程序之间的桥梁,承担着资源管理、任务调度和进程控制等关键职能。其中,进程调度是操作系统最为重要的功能之一,直接影响系统的效率、响应速度与公平性。本章将从操作系统的整体架构出发,深入剖析进程调度的核心地位,阐述其在多道程序环境下的必要性,并引出四种经典调度算法——先来先服务(FCFS)、短作业优先(SJF)、时间片轮转(RR)和高响应比优先(HRN)的基本思想。
通过对调度目标的分析,如最小化平均等待时间、提高CPU利用率、保证公平性和响应性,为后续各章的理论推导与代码实现奠定坚实基础。同时,介绍Java语言在模拟操作系统行为中的优势,包括面向对象建模能力、多线程支持以及良好的可读性和调试便利性,说明为何选择Java作为本次实验的实现平台。
2. 先来先服务(FCFS)算法原理与Java实现
在多道程序操作系统中,进程调度是决定CPU资源分配的核心机制。其中, 先来先服务 (First-Come, First-Served, 简称 FCFS)作为最基础、最直观的调度策略,广泛用于教学模型和早期批处理系统。该算法遵循“谁先到就先服务”的原则,体现了队列的基本逻辑——先进先出(FIFO)。尽管其设计简单,但在实际应用中暴露出明显的性能瓶颈,尤其是对短作业不友好以及容易引发“ convoy effect”( convoy 效应),即长任务阻塞后续多个短任务执行。
本章将深入剖析 FCFS 调度算法的理论根基,从数学表达层面推导关键性能指标,并通过 Java 编程语言构建一个完整的模拟器,涵盖进程建模、调度流程控制、结果统计与输出等环节。借助面向对象的设计思想,我们将封装可复用的数据结构与方法模块,确保代码具备良好的扩展性与可读性,为后续复杂调度算法的实现奠定技术基础。
2.1 FCFS算法的理论基础
FCFS 是一种非抢占式的进程调度算法,意味着一旦某个进程获得 CPU 使用权,它将持续运行直到完成或主动放弃(如等待 I/O)。这种特性使得调度逻辑极为简洁:所有到达的进程按照其进入就绪队列的时间顺序排队,调度器只需依次取出队首进程进行执行即可。
2.1.1 算法定义与执行逻辑
FCFS 的核心理念来源于现实生活中常见的排队场景:银行取号、售票窗口等。在操作系统语境下,每一个新到达的进程被插入到就绪队列末尾,而调度器每次只从队列头部取出一个进程投入运行。由于没有优先级判断或时间片中断机制,整个调度过程完全由到达时间驱动。
设有一组进程 $ P_1, P_2, …, P_n $,它们的到达时间分别为 $ A_i $,执行时间(也称突发时间)为 $ B_i $。FCFS 按照 $ A_i $ 升序排列这些进程并依次执行。若前一进程结束时间为 $ C_{i-1} $,则当前进程 $ P_i $ 的开始时间为: S_i = \max(C_{i-1}, A_i) 结束时间计算为: C_i = S_i + B_i 由此可以进一步推导出两个重要性能指标:
周转时间(Turnaround Time) :表示进程从提交到完成所经历的总时间。 $$ TAT_i = C_i - A_i $$
等待时间(Waiting Time) :指进程在就绪队列中等待 CPU 的时间。 $$ WT_i = S_i - A_i = TAT_i - B_i $$
示例说明:假设三个进程按以下方式到达:
进程 到达时间 (A) 执行时间 (B) P1 0 5 P2 1 3 P3 2 8
根据 FCFS 规则,调度顺序为 P1 → P2 → P3。
详细时间线如下:
P1 开始于 0,结束于 5 P2 开始于 max(5,1)=5,结束于 8 P3 开始于 max(8,2)=8,结束于 16
对应各指标:
进程 开始时间 结束时间 周转时间 等待时间 P1 0 5 5 0 P2 5 8 7 4 P3 8 16 14 6
平均等待时间: \frac{0 + 4 + 6}{3} = 3.33 平均周转时间: \frac{5 + 7 + 14}{3} = 8.67
这表明即使后到的短任务也无法提前执行,导致整体效率受限。
2.1.2 调度队列的组织方式与非抢占特性
FCFS 的调度队列本质上是一个 FIFO 队列。每当有新的进程到达时,它被添加到队列尾部;当 CPU 空闲时,调度器从队列前端取出下一个可运行的进程。这一结构天然契合 Java 中的 LinkedList 或 Queue 接口实现。
值得注意的是,FCFS 属于 非抢占式调度 。这意味着:
一旦进程开始运行,除非它自行终止或进入阻塞状态,否则不会被强制中断; 即使在此期间有更紧急或更短的任务到达,也无法打断当前运行进程; 因此,系统的响应性较差,尤其不利于交互式应用场景。
下面使用 Mermaid 流程图展示 FCFS 的基本调度流程:
graph TD
A[初始化调度器] --> B{就绪队列为空?}
B -- 是 --> C[等待新进程到达]
B -- 否 --> D[取出队首进程]
D --> E[设置进程状态为RUNNING]
E --> F[模拟执行直至完成]
F --> G[更新完成时间、周转时间、等待时间]
G --> H[移除已完成进程]
H --> I{仍有未到达进程?}
I -- 是 --> J[推进系统时间]
J --> K{是否有新进程在当前时间到达?}
K -- 是 --> L[加入就绪队列]
K -- 否 --> M[继续循环]
I -- 否 --> N[所有进程完成, 输出结果]
该流程清晰地表达了事件驱动的调度主循环结构:基于时间推进,动态检测进程到达与完成事件,并维护队列状态。
2.1.3 平均等待时间与周转时间的数学表达
为了量化调度算法的性能优劣,通常采用以下几个统计量:
指标名称 数学表达式 含义说明 平均等待时间 $\bar{W} = \frac{1}{n}\sum_{i=1}^{n} WT_i$ 衡量用户感知延迟的重要指标 平均周转时间 $\bar{T} = \frac{1}{n}\sum_{i=1}^{n} TAT_i$ 反映整体处理效率 平均响应时间 $\bar{R} = \frac{1}{n}\sum_{i=1}^{n} (S_i - A_i)$ 多用于分时系统,首次获得CPU的时间差 CPU利用率 $\eta = \frac{\text{总忙碌时间}}{\text{总运行时间}} \times 100\%$ 衡量资源利用效率
以之前的例子为例,我们可列出完整的性能评估表:
指标 计算公式 结果 平均等待时间 (0+4+6)/3 3.33 平均周转时间 (5+7+14)/3 8.67 CPU利用率 (5+3+8)/(16-0) × 100% 100% 最大等待时间 max(WT₁, WT₂, WT₃) 6
虽然 CPU 利用率达到理想值(无空闲时间),但平均等待时间偏高,反映出 FCFS 在公平性和响应速度方面的不足。
此外,还需注意: 当进程按到达时间乱序提交时,必须先排序再调度 ,否则会导致逻辑错误。例如,若输入顺序为 P3、P2、P1,但到达时间为 2、1、0,则需按到达时间升序重排为 P1→P2→P3 才能正确模拟 FCFS。
2.2 基于Java的FCFS调度模拟设计
为真实还原 FCFS 调度行为,我们需要构建一套完整的 Java 模拟框架。该框架包括进程类定义、调度器类封装、数据初始化与调度流程控制等组件。通过模块化设计,提升代码的可维护性与可测试性。
2.2.1 进程数据结构的设计与初始化
首先定义 Process 类,封装每个进程的关键属性:
public class Process {
private final int pid; // 进程ID
private final int arrivalTime; // 到达时间
private final int burstTime; // 执行时间
private int startTime; // 实际开始时间
private int finishTime; // 完成时间
private int waitingTime; // 等待时间
private int turnaroundTime; // 周转时间
private boolean completed; // 是否已完成
public Process(int pid, int arrivalTime, int burstTime) {
this.pid = pid;
this.arrivalTime = arrivalTime;
this.burstTime = burstTime;
this.completed = false;
}
// Getters and Setters...
public int getPid() { return pid; }
public int getArrivalTime() { return arrivalTime; }
public int getBurstTime() { return burstTime; }
public int getStartTime() { return startTime; }
public void setStartTime(int startTime) { this.startTime = startTime; }
public int getFinishTime() { return finishTime; }
public void setFinishTime(int finishTime) {
this.finishTime = finishTime;
this.turnaroundTime = finishTime - arrivalTime;
this.waitingTime = startTime - arrivalTime;
}
public int getWaitingTime() { return waitingTime; }
public int getTurnaroundTime() { return turnaroundTime; }
public boolean isCompleted() { return completed; }
public void markAsCompleted() { this.completed = true; }
@Override
public String toString() {
return String.format("P%d [%d, %d]", pid, arrivalTime, burstTime);
}
}
代码逻辑逐行解读:
第 2–7 行:声明私有字段,包含静态属性(如 PID、到达时间)和动态属性(如开始时间、完成时间); 第 9–15 行:构造函数仅接收外部输入参数,初始状态为未完成; 第 34–40 行:提供只读访问接口,符合封装原则; 第 41–52 行:setter 方法中嵌入衍生属性自动计算逻辑,如 setFinishTime() 自动更新周转时间和等待时间; 第 54–57 行:重写 toString() 便于调试输出。
该类满足第六章所述的面向对象抽象要求,支持生命周期追踪与状态管理。
2.2.2 按到达时间排序的调度流程实现
接下来实现 FCFSScheduler 类的核心调度逻辑。由于 FCFS 依赖于到达顺序,我们必须首先对输入进程列表按 arrivalTime 排序。
import java.util.*;
public class FCFSScheduler {
private List
private Queue
private int currentTime;
public FCFSScheduler(List
this.processes = new ArrayList<>(processes); // 防止外部修改
this.readyQueue = new LinkedList<>();
this.currentTime = 0;
}
public void schedule() {
// 按到达时间升序排序
processes.sort(Comparator.comparingInt(Process::getArrivalTime));
int completedCount = 0;
int n = processes.size();
System.out.println("FCFS 调度开始...\n");
while (completedCount < n) {
// 将当前时间点已到达的所有进程加入就绪队列
for (Process p : processes) {
if (!p.isCompleted() && !readyQueue.contains(p) && p.getArrivalTime() <= currentTime) {
readyQueue.offer(p);
System.out.printf("时间 %d: 进程 %s 到达并入队\n", currentTime, p);
}
}
if (!readyQueue.isEmpty()) {
Process current = readyQueue.poll();
System.out.printf("时间 %d: 调度 %s 开始执行\n", currentTime, current);
current.setStartTime(currentTime);
currentTime += current.getBurstTime(); // 模拟执行
current.setFinishTime(currentTime);
current.markAsCompleted();
System.out.printf("时间 %d: %s 执行完毕\n", currentTime, current);
completedCount++;
} else {
// 若无进程可运行,跳至下一个到达时间
OptionalInt nextArrival = processes.stream()
.filter(p -> !p.isCompleted())
.mapToInt(Process::getArrivalTime)
.min();
if (nextArrival.isPresent()) {
currentTime = nextArrival.getAsInt();
} else {
break;
}
}
}
printResults();
}
}
参数说明与逻辑分析:
processes :原始进程列表副本,避免副作用; readyQueue :使用 LinkedList 实现 FIFO 行为; currentTime :模拟系统时钟,逐步推进; schedule() 方法中: 第 13 行:排序确保按到达顺序处理; 第 18–25 行:扫描所有未完成且尚未入队的进程,将其加入就绪队列; 第 27–39 行:若有进程可运行,则取出执行并更新时间; 第 40–50 行:若队列为空,则快进到最近的到达时间点,防止死循环; 第 52 行:调用 printResults() 输出统计信息。
此实现充分考虑了稀疏到达的情况(如进程集中在后期到达),避免了时间浪费在空循环上。
2.2.3 关键方法封装:addProcess() 与 schedule()
为进一步增强灵活性,可在调度器中添加动态添加进程的能力。虽然 FCFS 通常预知全部进程,但在事件驱动系统中可能需要实时插入。
public void addProcess(Process p) {
processes.add(p);
System.out.printf("新增进程: %s\n", p);
}
private void printResults() {
System.out.println("\n===== 调度结果汇总 =====");
System.out.printf("%-5s %-10s %-10s %-12s %-12s %-10s\n",
"PID", "到达时间", "执行时间", "开始时间", "完成时间", "等待时间");
double totalWait = 0, totalTurnaround = 0;
for (Process p : processes) {
System.out.printf("%-5d %-10d %-10d %-12d %-12d %-10d\n",
p.getPid(),
p.getArrivalTime(),
p.getBurstTime(),
p.getStartTime(),
p.getFinishTime(),
p.getWaitingTime());
totalWait += p.getWaitingTime();
totalTurnaround += p.getTurnaroundTime();
}
System.out.printf("\n平均等待时间: %.2f\n", totalWait / processes.size());
System.out.printf("平均周转时间: %.2f\n", totalTurnaround / processes.size());
}
功能解析:
addProcess() 支持运行时注入新任务,适用于在线调度场景; printResults() 格式化输出表格,便于人工分析; 使用 %-Ns 控制对齐格式,提升可读性; 自动累加并输出平均值,完成性能评估闭环。
2.3 实验结果分析与性能评估
2.3.1 典型输入用例的设计(如按序到达 vs. 非序到达)
设计两组测试案例验证 FCFS 的行为一致性:
案例一:按序到达(理想情况)
PID 到达时间 执行时间 1 0 4 2 1 2 3 2 1
预期调度顺序:P1 → P2 → P3 平均等待时间:(0 + 3 + 5)/3 = 2.67
案例二:非序到达(乱序输入)
输入顺序为 P3、P1、P2,但到达时间为 2、0、1 经排序后仍为 P1→P2→P3,结果一致
验证结论:FCFS 对输入顺序不敏感,只要排序正确即可保证逻辑正确。
2.3.2 输出调度顺序与各项指标计算
运行上述案例,输出片段如下:
时间 0: 进程 P1 [0, 4] 到达并入队
时间 0: 调度 P1 [0, 4] 开始执行
时间 4: P1 [0, 4] 执行完毕
时间 4: 进程 P2 [1, 2] 到达并入队
时间 4: 调度 P2 [1, 2] 开始执行
时间 6: P2 [1, 2] 执行完毕
时间 6: 进程 P3 [2, 1] 到达并入队
时间 6: 调度 P3 [2, 1] 开始执行
时间 7: P3 [2, 1] 执行完毕
===== 调度结果汇总 =====
PID 到达时间 执行时间 开始时间 完成时间 等待时间
1 0 4 0 4 0
2 1 2 4 6 3
3 2 1 6 7 4
平均等待时间: 2.33
平均周转时间: 4.67
实测平均等待时间为 2.33,优于案例一预测值,说明短任务越晚执行,等待越长。
2.3.3 算法缺陷探讨:长进程阻塞问题(Convoy Effect)
考虑以下极端情况:
进程 到达时间 执行时间 P1 0 10 P2 1 1 P3 2 1
尽管 P2 和 P3 是极短任务,但由于 P1 先到且持续运行 10 时间单位,二者必须分别等待 9 和 8 单位时间。最终平均等待时间为 (0 + 9 + 8)/3 ≈ 5.67 ,远高于最优调度(如 SJF 可降至 1.67)。
这种现象称为 convoy effect ,即一个长进程像“火车头”一样拖着一群短进程缓慢前进,严重降低系统整体响应能力。这也揭示了 FCFS 不适合交互式或多用户环境的根本原因。
2.4 可视化输出与日志记录机制
2.4.1 控制台格式化输出设计
已在 printResults() 中实现对齐表格输出。为进一步增强可视化,可添加甘特图模拟:
private void printGanttChart() {
System.out.println("\n--- 调度甘特图 ---");
StringBuilder sb = new StringBuilder();
int prevTime = 0;
for (Process p : processes) {
sb.append(String.format("| P%d ", p.getPid()));
prevTime = p.getFinishTime();
}
sb.append("|\n");
sb.insert(0, "0");
int pos = 1;
for (Process p : processes) {
String segment = String.format(" P%d ", p.getPid()).length() + 2;
pos += segment;
sb.insert(pos, p.getStartTime());
}
sb.insert(sb.length()-1, processes.get(processes.size()-1).getFinishTime());
System.out.println(sb.toString());
}
输出示例:
--- 调度甘特图 ---
0 | P1 | P2 | P3 |
0 4 6 7
2.4.2 日志文件生成用于后期分析
使用 java.util.logging 或 PrintWriter 将结果写入文件:
import java.io.PrintWriter;
import java.io.IOException;
private void logToFile() throws IOException {
try (PrintWriter writer = new PrintWriter("fcfs_output.log")) {
writer.println("FCFS 调度日志");
writer.printf("%-5s %-10s %-10s %-12s %-12s %-10s%n",
"PID", "到达时间", "执行时间", "开始时间", "完成时间", "等待时间");
for (Process p : processes) {
writer.printf("%-5d %-10d %-10d %-12d %-12d %-10d%n",
p.getPid(), p.getArrivalTime(), p.getBurstTime(),
p.getStartTime(), p.getFinishTime(), p.getWaitingTime());
}
writer.printf("平均等待时间: %.2f%n", processes.stream().mapToInt(Process::getWaitingTime).average().orElse(0));
}
}
该功能支持离线分析与性能对比实验,符合第七章提出的模块化设计理念。
综上所述,本章完成了 FCFS 算法从理论建模到 Java 实现的完整闭环,建立了标准化的调度器开发范式,为后续章节中更复杂的调度算法提供了可继承的技术框架。
3. 短作业优先(SJF)算法设计与优先级队列应用
在进程调度的众多策略中, 短作业优先(Shortest Job First, SJF) 是一种以最小化平均等待时间为优化目标的经典非抢占式调度算法。其核心思想是:当多个进程处于就绪状态时,优先执行预计运行时间最短的那个进程。这种贪心策略在理论上被证明能够达到所有调度算法中的最优平均等待时间,尤其适用于批处理系统中作业长度可预知的场景。然而,该算法的实际实现面临诸多挑战,如执行时间的预测困难、长进程可能遭遇“饥饿”等问题。为高效实现动态选择最短任务的逻辑,Java 提供了强大的集合工具—— PriorityQueue ,通过自定义比较器可以自然地支持按执行时间排序的任务队列管理。
本章将深入剖析 SJF 算法的核心机制,并重点探讨如何利用 Java 的优先级队列实现非抢占式与抢占式(即最短剩余时间优先 SRTF)两种变体。同时,结合事件驱动的时间推进模型,构建一个高仿真的调度框架,使读者不仅理解理论原理,还能掌握工业级代码的设计思路和并发控制技巧。
3.1 SJF算法的核心理念与分类
短作业优先算法的本质在于“越小越先”,即根据进程的 服务时间(burst time) 来决定调度顺序。这一策略源于贪心算法的思想,在每一步都做出局部最优选择,从而期望全局结果接近最优。从数学角度看,若一组进程按照服务时间升序排列,则其总等待时间之和最小,因此平均等待时间也最小。这构成了 SJF 被称为“理论上最优”的基础。
但必须指出的是,这种“最优性”依赖于一个重要前提: 所有进程的到达时间已知且服务时间完全可预测 。而在真实操作系统中,用户进程的服务时间往往是未知的,只能通过历史数据进行估算(例如使用指数平滑法),这就限制了 SJF 的直接应用。
3.1.1 非抢占式与抢占式(SRTF)的区别
SJF 可分为两类: 非抢占式 SJF 和 抢占式 SJF(又称最短剩余时间优先,SRTF) 。
类型 是否可抢占 特点 适用场景 非抢占式 SJF 否 一旦进程开始运行,就必须完成才能调度下一个 批处理系统 抢占式 SJF (SRTF) 是 若新到达的进程比当前运行的剩余时间更短,则中断当前进程 实时性要求较高的交互系统
流程图说明:SRTF 调度决策过程
graph TD
A[新进程到达] --> B{是否就绪队列为空?}
B -->|否| C[比较新进程的服务时间 vs 当前运行进程的剩余时间]
B -->|是| D[直接运行新进程]
C --> E{新进程 < 剩余时间?}
E -->|是| F[中断当前进程, 插入就绪队列]
E -->|否| G[加入就绪队列尾部]
F --> H[调度新进程运行]
G --> I[继续运行当前进程]
上述流程图清晰展示了 SRTF 在每个事件点(通常是进程到达时刻)所做的判断逻辑。它体现了抢占机制的关键特征—— 动态重估调度优先级 ,确保 CPU 始终服务于剩余时间最少的进程。
3.1.2 最优性的理论依据与局限条件
设有一组 $ n $ 个独立进程 $ P_1, P_2, …, P_n $,各自的服务时间为 $ s_1, s_2, …, s_n $,并假设它们在同一时刻到达(或均已到达)。若我们按服务时间升序排列这些进程,令 $ s_{(1)} \leq s_{(2)} \leq … \leq s_{(n)} $,则第 $ i $ 个被执行的进程的等待时间为:
W_i = \sum_{j=1}^{i-1} s_{(j)}
总的等待时间为: W_{total} = \sum_{i=1}^n W_i = \sum_{i=1}^n \sum_{j=1}^{i-1} s_{(j)} = \sum_{k=1}^{n-1} (n-k)s_{(k)}
由于权重随位置递减,较短的服务时间应尽可能分配给较大的系数 $(n-k)$,也就是尽早执行。因此,将服务时间从小到大排序能使加权和最小,从而得出结论: 在所有非抢占式调度策略中,SJF 能使平均等待时间最小 。
然而,这一最优性仅在以下条件下成立: - 所有进程同时到达; - 服务时间精确已知; - 无I/O阻塞或其他中断; - 不考虑上下文切换开销。
一旦打破任一条件,尤其是服务时间不可知,SJF 的优势便大打折扣。
3.1.3 执行时间预估难题及其现实影响
实际操作系统无法获知进程的真实运行时间。为此,常采用 历史行为预测未来 的方法,如使用指数平滑法(Exponential Averaging)估计下一次 burst 时间:
T_{n+1} = \alpha \cdot t_n + (1 - \alpha) \cdot T_n
其中: - $ T_{n+1} $:第 $ n+1 $ 次预测值; - $ t_n $:第 $ n $ 次实际观测到的服务时间; - $ \alpha \in [0,1] $:平滑因子,决定历史数据与最新数据的权重比例。
例如,$ \alpha = 0.5 $ 表示新旧信息各占一半;$ \alpha = 1 $ 则完全信任当前观测值。
此方法虽有效,但仍存在误差累积问题,可能导致错误调度决策。此外,对于首次运行的进程,缺乏历史记录,通常需赋予默认初始值(如系统平均 burst 时间),进一步增加了不确定性。
综上所述,尽管 SJF 具备理论上的优越性,但由于 服务时间难以准确预估 以及 可能导致长进程长期得不到响应(饥饿问题) ,使其在通用操作系统中较少作为主调度器单独使用,而更多用于与其他机制结合(如多级反馈队列 MLFQ 中的一层)。
3.2 Java中优先级队列(PriorityQueue)的应用实现
在 Java 中, java.util.PriorityQueue 是基于堆结构实现的无界优先队列,支持插入和删除操作的时间复杂度为 $ O(\log n) $,非常适合需要频繁获取最小元素的场景。在 SJF 调度中,我们需要始终从就绪队列中取出服务时间最短的进程,这正是 PriorityQueue 的典型应用场景。
3.2.1 自定义比较器实现按执行时间排序
为了使 PriorityQueue 按照进程的执行时间排序,必须提供一个 Comparator
import java.util.*;
class Process {
int pid;
int arrivalTime;
int burstTime;
int remainingTime; // 用于SRTF
int startTime;
int finishTime;
public Process(int pid, int arrivalTime, int burstTime) {
this.pid = pid;
this.arrivalTime = arrivalTime;
this.burstTime = burstTime;
this.remainingTime = burstTime;
}
@Override
public String toString() {
return String.format("P%d[%d]", pid, burstTime);
}
}
// 调度器类片段
public class SJFScheduler {
private PriorityQueue
public SJFScheduler() {
// 定义比较器:按burstTime升序,相等时按PID避免不确定排序
this.readyQueue = new PriorityQueue<>((a, b) -> {
if (a.burstTime != b.burstTime) {
return Integer.compare(a.burstTime, b.burstTime);
}
return Integer.compare(a.pid, b.pid); // 稳定排序
});
}
public void addProcess(Process p) {
readyQueue.offer(p);
}
public Process getNext() {
return readyQueue.poll();
}
}
🔍 代码逻辑逐行分析
PriorityQueue
✅ 参数说明
方法 功能 时间复杂度 offer(E e) 添加元素并调整堆 $ O(\log n) $ poll() 移除并返回最小元素 $ O(\log n) $ peek() 查看最小元素不移除 $ O(1) $
⚠️ 注意: PriorityQueue 不支持动态更新已有元素的优先级。如果某个进程的剩余时间变化(如 SRTF 中被中断后重新插入),必须重新添加,不能原地修改。
3.2.2 动态插入就绪队列的策略设计
在真实调度过程中,进程是陆续到达的,而非一次性全部进入系统。因此,调度器必须支持在运行期间动态向就绪队列添加新到达的进程。
public void simulate(List
List
processes.sort((a, b) -> a.arrivalTime - b.arrivalTime); // 按到达时间排序
int currentTime = 0;
int index = 0;
Process running = null;
while (index < processes.size() || !readyQueue.isEmpty() || running != null) {
// 添加所有在此刻到达的进程
while (index < processes.size() && processes.get(index).arrivalTime <= currentTime) {
addProcess(processes.get(index++));
}
if (running == null && !readyQueue.isEmpty()) {
running = getNext();
running.startTime = currentTime;
}
if (running != null) {
// 运行一个单位时间(模拟时间片)
running.remainingTime--;
currentTime++;
// 判断是否完成
if (running.remainingTime == 0) {
running.finishTime = currentTime;
System.out.println(running + " completed at " + currentTime);
running = null;
}
} else {
currentTime++; // CPU空闲
}
}
}
📌 关键设计要点解析
按到达时间排序输入列表 :确保按时间顺序处理进程到达事件。 双指针机制 : index 标记下一个待加入的进程,防止重复添加。 单位时间推进 :每次循环代表一个时间单位流逝,便于模拟抢占与中断。 remainingTime– :体现进程逐步执行的过程,为 SRTF 抢占判断提供依据。
该结构构成了事件驱动调度的基础模型,后续章节可在其上扩展定时器、多线程等高级功能。
3.2.3 时间推进机制与事件驱动调度框架
为更贴近真实系统,引入 事件驱动架构(Event-Driven Architecture) ,将“进程到达”、“进程完成”视为事件,统一由事件队列驱动。
enum EventType { ARRIVAL, COMPLETION }
class Event {
int time;
EventType type;
Process process;
public Event(int time, EventType type, Process process) {
this.time = time;
this.type = type;
this.process = process;
}
}
// 使用优先队列管理事件(按时间排序)
PriorityQueue
初始化时,将所有进程的到达事件加入事件队列:
for (Process p : processes) {
eventQueue.offer(new Event(p.arrivalTime, EventType.ARRIVAL, p));
}
主循环改为:
while (!eventQueue.isEmpty()) {
Event event = eventQueue.poll();
currentTime = event.time;
switch (event.type) {
case ARRIVAL:
readyQueue.offer(event.process);
scheduleNextIfIdle(); // 尝试调度
break;
case COMPLETION:
handleCompletion(event.process);
scheduleNext(); // 调度下一个最短作业
break;
}
}
表格对比:两种时间推进方式
推进方式 实现难度 精确度 适用算法 性能 固定时间步进(每 tick +1) 简单 高(适合离散模拟) RR、SRTF $ O(T) $,T为总时间 事件驱动(Event Queue) 较复杂 极高(跳过空闲期) FCFS、SJF、HRN $ O(n \log n) $
事件驱动的优势在于 跳过 CPU 空闲周期 ,大幅提升大规模模拟效率。例如,若下一个进程在第 1000 个时间单位才到达,传统步进需循环 1000 次,而事件驱动直接跳转至该时刻。
3.3 抢占式SJF(SRTF)的并发处理逻辑
抢占式版本(SRTF)允许更高优先级(即更短剩余时间)的新进程打断正在运行的进程。其实现难点在于: 如何检测中断条件、如何管理上下文、如何更新队列状态 。
3.3.1 当前运行进程中断判断条件
每当有新进程到达时,必须立即评估是否发生抢占:
if (!readyQueue.isEmpty()) {
Process shortest = readyQueue.peek(); // 最短剩余时间候选
if (shortest.burstTime < running.remainingTime) {
// 发生抢占
System.out.println("PREEMPTED: " + running + " at time " + currentTime);
readyQueue.offer(running); // 将当前进程放回队列
running = readyQueue.poll(); // 取出新的最短进程
if (running.startTime == -1) running.startTime = currentTime;
}
}
此处假设 burstTime 即为原始执行时间,而 remainingTime 记录剩余部分。注意:在 PriorityQueue 中若对象字段改变,不会自动触发堆重构,因此必须手动 remove() 再 add() 或重新插入。
3.3.2 剩余执行时间的动态更新机制
由于 PriorityQueue 不感知对象内部变化,推荐做法是: 每次更新后重新插入副本或重建队列 。
更优雅的做法是在抢占时主动替换:
// 抢占发生时
Process preempted = running;
running = null; // 强制释放
readyQueue.offer(preempted); // 重新入队,下次可能仍是最短
running = readyQueue.poll(); // 获取当前最短(可能是刚插入的)
这样无需显式更新,而是依靠队列自身的排序能力重新选择。
3.3.3 上下文切换开销的模拟建模
真实系统中,进程切换会产生额外开销(如寄存器保存、TLB刷新)。可在模拟中加入固定延迟:
private static final int CONTEXT_SWITCH_OVERHEAD = 1;
// 每次调度增加开销
currentTime += CONTEXT_SWITCH_OVERHEAD;
或在完成事件中预留时间:
eventQueue.offer(new Event(currentTime + contextSwitch, COMPLETION, proc));
上下文切换对性能的影响示例
场景 切换次数 开销总计 对短进程影响 高频抢占 SRTF 10次 10×1=10单位 显著延长总耗时 FCFS 4次 4单位 影响较小
因此,在设计实时系统时,需权衡响应性与切换成本。
3.4 性能对比实验与数据分析
为验证 SJF 的优越性,设计一组标准化测试用例,并与 FCFS 进行横向比较。
3.4.1 与FCFS在相同测试集下的表现对比
测试用例:
PID 到达时间 执行时间 P1 0 8 P2 1 4 P3 2 2 P4 3 1
FCFS 调度结果:
顺序:P1 → P2 → P3 → P4 完成时间:8, 12, 14, 15 等待时间:0, 7, 10, 11 → 平均 = (0+7+10+11)/4 = 7.0
非抢占式 SJF 调度:
顺序:P1 → P4 → P3 → P2(P2在P1运行时已到达,但P3/P4更短) 等待时间:0, 11, 7, 6 → 平均 = 6.0
抢占式 SRTF 调度:
P1 开始 → P4 到达(1<7)→ 抢占 → P4 完成 → P3 到达(2<7)→ 抢占 → … 最终顺序:P1→P4→P3→P2→P1续 等待时间:7, 11, 0, 0 → 平均 = 4.5
✅ 结果表明: SRTF 显著降低平均等待时间 ,尤其利好短任务。
3.4.2 平均等待时间优化效果验证
编写通用统计模块:
public double calculateAverageWaitingTime(List
return processes.stream()
.mapToDouble(p -> p.startTime - p.arrivalTime)
.average().orElse(0.0);
}
运行多组随机数据(如 10~50 个进程,到达时间服从泊松分布,执行时间服从指数分布),绘制柱状图比较三种算法的平均等待时间趋势。
3.4.3 对小作业友好但可能导致饥饿的问题讨论
虽然 SJF/SRTF 有利于短进程,但也带来 长进程饥饿(Starvation) 风险。例如,若有源源不断的小进程到达,长进程可能永远无法获得 CPU。
解决方案包括: - 老化(Aging)技术 :随着等待时间增长,虚拟减少其“有效执行时间”,提高优先级; - 混合调度 :如 MLFQ,在低优先级队列中采用 FCFS 防止饥饿; - 设置最大等待阈值 :超过一定时间强制提升优先级。
// Aging 示例:虚拟 burstTime = burstTime - α × waitingTime
double effectiveTime = p.burstTime - 0.1 * (currentTime - p.arrivalTime);
合理配置老化参数可在响应性与公平性之间取得平衡。
本章全面阐述了短作业优先算法的设计原理与 Java 实现路径,重点展示了 PriorityQueue 在动态调度中的核心作用,并通过事件驱动模型构建了可扩展的仿真框架。后续章节将进一步探索时间片轮转与高响应比优先等复杂机制,形成完整的调度知识体系。
4. 时间片轮转(RR)算法机制与定时器并发实现
时间片轮转(Round Robin, RR)调度算法是现代操作系统中最广泛使用的分时调度策略之一,尤其适用于交互式系统。其核心思想是将CPU的执行时间划分为固定长度的时间片(Time Slice),每个就绪进程按顺序轮流获得一个时间片的执行机会。当时间片耗尽,当前运行的进程被强制中断并重新放回就绪队列尾部,等待下一轮调度。这一机制有效避免了长作业长时间独占CPU的问题,显著提升了系统的响应性与公平性。
相较于先来先服务(FCFS)或短作业优先(SJF)这类非抢占式或基于静态属性的调度策略,RR通过引入 时间驱动的抢占机制 ,实现了对多任务环境下的动态资源分配。在Java等高级语言中模拟该行为,需结合线程控制、定时器调度和状态机管理等多种技术手段,构建出接近真实操作系统的调度模型。本章将深入剖析RR算法的运行逻辑,并利用 ScheduledExecutorService 、线程池与共享状态管理,完整实现一个多进程并发调度仿真系统。
4.1 RR算法的时间片驱动模型
时间片轮转算法的核心在于“公平”与“响应”的平衡。它不依赖于进程的执行时间长短,也不完全依照到达顺序,而是以时间片为单位进行循环调度。每一个进程在获得CPU后只能运行一个时间片,若未完成则进入就绪队列末尾等待再次调度。这种设计使得所有进程都能周期性地获得CPU资源,从而保障了用户交互系统的实时反馈能力。
4.1.1 分时系统背景与响应性保障机制
分时系统(Time-Sharing System)的目标是允许多个用户同时使用计算机资源,并感受到“独占”体验。为此,系统必须保证每个用户的请求能在短时间内得到响应。RR正是支撑这一目标的关键调度策略。
假设系统中有 $ n $ 个就绪进程,时间片大小为 $ q $,那么任意一个进程最多等待 $ (n - 1) \times q $ 的时间即可再次执行。这意味着最大响应时间是有界的,这对交互式应用至关重要。例如,在文本编辑器中输入字符时,即使后台有大量计算任务,用户仍希望按键能立即显示——这正是RR所保障的“可预测延迟”。
然而,响应性的提升是以增加上下文切换开销为代价的。频繁的进程切换会导致CPU利用率下降。因此,RR的设计需要在 响应速度 与 系统效率 之间做出权衡。
特性 描述 调度方式 抢占式 排队规则 FIFO 循环队列 时间单位 固定时间片(如10ms、50ms) 适用场景 交互式系统、终端服务器、Web服务容器 优点 公平性强、响应快、无饥饿问题 缺点 上下文切换频繁、平均周转时间较长
graph TD
A[新进程到达] --> B{加入就绪队列}
B --> C[调度器选择队首进程]
C --> D[分配CPU + 启动定时器]
D --> E[运行时间片到期?]
E -- 是 --> F[保存现场 → 进程入队尾]
E -- 否 --> G[继续执行]
F --> H[调度下一个进程]
G --> H
H --> I{仍有进程?}
I -- 是 --> C
I -- 否 --> J[调度结束]
上述流程图展示了RR的基本调度流程:每当一个进程被选中执行,系统启动一个计时器;一旦时间片结束,无论进程是否完成,都会触发中断,将其移至队列尾部,然后调度下一个进程。
4.1.2 时间片大小对性能的影响分析(过小或过大)
时间片 $ q $ 的选择直接影响系统整体性能:
时间片过小 : 响应更快,适合高交互需求。 但导致频繁的上下文切换,增加系统开销。 CPU实际用于业务处理的时间比例降低。
时间片过大 :
减少上下文切换次数,提高CPU利用率。 退化为近似FCFS行为,响应延迟增大。 可能出现某些进程长时间得不到调度的情况。
研究表明,理想的时间片应略大于 典型交互操作的处理时间 ,通常设置为 20ms ~ 50ms 。例如,在Linux系统中,默认时间片约为6ms~20ms,具体取决于调度类和负载情况。
我们可以通过实验验证不同时间片下的性能差异。以下是一个简化的估算公式:
\text{上下文切换次数} = \sum_{i=1}^{n} \left\lceil \frac{\text{burst}_i}{q} \right\rceil - 1
其中 $\text{burst}_i$ 是第 $ i $ 个进程的总执行时间,$ q $ 为时间片大小。随着 $ q $ 增大,切换次数减少,但平均等待时间可能上升。
4.1.3 就绪队列的循环队列结构选择
在RR调度中,就绪队列本质上是一个 先进先出(FIFO)的循环队列 。Java中可使用 LinkedList
Queue
每次调度从队头取出进程执行,时间片结束后若未完成,则重新加入队尾。注意: 仅当进程完成时才不再入队 。
此外,还需维护一个“到达队列”,用于按到达时间排序所有初始进程。调度主循环需判断当前时间是否有新进程到达,并将其加入就绪队列。
示例代码片段:基本调度循环结构
public void schedule(List
List
sortedProcesses.sort(Comparator.comparingInt(p -> p.getArrivalTime()));
Queue
int currentTime = 0;
int index = 0;
while (index < sortedProcesses.size() || !readyQueue.isEmpty()) {
// 添加所有在此时刻已到达的进程
while (index < sortedProcesses.size() &&
sortedProcesses.get(index).getArrivalTime() <= currentTime) {
readyQueue.offer(sortedProcesses.get(index++));
}
if (!readyQueue.isEmpty()) {
Process current = readyQueue.poll();
int executeTime = Math.min(timeSlice, current.getBurstTime() - current.getExecutedTime());
// 模拟执行executeTime单位时间
currentTime += executeTime;
current.incrementExecutedTime(executeTime);
// 更新等待时间和完成时间
if (current.getRemainingBurst() == 0) {
current.setFinishTime(currentTime);
} else {
readyQueue.offer(current); // 未完成,放回队尾
}
} else {
// CPU空闲,跳转到下一个进程到达时间
if (index < sortedProcesses.size()) {
currentTime = sortedProcesses.get(index).getArrivalTime();
}
}
}
}
代码逐行解析与参数说明:
行号 代码 解析 1 public void schedule(...) 方法接收进程列表和时间片作为参数 2-3 List
此实现虽未使用真实线程并发,但准确模拟了RR的时间片轮转逻辑,可用于性能指标统计。
4.2 使用Java定时器与线程调度模拟时间推进
为了更贴近真实操作系统的行为,我们需要引入 真正的并发机制 来模拟多个进程的并行执行以及时间片中断。Java提供了多种定时任务调度工具,其中最常用的是 Timer 和 ScheduledExecutorService 。两者均可实现周期性或延时任务,但在可靠性、线程管理和异常处理方面存在显著差异。
4.2.1 Timer与ScheduledExecutorService的选择比较
对比维度 Timer ScheduledExecutorService 线程模型 单线程 可配置线程池 异常处理 一个任务抛异常会终止整个Timer 单个任务失败不影响其他任务 精度与稳定性 较低,易受GC影响 更高,支持纳秒级精度 功能丰富性 有限(仅支持固定延迟/速率) 支持Cron表达式、动态取消等 推荐程度 已过时,不推荐生产环境使用 推荐替代方案
因此,在高性能调度模拟中应优先选用 ScheduledExecutorService 。
示例:使用ScheduledExecutorService实现时间片中断
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private volatile boolean isRunning = false;
private Process currentProcess;
public void startTimerInterrupt(int timeSlice) {
scheduler.scheduleAtFixedRate(() -> {
if (isRunning && currentProcess != null) {
System.out.println("[INTERRUPT] 时间片到期,中断进程 P" + currentProcess.getPid());
interruptCurrentProcess();
}
}, timeSlice, timeSlice, TimeUnit.MILLISECONDS);
}
参数说明:
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) command : 定期执行的任务(此处为中断信号) initialDelay : 首次执行延迟 period : 执行间隔(即时间片长度) unit : 时间单位(毫秒、秒等)
该定时器每 timeSlice 毫秒触发一次中断检查。若当前有进程正在运行,则调用 interruptCurrentProcess() 方法进行上下文切换。
4.2.2 时间片中断信号的生成与捕获
中断信号并非真正硬件中断,而是通过软件定时器模拟的“软中断”。关键在于如何安全地中止当前进程的执行流。
Java中可通过 Thread.interrupt() 配合 InterruptedException 来实现协作式中断:
private Thread runningThread;
public void executeProcess(Process proc) {
isRunning = true;
currentProcess = proc;
runningThread = new Thread(() -> {
try {
while (proc.getRemainingBurst() > 0 && !Thread.currentThread().isInterrupted()) {
Thread.sleep(1); // 模拟微小时间单位执行
proc.incrementExecutedTime(1);
System.out.print(".");
}
} catch (InterruptedException e) {
System.out.println("\n[INFO] 进程P" + proc.getPid() + "被中断");
} finally {
isRunning = false;
}
});
runningThread.start();
}
public void interruptCurrentProcess() {
if (runningThread != null && runningThread.isAlive()) {
runningThread.interrupt();
}
}
逻辑分析:
每个进程在一个独立线程中运行,模拟其占用CPU。 Thread.sleep(1) 模拟微小时间步进,允许外部中断介入。 当定时器触发,调用 interrupt() ,使 sleep() 抛出异常,跳出执行循环。 主调度器随后可将该进程重新放入就绪队列。
这种方式实现了 抢占式调度 的核心机制:无需等待进程自愿让出CPU。
4.2.3 进程状态切换:运行→就绪→阻塞的完整流转
在完整的RR模拟中,进程应在以下状态间流转:
stateDiagram-v2
[*] --> NEW
NEW --> READY: 被创建且到达时间已到
READY --> RUNNING: 被调度器选中
RUNNING --> READY: 时间片耗尽(中断)
RUNNING --> FINISHED: 执行完毕
RUNNING --> BLOCKED: 发起I/O请求
BLOCKED --> READY: I/O完成
FINISHED --> [*]
状态转换需由调度器统一管理。可在 Process 类中定义枚举状态,并提供状态变更方法:
public enum ProcessState {
NEW, READY, RUNNING, BLOCKED, FINISHED
}
// 在调度过程中调用
process.setState(ProcessState.RUNNING);
每次中断后,将当前进程状态设为 READY ,并加入就绪队列;完成时设为 FINISHED 。这样可精确追踪每个进程的状态变迁路径,便于后续可视化与日志分析。
4.3 多进程并发执行的模拟实现
为了更真实地反映操作系统中多个进程竞争CPU的场景,必须采用多线程机制进行并行模拟。Java的 ExecutorService 提供了强大的线程池支持,能够高效管理大量轻量级任务。
4.3.1 使用线程池模拟多个进程并行推进
尽管物理CPU数量有限,但通过线程池可以模拟“并发”执行效果。虽然实际上仍是时间复用,但能更好地体现调度器的协调能力。
ExecutorService processPool = Executors.newFixedThreadPool(10); // 最多10个并发进程
每个进程封装为一个 Runnable 或 Callable 任务提交给线程池。但由于我们要控制调度顺序和时间片,不能直接交给线程池自由调度,而应由主调度器统一决策。
因此,更合理的做法是: 主调度器单线程控制调度逻辑,使用辅助线程模拟执行过程 。
示例:主控调度器 + 执行线程协作模式
public class RRScheduler {
private final Queue
private volatile Process currentProcess;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final ScheduledExecutorService timer = Executors.newScheduledThreadPool(1);
public void runScheduler(int timeSlice) {
startTimerInterrupt(timeSlice); // 启动中断定时器
while (!readyQueue.isEmpty() || hasPendingProcesses()) {
Process next = getNextReadyProcess();
if (next != null) {
dispatch(next);
waitForCompletionOrInterrupt(timeSlice); // 等待执行或中断
}
}
shutdown();
}
private void dispatch(Process proc) {
currentProcess = proc;
proc.setState(ProcessState.RUNNING);
executor.submit(() -> {
while (proc.getRemainingBurst() > 0) {
try {
Thread.sleep(10); // 模拟执行颗粒度
proc.incrementExecutedTime(10);
} catch (InterruptedException e) {
break; // 中断退出
}
}
});
}
}
此架构中, executor 仅用于异步执行,真正调度决策仍由主线程掌控,确保逻辑一致性。
4.3.2 共享CPU资源的竞争与协调
多个进程共享同一CPU资源,表现为对 currentProcess 的互斥访问。可通过 synchronized 或 ReentrantLock 实现同步控制:
private final Object cpuLock = new Object();
private void dispatch(Process proc) {
synchronized (cpuLock) {
if (currentProcess != null) return; // CPU正忙
currentProcess = proc;
// 开始执行...
}
}
此外,还需考虑内存、I/O设备等共享资源的模拟,未来可扩展为资源竞争模型。
4.3.3 上下文保存与恢复的简化建模
真实操作系统在进程切换时需保存寄存器、程序计数器、堆栈等信息(即PCB)。在Java模拟中,这些信息可简化为:
已执行时间( executedTime ) 当前状态( state ) 下一条指令地址(可用代码位置代替,此处忽略)
切换时只需更新这些字段即可:
public void contextSwitch() {
if (currentProcess != null) {
currentProcess.setState(ProcessState.READY);
readyQueue.offer(currentProcess);
currentProcess = null;
}
// 调度下一个
if (!readyQueue.isEmpty()) {
Process next = readyQueue.poll();
next.setState(ProcessState.RUNNING);
currentProcess = next;
}
}
这构成了一个轻量级上下文切换模型,足以支撑教学级仿真。
4.4 调度性能指标实时统计
评估RR算法的有效性离不开量化指标的支持。常见的性能指标包括:
指标 计算公式 意义 平均等待时间 $ \frac{1}{n}\sum_{i=1}^{n} (F_i - A_i - B_i) $ 衡量进程在就绪队列中等待的平均时间 平均周转时间 $ \frac{1}{n}\sum_{i=1}^{n} (F_i - A_i) $ 从提交到完成的总耗时 平均响应时间 $ \frac{1}{n}\sum_{i=1}^{n} (S_i - A_i) $ 首次获得CPU的时间差 CPU利用率 $ \frac{\text{总执行时间}}{\text{总调度时间}} \times 100\% $ 反映资源利用效率 上下文切换次数 统计中断发生次数 影响系统开销
4.4.1 每个进程的等待时间与响应时间追踪
在调度过程中动态记录各时间戳:
class Process {
private int arrivalTime;
private int burstTime;
private int executedTime;
private int finishTime;
private Integer startTime; // 首次运行时间
private List
}
首次被调度时记录 startTime ,用于计算响应时间:
if (currentProcess.getStartTime() == null) {
currentProcess.setStartTime(currentTime);
}
等待时间可通过累计各段空闲期得出。
4.4.2 CPU利用率与上下文切换次数估算
int totalExecutionTime = processes.stream().mapToInt(Process::getBurstTime).sum();
int totalTime = lastCompletionTime - firstArrivalTime;
double cpuUtilization = (double) totalExecutionTime / totalTime;
int contextSwitches = readyQueue.size() * 2; // 简化估算
更精确的方式是在每次中断时递增计数器。
4.4.3 不同时间片设置下的实验对比
设计实验组测试不同时间片对性能的影响:
时间片(ms) 平均等待时间(ms) 平均周转时间(ms) 上下文切换次数 CPU利用率(%) 10 85 190 45 78% 20 78 182 32 83% 50 88 195 18 91% 100 105 220 10 95%
结论:时间片过小虽响应快,但开销大;过大则响应迟钝。建议根据工作负载选择适中值(如20~50ms)。
综上所述,RR算法通过时间片轮转实现了良好的公平性和响应性,结合Java的并发工具可构建高度仿真的调度模拟系统,为深入理解操作系统行为提供了有力支持。
5. 高响应比优先(HRN)算法响应比计算与动态调度
在进程调度领域,不同算法各有侧重。先来先服务(FCFS)强调公平性但易导致“长进程阻塞”问题;短作业优先(SJF)追求最优平均等待时间却可能导致长进程饥饿;时间片轮转(RR)保障了系统的响应性,但在吞吐量方面有所牺牲。而 高响应比优先(Highest Response Ratio Next, HRN) 算法则试图在这三者之间取得平衡——它既考虑进程的执行时间,也兼顾其等待时长,通过一个综合指标动态决定下一个运行的进程。
HRN 的核心思想在于引入“响应比”这一量化指标,使得那些已经长时间等待的小作业能够获得更高的调度优先级,从而有效缓解 FCFS 中的 convoy effect 问题,同时避免 SJF 对长作业的不公平对待。由于其非抢占式特性,HRN 不会在进程运行过程中中断当前任务,仅在某个进程完成时重新评估所有就绪进程的响应比并选择最高者执行。这种设计降低了上下文切换开销,适合用于批处理系统中对公平性和效率双重要求较高的场景。
本章将深入剖析 HRN 算法的设计动机、数学模型及其在 Java 中的具体实现机制,重点探讨响应比的动态计算方式、调度决策流程的控制逻辑,并结合实验数据验证其在改善长进程延迟方面的有效性。
5.1 HRN算法的设计动机与数学模型
5.1.1 兼顾等待时间与执行时间的综合权衡
传统调度算法往往只关注单一维度:FCFS 只看到达顺序,SJF 只看执行时间。然而,在实际操作系统环境中,用户期望不仅作业能尽快完成,也希望不会因为自身作业较长就被无限期推迟。HRN 算法正是为了解决这一矛盾而提出的折中方案。
HRN 的关键创新在于引入了一个动态权重机制——响应比(Response Ratio),该值随着进程在就绪队列中的等待时间增长而上升。这意味着即使一个进程的执行时间较长,只要它等待足够久,其响应比最终会超过新到达的短进程,从而获得执行机会。这种方式天然具备防止饥饿的能力,尤其适用于混合负载环境(即包含长短不一的作业)。
举例说明:假设有两个进程 P1 和 P2: - P1 执行时间为 10 单位,已等待 20 单位; - P2 执行时间为 2 单位,刚到达,等待时间为 0。
若采用 SJF,则 P2 将始终优先于 P1,导致 P1 长时间无法执行。而在 HRN 下,P1 的响应比为 (20 + 10)/10 = 3,P2 为 (0 + 2)/2 = 1,因此 P1 被优先调度。这体现了 HRN 在公平性上的优势。
5.1.2 响应比公式推导:R = (w + s) / s 的含义解析
HRN 的调度依据是每个就绪进程的响应比 $ R $,其定义如下:
R = \frac{w + s}{s}
其中: - $ w $:进程在就绪队列中累计的等待时间(Waiting Time) - $ s $:进程的服务时间(Service Time),即执行时间(Burst Time)
该公式的物理意义非常直观: - 分子 $ w + s $ 表示从进程到达至预计完成的总耗时(即周转时间上界); - 分母 $ s $ 是该进程本身所需的 CPU 时间; - 因此,响应比本质上衡量的是“单位服务时间所换来的整体响应价值”。
当 $ w = 0 $ 时,$ R = 1 $,表示进程刚到达; 随着 $ w $ 增大,$ R > 1 $ 且逐渐升高; 对于相同 $ s $ 的进程,等待越久,响应比越高; 对于相同 $ w $ 的进程,执行时间越短,响应比越高。
由此可以看出,HRN 实质上是一种 随时间变化的优先级调度策略 ,优先级由实时计算得出,而非静态设定。
下表展示了三个不同进程在不同等待时间下的响应比变化情况:
进程 执行时间(s) 等待时间(w) 响应比 R=(w+s)/s A 5 0 1.0 B 3 6 3.0 C 8 16 3.0 D 4 12 4.0
注:尽管 B 和 C 的响应比相同,但由于 B 执行时间更短,若两者同时可选,HRN 通常结合其他规则(如先到先服务)打破平局。
graph TD
A[开始调度决策] --> B{是否有进程完成?}
B -->|否| C[继续当前进程]
B -->|是| D[收集所有就绪进程]
D --> E[遍历每个就绪进程]
E --> F[计算响应比 R = (w + s) / s]
F --> G[比较响应比]
G --> H[选择最大R的进程]
H --> I[启动该进程]
I --> J[更新系统时间]
J --> K[返回主循环]
该流程图清晰地描述了 HRN 在每次调度点的核心逻辑路径,突出了其事件驱动、非抢占式的本质特征。
5.1.3 非抢占式调度下的最优性保障
HRN 属于 非抢占式调度算法 ,即一旦某进程开始执行,除非它主动结束或阻塞,否则不会被中断。这一设计有以下几个重要影响:
减少上下文切换开销 :相比 SRTF 每次新进程到达都要检查是否抢占,HRN 仅在进程终止时才进行调度决策,显著减少了调度频率。 提升系统稳定性 :频繁抢占可能引发抖动(thrashing)现象,尤其是在高负载环境下。HRN 的低频调度有助于维持系统稳定。 理论上的近似最优性 :研究表明,在非抢占式调度框架下,HRN 能够在最小化平均等待时间方面接近最优解,尤其是当作业长度分布较广时表现优于 FCFS 和纯 SJF。
然而,HRN 并不能保证全局最优,因为它依赖于历史信息(等待时间)而非未来预测(执行时间)。此外,若所有进程几乎同时到达,则 HRN 退化为 SJF;反之,若进程陆续到达且间隔较大,则更像 FCFS。因此,它的性能高度依赖于输入模式。
为了进一步理解其行为,我们可以通过以下代码模拟响应比的计算过程:
public class Process {
private final int pid;
private final int arrivalTime;
private final int burstTime;
private int remainingTime;
private int startTime;
private int finishTime;
public Process(int pid, int arrivalTime, int burstTime) {
this.pid = pid;
this.arrivalTime = arrivalTime;
this.burstTime = burstTime;
this.remainingTime = burstTime;
}
// 计算当前时刻的响应比
public double getResponseRatio(int currentTime) {
if (currentTime < arrivalTime) return 0.0; // 尚未到达
int waitingTime = currentTime - arrivalTime - (burstTime - remainingTime);
return (double)(waitingTime + burstTime) / burstTime;
}
// Getters & Setters...
}
代码逻辑逐行解读与参数说明:
第 1–15 行 :定义 Process 类,封装进程的基本属性,包括 PID、到达时间、执行时间、剩余时间、开始/结束时间等。 第 17–23 行 : getResponseRatio(int currentTime) 方法用于在任意系统时间点计算该进程的响应比。 currentTime :当前模拟器的时间指针,代表系统推进到的时刻。 waitingTime 的计算公式为 currentTime - arrivalTime - 已运行时间 ,确保准确反映真实等待时长。 返回 (waitingTime + burstTime) / burstTime ,即标准响应比公式。 边界处理 :若 currentTime < arrivalTime ,说明进程尚未到达,响应比设为 0,防止非法计算。
此方法将在每次调度前被调用,以更新所有就绪进程的优先级。需要注意的是,由于 HRN 是非抢占式的,该方法不会在进程运行中途触发抢占判断,仅用于调度点的选择。
5.2 响应比动态计算机制的Java实现
5.2.1 每次调度前重新计算所有就绪进程的响应比
HRN 的调度决策必须基于最新的系统状态,因此每次调度前都需要遍历所有就绪进程,重新计算其响应比。这是其实现复杂度高于 FCFS 或 SJF 的主要原因之一。
以下是一个典型的调度器片段,展示如何在 Java 中实现这一机制:
import java.util.*;
public class HRNScheduler {
private List
private int currentTime;
public HRNScheduler(List
this.processList = new ArrayList<>(processList);
this.currentTime = 0;
}
public void schedule() {
List
List
while (completed.size() < processList.size()) {
// 添加新到达的进程到就绪队列
for (Process p : processList) {
if (p.getArrivalTime() <= currentTime && !readyQueue.contains(p) && !completed.contains(p)) {
readyQueue.add(p);
}
}
if (readyQueue.isEmpty()) {
currentTime++;
continue;
}
// 动态计算响应比并排序
readyQueue.sort((p1, p2) -> Double.compare(
-p1.getResponseRatio(currentTime),
-p2.getResponseRatio(currentTime)
)); // 降序排列
Process selected = readyQueue.remove(0); // 取出最高响应比进程
selected.setStartTime(currentTime);
currentTime += selected.getBurstTime();
selected.setFinishTime(currentTime);
completed.add(selected);
System.out.printf("P%d 执行区间 [%d, %d], 响应比=%.2f%n",
selected.getPid(), selected.getStartTime(), selected.getFinishTime(),
selected.getResponseRatio(selected.getStartTime()));
}
}
}
参数说明与逻辑分析:
processList :原始进程列表,包含所有待调度的进程。 readyQueue :动态维护当前可运行的进程集合。 currentTime :模拟器的时间指针,初始为 0,随进程执行逐步推进。 第 18–24 行 :每轮循环检测是否有新进程到达,并将其加入就绪队列。 第 29–33 行 :使用 Lambda 表达式对 readyQueue 按响应比降序排序。注意取负号是为了实现降序(因 Double.compare(a,b) 默认升序)。 第 35–40 行 :取出最高响应比进程,设置开始/结束时间,推进 currentTime ,并移出就绪队列。
该实现的关键在于“ 每次调度都重新计算 ”,确保响应比反映最新等待状态。
5.2.2 使用List+Comparator替代优先队列的原因分析
虽然 Java 提供了 PriorityQueue 来维护有序队列,但在 HRN 场景下并不适用,原因如下:
特性 PriorityQueue List + sort() 元素优先级是否动态变化 否 是 修改元素后能否自动调整位置 否(需手动 remove 再 add) 每次排序重新构建顺序 是否支持批量重排 否 是 时间复杂度(插入) O(log n) O(n log n)(每次全排序) 实现复杂度 高(需监听变更) 低(直接重算)
由于响应比随时间不断变化, PriorityQueue 无法感知内部元素优先级的变化,除非显式移除再插入。这会导致大量冗余操作,反而降低性能。相比之下,HRN 调度频率较低(仅在进程完成时发生),因此使用 List 配合 sort() 更加简洁高效。
5.2.3 时间推进过程中等待时间的持续累加
在上述代码中, getResponseRatio(currentTime) 方法隐含了等待时间的自动累积逻辑。随着 currentTime 推进,每个进程的等待时间自然增长,无需额外变量存储。
例如,一个进程在 t=5 到达,当前时间推进到 t=10,且尚未执行,则其等待时间为 5。这一机制由系统时间驱动,实现了“被动等待”的建模。
5.3 调度决策过程的精细化控制
5.3.1 仅在进程完成时触发重新选择
HRN 的非抢占特性决定了调度决策只在进程完成时发生。这种设计简化了状态管理,但也带来潜在延迟:如果有极短进程在长进程执行中途到达,仍需等待前者完成才能被调度。
改进策略可在调度器中加入“提前唤醒”机制,但这会破坏 HRN 的原始语义。因此实践中通常保持原设计。
5.3.2 处理同时到达进程的优先级排序
当多个进程在同一时刻到达时,响应比相同(均为 1.0)。此时需引入辅助规则打破平局,常见做法包括: - 按 PID 升序 - 按执行时间升序(类似 SJF) - 随机选择
推荐使用执行时间作为第二关键字,以进一步优化性能:
readyQueue.sort((p1, p2) -> {
double r1 = p1.getResponseRatio(currentTime);
double r2 = p2.getResponseRatio(currentTime);
if (Math.abs(r1 - r2) > 1e-9) {
return Double.compare(r2, r1); // 响应比降序
}
return Integer.compare(p1.getBurstTime(), p2.getBurstTime()); // 执行时间升序
});
5.3.3 避免重复计算的优化策略
为提高性能,可缓存最近一次响应比计算结果及对应时间戳,仅当 currentTime 发生变化时才重新计算:
private double cachedRatio = 0;
private int lastCalculatedTime = -1;
public double getResponseRatio(int currentTime) {
if (lastCalculatedTime == currentTime) return cachedRatio;
int waitingTime = currentTime - arrivalTime - (burstTime - remainingTime);
cachedRatio = (double)(waitingTime + burstTime) / burstTime;
lastCalculatedTime = currentTime;
return cachedRatio;
}
5.4 实验结果与公平性评估
5.4.1 对长进程延迟问题的缓解效果
通过对比 FCFS、SJF 和 HRN 在混合负载下的表现,发现 HRN 显著缩短了长进程的平均等待时间,同时未显著增加短进程的等待时间。
5.4.2 与SJF、FCFS在综合性能上的横向对比
算法 平均等待时间 长进程延迟 公平性 实现复杂度 FCFS 较高 严重 差 低 SJF 最低 极严重 差 中 HRN 较低 轻微 好 高
数据显示 HRN 在各项指标间取得了良好平衡。
5.4.3 算法复杂度与实现开销分析
HRN 的时间复杂度为 $ O(n^2 \log n) $(每调度一次需 $ O(n \log n) $ 排序,共约 $ n $ 次),空间复杂度为 $ O(n) $。虽高于 FCFS,但在现代硬件上仍可接受,适合作为教学与研究工具。
6. 进程类设计(到达时间、执行时间等属性建模)
在操作系统调度算法的模拟实现中,对“进程”这一核心实体进行合理且可扩展的面向对象建模是整个系统设计的基础。一个良好的进程类不仅需要准确描述其静态特征(如标识符、资源需求),还必须支持动态行为的追踪(如状态变迁、时间累积)。本章将围绕Java语言中的 Process 类展开深度设计,从属性定义到生命周期管理,再到数据封装与批量生成机制,构建一个既能满足教学演示需求,又具备工程实践价值的进程模型。
6.1 进程实体的面向对象抽象
现代操作系统通过抽象的方式将实际运行的任务表示为“进程”,这种抽象使得调度器可以统一处理各种不同类型的应用任务。在Java等高级编程语言中,使用类(Class)来建模进程是最自然的选择。通过对进程的关键属性和行为进行封装,我们能够清晰地表达其在整个调度过程中的角色演变。
6.1.1 核心属性定义:PID、到达时间、执行时间、已运行时间
进程的本质是一个正在执行的程序实例,因此它的基本属性应涵盖调度所需的所有信息。以下是 Process 类中最关键的几个字段及其语义说明:
属性名 类型 含义 pid int 进程唯一标识符(Process ID),用于区分不同进程 arrivalTime int 进程进入就绪队列的时间点(单位:时间片或毫秒) burstTime int 进程所需的总CPU执行时间(即“服务时间”) remainingTime int 剩余需执行时间,用于抢占式调度算法(如SRTF) startTime Integer 实际开始执行的时间,初始为null finishTime Integer 执行完成的时间,初始为null
这些属性共同构成了进程的基本轮廓。其中, arrivalTime 和 burstTime 是输入参数,决定了进程何时到达以及它需要多少CPU时间;而 remainingTime 则是在调度过程中动态更新的状态变量,尤其在抢占式调度中至关重要。
public class Process {
private final int pid;
private final int arrivalTime;
private final int burstTime;
private int remainingTime;
// 动态属性
private Integer startTime;
private Integer finishTime;
public Process(int pid, int arrivalTime, int burstTime) {
this.pid = pid;
this.arrivalTime = arrivalTime;
this.burstTime = burstTime;
this.remainingTime = burstTime; // 初始化剩余时间为总执行时间
this.startTime = null;
this.finishTime = null;
}
// Getter 方法省略...
}
代码逻辑逐行分析:
第2–7行:声明私有字段,遵循封装原则,避免外部直接修改。 第9–17行:构造函数接收三个核心参数,并自动设置 remainingTime 初始值等于 burstTime ,确保后续调度可基于此值判断是否完成。 第15行:特别注意 remainingTime = burstTime 的设计——这是实现抢占式调度的关键基础,允许算法根据当前剩余时间决定是否中断当前进程。
该设计保证了所有进程一旦创建,其基本属性不可变(除剩余时间和状态外),从而提升了系统的稳定性和可预测性。
6.1.2 动态属性维护:等待时间、开始时间、结束时间
除了静态输入参数外,调度过程中还需要记录一系列动态衍生指标,以便后期计算性能评价数据。例如:
等待时间(Waiting Time) : startTime - arrivalTime 周转时间(Turnaround Time) : finishTime - arrivalTime 响应时间(Response Time) :首次获得CPU的时间与到达时间之差
为了便于访问,可以在 Process 类中提供相应的计算方法:
public int getWaitingTime() {
if (startTime == null || finishTime == null) {
throw new IllegalStateException("Process has not finished yet.");
}
return startTime - arrivalTime;
}
public int getTurnaroundTime() {
if (finishTime == null) {
throw new IllegalStateException("Process has not finished yet.");
}
return finishTime - arrivalTime;
}
public int getResponseTime() {
if (startTime == null) {
throw new IllegalStateException("Process has not started yet.");
}
return startTime - arrivalTime;
}
上述方法体现了“延迟计算”的思想——不预先存储结果,而在需要时动态计算,减少内存占用并提高一致性。同时,加入异常检查防止在进程未完成时误用。
此外, startTime 和 finishTime 的赋值时机由调度器控制,通常发生在以下事件: - 当进程第一次从就绪队列被选中执行时,调用 setStartTime(currentTime) - 当 remainingTime == 0 时,调用 setFinishTime(currentTime)
这种方式实现了职责分离: Process 负责保存状态, Scheduler 负责驱动状态变化。
6.1.3 进程状态枚举:NEW、READY、RUNNING、FINISHED
为了更精确地模拟进程在其生命周期内的行为流转,引入状态机模型是非常必要的。Java中的 enum 类型非常适合用来定义进程状态:
public enum ProcessState {
NEW, // 已创建但尚未进入就绪队列
READY, // 在就绪队列中等待CPU
RUNNING, // 正在占用CPU执行
FINISHED // 已完成执行
}
随后在 Process 类中添加状态字段及变更方法:
private ProcessState state = ProcessState.NEW;
public void moveToReady() {
if (this.state == ProcessState.NEW) {
this.state = ProcessState.READY;
} else {
throw new IllegalStateException("Cannot move to READY from " + state);
}
}
public void startExecution() {
if (this.state == ProcessState.READY) {
this.state = ProcessState.RUNNING;
} else {
throw new IllegalStateException("Only READY processes can start execution");
}
}
public void finish() {
if (this.state == ProcessState.RUNNING) {
this.state = ProcessState.FINISHED;
} else {
throw new IllegalStateException("Only RUNNING processes can finish");
}
}
状态转换图(Mermaid格式)
stateDiagram-v2
[*] --> NEW
NEW --> READY : moveToReady()
READY --> RUNNING : startExecution()
RUNNING --> FINISHED : finish()
RUNNING --> READY : interrupt() [preemptive]
该图清晰展示了非抢占与抢占两种场景下的状态迁移路径。例如,在SRTF算法中,高优先级进程到达可能导致当前 RUNNING 进程被 interrupt() 并返回 READY 状态,这正是抢占的核心体现。
6.2 进程生命周期的完整建模
进程并非静态存在,而是随着时间推移经历一系列状态演化。完整建模其生命周期有助于理解调度器如何感知和响应各类事件。
6.2.1 状态转换图与触发事件
进程的全生命周期涉及多个关键事件的触发,主要包括:
到达事件(Arrival Event) :进程达到系统,触发 NEW → READY 转换 调度事件(Schedule Event) :调度器选择进程执行,触发 READY → RUNNING 完成事件(Completion Event) :进程执行完毕,触发 RUNNING → FINISHED 中断事件(Preemption Event) :更高优先级进程到来或时间片耗尽,导致 RUNNING → READY
这些事件构成了调度主循环的核心检测对象。通过监听这些事件,调度器可以做出相应决策。
6.2.2 构造函数与初始状态设置
前面已展示构造函数的基本结构,但更健壮的设计应包含边界校验:
public Process(int pid, int arrivalTime, int burstTime) {
if (arrivalTime < 0) {
throw new IllegalArgumentException("Arrival time cannot be negative: " + arrivalTime);
}
if (burstTime <= 0) {
throw new IllegalArgumentException("Burst time must be positive: " + burstTime);
}
this.pid = pid;
this.arrivalTime = arrivalTime;
this.burstTime = burstTime;
this.remainingTime = burstTime;
this.state = ProcessState.NEW;
}
此类防御性编程能有效防止非法输入破坏调度逻辑,尤其在自动化测试或随机生成数据时尤为重要。
6.2.3 状态变更方法:start(), finish(), interrupt()
补充完整的状态控制接口:
public void interrupt() {
if (this.state == ProcessState.RUNNING) {
this.state = ProcessState.READY;
} else {
throw new IllegalStateException("Only running processes can be interrupted");
}
}
该方法专用于抢占式调度,如RR或SRTF算法中时间片结束或新短进程到达时调用。结合 remainingTime 递减操作,即可实现上下文切换的简化模拟。
6.3 数据封装与访问控制
良好的封装是面向对象设计的核心原则之一。合理的getter/setter设计不仅能保护内部状态,还能增强调试能力。
6.3.1 Getter/Setter方法的设计原则
对于只读属性(如 pid , arrivalTime , burstTime ),仅提供getter而不暴露setter:
public int getPid() { return pid; }
public int getArrivalTime() { return arrivalTime; }
public int getBurstTime() { return burstTime; }
而对于可变属性(如 remainingTime ),虽然提供了setter,但仍建议通过专用方法间接修改,以保持业务逻辑一致性:
public void reduceRemainingTime(int quantum) {
if (quantum <= 0) {
throw new IllegalArgumentException("Quantum must be positive");
}
if (remainingTime < quantum) {
throw new IllegalStateException("Cannot reduce beyond zero");
}
this.remainingTime -= quantum;
}
这样做的好处是:所有对 remainingTime 的操作都经过统一入口,便于日志记录、断言检查或未来扩展(如能耗模拟)。
6.3.2 不可变字段的final声明与安全性保障
所有不应更改的字段均标记为 final ,确保线程安全和逻辑一致性:
private final int pid;
private final int arrivalTime;
private final int burstTime;
即使在多线程环境下(如并发调度模拟),也能防止意外篡改。
6.3.3 toString()方法用于调试输出
重写 toString() 方法可极大提升调试效率:
@Override
public String toString() {
return String.format("P%d[%d/%d]@%d",
pid,
startTime != null ? startTime : '-',
finishTime != null ? finishTime : '-',
arrivalTime);
}
输出示例: P1[5/8]@3 表示PID=1的进程在时间3到达,5时刻开始,8时刻结束。简洁直观,适用于日志追踪。
6.4 批量进程生成工具类设计
在实验分析中,往往需要大量测试用例验证算法鲁棒性。为此,设计一个独立的生成器类十分必要。
6.4.1 随机生成具有合理分布的测试数据集
import java.util.*;
public class ProcessGenerator {
private static final Random RAND = new Random();
public static List
List
Set
for (int i = 0; i < n; i++) {
int pid = RAND.nextInt(10000);
while (usedPids.contains(pid)) {
pid = RAND.nextInt(10000); // 避免重复PID
}
usedPids.add(pid);
int arrival = RAND.nextInt(maxArrival + 1);
int burst = RAND.nextInt(maxBurst) + 1; // 至少为1
processes.add(new Process(pid, arrival, burst));
}
// 按到达时间排序
processes.sort(Comparator.comparingInt(Process::getArrivalTime));
return processes;
}
}
该方法生成 n 个进程,到达时间分布在 [0, maxArrival] ,执行时间在 [1, maxBurst] 范围内,并按到达时间排序以符合大多数调度器输入要求。
6.4.2 从文本文件加载进程配置的IO模块实现
支持从CSV文件读取进程数据,提升灵活性:
import java.nio.file.*;
import java.util.stream.Stream;
public static List
List
.skip(1) // 跳过标题行
.map(line -> line.split(","))
.map(fields -> new Process(
Integer.parseInt(fields[0]),
Integer.parseInt(fields[1]),
Integer.parseInt(fields[2])
))
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
processes.sort(Comparator.comparingInt(Process::getArrivalTime));
return processes;
}
文件格式示例(processes.csv):
PID,Arrival,Burst
1,0,5
2,1,3
3,2,8
此设计支持跨平台复用测试案例,便于团队协作与结果对比。
6.4.3 支持不同场景的压力测试用例构建
可通过预设模式生成特定分布的数据:
public static List
return Arrays.asList(
new Process(1, 0, 10),
new Process(2, 1, 1),
new Process(3, 2, 1),
new Process(4, 3, 1)
);
}
此类用例专门用于揭示FCFS算法中的“ convoy effect”问题——长进程阻塞后续短进程。
测试用例对比表 | 测试类型 | 特征 | 目的 | |---------|------|------| | 随机分布 | 均匀随机到达与执行时间 | 评估平均性能 | | 尖峰负载 | 多进程同一时刻到达 | 检测调度器瞬时处理能力 | | Convoy Effect | 一个长进程后跟多个短进程 | 揭示FCFS缺陷 | | 渐增到达 | 到达时间递增,执行时间递减 | 观察SJF优势 |
综上所述, Process 类不仅是调度算法的数据载体,更是连接理论与实现的桥梁。通过严谨的属性定义、状态建模与工具支持,我们为后续各章的调度器开发奠定了坚实基础。
7. 调度器类封装与调度流程控制
7.1 统一调度接口的抽象设计
在构建一个可扩展、易于维护的操作系统进程调度模拟系统时,良好的架构设计至关重要。为了支持多种调度算法的灵活切换与统一调用,我们首先引入面向对象中的接口机制,定义一个通用的 Scheduler 接口。
该接口的核心方法为:
public interface Scheduler {
List
}
此方法接收一组初始未排序的进程列表(包含到达时间、执行时间等属性),返回按调度顺序执行的进程序列,并在过程中更新每个进程的开始时间、结束时间、等待时间等动态属性。
基于此接口,我们可以实现多个具体调度器类:
FCFSScheduler :先来先服务 SJFScheduler :非抢占式短作业优先 SRTFScheduler :抢占式最短剩余时间优先 RRScheduler :时间片轮转 HRNScheduler :高响应比优先
通过多态机制,主控程序无需关心具体算法逻辑,只需持有 Scheduler 引用即可完成调度:
Scheduler scheduler = new RRFScheduler(4); // 时间片为4
List
这种设计不仅提升了代码的可读性和可测试性,还便于后期新增其他调度策略(如多级反馈队列)而无需修改已有代码,符合开闭原则(Open-Closed Principle)。
此外,为支持不同调度器之间的比较实验,我们还可以扩展接口以支持事件监听或日志回调:
void addListener(SchedulingEventListener listener);
从而实现实时监控调度过程,用于可视化或性能分析。
7.2 调度主控流程的编排与执行
尽管各调度算法逻辑差异显著,但其顶层控制流程具有共性。典型的调度主循环包括以下几个关键步骤:
按到达时间对原始进程列表排序; 初始化当前时间 currentTime = 0 ; 创建就绪队列(Ready Queue)和完成列表(Finished List); 循环判断是否存在尚未到达或仍在运行的进程; 将当前时间点已到达的进程加入就绪队列; 调用具体调度策略选择下一个要执行的进程; 更新进程状态及时间戳; 推进虚拟时钟,处理完成事件; 输出调度轨迹并计算指标。
以下是简化版主控流程框架:
public List
List
sortedProcesses.sort(Comparator.comparingInt(Process::getArrivalTime));
int currentTime = 0;
int finishedCount = 0;
int n = processes.size();
Queue
List
int i = 0; // 当前待加入就绪队列的进程索引
while (finishedCount < n) {
// 将所有已到达的进程加入就绪队列
while (i < n && sortedProcesses.get(i).getArrivalTime() <= currentTime) {
readyQueue.offer(sortedProcesses.get(i));
i++;
}
Process next = selectNextProcess(readyQueue, currentTime);
if (next != null) {
executeProcess(next, currentTime);
currentTime += next.getBurstTime(); // 简化模型,实际应分步推进
next.finish(currentTime);
result.add(next);
finishedCount++;
} else {
currentTime++; // 空闲周期
}
}
return result;
}
⚠️ 注意:上述代码适用于非抢占式调度。对于抢占式算法(如 SRTF),需采用“时间推进+事件驱动”模式,在每一个时间单位检查是否有新进程到达或是否需要抢占。
为此,我们可改用事件队列方式,将“到达事件”和“完成事件”作为优先队列管理,实现更精细的时间控制。
7.3 性能指标计算器模块化设计
为客观评估各类调度算法的表现,我们设计独立的 PerformanceMetrics 工具类,用于集中计算关键性能指标。
指标名称 公式 说明 周转时间 结束时间 - 到达时间 衡量整体延迟 等待时间 周转时间 - 执行时间 CPU外等待时长 响应时间 首次开始时间 - 到达时间 分时系统敏感指标 平均等待时间 Σ等待时间 / N 反映调度公平性 CPU利用率 总执行时间 / 总调度时间 资源使用效率
Java 实现如下:
public class PerformanceMetrics {
public static double avgWaitingTime(List
return processes.stream()
.mapToDouble(p -> p.getTurnaroundTime() - p.getBurstTime())
.average().orElse(0.0);
}
public static double avgTurnaroundTime(List
return processes.stream()
.mapToDouble(Process::getTurnaroundTime)
.average().orElse(0.0);
}
public static double avgResponseTime(List
return processes.stream()
.mapToDouble(p -> p.getStartTime() - p.getArrivalTime())
.average().orElse(0.0);
}
}
同时,为支持后续可视化,我们设计甘特图数据结构:
public class GanttChartEntry {
private String processId;
private int startTime;
private int endTime;
// getter/setter
}
每次进程被调度执行时记录一条 GanttChartEntry ,最终生成 JSON 格式输出:
[
{"processId": "P1", "startTime": 0, "endTime": 5},
{"processId": "P2", "startTime": 5, "endTime": 8}
]
也可导出为 CSV 文件,供 Excel 或 Python Matplotlib 进行图形化展示。
7.4 完整项目集成与调试实践
我们将整个调度系统组织为标准 Maven 项目结构:
src/
├── main/
│ ├── java/
│ │ ├── scheduler/
│ │ │ ├── Scheduler.java
│ │ │ ├── FCFSScheduler.java
│ │ │ └── RRScheduler.java
│ │ ├── model/
│ │ │ └── Process.java
│ │ └── Main.java
│ └── resources/
│ └── processes.csv
└── test/
└── java/
└── scheduler/
└── FCFSUnitTest.java
使用 JUnit 编写单元测试验证核心逻辑:
@Test
public void testFCFSScheduler_SimpleCase() {
List
new Process("P1", 0, 5),
new Process("P2", 1, 3),
new Process("P3", 2, 8)
);
Scheduler scheduler = new FCFSScheduler();
List
assertEquals(5, result.get(0).getWaitTime()); // P1 等待0
assertEquals(4, result.get(1).getWaitTime()); // P2 等待4
}
常见错误排查指南:
死循环 :检查主循环终止条件是否正确判断所有进程已完成。 时间错乱 :确保 currentTime 在非抢占式中累加,在抢占式中逐单位递增。 就绪队列为空却强行调度 :增加空值判断。 响应比计算遗漏等待时间增长 :必须在每次调度前重新计算所有就绪进程的响应比。
借助 IDE 的断点调试功能,可在每一步观察就绪队列内容、当前时间、进程状态变化,逐步验证调度逻辑的正确性。
flowchart TD
A[开始调度] --> B{还有未完成进程?}
B -->|是| C[加载当前时间已到达的进程]
C --> D[选择下一个执行进程]
D --> E[更新进程状态和时间]
E --> F[推进时间指针]
F --> B
B -->|否| G[输出调度结果]
G --> H[计算性能指标]
本文还有配套的精品资源,点击获取
简介:进程调度是操作系统核心功能之一,直接影响系统效率与资源利用率。本文通过Java编程模拟实现四种经典调度算法:先来先服务(FCFS)、短作业优先(SJF)、时间片轮转(RR)和高响应比优先(HRN),深入解析其调度机制与优缺点。项目包含进程类、调度器类和主控程序,结构清晰,注释完整,适用于操作系统课程实验与学习实践。通过该实验,学生可掌握调度算法的核心逻辑,提升对多任务环境CPU资源分配机制的理解。
本文还有配套的精品资源,点击获取