1.实验目的
加深对进程概念的理解,明确进程和程序的区别;
深入了解系统如何组织进程、创建进程;
进一步认识如何实现处理器调度。
2.实验预备知识
进程的概念;
进程的组织方式;
进程的创建;
进程的调度。
3.实验内容
编写程序完成单处理机系统中的进程调度,要求采用时间片轮转调度算法。实验具体包括:首先确定进程控制块的内容,进程控制块的组成方式;然后完成进程创建原语和进程调度原语;最后编写主函数对所作工作进程测试。
4.提示与讲解
这个实验主要要考虑三个问题:如何组织进程、如何创建进程和如何实现处理器调度。
考虑如何组织进程,首先就要设定进程控制块的内容。进程控制块PCB记录各个进程执行时的情况。不同的操作系统,进程控制块记录的信息内容不一样。操作系统功能越强,软件也越庞大,进程控制块记录的内容也就越多。这里的实验只使用了必不可少的信息。一般操作系统中,无论进程控制块中信息量多少,信息都可以大致分为以下四类:
① 标识信息
每个进程都要有一个惟一的标识符,用来标识进程的存在和区别于其他进程。这个标识符是必不可少的,可以用符号或编号实现,它必须是操作系统分配的。在后面给出的参考程序中,采用编号方式,也就是为每个进程依次分配一个不相同的正整数。
② 说明信息
用于记录进程的基本情况,例如进程的状态、等待原因、进程程序存放位置、进程数据存放位置等等。实验中,因为进程没有数据和程序,仅使用进程控制块模拟进程,所以这部分内容仅包括进程状态。
③ 现场信息
现场信息记录各个寄存器的内容。当进程由于某种原因让出处理器时,需要将现场信息记录在进程控制块中,当进行进程调度时,从选中进程的进程控制块中读取现场信息进行现场恢复。现场信息就是处理器的相关寄存器内容,包括通用寄存器、程序计数器和程序状态字寄存器等。在实验中,可选取几个寄存器作为代表。用大写的全局变量AX、BX、CX、DX模拟通用寄存器、大写的全局变量PC模拟程序计数器、大写的全局变量PSW模拟程序状态字寄存器。
④ 管理信息
管理信息记录进程管理和调度的信息。例如进程优先数、进程队列指针等。实验中,仅包括队列指针。
因此可将进程控制块结构定义如下:
struct pcb
{int name; //进程标识符
int status; //进程状态
int ax, bx, cx,dx; //进程现场信息,通用寄存器内容
int pc; //进程现场信息,程序计数器内容
int psw; //进程现场信息,程序状态字寄存器内容
int next; //下一个进程控制块的位置
}
确定进程控制块内容后,要考虑的就是如何将进程控制块组织在一起。多道程序设计系统中,往往同时创建多个进程。在单处理器的情况下,每次只能有一个进程处于运行态,其他的进程处于就绪状态或等待状态。为了便于管理,通常把处于相同状态的进程的进程控制块链接在一起。单处理器系统中,正在运行的进程只有一个。因此,单处理器系统中进程控制块分成一个正在运行进程的进程控制块、就绪进程的进程控制块组织成的就绪队列和等待进程的进程控制块组成的等待队列。由于实验模拟的是进程调度,没有对等待队列的操作,所以实验中只有一个指向正在运行进程的进程控制块指针和一个就绪进程的进程控制块队列指针。操作系统的实现中,系统往往在主存中划分出一个连续的专门区域存放系统的进程控制块,实验中应该用数组模拟这个专门的进程控制块区域,定义如下:
#define n 10 //假定系统允许进程个数为10
struct pcb pcbarea[n]; //模拟进程控制块区域的数组
这样,进程控制块的链表实际上是数据结构中使用的静态链表。进程控制块的链接方式可以采用单向和双向链表,实验中,进程控制块队列采用单向不循环静态链表。为了管理空闲进程控制块,还应该将空闲控制块链接成一个队列。
实验中采用时间片轮转调度算法,这种算法是将进程控制块按照进入就绪队列的先后次序排成队列。关于就绪队列的操作就是从队头摘下一个进程控制块和从队尾挂入一个进程控制块。因此为就绪队列定义两个指针,一个头指针,指向就绪队列的第一个进程控制块;一个尾指针,指向就绪队列的最后一个进程控制块。
实验中指向运行进程的进程控制块指针、就绪队列指针和空闲进程控制块队列指针定义如下:
int run; //定义指向正在运行进程的进程控制块的指针
struct
{int front;
int rear;
}ready; //定义指向就绪队列的头指针front和尾指针rear
int pfree; //定义指向空闲进程控制块队列的指针
以上是如何组织进程,下面考虑如何创建进程。
进程创建是一个原语,因此在实验中应该用一个函数实现,进程创建的过程应该包括:
①申请进程控制块:进程控制块的数量是有限的,如果没有空闲进程控制块,则进程不能创建,如果申请成功才可以执行第②步;
②申请资源:除了进程控制块外,还需要有必要的资源才能创建进程,如果申请资源不成功,则不能创建进程,并且归还已申请的进程控制块;如果申请成功,则执行第三步,实验无法申请资源,所以模拟程序忽略了申请资源这一步;
③填写进程控制块:将该进程信息写入进程控制块内,实验中只有进程标识符、进程状态可以填写,每个进程现场信息中的寄存器内容由于没有具体数据而使用进程(模拟进程创建时,需输入进程标识符字,进程标识符本应系统建立,并且是惟一的,输入时注意不要冲突),刚刚创建的进程应该为就绪态,然后转去执行第四步;
④挂入就绪队列:如果原来就绪队列不为空,则将该进程控制块挂入就绪队列尾部,并修改就绪队列尾部指针;如果原来就绪队列为空,则将就绪队列的头指针、尾指针均指向该进程控制块,进程创建完成。
进程创建流程图如图2.2所示。
多道程序设计的系统中,处于就绪态的进程往往是多个,它们都要求占用处理器,可是单处理器系统的处理器只有一个,进程调度就是解决这个处理器竞争问题的。进程调度的任务就是按照某种算法从就绪进程队列中选择一个进程,让它占有处理器。因此进程调度程序就应该包括两部分,一部分是在进程就绪队列中选择一个进程,并将其进程控制块从进程就绪队列中摘下来,另一部分工作就是分配处理器给选中的进程,也就是将指向正在运行进程的进程控制块指针指向该进程的进程控制块,并将该进程的进程控制块信息写入处理器的各个寄存器中。
图2.2 进程创建CreateProcess流程图
实验中采用时间片轮转调度算法。时间片轮转调度算法让就绪进程按就绪的先后次序排成队列,每次总是选择就绪队列中的第一个进程占有处理器,但是规定只能使用一个“时间片”。时间片就是规定进程一次使用处理器的最长时间。实验中采用每个进程都使用相同的不变的时间片。
在时间片轮转调度算法中,当一个进程的时间片用完或者被其他高优先级进程抢占时,该进程会被放入就绪队列的尾部等待下一次调度。如果该进程在时间片用完之前已经执行完毕,那么它将被移出就绪队列。当调度器需要选择下一个要执行的进程时,它会从就绪队列的头部开始,
完成上述功能后,编写主函数进行测试:首先建立一个就绪队列,手工输入信息建立几个进程;然后进行进程调度;最后将指向正在运行进程的指针指向的进程控制块的内容输出,察看结果。
图2.3 进程调度ProcessSchedule流程图
#include <stdio.h>
#include <unistd.h>
#define n 10 //假定系统允许进程个数为10
#define aready 1
#define running 2
#define T 5
struct pcb
{
int name; //进程标识符
int status; //进程状态
int ax,bx,cx,dx; //进程现场信息 通用寄存器内容
int pc; //进程现场信息 程序计数器内容
int psw; //进程现场信息 程序状态字寄存器内容
int next;
float totalTime; //总的运行时间
};
struct pcb pcbarea[n]; //模拟进程控制块区域的数组
int run; //定义指向正在运行进程的进程控制块的指针
struct //指向就绪队列的头指针head和尾指针tail
{
int front;
int rear;
}ready;
int pfree; //指向空闲进程队列的指针
float time;
void CreateProcess();
void RoundRobinSchedule();
void ProcessSchedule();
void AddAlready();
int main()
{
ready.rear=-1,ready.front=-1; //初始化
pfree = 0;
for (int i=0;i<n-1;i++)
pcbarea[i].next=i+1;
pcbarea[n-1].next=-1; //初始化数组里的next
CreateProcess(); //创建进程
RoundRobinSchedule(); //时间片轮转调度算法
}
void CreateProcess()
{
if (pfree==-1)
{
printf("进程创建失败,空间已满");
return;
}
while (pfree!=-1) //没有空一直循环输入创建进程
{
int i = pfree;
pfree = pcbarea[pfree].next;
int name,ax,bx,cx,dx,pc,psw,totaltime;
printf("请输入8个数字 空格间隔\n进程标识符 通用寄存器内容(4个),程序计数器内容,程序状态字寄存器内容,运行总时间 \n");
scanf("%d%d%d%d%d%d%d%d",&name,&ax,&bx,&cx,&dx,&pc,&psw,&totaltime);
pcbarea[i].name = name;
pcbarea[i].status = aready;
pcbarea[i].ax = ax;
pcbarea[i].bx = bx;
pcbarea[i].cx = cx;
pcbarea[i].dx = dx;
pcbarea[i].pc = pc;
pcbarea[i].psw = psw;
pcbarea[i].totalTime = totaltime;
if (ready.front == -1) //如果队列为空
ready.front = i; //头指针指向新创建的
else
pcbarea[ready.rear].next = i; //不为空让队列里最后一个next指向新创建的
ready.rear = i; //就绪队列的尾指针指向新创建的
pcbarea[i].next = -1; //这个新的进程后面为空
}
if (pfree==-1)
printf("进程创建成功,空间已满\n");
}
void RoundRobinSchedule()
{
time = T/(float)(ready.rear-ready.front+1); //时间片与总的进程数量成反比,与一个周期总时间T成正比
printf("\n%f\n",time);
while(ready.front!=-1) //如果就绪队列不为空
{
ProcessSchedule(); //进程调度1个
sleep(time); //处理机处理时间
if(pcbarea[run].totalTime>0) //如果这个进程还没有执行完
AddAlready(); //继续把他加入就绪队列的尾部
}
}
void ProcessSchedule()
{
int i=ready.front; //取就绪队列的第一个
float TIME = time; //把时间片的值传递给处理机
if (pcbarea[ready.front].next==-1) //如果就绪队列只有1个
ready.rear = -1; //把尾部指向空
ready.front = pcbarea[ready.front].next; //把就绪队列的头指针指向下一个 如果只有一个那么为空
pcbarea[i].status = running; //状态改为运行中
pcbarea[i].totalTime = pcbarea[i].totalTime-TIME; //更新总的时间 没运行一次减少一个时间片
int AX=pcbarea[i].ax,BX=pcbarea[i].bx,CX=pcbarea[i].cx,DX=pcbarea[i].dx,
PC=pcbarea[i].pc,PSW=pcbarea[i].psw,REMAIN=pcbarea[i].totalTime; //现场恢复 pcb中的数据传递给处理机
run = i; //运行进程的指针指向这一个
printf("ax:%d bx:%d cx:%d dx:%d pc:%d psw:%d totalTime:%f\n",pcbarea[run].ax,pcbarea[run].bx,pcbarea[run].cx,
pcbarea[run].dx,pcbarea[run].pc,pcbarea[run].psw,pcbarea[run].totalTime);
}
void AddAlready()
{
if (ready.front==-1) //如果队列为空
ready.front=run; //把头指针指向正在运行的
else
pcbarea[ready.rear].next = run; //不为空直接挂在最后一个进程后面
ready.rear = run; //尾指针向后移动
pcbarea[run].next = -1; //把新加入的后面置为空
pcbarea[run].status = aready;
}