进程调度算法FCFS、SJF(SPF)、SRT、RR原理及代码实现

557 阅读8分钟

关于进程调度算法的代码实现,我两年前写过一个(传送门在此),但是写的不太好,说实话我现在回头看那时写的代码已经看不懂了,因为写的太乱了,而且代码冗余很严重,代码结构也不合理,(lll¬ω¬)汗。

最近正好开始复习操作系统,为了加深自己对这些算法的理解,所以又重新用代码模拟了一遍。

学计算机的,大都做过进程调度的计算题吧,这些题目一般是给出四五六个进程的信息,比如到达时间、需要服务时长等等,然后需要做的是计算出这些进程的执行结果,比如周转时长、结束时间、运行时段等等。

用笔在草稿纸上算是挺简单的,毕竟数据量小,而且这些算法的原理并不复杂。

但是,用代码实现这些算法还真的蛮难的,我起码写了四五个小时,才把下面的代码全部写出来。

由于缺少足够的测试数据,我的代码可能存在一些未被发现的bug,如果大家发现了这些错误,可以把相关内容输入输出数据发布在评论区,我会重新审查代码并改正错误。

FCFS

First Come First Serve,也就是先来的进程先享受cpu服务。实现步骤很简单,把进程按照到达时间升序排序,然后从前往后依次执行它们即可。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct TimeSection{
	double startTime;
	double endTime;
};
struct Process{
	int id;
	
	//用户输入的进程信息
    double arrivingTime;//到达时间
    double serviceTime;	//服务时长,也就是需要占用cpu的时长
    
    //为了方便计算而创建的动态变量
    double remainingTime;	//剩余时间
    
    //通过模拟调度算法,得出的进程执行结果
    double leavingTime;	//结束时间
    double cyclingTime;	//周转时间
    double wCyclingTime;//带权周转时间
    vector<TimeSection> timeSections; //在cpu中运行的时间段
};

// 全局变量
int n; //进程个数
vector<Process> p; //进程列表

void printResult(){
	printf("\n");
	for(auto process : p){
		printf(
			"%d号进程到到达时间为%.1lf,服务时长为%.1lf,结束时间为%.1lf,周转时间为%.1lf,带权周转时间为%.2lf\n",
			process.id, process.arrivingTime, process.serviceTime, 
			process.leavingTime, process.cyclingTime, process.wCyclingTime
		);
		printf("它在cpu中运行的时间段为:");
		for(auto timeSection : process.timeSections){
			printf("[%.1lf, %.1lf] ", timeSection.startTime, timeSection.endTime);
		}
		printf("\n\n");
	}
}

void readIn(){
	printf("请输入进程个数:");
	cin >> n;
	for(int i = 0; i < n; i++){
		Process tmp;
		tmp.id = i;
		printf("请输入%d号进程的到达时间和需要服务时长:", i);
		cin >> tmp.arrivingTime >> tmp.serviceTime;
		tmp.remainingTime = tmp.serviceTime;
		p.push_back(tmp);
	}
}

bool FCFScmp(Process x, Process y){
	return x.arrivingTime < y.arrivingTime;
}

void FCFS(){
	sort(p.begin(), p.end(), FCFScmp);
	double t = p[0].arrivingTime;
	for(int i = 0; i < n; i++){
		p[i].leavingTime = t + p[i].serviceTime;
		p[i].timeSections.push_back({t, p[i].leavingTime});
		p[i].cyclingTime = p[i].leavingTime - p[i].arrivingTime;
		p[i].remainingTime = 0;
		p[i].wCyclingTime = p[i].cyclingTime / p[i].serviceTime;
		t = p[i].leavingTime;
	}
}

int main(){
	readIn();
	FCFS();
	printResult();
}

SJF(SPF)

Shortest Job(Process) First,短进程优先,如果有多个进程正在等待cpu,那么当cpu空闲时,会优先执行等待队列中需要服务时间最短的那个进程。

这个实现步骤稍微复杂一点。

首先,我们要明确进程调度的时机,SPF算法并不是抢占式的,也就是说当一个进程正在cpu中执行时,不会进行调度,直到该进程执行结束。只有当一个进程结束执行或者一个新进程到达时,才有可能会执行调度算法,才有可能为进程分配cpu。

接下来,我们要用代码模拟执行调度的时机和调度的操作。在下面的代码中,我使用一个变量t来表示进程结束和进程到达的时间点,也就是可能进行进程调度的时刻。getExcute(t)方法返回在t时刻执行SPF调度算法的结果,也就是要执行进程的下标。getMinT()方法返回所有未执行进程的最早到达时间。

最后,我们定义一个n次的循环,在每次循环中:

  • 首先调用getExcute(t)获取要执行的进程下标,如果找不到符合条件的进程,说明t之前所有进程都被执行了,那么就使用getMinT()更新t,然后再次获取要执行的进程的下标
  • 然后,执行该进程,其实就是更新该进程的属性
  • 最后,更新t的值为 t + 进程的服务时长

这样执行n次循环,n个进程就全部被调度并执行完毕了,输出即可。

结构体定义、输入输入函数、一部分全局变量的定义和上面的算法是完全一致的,对于这部分直接复用上面的代码。

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
// 定义精度和无穷大值
const double EPS = 1e-7;
const double INF = 1e15;
struct TimeSection{
	double startTime;
	double endTime;
};
struct Process{
	int id;
	
	//用户输入的进程信息
    double arrivingTime;//到达时间
    double serviceTime;	//服务时长,也就是需要占用cpu的时长
    
    //为了方便计算而创建的动态变量
    double remainingTime;	//剩余时间
    
    //通过模拟调度算法,得出的进程执行结果
    double leavingTime;	//结束时间
    double cyclingTime;	//周转时间
    double wCyclingTime;//带权周转时间
    vector<TimeSection> timeSections; //在cpu中运行的时间段
};

// 全局变量
int n; //进程个数
vector<Process> p; //进程列表

void printResult(){
	printf("\n");
	for(auto process : p){
		printf(
			"%d号进程到到达时间为%.1lf,服务时长为%.1lf,结束时间为%.1lf,周转时间为%.1lf,带权周转时间为%.2lf\n",
			process.id, process.arrivingTime, process.serviceTime, 
			process.leavingTime, process.cyclingTime, process.wCyclingTime
		);
		printf("它在cpu中运行的时间段为:");
		for(auto timeSection : process.timeSections){
			printf("[%.1lf, %.1lf] ", timeSection.startTime, timeSection.endTime);
		}
		printf("\n\n");
	}
}

void readIn(){
	printf("请输入进程个数:");
	cin >> n;
	for(int i = 0; i < n; i++){
		Process tmp;
		tmp.id = i;
		printf("请输入%d号进程的到达时间和需要服务时长:", i);
		cin >> tmp.arrivingTime >> tmp.serviceTime;
		tmp.remainingTime = tmp.serviceTime;
		p.push_back(tmp);
	}
}
// ↑↑↑上面结构体和函数的代码不变


// 返回在t之前已经到达,而且服务时长最短、没有完成执行的进程下标
// 其实就是根据SPF算法,返回下一个要执行的进程的下标
int getExcute(double t){
	int idx = -1;
	double minService = INF;
	for(int i = 0; i < p.size(); i++){
		if(p[i].remainingTime > EPS && p[i].arrivingTime < t && p[i].serviceTime < minService){
			minService = p[i].serviceTime;
			idx = i;
		}
	}
	return idx;
}

// 返回所有未完成执行进程的最早到达时间
double getMinT(){
	double t = INF;
	for(auto process : p){
		if(process.remainingTime > EPS){
			t = min(t, process.arrivingTime);
		}
	}
	return t;
}

void SPF(){
	// t存放的是需要做进程调度的时刻,比如进程运行结束或者进程到达的时刻
	double t = getMinT();
	// 根据t获取一个执行的进程,然后执行它,共循环n次
	for(int i = 0; i < n; i++){
		int idx = getExcute(t + EPS);
		// 如果idx == -1,说明t之前到达的所有进程已被执行,则更新t
		if(idx == -1){
			t = getMinT();
			idx = getExcute(t + EPS);
		}
		// 执行p[idx]进程
		p[idx].leavingTime = t + p[idx].serviceTime;
		p[idx].timeSections.push_back({t, p[idx].leavingTime});
		p[idx].cyclingTime = p[idx].leavingTime - p[idx].arrivingTime;
		p[idx].remainingTime = 0;
		p[idx].wCyclingTime = p[idx].cyclingTime / p[idx].serviceTime;
		// 更新t为p[idx]进程结束的时间
		t = p[idx].leavingTime;
	}
}

int main(){
	readIn();
	SPF();
	printResult();
}

SRT

Shortest Remaining Time,最短剩余时间优先。有多个进程处于等待队列当中时,优先执行剩余时间最短的算法;如果新到达的进程剩余时间比正在执行的进程更短,那么新到达的进程会抢占cpu,原进程退出cpu。

其实是在SPF的基础上增加了抢占机制。

这个实现步骤要比SPF更复杂一点。

依然是同样的思考方法,首先,我们要明确进程调度的时机。和上面SPF相同的是,只有当一个进程结束执行或者一个新进程到达时,才有可能会执行调度算法,才有可能为进程分配cpu;和上面SPF不同的是,SRT是抢占式的,当一个进程正在被执行时,如果新到达了一个剩余时间小于它的进程,那么这时需要执行调度算法,使它让出cpu,执行新到达的那个进程。

我们对getExcute(t)方法做一下修改,使其返回在t之前已经到达,而且剩余时长最短、没有完成执行的进程下标。我们声明一个getNextT(t0)方法,该方法返回所有在t0时刻之后到达、未完成执行的所有进程的最早到达时间。

我们定义一个while循环,为什么是while循环呢,因为和SPF不同,在SRT中我们不知道一个进程会被执行多少次,进而不能确定循环次数,所以使用while循环,当所有的进程都被执行完毕就退出循环。在循环内要做的事情如下:

  • 首先,根据t获取要执行的进程的idx、和下一次进程调度的时刻nt,注意nt不一定等于getNextT(t0),也有可能是当前进程完成执行的时刻。
  • 为idx进程增加一个运行时间段[t, nt],判断该进程是否执行完成,然后进行相应操作
  • 令 t = nt,进入下一次循环

在上述步骤中,我们发现,当前循环中为某个进程增加了一个运行时间段,如果该进程没有执行完成的话,那么下个循环中要执行的还可能是该进程。也就是说,该进程的执行时间段列表可能是这样的形式[0, 1.0], [1.0, 3.0], [3.0, 4.0]......所以我们需要对它进行合并,合并成[0.0, 4.0],我定义了一个doMerge函数,来进行这个合并操作。

下面是运行截图和代码: 在这里插入图片描述

结构体定义、输入输入函数、一部分全局变量的定义和上面的算法是完全一致的,对于这部分直接复用上面的代码。

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
const double EPS = 1e-7;
const double INF = 1e15;
struct TimeSection{
	double startTime;
	double endTime;
};
struct Process{
	int id;
	
	//用户输入的进程信息
    double arrivingTime;//到达时间
    double serviceTime;	//服务时长,也就是需要占用cpu的时长
    
    //为了方便计算而创建的动态变量
    double remainingTime;	//剩余时间
    
    //通过模拟调度算法,得出的进程执行结果
    double leavingTime;	//结束时间
    double cyclingTime;	//周转时间
    double wCyclingTime;//带权周转时间
    vector<TimeSection> timeSections; //在cpu中运行的时间段
};

// 全局变量
int n; //进程个数
vector<Process> p; //进程列表

void printResult(){
	printf("\n");
	for(auto process : p){
		printf(
			"%d号进程到到达时间为%.1lf,服务时长为%.1lf,结束时间为%.1lf,周转时间为%.1lf,带权周转时间为%.2lf\n",
			process.id, process.arrivingTime, process.serviceTime, 
			process.leavingTime, process.cyclingTime, process.wCyclingTime
		);
		printf("它在cpu中运行的时间段为:");
		for(auto timeSection : process.timeSections){
			printf("[%.1lf, %.1lf] ", timeSection.startTime, timeSection.endTime);
		}
		printf("\n\n");
	}
}

void readIn(){
	printf("请输入进程个数:");
	cin >> n;
	for(int i = 0; i < n; i++){
		Process tmp;
		tmp.id = i;
		printf("请输入%d号进程的到达时间和需要服务时长:", i);
		cin >> tmp.arrivingTime >> tmp.serviceTime;
		tmp.remainingTime = tmp.serviceTime;
		p.push_back(tmp);
	}
}
// ↑↑↑上面结构体和函数的代码不变 

// 合并所有进程的运行时间段,比如把[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]合并成[1.0, 4.0]
void doMerge(){
	for(int i = 0; i < p.size(); i++){
		vector<TimeSection> tmp;
		double st = p[i].timeSections[0].startTime;
		double ed = p[i].timeSections[0].endTime;
		for(int j = 1; j < p[i].timeSections.size(); j++){
			if(fabs(ed - p[i].timeSections[j].startTime) < EPS){
				ed = p[i].timeSections[j].endTime;
			}
			else {
				tmp.push_back({st, ed});
				st = p[i].timeSections[j].startTime;
				ed = p[i].timeSections[j].endTime;
			}
		}
		tmp.push_back({st, ed});
		p[i].timeSections = tmp;
	}
}

// 返回在t之前已经到达,而且剩余时长最短、没有完成执行的进程下标
// 其实就是根据SRT算法,返回下一个要执行的进程的下标
int getExcute(double t){
	int idx = -1;
	double minRT = INF;
	for(int i = 0; i < p.size(); i++){
		if(p[i].remainingTime > EPS && p[i].arrivingTime < t && p[i].remainingTime < minRT){
			minRT = p[i].remainingTime;
			idx = i;
		}
	}
	return idx;
}

// 返回所有未完成执行进程的最早到达时间
double getMinT(){
	double t = INF;
	for(auto process : p){
		if(process.remainingTime > EPS){
			t = min(t, process.arrivingTime);
		}
	}
	return t;
}

// 返回所有在t0时刻之后到达、未完成执行的所有进程的最早到达时间
double getNextT(double t0){
	double t = INF;
	for(auto process : p){
		if(process.remainingTime > EPS && process.arrivingTime > t0){
			t = min(t, process.arrivingTime);
		}
	}
	return t;
}

void SRT(){
	
	// t存放的是当前进程调度的时刻,比如进程运行结束或者进程到达的时刻
	// nt存放的是在t之后下一次做进程调度的时刻,比如进程结束或新进程到达
	// 可能上面的解释有点抽象,总之[t, nt]代表某个进程的某个被执行时间段
	double t = getMinT();
	double nt;
	int cnt = 0;
	// 根据t获取一个执行的进程,并且获取nt,在[t, nt]时间段执行该进程,然后令t = nt,进入下一个循环
	while(cnt != n){
		int idx = getExcute(t + EPS);
		// 如果idx == -1,说明t之前到达的有进程已全部被执行,则更新t
		if(idx == -1){
			t = getMinT();
			idx = getExcute(t + EPS);
		}
		nt = getNextT(t + EPS);
		nt = min(nt, t + p[idx].remainingTime);
		p[idx].remainingTime -= nt - t;
		p[idx].timeSections.push_back({t, nt});
		// 如果该进程剩余时间为0,也就是被执行完毕
		if(p[idx].remainingTime < EPS){
			p[idx].leavingTime = nt;
			p[idx].cyclingTime = p[idx].leavingTime - p[idx].arrivingTime;
			p[idx].wCyclingTime = p[idx].cyclingTime / p[idx].serviceTime;
			cnt++;
		}
		t = nt;
	}
	doMerge();
}

int main(){
	readIn();
	SRT();
	printResult();
}

RR算法

Round Robin,时间片轮转算法。它的原理是这样的:

  • 新到达的进程,被加入就绪队列的末尾(或队首);
  • 从就绪队列首部取出一个进程并执行;
  • 每个进程的单次执行时间小于等于时间片长度,如果该进程执行完毕或者执行时间已经达到了时间片长度,那么该进程退出cpu,并从就绪队列首部取出一个进程;
  • 如果退出cpu的进程还未被执行完毕,那么加入到就绪队列末尾。
  • 队列为空,则算法结束。

具体实现步骤是:首先声明一个inQueue方法,该方法的功能是判断某进程是否存在于就绪队列,如果不存在则将该进程插入队尾(队首),然后写一个while循环,每次循环代表一个执行一个时间片,在循环中用t存该时间片的开始时间,用nt存该时间片的结束时间。循环内容如下:

  • 首先,把在t之前到达的、未执行完毕的进程全部进行inQueue操作
  • 然后判断全局变量lastP是否为NULL,如果不为NULL则加入队尾,lastP的含义是上一次时间片中被执行但未执行完毕的进程。
  • 取出队首的进程并执行,nt为该进程在本次时间片中结束执行的时刻
  • 判断该进程是否被执行完毕,并进行相应操作,如果未执行完毕则把该进程赋值给lastP,否则把NULL赋值给lastP
  • 最后更新t,t是下一轮时间片的开始时刻

同样的,我们要对所有进程的运行时间段进行doMerge操作

注意,RR算法有两种实现方法,当新进程到达时,既可以插入队首也可以插入队尾,我两种方式都在代码中实现了。在我的代码中,未被注释的inQueue()方法是插入队尾,注释掉的inQueue()方法是插入队首。运行截图及代码如下:

插入到队尾: 在这里插入图片描述

插入到队首: 在这里插入图片描述

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <queue>
using namespace std;
const double EPS = 1e-7;
const double INF = 1e15;
struct TimeSection{
	double startTime;
	double endTime;
};
struct Process{
	int id;
	
	//用户输入的进程信息
    double arrivingTime;//到达时间
    double serviceTime;	//服务时长,也就是需要占用cpu的时长
    
    //为了方便计算而创建的动态变量
    double remainingTime;	//剩余时间
    
    //通过模拟调度算法,得出的进程执行结果
    double leavingTime;	//结束时间
    double cyclingTime;	//周转时间
    double wCyclingTime;//带权周转时间
    vector<TimeSection> timeSections; //在cpu中运行的时间段
};

// 全局变量
int n; //进程个数
vector<Process> p; //进程列表

void printResult(){
	printf("\n");
	for(auto process : p){
		printf(
			"%d号进程到到达时间为%.1lf,服务时长为%.1lf,结束时间为%.1lf,周转时间为%.1lf,带权周转时间为%.2lf\n",
			process.id, process.arrivingTime, process.serviceTime, 
			process.leavingTime, process.cyclingTime, process.wCyclingTime
		);
		printf("它在cpu中运行的时间段为:");
		for(auto timeSection : process.timeSections){
			printf("[%.1lf, %.1lf] ", timeSection.startTime, timeSection.endTime);
		}
		printf("\n\n");
	}
}

void readIn(){
	printf("请输入进程个数:");
	cin >> n;
	for(int i = 0; i < n; i++){
		Process tmp;
		tmp.id = i;
		printf("请输入%d号进程的到达时间和需要服务时长:", i);
		cin >> tmp.arrivingTime >> tmp.serviceTime;
		tmp.remainingTime = tmp.serviceTime;
		p.push_back(tmp);
	}
}
// ↑↑↑上面结构体和函数的代码不变 

double r;	// 时间片大小
queue<Process*> q; 	// 就绪队列
Process* lastP = NULL; // 存放上一轮时间片结束,但还没有执行完毕的进程

// 合并所有进程的运行时间段,比如把[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]合并成[1.0, 4.0]
void doMerge(){
	for(int i = 0; i < p.size(); i++){
		vector<TimeSection> tmp;
		double st = p[i].timeSections[0].startTime;
		double ed = p[i].timeSections[0].endTime;
		for(int j = 1; j < p[i].timeSections.size(); j++){
			if(fabs(ed - p[i].timeSections[j].startTime) < EPS){
				ed = p[i].timeSections[j].endTime;
			}
			else {
				tmp.push_back({st, ed});
				st = p[i].timeSections[j].startTime;
				ed = p[i].timeSections[j].endTime;
			}
		}
		tmp.push_back({st, ed});
		p[i].timeSections = tmp;
	}
}

bool cmp(Process a, Process b){
	return a.arrivingTime < b.arrivingTime;
}

// 返回所有未完成执行进程的最早到达时间
double getMinT(){
	double t = INF;
	for(auto process : p){
		if(process.remainingTime > EPS){
			t = min(t, process.arrivingTime);
		}
	}
	return t;
}

// 如果当前进程在队中或者等于lastP,则不进行操作;否则,说明是新到达的进程,插入队尾。
void inQueue(Process* process){
	auto tmp = q;
	if(process == lastP){
		return ;
	}
	while(!tmp.empty()){
		if(process == tmp.front()){
			return ;
		}
		tmp.pop();
	}
	q.push(process);
}

//// 插入队首。
//void inQueue(Process* process){
//	auto tmp = q;
//	if(process == lastP){
//		return ;
//	}
//	while(!tmp.empty()){
//		if(process == tmp.front()){
//			return ;
//		}
//		tmp.pop();
//	}
//	
//	// 把process插入q的队首
//	queue<Process*> res;
//	res.push(process);
//	while(!q.empty()){
//		res.push(q.front());
//		q.pop();
//	}
//	q = res;
//}

void RR(){
	sort(p.begin(), p.end(), cmp);
	// t存放当前时间片的开始时间,nt存放当前时间片的结束时间
	double t = getMinT();
	double nt;
	int cnt = 0;
	
	// 根据t获取一个执行的进程,并且获取nt,在[t, nt]时间段执行该进程,然后令t = max(nt, getMinT()),进入下一个循环
	while(cnt != n){
		// 把t之前到达的进程,进行inQueue操作
		for(int i = 0; i < p.size(); i++){
			if(p[i].arrivingTime < t + EPS && p[i].remainingTime > EPS){
				inQueue(&p[i]);
			}
		}
		// 如果上一轮时间片中进程未执行完毕,则加入队列末尾
		if(lastP != NULL){
			q.push(lastP);
		}
		// nowP就是要执行的进程
		auto nowP = q.front();
		q.pop();
		
		// 有可能时间片结束,进程未完成,也有可能反过来,所以取最小值
		nt = t + min(r, nowP->remainingTime);
		
		nowP->remainingTime -= nt - t;
		nowP->timeSections.push_back({t, nt});
		if(nowP->remainingTime < EPS){
			nowP->leavingTime = nt;
			nowP->cyclingTime = nowP->leavingTime - nowP->arrivingTime;
			nowP->wCyclingTime = nowP->cyclingTime / nowP->serviceTime;
			cnt++;
			lastP = NULL;
		}
		else{
			lastP = nowP;
		}
		// 有可能时间片结束的那一时刻,新进程还没有到达
		// 所以取时间片结束和新进程到达时刻的最大值,赋值给t
		t = max(nt, getMinT());
	}
	doMerge();
}

int main(){
	printf("请输入时间片大小:");
	cin >> r;
	readIn();
	RR();
	printResult();
}