关于进程调度算法的代码实现,我两年前写过一个(传送门在此),但是写的不太好,说实话我现在回头看那时写的代码已经看不懂了,因为写的太乱了,而且代码冗余很严重,代码结构也不合理,(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++){
// 若t时刻无进程到达,则使t等于下一格进程到达的时间
t = max(t, p[i].arrivingTime);
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的进程还未被执行完毕,那么加入到就绪队列末尾。
- 队列为空,则算法结束。
首先,我们要明确进程调度的时机,对于RR算法,当时间片结束或者进程被执行完成时,会进行调度,按照上述原理从就绪队列首部取出一个进程并为它分配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();
}