操作系统实验报告
实验四:进程调度的模拟
一、实验内容
- 熟悉进程调度的各种算法;
- 对模拟程序给出数据和流程的详细分析,并画出流程图;
- 参考模拟程序写出时间片轮转调度算法的程序。
二、实验目的
通过本实验,加深对进程调度算法原理和过程的理解。
三、实验要求
- 对调度算法进行详细分析,在仔细分析的基础上,完全理解主要数据结构和过程的作用,给出主要数据结构的说明及画出主要模块的流程图。
- 根据提示信息,把函数写完整,使成为一个可运行程序。
- 反复运行程序,观察程序执行的结果,验证分析的正确性,然后给出一次执行的最后运行结果,并由结果计算出周转时间和带权周转时间。
说明:进程生命周期数据。即CPU-I/O时间序列,它是进程运行过程中进行调度、进入不同队列的依据。如序列:10秒(CPU),500秒(I/O),20秒(CPU),100秒(I/O),30秒(CPU),90秒(I/O),110秒(CPU),60秒(I/O)……,此序列在进程创建时要求自动生成。
四、进程的五种基本状态及转换
进程拥有以下五种状态:
- 创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,然后为该进程分配运行时所需的资源,最后将该进程转入就绪状态并插入就绪队列之中。
- 就绪状态:进程已处于准备好运行的状态,即已分配到除CPU以外的所需资源,只要分配到CPU就能够立即运行。
- 执行状态:进程已获得CPU,其程序正在执行。
- 阻塞状态:正在执行的进程由于某事件(I/O请求,申请缓存区失败等)暂时无法继续执行的状态。在满足请求时进入就绪状态等待系统调用。
- 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。
其相互之间的转换关系如下图1所示:
图1 进程基本状态转换图
五、进程调度的各种算法说明
5.1 先来先服务(FCFS)调度算法
从就绪的进程队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后,进程调度程序才将处理机分配给其他进程。
5.2 短作业优先(SJF)调度算法
从就绪队列中选择运行时间最短的进程,为之分配处理机。该进程一直运行到完成或发生某事件而阻塞后,进程调度程序才将处理机分配给其他进程。
5.3 优先级调度算法(PSA)
系统将CPU分配给就绪队列中优先权最高的进程。
(1)根据新进程能否抢占正在执行的进程,可将该调度算法分为:
- 非抢占式优先权调度算法:运行期间有更高优先权的进程到来也不剥夺CPU;
- 抢占式优先权调度算法:运行期间有更高优先权的进程到来可以抢占CPU;
(2)根据进程优先级是否可以改变,可将该调度算法分为:
- 静态优先权调度算法:优先级在创建进程时确定,且在进程的整个运行期间保持不变。确定优先级的主要依据有进程类型、进程对资源的要求、用户要求;
- 动态优先权调度算法:在运行过程中,根据进程状态的变化动态调整优先级。优先级的主要依据为进程占有CPU时间的长短、就绪进程等待CPU时间的长短。
5.4 时间片轮转调度算法
系统根据FCFS策略,将所有的就绪进程排成一个就绪队列。每次调度时把CPU分给队首进程,令其执行一个时间片。当该进程的时间片用完,调度程序终止当前进程的执行,并将它送到就绪队列的队尾,等待下次CPU执行。
5.5 多级队列调度算法
建立多个优先权不同的就绪队列,所有队列的优先权从大到小依次排列,每个队列有自己的调度算法。
5.6 多级反馈队列(MFQ)调度算法
设置多个就绪队列,并为每个队列赋予不同的优先级,该算法中不同队列中的进程所赋予的执行时间片大小不同,优先权越高的队列中,其进程时间片越小。进程按照队列的优先级调度,且每个队列都采用FCFS算法。
六、时间片轮转调度算法
6.1 算法描述
将进程按照FCFS原则进行排序放入就绪队列中。CPU每次调用就绪队列中的队首进程,当CPU执行完一个时间片后,当即进行切换。若此前的进程仍未完成运算,则继续进入就绪队列队尾;若完成运算且后续没有运行需求,则进程结束;若完成运算但后续仍有运行需求,则进程进入阻塞队列队尾,等待I/O操作。当阻塞队列中的进程完成I/O操作,若后续仍有运行需求,则继续进入就绪队列队尾;若没有需求则进程结束。其整体算法思想如下图所示:
图2 时间片轮转调度算法示意图
6.2 进程的数据结构说明
struct ProcStruct {
int p_pid;
char p_state;
int p_rserial[10];
int p_pos;
int p_starttime;
int p_endtime;
int p_cputime;
int p_iotime;
int p_next;
} proc[10];
表1 进程数据结构说明表
属性 | 含义 |
p_pid | 进程的标识号 |
p_state | 进程的状态 (C:运行,R:就绪,W:阻塞,B:后备,F:完成) |
p_rserial[10] | 模拟进程执行的CPU和I/O时间数据序列,间隔存储,第0项存储随后序列的长度(项数),以便知晓啥时该进程执行结束 |
p_pos | 当前进程运行到的位置,用来记忆执行到序列中的哪项 |
p_starttime | 进程的建立时间 |
p_endtime | 进程的运行结束时间 |
p_cputime | 当前运行时间段进程剩余的需要运行时间 |
p_iotime | 当前I/O时间段进程剩余的I/O时间 |
p_next | 进程控制块的链接指针 |
6.3 算法实现流程
在轮转算法中,时间片的大小设置对系统性能有很大的影响。若时间片小,有利于短进程,因为它能在该时间片内完成;但时间片过小,会频繁地执行进程调度和进程上下文的切换,增加系统的开销。反之,若时间片过长,会使算法退化为FCFS算法。在本次实验中,算法采用后者情形,使每次运行都能在一个时间片内完成计算。其算法流程图如下图3所示:
图3 时间片轮转调度算法流程图
七、代码实现
#include <iostream>
#include <cstdlib>
#include <fstream>
#include <sysinfoapi.h>
#include <conio.h>
#include <synchapi.h>
using namespace std;
//全局变量
int RunPoint; // 运行进程指针,-1时为没有运行进程
int WaitPoint; // 阻塞队列指针,-1时为没有阻塞进程
int ReadyPoint; // 就绪队列指针,-1时为没有就绪进程
long ClockNumber; // 系统时钟
int ProcNumber; // 系统中模拟产生的进程总数
int FinishedProc; // 系统中目前已执行完毕的进程总数
//进程信息结构
struct ProcStruct {
int p_pid; // 进程的标识号
char p_state; // 进程的状态,C--运行 R--就绪 W--组塞 B--后备 F--完成
int p_rserial[10]; // 模拟的进程执行的CPU和I/O时间数据序列,间隔存储,第0项存储随后序列的长度(项数),以便知晓啥时该进程执行结束
int p_pos; // 当前进程运行到的位置,用来记忆执行到序列中的哪项
int p_starttime; // 进程建立时间
int p_endtime; // 进程运行结束时间
int p_cputime; // 当前运行时间段进程剩余的需要运行时间
int p_iotime; // 当前I/O时间段进程剩余的I/O时间
int p_next; // 进程控制块的链接指针
} proc[10];
//函数
void Create_ProcInfo(); // 随机创建进程
void DisData(); // 展示所创建的进程序列
void Scheduler_FF(); // 进程调度函数
void NewReadyProc(); // 判断当前是否有新进程到达
void Cpu_Sched(); // CPU调度模拟, 每次只执行一个CPU时间片p=1
void IO_Sched(); // I/O调度模拟
void Display_ProcInfo(); // 显示系统当前状态
void Read_Process_Info(); // 从磁盘读取最后一次生成的进程信息的文件,执行调度,以重现调度情况
void DisResult(); // 显示运行结果
void NextRunProcess(); // 选择下一个运行的进程
//主函数
int main() {
char ch;
RunPoint = -1; // 运行进程指针,-1时为没有运行进程
WaitPoint = -1; // 阻塞队列指针,-1时为没有阻塞进程
ReadyPoint = -1; // 就绪队列指针,-1时为没有就绪进程
ClockNumber = 0; // 系统时钟
ProcNumber = 0; // 当前系统中的进程总数
while (true) {
printf("***********************************\n");
printf(" 1: 建立进程调度数据序列 \n") ;
printf(" 2: 读进程信息,执行调度算法\n") ;
printf("***********************************\n");
printf( "Enter your choice (1 ~ 2): ");
do {
cin >> ch; // 如果输入信息不正确,继续输入
} while (ch != '1' && ch != '2');
if (ch == '1') {
Create_ProcInfo(); // 选择1
} else if (ch == '2'){
Scheduler_FF(); // 选择2
} else {
break;
}
}
return 0;
}
/* 随机创建进程:
随机生成5~10个进程和对应的1~10个CPU--I/O时间数据序列
CPU和I/O的时间数据值在5~15之间 */
void Create_ProcInfo() {
srand(GetTickCount()); // 初始化随机数队列的"种子"
ProcNumber=((float) rand() / 32767) * 5 + 5; // 随机产生5~10个进程
for(int i = 0; i < ProcNumber; i++) {
// 生成进程的CPU--I/O时间数据序列
proc[i].p_pid = ((float) rand() / 32767) * 1000; // 初始化随机的进程ID号
proc[i].p_state = 'B'; // 初始状态都为后备状态
proc[i].p_rserial[0] = ((float) rand() / 32767) * 9 + 1; // 随机生成时间序列长度1~10
for (int j = 1; j <= proc[i].p_rserial[0]; j++) {
proc[i].p_rserial[j] = ((float) rand() / 32767) * 10 + 5; // 随机生成CPU和I/O时间数据序列5~15
}
proc[i].p_pos = 1;
proc[i].p_starttime = ((float) rand() / 32767) * 19 + 1; // 随机生成开始时间,取值范围1~20ms
proc[i].p_endtime = 0;
proc[i].p_cputime = proc[i].p_rserial[1];
proc[i].p_iotime = proc[i].p_rserial[2];
proc[i].p_next = -1;
}
printf("\n---------------------------\n 建立了%2d 个进程数据序列\n\n", ProcNumber);
DisData(); // 该函数为在屏幕上打印所创建的进程的具体信息,Dis是Display的缩写.
}
/* 展示所创建的进程序列,并将其写入磁盘的Process_Info.txt文件中 */
void DisData() {
ofstream outFile;
outFile.open("Process_Info.txt") ; //创建并打开Process_Info.txt文件
for(int i=0; i<ProcNumber; i++) {
// 写到txt文件中
outFile << "ID=" << proc[i].p_pid
<< "(len=" << proc[i].p_rserial[0]
<< ",start=" << proc[i].p_starttime
<< "):\t";
for (int j = 1; j <= proc[i].p_rserial[0]; j++) {
outFile << "\t" << proc[i].p_rserial[j];
}
outFile << endl;
// 打印到屏幕上
cout << "ID=" << proc[i].p_pid
<< "(len=" << proc[i].p_rserial[0]
<< ",start=" << proc[i].p_starttime
<< "):\t";
for (int j = 1; j <= proc[i].p_rserial[0]; ++j) {
cout << "\t" << proc[i].p_rserial[j];
}
cout << endl;
}
outFile.close(); // 写入txt文件的流被冲刷,保存到磁盘上
}
//调度模拟算法
void Scheduler_FF() {
//用户没创建进程,从磁盘读取上次创建的进程信息,赋值给相应变量
if(ProcNumber == 0) {
Read_Process_Info();
}
ClockNumber = 0; // 时钟开始计时, 开始调度模拟
// 执行算法
while(FinishedProc < ProcNumber) {
ClockNumber++; // 时钟前进1个单位
NewReadyProc(); // 判别新进程是否到达
Cpu_Sched(); // CPU调度
IO_Sched(); // IO调度
Display_ProcInfo(); //显示当前状态
}
DisResult();
}
/* 判断当前是否有新进程到达,有则放入就绪队列 */
void NewReadyProc() {
for (int i = 0; i < ProcNumber; i++) {
if (proc[i].p_starttime == ClockNumber) {
// 进程进入时间达到系统时间,ClockNumber是当前的系统时间
proc[i].p_state = 'R'; // 进程状态修改为就绪
proc[i].p_next = -1; // 该进行即将要挂在队列末尾,它肯定是尾巴,后面没人的,所以先设置next=-1
//当前就绪队列无进程
if (ReadyPoint == -1) {
ReadyPoint = i;
} else {
//就绪队列有进程,放入队列尾
int n = ReadyPoint;
while (proc[n].p_next != -1){
n = proc[n].p_next; //找到原来队伍中的尾巴
}
proc[n].p_next = i; //挂在这个尾巴后面
}
}
}
}
//CPU调度模拟, 每次只执行一个CPU时间片p=1
void Cpu_Sched() {
int n;
// 没有进程在CPU上执行
if (RunPoint == -1) {
NextRunProcess();
return;
}
proc[RunPoint].p_cputime--; // 进程CPU执行时间减少1个时钟单位
if (proc[RunPoint].p_cputime > 0) return; // 还需要CPU时间,下次继续,这次就返回了
//如果不满足以上>0的条件,就意味着=0,就不会自动返回,接着做以下事情
// 进程完成本次CPU后的处理
if (proc[RunPoint].p_rserial[0] == proc[RunPoint].p_pos) {
//进程全部序列执行完成
FinishedProc++;
proc[RunPoint].p_state = 'F';
proc[RunPoint].p_endtime = ClockNumber;
RunPoint = -1; //无进程执行
NextRunProcess(); //找分派程序去,接着做下一个
} else {
//进行IO操作,进入阻塞队列
proc[RunPoint].p_pos++;
proc[RunPoint].p_state ='W';
proc[RunPoint].p_iotime = proc[RunPoint].p_rserial[proc[RunPoint].p_pos];
proc[n].p_next = -1; //标记下,就自己一个进程,没带尾巴一起来;否则,当p_next不为-1时,后面的那一串都是被阻塞者
n = WaitPoint;
if(n == -1) //是阻塞队列第一个I/O进程
WaitPoint = RunPoint;
else {
do {
//放入阻塞队列第尾
if(proc[n].p_next == -1) {
proc[n].p_next = RunPoint;
break;
}
n = proc[n].p_next;
} while(n != -1) ;
}
RunPoint = -1;
NextRunProcess();
}
return;
}
// I/O调度模拟, 与CPU调度操作类似。排在队首的进程得到一个时间片服务
void IO_Sched(){
int n,m;
// 没有等待I/O的进程,直接返回
if (WaitPoint == -1) return;
proc[WaitPoint].p_iotime--; // 进行1个时钟的I/O时间
if (proc[WaitPoint].p_iotime > 0) return; // 还没有完成本次I/O
// 进程完成本次I/O的处理
if (proc[WaitPoint].p_rserial[0] == proc[WaitPoint].p_pos) {
//进程全部任务执行完成
FinishedProc++;
proc[WaitPoint].p_endtime = ClockNumber;
proc[WaitPoint].p_state ='F';
if(proc[WaitPoint].p_next == -1) {
WaitPoint = -1;
return;
} else {
//调度下一个进程进行I/O操作
n = proc[WaitPoint].p_next;
proc[WaitPoint].p_next = -1;
WaitPoint = n;
proc[WaitPoint].p_iotime = proc[WaitPoint].p_rserial[proc[WaitPoint].p_pos] ;
return ;
}
} else {
//进行下次CPU操作,进就绪队列
m = WaitPoint;
WaitPoint = proc[WaitPoint].p_next;
proc[m].p_pos++;
proc[m].p_state ='R'; //进程状态为就绪
proc[m].p_next = -1;
n = ReadyPoint;
if(n == -1) {
//是就绪队列的第一个进程
ReadyPoint = m;
return;
} else {
do {
if(proc[n].p_next == -1) {
proc[n].p_next = m;
break ;
}
n = proc[n].p_next;
} while(n != -1);
}
}
return ;
}
//显示系统当前状态
void Display_ProcInfo(){
printf("\n当前系统模拟%d个进程的运行 时钟:%ld", ProcNumber, ClockNumber);
printf("\n就绪指针 = %d, 运行指针 = %d, 阻塞指针 = %d\n", ReadyPoint, RunPoint, WaitPoint);
printf("\n......... Running Process .........\n");
if (RunPoint != -1) {
// Print 当前运行的进程的信息
printf("No.%d ID:%d(%d,%2d)\t总CPU时间=%2d,剩余CPU时间=%2d\n",
RunPoint, proc[RunPoint].p_pid, proc[RunPoint].p_rserial[0], proc[RunPoint].p_starttime,
proc[RunPoint].p_rserial[proc[RunPoint].p_pos], proc[RunPoint].p_cputime);
} else {
printf(" No Process Running !\n");
}
int n = ReadyPoint;
printf("\n......... Ready Process .........\n");
if (n != -1) {
while (n != -1) {
// 显示就绪进程信息
printf("No.%d ID:%d(%d,%2d),第%2d个/总时间=%2d serial:",
n, proc[n].p_pid, proc[n].p_rserial[0], proc[n].p_starttime,
proc[n].p_pos, proc[n].p_rserial[proc[n].p_pos]);
for (int i = 1; i <= proc[n].p_rserial[0]; ++i) {
printf("%4d", proc[n].p_rserial[i]);
}
printf("\n");
n = proc[n].p_next;
}
} else {
printf(" No Process Ready !\n");
}
n = WaitPoint;
printf("\n......... Waiting Process .........\n");
if (n != -1) {
while (n != -1) {
// 显示阻塞进程信息
printf("No.%d ID:%d(%d,%2d),I/O执行到序列中的第%2d个,总I/O时间=%2d/ 剩余I/O时间=%2d serial:",
n, proc[n].p_pid, proc[n].p_rserial[0], proc[n].p_starttime,
proc[n].p_pos, proc[n].p_rserial[proc[n].p_pos], proc[n].p_iotime);
for (int i = 1; i <= proc[n].p_rserial[0]; ++i) {
printf("%4d", proc[n].p_rserial[i]);
}
printf("\n");
n = proc[n].p_next;
}
} else {
printf(" No Process Waiting !\n");
}
printf("\n=================== 后备进程 ====================\n");
for (int i = 0; i < ProcNumber; i++) {
if (proc[i].p_state == 'B') {
printf("No.%d ID:%d(%d,%2d)", i, proc[i].p_pid, proc[i].p_rserial[0], proc[i].p_starttime);
for (int j = 1; j <= proc[i].p_rserial[0]; ++j) {
printf("%4d", proc[i].p_rserial[j]);
}
printf("\n");
}
}
printf("\n================ 已经完成的进程 =================\n");
for (int i = 0; i < ProcNumber; i++) {
if (proc[i].p_state == 'F') {
printf("No.%d ID:%d(%d,%2d),Endtime=%d serial:",
i, proc[i].p_pid, proc[i].p_rserial[0],
proc[i].p_starttime, proc[i].p_endtime);
for (int j = 1; j <= proc[i].p_rserial[0]; ++j) {
printf("%4d", proc[i].p_rserial[j]);
}
printf("\n");
}
}
printf("\n");
}
//从磁盘读取最后一次生成的进程信息的文件,执行调度,以重现调度情况。
//否则,都是随机生成的,每次都不一样,难以重现当时场景。
void Read_Process_Info() {
ifstream inFile; // 定义打开文件的文件流
char ch;
int i,j,k,tmp;
inFile.open("Process_Info.txt") ; //打开上次写的txt进行信息文件流
i = 0;
while(inFile) {
inFile.get(ch);
for(j = 0; j < 3; j++) inFile.get(ch); //每一行前3个字符都是ID=,扔掉
inFile >> proc[i].p_pid;
for(j = 0; j < 5; j++) inFile.get(ch); //继续读(len=,扔掉5个字符
inFile >> proc[i].p_rserial[0]; //读序列项数
for(j = 0; j < 7; j++) inFile.get(ch); //继续读start=,扔掉7个字符
inFile >> proc[i].p_starttime;
for(j = 0 ; j < 2; j++) inFile.get(ch); //继续读,扔掉2个字符
for(k = 1; k <= proc[i].p_rserial[0]; k++) {
inFile >> tmp;
proc[i].p_rserial[k]=tmp;
}
proc[i].p_state = 'B';
proc[i].p_pos = 1;
proc[i].p_endtime = 0;
proc[i].p_next = -1;
proc[i].p_cputime = proc[i].p_rserial[1];
proc[i].p_iotime = proc[i].p_rserial[2];
i++; //本行结束,一个进程信息读完,序号+1, 准备 next process
}
ProcNumber = i-1; //给ProcNumber赋值,i最后有++,回位下
inFile.close(); //完工后关闭文件
}
// 显示运行结果
void DisResult(){
printf("\n=================== 运行结果 ====================\n");
printf("标识号---时间序列---建立时间---结束时间---周转时间---带权周转时间\n");
for (int i = 0; i < ProcNumber; i++) {
int schedule_time = proc[i].p_endtime - proc[i].p_starttime;
int runtime = 0;
for (int j = 1; j <= proc[i].p_rserial[0]; j += 2) {
runtime += proc[i].p_rserial[j];
}
printf("ID=%d %d %d %d %d %.2f\n", proc[i].p_pid, proc[i].p_rserial[0],
proc[i].p_starttime, proc[i].p_endtime, schedule_time, schedule_time * 1.0 / runtime);
}
printf("\n");
}
void NextRunProcess(){
if (ReadyPoint == -1) {
//就绪队列没有等待的进程,结束所有
RunPoint = -1;
return;
}
proc[ReadyPoint].p_state = 'C';
RunPoint = ReadyPoint;
proc[ReadyPoint].p_cputime = proc[ReadyPoint].p_rserial[proc[ReadyPoint].p_pos] ;
ReadyPoint = proc[ReadyPoint].p_next;
proc[RunPoint].p_next = -1;
}
八、运行结果验证及分析
8.1 运行结果展示
(1)建立进程调度数据序列
(2)读进程信息,执行调度算法
初始状态:
时钟4:
进程4在第4s创建完成,CPU调用进程4开始运行。
时钟8:
进程0在第8s创建完成,进入就绪队列。而此前没有其他进程创建,故在前段时间内进程4一直占用CPU。
第9s:
进程4的CPU段运行完成,但后续仍有I/O请求操作,故进入阻塞队列,等待I/O操作完成。CPU调度就绪队列的队首元素进程0执行。
第16s:
进程1、进程3分别在第15s和第16s创建完成,先后进入就绪队列队尾。
第17s:
进程4的I/O操作执行完毕,从阻塞队列进入到就绪队列队尾。
以此算法继续执行,最终所有进程在第95s运行结束:
(3)周转时间和带权周转时间运算
通过计算各进程结束时间与开始时间的差值,求得各进程的周转时间,再除以其各自对应的进程的CPU时钟数,得到各进程的带权周转时间。
8.2 运行结果验证
(1)将题给进程信息存入txt文件
将实验任务书中的进程信息存入txt文件,如上图所示。因此只需运行程序,对照程序运行结束后的结果与任务书中给出的结果是否一致,即可检验所编写的轮转调度法模拟程序是否正确。
(2)运行程序并对照运行结果
读取txt文件,修改Read_Process_Info()函数,如上图所示。
运行程序,得到运行结果,与任务书中情况对照:
时钟2时:
时钟35时:
时钟88时:
时钟101时:
时钟155时:
对比运行结果和正确答案,两者在各时钟结果完全一致,证明实验程序正确实现了轮转调度算法。
该情况下的各进程的周转时间和带权周转时间结果如下: