实验3 处理机管理
一、实验目的
在多道程序或多任务系统中,系统中同时处于就绪态的进程有若干个,即能运行的进程数远远大于处理机个数。为了使系统中的各个进程能有条不紊的运行,必须按照某种调度策略,选择一个进程占用处理机。
本次实验要求设计并实现一个模拟单处理机调度的算法,以加深对处理机调度基本概念和基本算法的理解。
二、实验内容
1、设计一个按时间片轮转法实现处理机调度的程序(难度系数0.95)
(1)假设系统中有N个进程,每个进程由一个进程控制块(PCB)来标识,进程控制块结构如下图所示。
进程名 |
到达时间 |
估计运行时间 |
进程状态 |
…… |
链接指针 |
图1 进程控制块PCB结构图
进程名:即进程标识。
到达时间:进程创建时的系统时间或由用户制定。
估计运行时间:可由设计者指定一个时间值。
进程状态:这里假定进程有两种状态:就绪状态和完成状态。就绪状态用“R”表示,完成状态用“C”表示。假定进程创建后就处于就绪状态,运行结束时,就被置成完成状态。
链接指针:按照进程到达系统的时间将处于就绪状态的进程连接成一个就绪队列。指针指出下一个到达的进程控制块首地址。最后一个进程的链指针为NULL。
(2)按照进程到达的先后顺序排成一个循环队列。
(3)执行处理机调度时,首先选择队首的第一个进程运行。
(4)由于本实验是模拟实验,所以对被选中进程并不实际启动运行,而只是执行:
估计运行时间减1,用这个操作来模拟进程的一次运行。
(5)进程运行一次后,以后的调度则将当前指针依次下移一个位置,指向下一个进程。同时还应判断该进程的剩余运行时间是否为0,若不为0,则等待下一轮的运行;若该进程的剩余运行为0,则将该进程的状态置为完成状态,并退出循环队列。
(6)若就绪队列不空,则重复上述的步骤(4)和(5)直到所有进程都运行完为止。
(7)在所设计的调度程序中,应包含显示或打印语句,以便显示或打印每次选中进程的名称及运行一次后队列的变化情况。
(8)计算每个进程的周转时间、带权周转时间以及进程平均周转时间。
2、设计一个按优先级调度的算法实现处理机调度(难度系数1.0)
(1) 系统有N个进程,每个进程用一个进程控制块来代表,进程控制块的结构如下图所示。
进程名 |
到达时间 |
估计运行时间 |
进程优先级 |
进程状态 |
…… |
链接指针 |
图2 进程控制块PCB结构
进程的优先级、要求运行时间和估计运行时间由用户程序任意设定。调度时,总是选择优先级最高的进程运行。
(2)为了调度方便,设计一个指针指向就绪队列中的第一个进程。另外再设一个当前运行进程指针,指向当前正运行的进程。
(3)处理机每个时间片调度1次,总是选择已经到达队列的优先级最高的进程运行。为了采用动态优先级调度,进程每运行一次,其优先级就减1。由于本实验是模拟实验,所以对选中进程并不实际启动运行,而只是执行:
优先级减1和估计运行时间减1,用这两个操作来模拟进程的一次运行。
(4)进程运行一次后,若剩余时间不为0,且其优先级低于就绪队列的其他进程优先级,则选择一个高优先级进程抢占CPU;若剩余时间为0,把它的状态改为完成状态“C”,并撤出就绪队列。
(5)若就绪队列不空,则重复上述的步骤(3)和(4),直到所有进程成为完成状态。
(6)在所设计的调度程序中,应包含显示或打印语句,以便显示或打印每次选中进程的名称及运行一次后队列的变化情况。
(7)计算每个进程的周转时间、带权周转时间以及进程平均周转时间。
三、实验结果和分析
(1)程序中使用的数据结构说明;
【1】实验内容1使用的数据结构说明:
如上图所示,进程控制块使用结构体进行构建,包含以下几个属性:进程名字、进程状态、就绪队列中的累计排队序号、进程到达时间、进程服务时间、进程完成时间、进程剩余时间。其中,进程名字使用字符串变量,其他属性均使用整形变量。
如上图所示,在执行RR算法之前,首先利用字符串存放当前的就绪队列,利用数组list存放在就绪队列中的进程的编号,利用数组waitnumber存放进程在就绪队列中的累计排队序号。然后,通过对数组waitnumber进行从小到大的排序,同时对应给数组list进行排序。最后,在数组list中得到按照等待顺序递增的进程序列,并将进程名字按序写入ready中。
【2】实验内容2使用的数据结构说明:
如上图所示,进程控制块使用结构体进行构建,包含以下几个属性:进程名字、进程状态、进程到达时间、进程服务时间、进程完成时间、进程剩余时间、进程优先级、进程上一次变为就绪态的时间。其中,进程名字使用字符串变量,其他属性均使用整形变量。此外,进程上一次变为就绪态的时间初始化为进程到达时间,如果进程后续有被CPU调用,则更新为被CPU调用后的时间。
如上图所示,在执行PSA算法之前,首先利用数组存放当前的就绪队列所对应的进程编号,并调用sortlist()函数进行当前时间的就绪队列计算。
(2)程序流程图和带有注释的源程序;
【1】实验内容1的流程图:如(4)处的总结1所示,并主要包含以下四个内容:
1)整体的程序流程图;
2)进程初始化的流程图;
3)按照到达时间排序进程的流程图;
4)时间片轮转算法的流程图。
【2】实验内容2的流程图:如(4)处的总结2所示,并主要包含以下四个内容:
1)整体的程序流程图;
2)进程初始化的流程图;
3)就绪队列进行排序的流程图;
4)优先级调度算法的流程图。
【3】实验内容1 & 实验内容2的带有注释的源程序:
实验内容1:exp3-1.cpp |
#include <iostream> #include <stdio.h> #include <iomanip> #include <string.h> #include <stdlib.h> using namespace std; //pcb的结构体设置 typedef struct{ string name; //进程名字 int flag; //是否完成,状态检测(1是完成,0是未完成) int number; //就绪队列中的顺序 int arrivetime; //到达时间 int runtime; //服务时间 int completetime; //完成时间 int remaintime; //剩余时间 }PCB; //进程指标 (全局) PCB p[100]; //可输入的进程数量 int timelen=1; //时间片长度 int total; //进程总数(由main中的用户输入) //就绪队列指标 (全局) int current=0; //当前应该执行的进程编号 int head=0; //就绪队列中的队首 //进程初始化 void initialize(){ for(int i=0;i<total;i++){ cout<<"请输入第"<<i+1<<"个进程的名字、到达时间、服务时间:"; cin>>p[i].name>>p[i].arrivetime>>p[i].runtime;
//其他属性的初始化 p[i].remaintime=p[i].runtime; //初始剩余时间 = 服务时间 p[i].flag=0; //完成状态 = 未完成 p[i].number=0; //就绪队列中的顺序 = 0 } } //按照到达时间排序进程(冒泡排序) void sortarrive(){ for(int i=0;i<total-1;i++){ for(int j=0;j<total-i-1;j++){ //到达时间从小到大排序 if(p[j].arrivetime>p[j+1].arrivetime){ //如果前一个的时间大于后一个,则交换两个进程的顺序 PCB t=p[j+1]; p[j+1]=p[j]; p[j]=t; } } } } //时间片轮转 void timeprocess(){ int time=p[0].arrivetime; //系统开始的时间是最早就绪进程的到达时间 int complete=0; //已经完成的进程个数 int n=0; //记录while循环次数的变量
//所有进程执行的过程 while(complete<total){ for(int i=0;i<total;i++){ //当前这个进程和cur相同,且状态为未完成 if(p[i].number==current && p[i].flag==0){ //print current cycle cout<<endl<<"当前的时钟周期是:T"<<current<<endl;
//process ready list string ready=""; int list[100]; int waitnumber[100]; int cnt=0; for(int k=0;k<total;k++){ //大于当前current的,都是在就绪队列里面的进程 if(p[k].number>current && p[k].flag==0){ list[cnt]=k; waitnumber[cnt]=p[k].number; cnt++; } } cout<<"就绪队列里面一共有:"<<cnt<<"个进程,";
//按照waitnumber排序 (冒泡排序) for (int k = 0; k < cnt - 1; k++) { for (int kk = 0; kk < cnt - 1 - k; kk++) { if (waitnumber[kk] > waitnumber[kk + 1]) { int tempfig = waitnumber[kk + 1]; waitnumber[kk + 1] = waitnumber[kk]; waitnumber[kk] = tempfig; int templist = list[kk + 1]; list[kk + 1] = list[kk]; list[kk] = templist; } } }//不知道为啥死循环了。。调好了
//按照waitnumber的大小进行push for(int k=0;k<cnt;k++){ int id=list[k]; ready+=p[id].name; ready+=" "; } //print ready if(ready==""){ cout<<"就绪队列为空"<<endl; } else{ cout<<"当前的就绪队列为:"<<ready<<endl; }
//时间片工作 cout<<"判断时间片ing,当前应该执行进程"<<p[i].name<<endl; //current进程能在当前时间片运行完 if(p[i].remaintime<=timelen){ time+=p[i].remaintime; //系统时间,增加该进程的剩余时间 p[i].flag=1; //状态变为完成 p[i].completetime=time; //此进程的完成时间为当前系统时间 p[i].remaintime=0; //剩余时间,设置为0
complete++; //系统中完成的进程数+=1 current++; //cur+=1
//检查是否有新的到达进程 for(int j=i+1;j<total;j++){ //有新的到达进程 if(p[j].arrivetime<=time && p[j].number==0){ head++; //就绪队列个数+=1 p[j].number=head; //这个进程的就绪编号为head } } } //current进程不能在当前时间片运行完 else{ time+=timelen; p[i].remaintime-=timelen; current++;
//检查是否有新的到达进程 for(int j=i+1;j<total;j++){ if(p[j].arrivetime<=time && p[j].number==0){ head++; p[j].number=head; } }
//重新给这个进程加入就绪队列 head++; //刚刚执行的这个进程继续进入就绪队列 p[i].number=head; //更改就绪编号 }
} } n++; //检测while循环的次数 } cout<<"执行完毕,总共花费时钟周期:T"<<current<<endl; } void show(){ double x=0; //总周转时间 周转时间 = 完成时间 - 到达时间 double y=0; //总带权周转时间 带权周转时间 = 周转时间 / 服务时间
//输出每个进程的基本信息 for(int i=0;i<total;i++){ double px=(double)p[i].completetime-p[i].arrivetime; //当前进程的周转时间 double py=px/(double)p[i].runtime; //当前进程的带权周转时间 cout<<"进程名字:"<<p[i].name; cout<<"\t"<<"周转时间:"<<px; cout<<"\t"<<"带权周转时间:"<<py; cout<<"\t"<<"到达时间:"<<p[i].arrivetime; cout<<"\t"<<"服务时间:"<<p[i].runtime; cout<<"\t"<<"完成时间:"<<p[i].completetime; cout<<endl; x+=px; y+=py; } double ret1=x/total; //平均周转时间 double ret2=y/total; //平均带权周转时间 cout<<"平均周转时间:"<<ret1<<endl; cout<<"平均带权周转时间:"<<ret2<<endl; } /* //RR 测试用例1: 5 p1 0 3 p2 2 6 p3 4 4 p4 6 5 p5 8 2 //优先级 测试用例2: 4 p1 0 2 2 p2 0 2 3 p3 0 4 1 p4 0 3 4 进程执行的顺序应该是:p4 p2 p4 p1 p2 p4 p3 p1 p3 p3 p3 7.5 2.8125 */ int main(){ cout<<"请输入进程总数:"; cin>>total; //用户输入进程的总数 initialize(); //初始化 sortarrive(); //到达时间处理 timeprocess(); //时间片处理 cout<<endl; show(); //输出最终的结果 return 0; } |
实验内容2:exp3-2.cpp |
#include <iostream> #include <stdio.h> #include <iomanip> #include <string.h> #include <stdlib.h> #include <vector> using namespace std; //pcb的结构体设置 typedef struct{ string name; //进程名字 int flag; //是否完成,状态检测(1是完成,0是未完成) int arrivetime; //到达时间 int runtime; //服务时间 int completetime; //完成时间 int remaintime; //剩余时间 int prior; //优先级 int lasttime; //上一次运行的时间 }PCB; //进程指标 (全局) PCB p[100]; //可输入的进程数量 int timelen=1; //时间片长度 int total; //进程总数(由main中的用户输入) //进程初始化 void initialize(){ for(int i=0;i<total;i++){ cout<<"请输入第"<<i+1<<"个进程的名字、到达时间、服务时间、优先级:"; cin>>p[i].name>>p[i].arrivetime>>p[i].runtime>>p[i].prior;
//其他属性的初始化 p[i].remaintime=p[i].runtime; //初始剩余时间 = 服务时间 p[i].flag=0; //完成状态 = 未完成 p[i].lasttime=p[i].arrivetime; //上一次运行的时间 } } //初始化就绪队列 void sortlist(vector<int> &wait,int cycle){ //遍历已经到来且未完成的进程 for(int i=0;i<total;i++){ if(p[i].arrivetime<=cycle && p[i].flag==0){ wait.push_back(i); } }
int n=wait.size(); //按照prior划分(选择排序) for(int i=0;i<n-1;i++){ int id=i; //max to process for(int j=i+1;j<n;j++){ int curprocess=wait[j]; //遍历到的后面的进程序号 int maxprocess=wait[id]; //当前优先级最大的进程序号 if(p[maxprocess].prior<p[curprocess].prior){ //如果后面进程的prior更大,则更新优先级最大的进程序号 id=j; } } if(id!=i){ //如果有更新,则交换两个pcb的顺序 int t=wait[id]; wait[id]=wait[i]; wait[i]=t; } }
//相同prior按照lasttime划分 int i=0; //从队首开始分析 while(i<n-1){ int pos=i; //记录离当前最远,且和当前进程优先级相同的进程 int nowid=wait[i]; for(int j=1+i;j<n;j++){ int processid=wait[j]; if(p[processid].prior==p[nowid].prior){ pos=j; //后面的进程和现在的进程优先级相同,更新最远记录 } } if(pos!=i){ //sort between wait[i] and wait[pos] according to lasttime(选择排序) for(int a=i;a<pos;a++){ int aid=a; for(int b=a+1;b<=pos;b++){ int cur=wait[b]; int min=wait[aid]; if(p[min].lasttime>p[cur].lasttime){ //如果后面的lasttime更小 aid=b; } } if(aid!=a){ //如果有更新,则交换两个pcb的顺序 int tt=wait[aid]; wait[aid]=wait[a]; wait[a]=tt; } } } i=pos+1; //移动到下一个还没有判断过优先级是否相等的pcb块 } } void psa(){ //系统开始时间 (初始化) int starttime=p[0].arrivetime; for(int i=1;i<total;i++){ if(starttime>p[i].arrivetime){ starttime=p[i].arrivetime; //更新进程最早到达的时间 } }
int cycle=0; //执行的周期数 int complete=0; //完成的进程数
//执行所有进程 while(complete<total){ //wait list vector<int> wait; sortlist(wait,cycle); cout<<endl<<"在执行该时间片前,T"<<cycle<<"的就绪队列为:"; for(int i=0;i<wait.size();i++){ int proid=wait[i]; cout<<p[proid].name<<" "; } cout<<endl;
//执行wait[0](位于就绪队列队首的时间片) int curpro=wait[0]; cout<<"判断时间片ing,当前应该执行进程:"<<p[curpro].name<<endl;
//判断当前时间片内,curpro是否能执行完毕 if(p[curpro].remaintime<=timelen){ //can p[curpro].flag=1; //修改进程状态,且flag==1之后,就不需要管prior了 p[curpro].completetime=starttime+cycle+1; //修改完成时间 p[curpro].remaintime=0; //修改剩余时间 complete++; //完成的进程数 += 1 cout<<"在这个时间片内,已经执行完进程:"<<p[curpro].name<<endl; } else{ //cannot p[curpro].remaintime-=timelen; //修改剩余时间 p[curpro].prior--; //修改优先级 p[curpro].lasttime=cycle+1; //修改上次使用时间 }
cycle++; //时间周期 += 1 //complete++; //temporary break while to debug } cout<<endl<<"执行完毕,总共花费时钟周期:T"<<cycle<<endl; } void show(){ double x=0; //总周转时间 周转时间 = 完成时间 - 到达时间 double y=0; //总带权周转时间 带权周转时间 = 周转时间 / 服务时间
//输出每个进程的基本信息 for(int i=0;i<total;i++){ double px=(double)p[i].completetime-p[i].arrivetime; //当前进程的周转时间 double py=px/(double)p[i].runtime; //当前进程的带权周转时间 cout<<"进程名字:"<<p[i].name; cout<<"\t"<<"周转时间:"<<px; cout<<"\t"<<"带权周转时间:"<<py; cout<<endl; cout<<"\t"<<"到达时间:"<<p[i].arrivetime; cout<<"\t"<<"服务时间:"<<p[i].runtime; cout<<"\t"<<"完成时间:"<<p[i].completetime; cout<<endl; x+=px; y+=py; } double ret1=x/total; //平均周转时间 double ret2=y/total; //平均带权周转时间 cout<<"平均周转时间:"<<ret1<<endl; cout<<"平均带权周转时间:"<<ret2<<endl; } /* //优先级 测试用例2: 4 p1 0 2 2 p2 0 2 3 p3 0 4 1 p4 0 3 4 进程执行的顺序应该是:p4 p2 p4 p1 p2 p4 p3 p1 p3 p3 p3 0 1 2 3 4 5 6 7 8 9 10 7.5 2.8125 */ int main(){ cout<<"请输入进程总数:"; cin>>total; //用户输入进程的总数 initialize(); //初始化 psa(); //psa process cout<<endl; show(); //输出最终的结果 return 0; } |
(3)执行程序名,并打印程序运行时的初值和运行结果,其中包括:
①各进程控制块的初始状态;
【实验内容1】
在本实验内容中,测试用例的内容如下表所示。
进程总数 | 5 | |
进程名字 | 进程到达时间 | 进程服务时间 |
P1 | 0 | 3 |
P2 | 2 | 6 |
P3 | 4 | 4 |
P4 | 6 | 5 |
P5 | 8 | 2 |
如下图所示,在初始化函数initialize()中,在初始化各个进程的各个属性之后,将打印各个进程的基本信息(完成时间除外)。
测试用例的结果如下图所示。
由上图可知,进程初始化成功,各个进程控制块的初始状态符合预期。
【实验内容2】
在本实验内容中,测试用例的内容如下表所示。
进程总数 | 4 | ||
进程名字 | 进程到达时间 | 进程服务时间 | 进程优先级 |
P1 | 0 | 2 | 2 |
P2 | 0 | 2 | 3 |
P3 | 0 | 4 | 1 |
P4 | 0 | 3 | 4 |
如下图所示,在初始化函数initialize()中,在初始化各个进程的各个属性之后,将打印各个进程的基本信息(完成时间除外)。
测试用例的结果如下图所示。
由上图可知,进程初始化成功,各个进程控制块的初始状态符合预期。
②选中运行进程的名字、运行后各进程控制块状态以及每次调度时,就绪队列的进程排列顺序;
【实验内容1】
在RR算法执行过程中,程序会输出当前的时间、就绪队列的结果、当前时间片选中运行进程的名字、当前时间片运行后执行完毕的进程,并在算法执行完毕后输出系统总花费的时间。程序输出的结果如下图所示。
在PSA算法执行完毕后,程序会输出各个进程的周转时间和带权周转时间,并同时输出到达时间、服务时间、完成时间,以便核对计算是否正确。在输出完所有进程的信息后,程序会输出系统的平均周转时间和平均带权周转时间。程序输出的结果如下图所示。
核验实验3的ppt中关于RR算法的测试案例后,发现实验结果和理论计算结果一致。
【实验内容2】
在PSA算法执行过程中,程序会输出就绪队列的结果、当前时间片选中运行进程的名字、当前时间片运行后执行完毕的进程,并在算法执行完毕后输出系统总花费的时间。程序输出的结果如下图所示。
在PSA算法执行完毕后,程序会输出各个进程的周转时间和带权周转时间,并同时输出到达时间、服务时间、完成时间,以便核对计算是否正确。在输出完所有进程的信息后,程序会输出系统的平均周转时间和平均带权周转时间。
核验实验3的ppt中关于PSA算法的测试案例后,发现实验结果和理论计算结果一致。
(4)总结本次实验的心得与体会。
【流程图】
1:实验内容1的流程图:
1)整体的程序流程图(main函数)
2)进程初始化的流程图(initialize函数)
3)按照到达时间排序进程的流程图(sortarrive函数)
4)时间片轮转算法的流程图
5)运行结果输出的流程图(show函数)
2:实验内容2的流程图:
注:运行结果输出的流程图(show函数)与实验内容1相同,此处不再赘述。
1)整体的程序流程图
2)进程初始化的流程图
3)就绪队列进行排序的流程图
4)优先级调度算法的流程图
【实验总结】
3:时间片原则:各进程按时间片运行,当一个时间片用完后,便停止该进程的执行而重新调度,适用于分时系统。
4:优先权原则:通常对一些重要的和紧急的进程赋予较高的优先权。当这种进程进入就绪队列时,如果其优先权比正在执行的进程优先权高,便停止正在执行的进程,将处理机分配给优先权高的进程。
5:进程调度方式分为两种——【抢占方式】和【非抢占方式】。抢占方式:把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生某事件而阻塞时,才把处理机分配给另一个进程。非抢占方式:当一个进程正在运行时,系统可以基于某种原则,剥夺已分配给它的处理机,将之分配给其它进程。因此,时间片轮转法(RR)和优先级调度算法(PSA)均是抢占方式,因为这两种算法都并没有一个进程完整地运行。
6:各种调度算法的优点和缺点如下图所示。
FCFS:先来先服务算法。
SJF:最短作业优先算法。
HRRF:最高响应比优先算法。
RR:时间片轮转算法。
7:在本实验中,主要使用了RR和PSA。RR的特点是:公平性高,每个进程都有机会执行;可能导致较高的上下文切换开销,实时性较差。PSA的特点是:能够更好地满足紧急任务的需要,对于重要或紧急的任务更加有效;可能导致低优先级进程长时间等待,甚至发生饥饿现象。
8:饥饿现象:在操作系统中是指某一个或多个进程由于种种原因长时间得不到所需的资源,从而无法继续执行的情况。
四、实验指导
在Linux环境下, 用C语言编写,程序中的数据结构定义和程序主框架如下,供参考。
typedef struct pcb //进程控制块定义
{ int pid; //进程ID
char pname[N]; //进程名
int runtime; //运行时间
int arrivetime; //到达时间
char state; //进程状态
int priority; //进程优先级
int finishtime; //进程结束时间
struct pcb *next; //链接指针
……
} PCB;
PCB *head_input; //就绪队列头指针
PCB *head_run; //运行进程指针
unsigned long current; //记录系统当前时间的变量
……
void inputprocess( ); //建立进程,输入进程数目N,输入各个进程信息,并建立链表;
void printreadylist( ); //输出就绪队列
int readyprocess( ); //检查就绪队列并运行进程
int runprocess( ); //运行进程的函数;
……
void main() //主函数
{ //fp=open("result.txt”, “w”);
current=0;
inputprocess( );
readyprocess( );
……
getch( );
//fclose(fp);
}
void inputprocess( ) //建立进程,输入进程数目N,输入各个进程信息,并建立链表;
{ // 输入需要运行的进程数量n
…..
// 输入进程信息:进程名,运行时间,到达时间,优先级等信息
…...
}
int readyprocess( ) //检查就绪队列并运行进程
{ // 就绪队列为空时,结束运行
…..
// 根据调度算法选择一个进程;
runprocess();
//如果是时间片调度算法,则将时间减1
//如果是FCFS调度算法,则该进程运行结束,将当前时间+运行时间
//如果是优先级调度算法,运行时间-1,优先级-1
// 修改进程的状态
……
// 输出进程运行信息
}
五、测试用例
1. 时间片调度算法
2、 优先级调度算法
进程 优先级 运行时间 到达时间 结束时间
P1 12 2 0 8
P2 13 2 0 5
P3 11 4 0 11
P4 14 3 0 6
程序执行过程:
每个时间片调度一次,执行完成的进程优先级减1,重新调度
0 1 2 3 4 5 6 7 8 9 10 time
P4——p2——p4——p1——P2——p4——p3——p1——p3——p3——p3
13 12 12 11 11end 11end 10 10end 9 8 7end