处理机调度¶
此为校内课程笔记
处理机调度层次¶
高级调度¶
高级调度又称为作业调度或长程调度。其主要功能是根据某种算法,决定将外存上处于后备队列中的哪些作业调入内存,为它们创建进程、分配必要资源,并放入就绪队列。
简单来说,高级调度用于决定哪些作业可以进入内存运行。
高级调度主要用于多道批处理系统,分时系统与实时系统不设置高级调度。
中级调度¶
中级调度又称内存调度,其主要目的是提高内存利用率和系统吞吐量,改善系统性能。
为实现这一目的,中级调度需要完成两项工作(“对换”功能):
-
将暂不运行的进程调至外存等待
-
将存于外存上急需运行的进程调入内存运行
低级调度¶
低级调度也称为进程调度或短程调度。其主要功能是按照某种算法,决定就绪队列中的哪个进程(或内核级线程)获得处理机资源。
低级调度是操作系统中最为活跃的调度,因此其算法设计的好坏直接影响到系统的性能。
多道批处理系统、分时系统、实时系统均有应用低级调度。
进程调度¶
任务¶
进程调度的主要任务有三:
-
保存CPU现场信息
-
按某种算法选取进程
-
把处理器分配给进程
调度机制¶
实现进程调度,应具有三个基本部分:
-
排队器: 用于将就绪进程插入相应的就绪队列
-
分派器: 用于将选定的进程移出就绪队列,并将其分配给CPU执行
-
上下文切换器: 进行新旧进程之间的上下文切换
调度模式¶
-
非抢占式调度(Non-preemptive Scheduling)
一旦进程开始执行,就会一直运行到完成,不会被其他进程抢占。
-
抢占式调度(Preemptive Scheduling)
进程在执行过程中有可能基于某种原因被其他进程抢占,从而导致进程的执行被打断。
处理机调度算法¶
调度算法的目标¶
共同目标¶
-
资源利用率: 使系统中的处理机和其他所有资源都尽可能地保持忙碌状态
-
公平性: 确保每个进程都能获得合理的CPU时间,避免某些进程饥饿
饥饿
饥饿(starvation)是指进程因为某种原因长时间无法获得所需资源(特别是CPU时间),导致无法继续执行的现象
-
平衡性: 根据资源使用情况,可将系统中的进程划分为计算密集型与I/O密集型。调度算法应尽可能保持系统资源使用的平衡性,从而满足第一条目标。
计算密集型与I/O密集型进程
-
计算密集型进程:需要大量CPU计算资源的进程,如科学计算、图像处理等
-
I/O密集型进程:需要大量I/O操作的进程,如文件读写、网络通信等
-
-
策略强制执行: 对于所制定的策略(包括安全策略等),只要需要,必须予以准确地执行,即使会造成某些工作的延迟
部分评价指标¶
-
CPU利用率
\[ \text{CPU利用率} = \frac{\text{CPU有效工作时间}}{\text{CPU有效工作时间} + \text{CPU空闲等待时间}} \] -
周转时间
从作业提交到系统开始到作业完成的时间间隔。
-
平均周转时间: 所有作业的周转时间之和与作业个数之比
\[ T = \frac{1}{n} \sum_{i=1}^{n} T_i \] -
带权周转时间: 权值为作业周转时间 \(T_i\) 与系统为之服务时间 \(T_s\) 之比
\[ T_{W_i} = \frac{T_i}{T_s} \] -
平均带权周转时间: 所有作业的带权周转时间之和与作业个数之比
\[ T_{W} = \frac{1}{n} \sum_{i=1}^{n} T_{W_i} = \frac{1}{n} \sum_{i=1}^{n} \frac{T_i}{T_s} \]
-
-
响应时间: 从作业提交到首次产生响应的时间间隔。
-
等待时间(进程调度): 进程在就绪队列中等待处理机的时间之和。
批处理系统的目标¶
-
平均周转时间短
-
系统吞吐量高
-
处理机利用率高
分时系统的目标¶
-
响应时间短
-
均衡性
实时系统的目标¶
-
保证满足截止时间的要求
-
保证可预测性
调度算法¶
概述¶
调度算法按照服务对象可分为作业调度算法与进程调度算法,分别对应前文提到的高级调度与低级调度。
常见的作业调度算法有:
常见的进程调度算法有:
-
多级队列(MQ)
-
公平调度(Fair)
-
公平共享调度(Fair Share)
先来先服务¶
定义¶
先来先服务调度算法(First Come First Served, FCFS)是最简单的调度算法,既可以用于作业调度,也可以用于进程调度。顾名思义,其基本思想是按照作业到达的先后顺序进行调度。
// 伪代码示例
void FCFS_Scheduler() {
while (!ready_queue.empty()) {
Process current = ready_queue.front(); // 取队首进程
ready_queue.pop();
execute_process(current); // 执行到完成
}
}
特点与优缺点¶
-
非抢占式调度:一旦进程开始执行,就会一直运行到完成
-
按到达时间排序:严格按照进程到达就绪队列的时间顺序进行调度
-
简单公平:实现简单,对所有进程一视同仁
-
优点
-
实现简单:逻辑清晰,易于理解和实现
-
公平性:所有进程按到达顺序获得服务
-
无饥饿现象:每个进程最终都会被执行
-
-
缺点
-
护航效应:短进程可能被长进程阻塞
护航效应
护航效应(convoy effect)是指长进程会阻塞短进程的执行,导致短进程的响应时间变长。
想象一下,如果有一个执行时间很长的进程排在队列前面,后面所有进程都要等待它完成。就像高速公路上的慢车,会拖慢整条车流的速度。
-
平均等待时间长:特别是当短进程排在长进程后面时
-
响应时间差:不适合交互式系统
-
Example
假设有三个进程:
-
进程A:到达时间0,执行时间3
-
进程B:到达时间1,执行时间1
-
进程C:到达时间2,执行时间2
使用先来先服务调度算法,甘特图(Gantt Chart)如下:
时间轴: 0----1----2----3----4----5----6
进程A: [==============]
进程B: [----------====]
进程C: [----------=========]
性能指标:
-
进程A:等待时间 \(0\),周转时间 \(3\)
-
进程B:等待时间 \(2\),周转时间 \(3\)
-
进程C:等待时间 \(2\),周转时间 \(4\)
平均等待时间: \(\frac{0+2+2}{3} = 1.33\)
平均周转时间: \(\frac{3+3+4}{3} = 3.33\)
短作业优先¶
定义¶
顾名思义,短作业优先调度算法(Short Job First, SJF)以作业(或进程)执行时间的长短来决定调度的优先级,时间越短,优先级越高。
SJF 既可以用于作业调度,也可以用于进程调度
-
针对作业调度,SJF 从后备队列中选择若干估计运行时间最短的作业,将它们调入内存运行
-
针对进程调度,SJF 关联到每个进程下次运行的CPU区间长度,调度最短的进程执行
-
另外,针对进程调度,SJF 有两种模式:
-
非抢占式SJF: 一旦进程开始执行,就会一直运行到完成
-
抢占式SJF(Shortest Remaining Time First, SRTF): 进程执行一段时间后,如果新的进程进入就绪队列,且新进程的执行时间比当前进程的剩余执行时间短,则新进程会抢占当前进程,当前进程会重新进入就绪队列
-
-
// 非抢占式SJF调度算法
void nonPreemptiveSJF(vector<Process>& processes) {
int n = processes.size();
int currentTime = 0;
vector<bool> completed(n, false);
cout << "Non-preemptive SJF scheduling order:\n";
for (int i = 0; i < n; i++) {
// 找到当前时间已到达且未完成的最短作业
int shortestIndex = -1;
int shortestTime = INT_MAX;
for (int j = 0; j < n; j++) {
if (!completed[j] &&
processes[j].arrivalTime <= currentTime &&
processes[j].burstTime < shortestTime) {
shortestTime = processes[j].burstTime;
shortestIndex = j;
}
}
if (shortestIndex == -1) {
// 没有进程到达,等待下一个进程到达
int nextArrival = INT_MAX;
for (int j = 0; j < n; j++) {
if (!completed[j]) {
nextArrival = min(nextArrival, processes[j].arrivalTime);
}
}
currentTime = nextArrival;
i--; // 重新尝试
continue;
}
// 执行选中的进程
Process& p = processes[shortestIndex];
p.completionTime = currentTime + p.burstTime;
p.turnaroundTime = p.completionTime - p.arrivalTime;
p.waitingTime = p.turnaroundTime - p.burstTime;
cout << "Process P" << p.id << " execution time: " << currentTime
<< " - " << p.completionTime << endl;
currentTime = p.completionTime;
completed[shortestIndex] = true;
}
}
// 抢占式SJF调度算法
void preemptiveSJF(vector<Process>& processes) {
int n = processes.size();
int currentTime = 0;
int completed = 0;
// 初始化剩余时间
for (auto& p : processes) {
p.remainingTime = p.burstTime;
}
cout << "Preemptive SJF scheduling order:\n";
while (completed < n) {
// 找到当前时间已到达且剩余时间最短的进程
int shortestIndex = -1;
int shortestTime = INT_MAX;
for (int i = 0; i < n; i++) {
if (processes[i].remainingTime > 0 &&
processes[i].arrivalTime <= currentTime &&
processes[i].remainingTime < shortestTime) {
shortestTime = processes[i].remainingTime;
shortestIndex = i;
}
}
if (shortestIndex == -1) {
// 没有进程可执行,跳到下一个到达时间
int nextArrival = INT_MAX;
for (int i = 0; i < n; i++) {
if (processes[i].remainingTime > 0) {
nextArrival = min(nextArrival, processes[i].arrivalTime);
}
}
currentTime = nextArrival;
continue;
}
Process& p = processes[shortestIndex];
// 检查是否有新进程到达并可能抢占
bool preempted = false;
for (int i = 0; i < n; i++) {
if (processes[i].arrivalTime == currentTime + 1 &&
processes[i].remainingTime < p.remainingTime) {
// 新进程到达且更短,当前进程被抢占
cout << "Process P" << p.id << " preempted, execution time: "
<< currentTime << " - " << currentTime + 1 << endl;
p.remainingTime -= 1;
currentTime++;
preempted = true;
break;
}
}
if (!preempted) {
// 执行到完成
cout << "Process P" << p.id << " execution time: " << currentTime
<< " - " << currentTime + p.remainingTime << endl;
currentTime += p.remainingTime;
p.remainingTime = 0;
p.completionTime = currentTime;
p.turnaroundTime = p.completionTime - p.arrivalTime;
p.waitingTime = p.turnaroundTime - p.burstTime;
completed++;
}
}
}
特点与优缺点¶
对于平均等待时间而言,SJF是最优的(对一组指定的进程而言),它给出了最短的平均等待时间。
-
优点
-
平均等待时间最短:数学上可证明SJF能给出最优解
-
系统吞吐量高:短作业快速完成,释放资源
-
简单直观:逻辑清晰,易于理解
-
-
缺点
-
长作业饥饿:长作业可能永远得不到执行
-
需要预知时间:实际中很难准确预测执行时间
-
不公平:对长作业用户不友好
-
Example
假设有四个进程:
| 进程 | 到达时间 | 执行时间 |
|---|---|---|
| A | 0 | 5 |
| B | 1 | 4 |
| C | 2 | 2 |
| D | 3 | 3 |
-
使用非抢占式SJF调度算法,甘特图如下:
时间轴: 0----1----2----3----4----5----6----7----8----9----10----11----12----13----14 进程A: [========================] 进程B: [------------------------------====================] 进程C: [---------------=========] 进程D: [-----------------------------------------==================]性能指标:
-
进程A: 等待时间 \(0\),周转时间 \(7\)
-
进程B: 等待时间 \(6\),周转时间 \(10\)
-
进程C: 等待时间 \(3\),周转时间 \(5\)
-
进程D: 等待时间 \(8\),周转时间 \(11\)
平均等待时间: \(\frac{0+6+3+8}{4} = 4.25\)
平均周转时间: \(\frac{7+10+5+11}{4} = 8.25\)
-
-
若使用抢占式SJF调度算法,则甘特图如下:
时间轴: 0----1----2----3----4----5----6----7----8----9----10----11----12----13----14 进程A: [=========----------===============] 进程B: [--------------------------------------------=========================] 进程C: [=========] 进程D: [-------------------===============]性能指标:
-
进程A: 等待时间 \(2\),周转时间 \(7\)
-
进程B: 等待时间 \(9\),周转时间 \(13\)
-
进程C: 等待时间 \(0\),周转时间 \(2\)
-
进程D: 等待时间 \(4\),周转时间 \(7\)
平均等待时间: \(\frac{2+9+0+4}{4} = 3.75\)
平均周转时间: \(\frac{7+13+2+7}{4} = 7.25\)
-
优先级调度¶
定义¶
优先级调度算法(Priority Scheduling, PS)是一种基于进程优先级进行调度的算法,系统为每个进程分配一个优先级值,调度器总是选择优先级最高的进程执行,既可用于作业调度,也可用于进程调度。
PS 是当前主流的OS调度算法,基于作业/进程的紧迫程度,由外部赋予作业相应的优先级,调度算法根据优先级进行调度。
struct Process {
int id;
int priority; // 优先级(数值越小优先级越高)
int arrivalTime;
int burstTime;
int remainingTime;
int completionTime;
int turnaroundTime;
int waitingTime;
};
// 优先级调度算法实现
void priorityScheduling(vector<Process>& processes) {
int n = processes.size();
int currentTime = 0;
int completed = 0;
// 初始化剩余时间
for (auto& p : processes) {
p.remainingTime = p.burstTime;
}
cout << "Priority scheduling order:\n";
while (completed < n) {
// 找到当前时间已到达且优先级最高的进程
int highestPriorityIndex = -1;
int highestPriority = INT_MAX;
for (int i = 0; i < n; i++) {
if (processes[i].remainingTime > 0 &&
processes[i].arrivalTime <= currentTime &&
processes[i].priority < highestPriority) {
highestPriority = processes[i].priority;
highestPriorityIndex = i;
}
}
if (highestPriorityIndex == -1) {
// 没有进程可执行,跳到下一个到达时间
int nextArrival = INT_MAX;
for (int i = 0; i < n; i++) {
if (processes[i].remainingTime > 0) {
nextArrival = min(nextArrival, processes[i].arrivalTime);
}
}
currentTime = nextArrival;
continue;
}
// 执行选中的进程
Process& p = processes[highestPriorityIndex];
cout << "Process P" << p.id << " (Priority: " << p.priority
<< ") execution time: " << currentTime
<< " - " << currentTime + p.remainingTime << endl;
currentTime += p.remainingTime;
p.remainingTime = 0;
p.completionTime = currentTime;
p.turnaroundTime = p.completionTime - p.arrivalTime;
p.waitingTime = p.turnaroundTime - p.burstTime;
completed++;
}
}
特点与优缺点¶
-
优点
-
灵活性高:可根据业务需求调整优先级
-
实时性好:高优先级进程能快速响应
-
资源优化:重要任务优先获得资源
-
-
缺点
-
饥饿问题:低优先级进程可能永远得不到执行
老化
为了避免饥饿问题,可以采用老化技术(Aging),即随着进程的等待时间增加,其优先级逐渐提高。
-
优先级倒置:可能出现高优先级进程等待低优先级进程的情况
-
复杂度高:需要合理设计优先级分配策略
-
应用方式¶
-
按调度模式可分为抢占式和非抢占式
-
按调度优先级类型可分为静态优先级和动态优先级
-
静态优先级:在进程创建时分配,进程运行期间不变
-
动态优先级:在进程运行期间根据某种规则进行动态调整
-
最高响应比优先¶
定义¶
最高响应比优先调度算法(Highest Response Ratio Next, HRRN)是一种非抢占式调度算法,是优先级调度算法的一个特例,通过计算每个进程的响应比来决定调度顺序,通常用于作业调度。
其中,这里的响应比与优先级分别通过如下公式计算:
// HRRN调度算法实现
struct Process {
int id;
int arrivalTime;
int burstTime;
int waitingTime;
int completionTime;
int turnaroundTime;
double responseRatio;
};
void HRRN_Scheduling(vector<Process>& processes) {
int n = processes.size();
int currentTime = 0;
vector<bool> completed(n, false);
cout << "HRRN scheduling order:\n";
for (int i = 0; i < n; i++) {
// 计算所有已到达且未完成进程的响应比
int bestIndex = -1;
double highestRatio = -1;
for (int j = 0; j < n; j++) {
if (!completed[j] && processes[j].arrivalTime <= currentTime) {
// 计算响应比
int waitingTime = currentTime - processes[j].arrivalTime;
processes[j].responseRatio = (double)(waitingTime + processes[j].burstTime) / processes[j].burstTime;
if (processes[j].responseRatio > highestRatio) {
highestRatio = processes[j].responseRatio;
bestIndex = j;
}
}
}
if (bestIndex == -1) {
// 没有进程到达,等待下一个进程到达
int nextArrival = INT_MAX;
for (int j = 0; j < n; j++) {
if (!completed[j]) {
nextArrival = min(nextArrival, processes[j].arrivalTime);
}
}
currentTime = nextArrival;
i--; // 重新尝试
continue;
}
// 执行选中的进程
Process& p = processes[bestIndex];
p.waitingTime = currentTime - p.arrivalTime;
p.completionTime = currentTime + p.burstTime;
p.turnaroundTime = p.completionTime - p.arrivalTime;
cout << "Process P" << p.id << " (Response Ratio: " << p.responseRatio
<< ") execution time: " << currentTime << " - " << p.completionTime << endl;
currentTime = p.completionTime;
completed[bestIndex] = true;
}
}
特点与优缺点¶
HRRN 即考虑了作业的等待时间,有考虑了作业的运行时间:
-
如等待时间相同,运行时间越短的响应比越高,类似SJF
-
如运行时间相同,则取决于等待时间,类似FCFS
-
长作业可随其等待时间的增加而提高响应比,从而获得调度机会,克服饥饿问题
-
缺点:每次调度前都需要计算响应比,增加了计算开销
Example
假设有五个作业:
| 作业 | 到达时间 | 执行时间 |
|---|---|---|
| A | 0 | 5 |
| B | 1 | 4 |
| C | 2 | 2 |
| D | 3 | 3 |
| E | 4 | 1 |
使用HRRN调度算法:
-
第一次调度:
A 作业结束,其他作业的响应比 \(R_p\) 分别为:
-
\(R_{p_{B}} = \frac{5-1 + 4}{4} = 2\)
-
\(R_{p_{C}} = \frac{5-2 + 2}{2} = 2.5\)
-
\(R_{p_{D}} = \frac{5-3 + 3}{3} = 1.67\)
-
\(R_{p_{E}} = \frac{5-4 + 1}{1} = 2\)
作业C响应比最高,调度
-
-
第二次调度:
C 作业结束,其他作业的响应比 \(R_p\) 分别为:
-
\(R_{p_{B}} = \frac{5+2-1 + 4}{4} = 1.5\)
-
\(R_{p_{D}} = \frac{5+2-3 + 3}{3} = 2.33\)
-
\(R_{p_{E}} = \frac{5+2-4 + 1}{1} = 4\)
作业D响应比最高,调度
-
-
第三次调度:
D 作业结束,其他作业的响应比 \(R_p\) 分别为:
-
\(R_{p_{B}} = \frac{5+2+3-1 + 4}{4} = 3.25\)
-
\(R_{p_{E}} = \frac{5+2+3-4 + 1}{1} = 7\)
作业E响应比最高,调度
-
最终的调度顺序为: A -> C -> D -> E -> B,甘特图如下:
时间轴: 0----1----2----3----4----5----6----7----8----9----10----11----12----13----14----15
作业A: [========================]
作业B: [----------------------------------------------------=======================]
作业C: [---------------=========]
作业D: [--------------------===============]
作业E: [-------------------------------====]
性能指标:
-
作业A: 等待时间 \(0\),周转时间 \(5\)
-
作业B: 等待时间 \(10\),周转时间 \(4\)
-
作业C: 等待时间 \(3\),周转时间 \(5\)
-
作业D: 等待时间 \(4\),周转时间 \(7\)
-
作业E: 等待时间 \(6\),周转时间 \(7\)
平均等待时间: \(\frac{0+10+3+4+6}{5} = 4.6\)
平均周转时间: \(\frac{5+4+5+7+7}{5} = 5.6\)
时间片轮转调度¶
在时间片轮转调度算法(Round Robin, RR)中,系统将按FCFS策略组织的就绪队列中的进程以时间片为单位进行轮转,每个进程在时间片内执行,时间片结束后,进程将被抢占并插入到就绪队列的末尾。
- 时间片: 系统可设置每隔一定时间(通常为10~100毫秒)便产生一次中断,去激活进程调度程序进行调度,把CPU分配给队首进程,并令其执行一个时间片。
#include <vector>
#include <queue>
#include <algorithm>
struct Process {
int id;
int arrival_time;
int burst_time;
int remaining_time;
int completion_time;
Process(int id, int arrival, int burst)
: id(id), arrival_time(arrival), burst_time(burst),
remaining_time(burst), completion_time(0) {}
};
void round_robin_scheduling(std::vector<Process>& processes, int time_quantum) {
std::queue<Process*> ready_queue;
int current_time = 0;
// 按到达时间排序
std::sort(processes.begin(), processes.end(),
[](const Process& a, const Process& b) {
return a.arrival_time < b.arrival_time;
});
int process_index = 0;
while (process_index < processes.size() || !ready_queue.empty()) {
// 将到达的进程加入就绪队列
while (process_index < processes.size() &&
processes[process_index].arrival_time <= current_time) {
ready_queue.push(&processes[process_index]);
process_index++;
}
if (!ready_queue.empty()) {
Process* current_process = ready_queue.front();
ready_queue.pop();
// 执行时间 = min(剩余时间, 时间片)
int execution_time = std::min(current_process->remaining_time, time_quantum);
// 更新进程状态
current_process->remaining_time -= execution_time;
current_time += execution_time;
// 如果进程未完成,重新加入队列
if (current_process->remaining_time > 0) {
ready_queue.push(current_process);
} else {
// 进程完成,记录完成时间
current_process->completion_time = current_time;
}
}
}
}
进程切换时机¶
-
若时间片尚未结束,但正在运行的进程便已经完成,则立即激活调度程序,从就绪队列删除该进程,并调度就绪队列中的队首进程
-
若时间片结束时,正在运行的进程还未完成,则剥夺其CPU使用权,并插入就绪队列的末尾,然后调度就绪队列中的队首进程
时间片大小的确定¶
时间片的大小应选择适中,使得大多数进程能在一个时间片内完成,从而减少进程切换的次数。时间片的大小应根据进程的类型和数量来确定,通常为10~100毫秒
-
时间片过大: 导致所有进程都到达就绪队列后,长作业会持续占用CPU,导致其他进程无法获得CPU资源
-
时间片过小: 导致进程切换过于频繁,增加系统开销
-
一般准则: 时间片/10 > 进程上下文切换时间
优点与缺点¶
-
优点
-
公平性:每个进程都能获得CPU时间
-
响应性:交互式进程能及时响应
-
简单性:算法简单,易于实现
-
无饥饿:不会出现进程永远得不到CPU的情况
-
-
缺点
-
上下文切换开销:频繁切换影响性能
-
时间片选择困难:需要权衡响应时间和系统开销
-
不适合I/O密集型进程:可能浪费CPU时间片
-
多级反馈队列调度¶
定义¶
在 RR 中,我们假设所有进程的紧迫性是相同的,但显然事实并非如此。
多级反馈队列调度算法(Multi-Level Feedback Queue, MLFQ)便是在 RR 的基础上,根据进程的紧迫性不同,设置多个优先级不同的就绪队列,每个优先级对应一个时间片,同时每个就绪队列都采用FCFS算法,从而实现多级反馈队列调度。

