1. 操作系统作业调度算法概述
在操作系统的核心机制中,作业调度算法扮演着资源分配者的关键角色。作为计算机科学专业的学生,我在完成操作系统课程实验时,深刻体会到理解这些算法的重要性。作业调度算法决定了系统如何从后备队列中选择作业进入内存执行,直接影响着CPU利用率和用户体验。
常见的三种经典调度算法各有特点:
- 先来先服务(FCFS):最简单的调度方式,按照作业到达顺序处理
- 短作业优先(SJF):优先执行预计运行时间短的作业
- 最高响应比优先(HRRF):综合考虑等待时间和执行时间的动态算法
这些算法不仅是理论概念,更是操作系统实际运行的基础。通过模拟实现这些算法,我们可以直观地观察到不同策略对系统性能的影响,为后续学习更复杂的调度机制打下基础。
2. 实验环境搭建与数据结构设计
2.1 实验环境准备
我选择在Linux环境下使用C++进行本次实验模拟。Linux系统提供了良好的开发环境和丰富的系统调用接口,非常适合操作系统相关实验。以下是环境配置步骤:
- 安装必要的开发工具:
bash复制sudo apt update
sudo apt install g++ build-essential
- 创建项目目录结构:
code复制scheduling_algo/
├── include/ # 头文件
├── src/ # 源文件
├── test/ # 测试数据
└── Makefile # 编译配置
2.2 核心数据结构设计
作业调度模拟需要合理的数据结构来表示作业和调度队列。我设计了以下结构体:
cpp复制struct Job {
int id; // 作业ID
int arrive_time; // 到达时间
int burst_time; // 执行时间
int start_time; // 开始执行时间
int finish_time; // 完成时间
int waiting_time; // 等待时间
float response_ratio; // 响应比(HRRF使用)
};
class Scheduler {
private:
vector<Job> job_queue; // 作业队列
vector<Job> result; // 调度结果
public:
void loadJobs(const string& filename); // 从文件加载作业
void FCFS(); // 先来先服务
void SJF(); // 短作业优先
void HRRF(); // 最高响应比优先
void printResult(); // 输出调度结果
};
这个设计充分考虑了三种算法的共性需求,同时为每种算法保留了特定的计算字段。response_ratio字段专为HRRF算法设计,其他算法可以忽略该字段。
3. 先来先服务(FCFS)算法实现
3.1 FCFS算法原理
FCFS(First Come First Serve)是最直观的调度算法,其核心原则就是"先到先得"。操作系统按照作业到达的先后顺序将它们放入就绪队列,先进入队列的作业优先获得CPU资源。
FCFS的特点:
- 非抢占式:一旦作业开始执行,就会一直运行到完成
- 实现简单:不需要复杂的优先级计算
- 公平性:所有作业按到达顺序平等对待
3.2 FCFS算法实现细节
在我的实现中,FCFS算法的核心逻辑如下:
cpp复制void Scheduler::FCFS() {
// 按到达时间排序
sort(job_queue.begin(), job_queue.end(),
[](const Job& a, const Job& b) {
return a.arrive_time < b.arrive_time;
});
int current_time = 0;
for (auto& job : job_queue) {
if (current_time < job.arrive_time) {
current_time = job.arrive_time;
}
job.start_time = current_time;
job.finish_time = current_time + job.burst_time;
job.waiting_time = job.start_time - job.arrive_time;
current_time = job.finish_time;
result.push_back(job);
}
}
关键点说明:
- 首先对作业按到达时间进行排序
- 维护一个current_time变量跟踪系统当前时间
- 每个作业的开始时间取决于前一个作业的完成时间和自身到达时间
- 等待时间 = 开始时间 - 到达时间
3.3 FCFS算法的性能分析
通过实验数据测试,我发现FCFS算法存在明显的"护航效应"(Convoy Effect)。当一些长作业先到达系统时,短作业需要等待很长时间,导致平均等待时间增加。
测试用例:
code复制作业ID 到达时间 执行时间
1 0 10
2 1 1
3 2 2
计算结果:
- 作业1:等待时间=0
- 作业2:等待时间=9
- 作业3:等待时间=8
- 平均等待时间=(0+9+8)/3=5.67
可以看到,虽然作业1的等待时间为0,但后续短作业的等待时间明显增加。这是FCFS算法的主要缺点。
4. 短作业优先(SJF)算法实现
4.1 SJF算法原理
短作业优先(Shortest Job First)算法试图解决FCFS的护航效应问题。其核心思想是优先调度执行时间短的作业,从而减少平均等待时间。
SJF有两种变体:
- 非抢占式:作业一旦开始执行就运行到完成
- 抢占式:当新作业到达时,如果其执行时间比当前剩余执行时间短,则抢占
本次实验我实现了非抢占式SJF,因为它更接近传统意义上的作业调度场景。
4.2 SJF算法实现细节
cpp复制void Scheduler::SJF() {
vector<Job> temp_queue = job_queue;
vector<Job> ready_queue;
int current_time = 0;
while (!temp_queue.empty() || !ready_queue.empty()) {
// 将已到达作业加入就绪队列
for (auto it = temp_queue.begin(); it != temp_queue.end(); ) {
if (it->arrive_time <= current_time) {
ready_queue.push_back(*it);
it = temp_queue.erase(it);
} else {
++it;
}
}
if (!ready_queue.empty()) {
// 找出执行时间最短的作业
auto shortest = min_element(ready_queue.begin(), ready_queue.end(),
[](const Job& a, const Job& b) {
return a.burst_time < b.burst_time;
});
Job job = *shortest;
ready_queue.erase(shortest);
job.start_time = current_time;
job.finish_time = current_time + job.burst_time;
job.waiting_time = job.start_time - job.arrive_time;
current_time = job.finish_time;
result.push_back(job);
} else {
// 没有就绪作业,时间推进到下一个作业到达
if (!temp_queue.empty()) {
current_time = temp_queue.front().arrive_time;
}
}
}
}
实现要点:
- 维护两个队列:temp_queue保存未到达作业,ready_queue保存已到达但未执行的作业
- 每次从ready_queue中选择执行时间最短的作业
- 如果没有就绪作业,时间推进到下一个作业到达时间
4.3 SJF算法的优缺点分析
使用相同的测试数据:
code复制作业ID 到达时间 执行时间
1 0 10
2 1 1
3 2 2
SJF调度顺序:作业1(到达时间0)→作业2(执行时间1)→作业3(执行时间2)
计算结果:
- 作业1:等待时间=0
- 作业2:等待时间=0(立即在作业1完成后执行)
- 作业3:等待时间=1
- 平均等待时间=(0+0+1)/3=0.33
相比FCFS的5.67,SJF显著降低了平均等待时间。但是SJF也存在问题:
- 需要预知作业执行时间,实际系统中难以准确获取
- 可能导致长作业饥饿(starvation),如果不断有短作业到达,长作业可能永远得不到执行
5. 最高响应比优先(HRRF)算法实现
5.1 HRRF算法原理
最高响应比优先(Highest Response Ratio Next)算法试图平衡等待时间和执行时间,克服SJF的缺点。响应比定义为:
响应比 = (等待时间 + 执行时间) / 执行时间
这个公式意味着:
- 短作业仍然有优势(分母小)
- 但长时间等待的作业其响应比会逐渐增大,最终获得执行机会
5.2 HRRF算法实现细节
cpp复制void Scheduler::HRRF() {
vector<Job> temp_queue = job_queue;
vector<Job> ready_queue;
int current_time = 0;
while (!temp_queue.empty() || !ready_queue.empty()) {
// 将已到达作业加入就绪队列
for (auto it = temp_queue.begin(); it != temp_queue.end(); ) {
if (it->arrive_time <= current_time) {
it->waiting_time = current_time - it->arrive_time;
it->response_ratio = (it->waiting_time + it->burst_time) / (float)it->burst_time;
ready_queue.push_back(*it);
it = temp_queue.erase(it);
} else {
++it;
}
}
if (!ready_queue.empty()) {
// 找出响应比最高的作业
auto highest = max_element(ready_queue.begin(), ready_queue.end(),
[](const Job& a, const Job& b) {
return a.response_ratio < b.response_ratio;
});
Job job = *highest;
ready_queue.erase(highest);
job.start_time = current_time;
job.finish_time = current_time + job.burst_time;
job.waiting_time = job.start_time - job.arrive_time;
current_time = job.finish_time;
result.push_back(job);
} else {
// 没有就绪作业,时间推进到下一个作业到达
if (!temp_queue.empty()) {
current_time = temp_queue.front().arrive_time;
}
}
}
}
关键实现点:
- 对于每个到达的作业,实时计算其响应比
- 响应比计算考虑了等待时间和执行时间的平衡
- 每次选择响应比最高的作业执行
5.3 HRRF算法的优势验证
再次使用相同的测试数据:
code复制作业ID 到达时间 执行时间
1 0 10
2 1 1
3 2 2
HRRF调度过程:
- 时间0:只有作业1到达,响应比=(0+10)/10=1.0 → 执行作业1
- 时间1:作业2到达,作业1剩余执行时间9
- 作业2响应比=(0+1)/1=1.0
- 作业1响应比=(1+9)/9=1.11 → 继续执行作业1
- 时间2:作业3到达,作业1剩余执行时间8
- 作业2响应比=(1+1)/1=2.0
- 作业3响应比=(0+2)/2=1.0
- 作业1响应比=(2+8)/8=1.25 → 执行作业2(响应比最高)
- 作业2完成后(时间3),重新计算:
- 作业3响应比=(1+2)/2=1.5
- 作业1响应比=(3+7)/7=1.43 → 执行作业3
- 最后执行作业1剩余部分
最终调度顺序:作业1(部分)→作业2→作业3→作业1(剩余部分)
计算结果:
- 作业1:等待时间=0(开始执行)+3(被抢占后等待)=3
- 作业2:等待时间=1
- 作业3:等待时间=1
- 平均等待时间=(3+1+1)/3=1.67
HRRF在FCFS(5.67)和SJF(0.33)之间取得了平衡,既照顾了短作业,又避免了长作业过度等待。
6. 实验结果分析与可视化比较
6.1 综合测试案例设计
为了全面比较三种算法,我设计了更复杂的测试案例:
code复制作业ID 到达时间 执行时间
1 0 8
2 1 4
3 2 9
4 3 5
5 4 2
6.2 各算法调度结果
FCFS结果:
调度顺序:1→2→3→4→5
平均等待时间:(0+7+9+13+15)/5 = 8.8
SJF结果:
调度顺序:1→5→2→4→3
平均等待时间:(0+0+6+4+15)/5 = 5.0
HRRF结果:
调度顺序:1→5→2→4→3
平均等待时间:(0+0+6+4+15)/5 = 5.0
在这个案例中,SJF和HRRF得到了相同的结果,这是因为短作业的到达时间分布使得它们能及时获得调度。
6.3 性能指标对比
我使用以下指标评估算法性能:
- 平均等待时间:越低越好
- 平均周转时间(完成时间-到达时间):越低越好
- 公平性:长作业不被过度延迟
通过多次测试,我发现:
- FCFS在作业到达顺序和执行时间差异大时表现最差
- SJF在大多数情况下能提供最佳平均等待时间
- HRRF在保证较短平均等待时间的同时,对长作业更公平
6.4 可视化甘特图
使用ASCII艺术绘制调度过程的甘特图能直观展示差异:
FCFS:
code复制作业1: |========|
作业2: |====|
作业3: |=========|
作业4: |=====|
作业5: |==|
SJF:
code复制作业1: |=======|
作业5: |==|
作业2: |====|
作业4: |=====|
作业3: |=========|
从图中可以清晰看到SJF如何通过重排顺序减少短作业的等待时间。
7. 实验心得与实用建议
通过这次作业调度算法模拟实验,我获得了许多宝贵的实践经验:
-
输入数据设计:测试案例应该覆盖各种场景,包括:
- 短作业先到达和长作业先到达的情况
- 作业集中到达和分散到达的情况
- 执行时间差异大和相近的情况
-
时间管理技巧:
- 在模拟器中维护一个全局时钟变量
- 使用事件驱动的方式推进时间,而不是固定步长
- 将时间更新逻辑封装成函数,避免重复代码
-
调试建议:
- 为每个作业分配唯一颜色,便于跟踪
- 在关键决策点打印调度状态
- 实现逐步执行模式,观察算法决策过程
-
扩展思考:
- 如何预测作业执行时间?可以尝试使用指数平均法
- 多级反馈队列如何实现?需要设计优先级队列和动态优先级调整
- 如何引入I/O操作的影响?需要模拟I/O等待事件
-
实际应用启示:
- 操作系统的调度器通常是多种算法的组合
- 现代系统更关注响应时间和公平性的平衡
- 在实时系统中,调度算法需要满足截止时间要求
这个实验不仅加深了我对调度算法的理解,还让我认识到理论算法与实际系统实现之间的差距。在真实操作系统中,调度器需要考虑更多因素,如进程优先级、I/O绑定与CPU绑定进程的区别、多核处理等。这些发现激励我继续深入学习操作系统内核的实现原理。
