操作系统必修实验:多进程并发环境模拟以及低级调度算法的仿真实现
The principle, process and implementation steps of the concurrent environment, process PCB and control operation primitives, process switching and process scheduling algorithms are mastered through program simulation.
通过程序仿真掌握并发环境、进程PCB与控制操作原语、进程切换以及进程调度算法的原理、过程与实现步骤。
根据设计要求,提出了以下的设计工作流程以及代码编写时间安排。
- 研究实验要求,理解每一个要求需要实现的内容及呈现效果。
- 根据实验要求阅读书籍,了解相关知识。
- 设计程序框架,理清各个程序模块本身的属性及所提供的功能,和模块之间的相互调用关系。
- 为每一个模块编写代码,实现各个模块的独立的功能,并完成测试。
- 设计控制模块,依照原先设计的程序结构,将各个模块连接,并实现CPU模式切换、文件读取、进程调度调度等功能。
- 程序调制及bug修复,设计各种不同的情形进行进程调度的测试,与实际计算的值进行比较。
- 进一步优化程序结构,力求在实现同样功能的前提下减少程序的多线程功能对计算机CPU资源及内存资源的占用。
- 撰写设计报告,提交整套材料,等待答辩。
根据设计要求及实际的CPU模型,程序设计并实现了以下模块。通过控制模块对这些模块的操作及调用,可以很好地模拟出CPU的进程调度情况,并实时地显示出各个进程的执行情况。
这里只对这些模块进行简单的介绍,具体的介绍将会在后面的“四、程序结构及核心模块的实现”小节中做具体的介绍。
该模块在代码中体现为一个class类,名称为“TimeClock”,主要负责时钟中断的产生与中断检测的反馈。该模块利用了多线程的设计思想,将10ms的计时与主程序分离开来,能够准确的为主程序提供10ms的计时。
该模块在代码中体现为一个class类,名称为“FileOperation”,主要负责对文件(pcb-inputs.txt文件与Results.txt文件)进行读写操作。为了规避存在多个文件指针导致的文件写入错乱、文件拒绝访问等问题,故程序全局只有一个该模块的实例,要读写的文件的文件指针只存在有一个,以保证文件读取与写入的顺序。
该模块在代码中体现为一个class类,名称为“TaskRequest”,主要负责监测作业的请求及作业的调入操作。模块对需要调入的作业的调入时间进行实时监控,每当时间符合时,即开始调入,为其分配PCB,进入就绪态运行。
该模块在代码中体现为一个class类,名称为“PCB”,主要负责仿真实现一个PCB数据结构中所拥有的全部属性,以及为了访问、修改这些属性所需要的方法。
该模块在代码中体现为一个class类,名称为“ProcessSchedule”,主要负责各个进程的状态检测与进程调度功能。该模块中存在有三个队列,运行队列、就绪队列、等待队列,分别存放三种不同状态的PCB,根据调度算法等待被调用。
该模块在代码中不再写为class类的形式,而是大部分以constexpr数据类型的形式给出,这些数据数值规定了一些程序运行所需要用到的常量。为了方便日后程序结构的重新设计与修改,故不将这些数值直接内嵌到代码中,而是独立的在该模块中给出,也可方便开发人员的二次设计。
该模块在代码中体现为一个class类,名称为“CPU”,主要负责模拟仿真CPU,其中含有各个cpu运行所需要的属性及访问、修改这些属性所需要的方法。CPU模块的存在也是为了能够模拟仿真进程的恢复现场与保护现场的操作。
该模块在代码中体现为一个class类,名称为“Control”,主要负责各个模块之间的协调工作。为了保证程序功能的正常以及读写文件的顺序,故以上所述的所有模块在控制模块中都有唯一的一个实例,控制模块的各个方法只能够操作这些已经声明的实例而不能再另外声明,所有的传递及返回值都以引用的方式给出。
控制模块对其他模块的声明及调度关系如图Control类包含调度所给出。
为了实现设计要求的各种需求,则需要将以上所给出的模块进行详细的规划设计,在各个模块中尽可能多的提供操作该模块的方法。各个模块设计的越详细具体,最后系统所能够实现的功能也就越多。
此处将给出各个模块中的一些属性与方法的介绍,但只给出属性的定义及方法函数的形式,有些函数的实现过于简单,不做更详细的叙述。有些函数涉及到程序的核心功能,该部分请读者跳转至“四、程序结构及核心模块的实现”阅读。
因为模块的功能设计极大部分都体现在类的设计上面,所以此处直接给出该程序中各个类的设计,读者可以根据相对应的注释理解每个属性及方法的功能。
为了仿真真实的进程调度操作,则首先需要仿真一个可用的CPU。
根据上文的介绍,本程序在设计时,将CPU单独抽象成为一个CPU模块,在该模块中,定义有CPU的不同属性与方法,这些方法为CPU的现场保护、现场恢复、中断执行等功能提供了支持。
为了模拟仿真10ms间隔的时钟中断,故设计时钟中断模块,在该模块中,有一个专门的函数,被设计为多线程的一部分,专用来发生10ms的中断。
同时,该模块还提供中断信号的监测函数。即每当发生10ms一次的中断时,发生函数将模块中的if_break变量设置为true,监测函数响应外部的请求,返回if_break的值,当返回后,再将if_break变量初始化,这样的功能分离的设计,可保证10ms间隔的准确。
PCB模块的数据结构已经在上文的“功能设计”中详细给出,即class PCB中的内容。因为考虑到需要保证PCB的稳定性,使得外部操作不能够轻易的改变PCB的内容,故放弃结构体的设计,转而采用class类的设计。
PCB中基本的数据如下:
进程编号(ProID)、进程优先数(Priority)、进程创建时间(InTimes)、进程状态(ProState)、进程运行时间(RunTimes)、进程包含的指令数目( InstrucNum)、编号指令( Instruc_ID )、每条指令的状态标志(Instruc_State,0表示核心态、1表示用户态、2表示PV操作)、单指令运行时间(Instruc_Times)、进程当前状态(PSW)等信息。
请求运行的并发进程个数随机产生5-10以内随机整数,即说明在该程序中,总共最少执行5个进程,最多执行10个进程,这些进程之间一定是并发执行的。
进程创建时间(InTimes):程序运行从0开始计时,生成进程的时间间隔控制在1 分钟以内,需要保证出现进程并发情景。
进程运行时间(RunTime):统计记录进程当前已运行了多少时间,此字段开始时为空,进程运行过程中不断保存和记录。
进程包含的指令数目(InstrucNum):用5-20以内的随机整数产生;
编号指令(Instruc_ID):进程所执行指令序号,根据InstrucNum 数字生成。本试验假设CPU 在执行一条机器指令时必须完成,不可以中断。
每条指令的类型标志(Instruc_State,0 表示系统调用、1 表示用户态计算操作、2 表示PV 操作):用随机数产生0、1、2。
当Instruc_State=0 时,发生系统调用,CPU 进行模式切换,原进程进入阻塞态;
当Instruc_State=2 时,发生系统调用,进程进入阻塞态;
指令运行时间(Instruc_Times):该条指令完成需要的时间,本实验假设CPU 在执行一条机器指令时必须完成,不可以中断。
当Instruc_State=0 或1 时, Instruc_Times=产生[10-40]之间的随机10ms 倍数的整数;
当Instruc_State=2 时, Instruc_Times=50,用户进程发生PV 阻塞请求,并假设完成I/O数据通信时间为50ms,之后可唤醒。
PSW 中保存该进程当前执行的指令编号。
系统PCB表使用数组进行实现,根据题目的要求,整个运行期间的进程个数都是小于等于10的,所以此处只需要使用数组实现即可。
在PCB类中同时提供进程控制原语,即进程创建、进程撤销、进程阻塞、进程唤醒的原语。
程序实现了进程的上下文切换,在基本要求的基础上,添加了阻塞态(等待态)的模式,对于2型(PV操作),可以自动识别并立即将此进程加入阻塞队列等待,50ms后唤醒进入就绪态。0型(系统调用)与1型用户计算)指令运行在就绪态与运行态之间。
0型(系统调用)与2型(PV操作)指令都需要用到系统调用,当CPU运行这些指令时,将会自动识别并将CPU的模式切换为核心态运行。1型(用户计算)指令,要求CPU工作在用户模式,此时CPU将会切换到用户模式运行。
程序利用自己的随机数算法,依据用户输入的总进程数量,严格按照一定的格式生成PCB,并生成一定的序列导入。
程序事先读取所有的PCB序列,并将他们的调入时间进行记载。当程序在运行时,实时监测当前系统时间与某一个PCB的进入时间是否匹配,如果匹配,则立即调入,为期分配可用的PCB,并进入就绪队列进行等待;如果不匹配,则继续等待,直至所有请求的PCB全部被调入才停止。
本程序中很好地实现了Unix的新进程调度算法。由于时间片轮转算法、时间片轮转+优先数调度算法、动态优先级算法的核心思想在Unix调度算法中都有体现,故此程序直接实现了Unix算法,而没有实现前面三种较简单的算法。
在Unix调度算法中,进程占用CPU的时间片的时机由每个进程的优先数来确定,规则如下:
① 进程的优先数计算公式:p-pri=(p-cpu/2)+用户基本优先数
② 用户基本优先数定位60,核心态进程优先数在60以下,而用户态进程至少为60,故前者优先权高于后者。
③ 每到1秒时,系统时钟中断处理程序会根据衰减函数计算p-cpu:decay(p-cpu)=p-cpu/2
④ 一个新进程,未使用处理器,所以,(p-cpu/2)=0,新建进程最有可能被调度。
⑤ 系统时钟中断处理程序每工作一次,将当前运行进程的p-cpu加1,进程的优先数p-pri就会增大,随着进程占用处理器的时间不断地增加,它的优先权便会逐步降低。
⑥ 每到1秒时,系统就依次检查并计算所有进程的p-cpu,当进程的p-cpu增加到一定程度时,在钟中断处理可能被其他的p-cpu较小的进程抢先,将进入到下一个等待调度队列的轮转中。
⑦ 每个时钟中断系统,都会对每个进程的p-cpu进行除2操作,对不占用处理器的进程来说p-cpu的值衰减很快,因此其优先数会不断发生变化。当值小于前被调度的进程及其他进程时,它就有机会重新占处理器并被调度执行。
程序会将进程事件请求调度的原始信息、进程执行状态、CPU状态的变化情况、进程调度序列、每个进程的指令执行序列和情况、每个进程获得CPU的调度时间、周转时间、带权周转时间以及CPU利用率等动态信息加标记保存为进程运行记录文件。
程序会将进程请求调度信息、进程调度运行记录信息、CPU寄存器信息等加以解释说明,显示在屏幕上。