操作系统课程设计报告指导书_终结版

上传人:痛*** 文档编号:97063224 上传时间:2022-05-26 格式:DOC 页数:70 大小:1.60MB
返回 下载 相关 举报
操作系统课程设计报告指导书_终结版_第1页
第1页 / 共70页
操作系统课程设计报告指导书_终结版_第2页
第2页 / 共70页
操作系统课程设计报告指导书_终结版_第3页
第3页 / 共70页
点击查看更多>>
资源描述
.操作系统课程设计指导书万彬 伟华 梁红兵 2021年9月 计算机学院 目录第一章操作系统课程设计的容与实施方法- 3 -1.1 操作系统课程设计总体要求- 3 -1.2 操作系统课程设计的容- 3 -1.3 操作系统课程设计实施方案- 4 -第二章基于DOS的多任务系统的实现- 5 -2.1 设计目的和容要求- 5 -2.2线程描述- 6 -2.3线程的创立和撤消- 8 -2.4线程调度设计- 11 -2.5根本实例程序的实现- 25 -2.6线程的阻塞和唤醒- 28 -2.7 线程的同步与互斥- 29 -2.8 利用消息缓冲队列通信机制实现线程间通信- 30 -第三章简单文件系统的实现- 35 -3.1 设计目的和容要求- 35 -3.2 预备知识- 36 -3.3 实例系统的设计与实现- 39 -第四章操作系统课程设计平台使用说明- 51 -4.1 系统软硬件配置要求- 51 -4.2 课程设计平台安装- 52 -4.3 课程设计平台操作说明- 62 - 操作系统课程设计第一章 操作系统课程设计的容与实施方法1.1操作系统课程设计总体要求遵守机房纪律,服从机房调度。课程设计的设计和上机调试要求独立完成,不能拷贝。上机前,努力准备上机容,并预先作一些情况分析。仔细观察上机时出现的各种现象,记录上机的结果。认真书写课程设计报告。报告中应包括:课程设计的目的及要求、程序的设计思想及流程图、程序调试中遇到的问题及分析、程序代码清单和结果分析;程序的缺乏之处及修改方案等。程序要带注释。1.2 操作系统课程设计的容本次课程设计共设置了以下两个题目:1基于DOS的多任务系统的实现DOS系统是一个典型的单用户单任务操作系统。“基于DOS的多任务系统的实现的根本设计思想是设计一个运行在DOS系统中的应用程序,该应用程序能实现多线程机制,即能完成所有与线程管理有关的工作,包括线程创立与撤销、线程阻塞与唤醒、线程互斥与同步、线程调度、线程通信等。我们利用这些功能创立多个线程,并调度这些线程在CPU上并发执行,每个线程执行一个函数完成指定的功能。2简单文件系统的实现文件系统是操作系统核中非常重要的组成局部之一。一个相对完整的文件系统应该具备以下几个方面的功能:磁盘存储空间管理、目录管理、文件读写管理、文件保护与共享。由于对磁盘的存取操作必然涉及到磁盘驱动程序设计,为了降低设计难度,本实验的根本设计思想是在存中申请一块存储空间作为虚拟磁盘,在其上建立一个类似于FAT的文件系统,所有对文件系统的操作都是在该虚拟磁盘空间中进展。为了保存该文件系统中的容,如我们创立的目录、文件等,在退出文件系统的使用之前必须将整个虚拟磁盘上的容以一个文件的形式全部保存到系统真正的磁盘上;以后想再次使用该文件系统时又必须首先从磁盘上读入这个文件的容到存中的虚拟磁盘上,然后才能继续使用。1.3 操作系统课程设计实施方案操作系统是计算机系统中最核心最重要的一组软件集合,用来控制系统中的所有硬件及其他软件的运行,各程序模块部的控制流程及相互间的接口都很复杂。本课程设计虽然只是实现其中的一局部功能,但对学生的综合要求依然较高,既要求对原理知识的综合掌握,又要求具有一定的C语言编程能力,特别是“基于DOS的多任务系统的实现这个题目,由于要利用TurboC的interrupt类型的函数来实现线程切换过程中的线程运行现场及环境信息的自动保存及恢复,因此程序开发工具是采用字符型界面的Turbo C。而不同学生在编程能力上存在差异,且大多数学生对字符型界面的开发平台存在畏惧心理,为了到达因材施教的目的,保证每个学生都能根据自己的实际情况参与到课程设计过程中,我们开发设计了一个可视化的操作系统课程设计平台软件该平台软件的使用方法见后面第三篇容,该软件系统最大的特点是提供了模块式替换功能,即将每个课程设计题目的容分解成假设干个相对“较小的功能模块模块具体划分情况见后面课程设计的详细介绍,允许每个学生根据自身能力情况选择实现课程设计的全部或局部功能模块,学生完成一个或多个模块后可在软件系统中进展模块替换操作,替换后需要重新进展编译、工作,然后就可以运行程序,从而及时看到所编写模块的功能实现情况。这样能够提高所有学生主动学习的兴趣,提高实际动手能力。第二章基于DOS的多任务系统的实现2.1 设计目的和容要求1设计目的通过对线程和进程的创立和撤消、CPU的调度、同步机制、通信机制的实现,到达以下目的:(1) 加深对线程和进程概念的理解,明确进程和程序的区别。(2) 加深对CPU调度过程现场保护、CPU的分派和现场恢复的理解。(3) 进一步认识并发执行的概念,明确顺序执行和并发执行的区别。(4) 加深对临界资源、临界区、信号量以及同步机制的理解。(5) 加深对消息缓冲通信的理解。2容要求1 用C语言完成线程的创立和撤消,并按先来先效劳方式对多个线程进展调度。2 将线程调度算法修改为时间片轮转算法,实现时间片轮转调度。也可以结合优先权,实现优先权加时间片轮转算法的线程调度。3 改变时间片的大小,观察结果的变化。思考:为什么时间片不能太小或太大。4 假设两个线程共用同一软件资源如*一变量,或*一数据构造,请用记录型信号量来实现对它的互斥。5 假设有两个线程共享一个可存放个整数的缓冲,其中一个线程不停地计算至的平方,并将结果放入缓冲中,另一个线程不断地从缓冲中取出结果,并将它们打印出来,请用记录型信号量实现这一生产者和消费者的同步问题。6 实现消息缓冲通信,并与4、5中的简单通信进展比较。7 思考:在线程间进展消息缓冲通信时,假设对消息队列的没有满足互斥要求,情况将会怎样.3.学时安排共21学时(1) 授课3学时,容包括线程的创立、撤消、调度等容。(2) 线程的创立、撤消、先来先效劳调度,8学时上机。(3) 时间片轮转调度,3学时上机。(4) 信号量的实现,3学时上机。(5) 线程间的消息缓冲队列通信,4学时上机。4 开发平台 TurboC 1.0、2.0或3.0。2.2 线程描述2.2.1线程根本概念在一些多任务的环境下,用户可以同时运行多个完整的程序。例如,在UNI*环境下,你可以用命令编译一个程序,并把它作为一个后台进程运行只需在命令行后加上字符;在前台,你又可以做其他的事情,比方,编辑另一个文件。我们把这种系统称为基于进程的多任务系统。另外有一种多任务系统,在其下,一个程序的多个局部可同时运行,我们把这种环境下的任务,即程序的每个局部叫做线程,称这种系统为基于线程的多任务系统。在这种环境下,处理机的调度单位为线程,它们共享整个进程的资源,还拥有一些自己的私有资源。我们将通过本课程设计实现多个线程的并发执行。线程,有时也叫做轻进程lightweight process,是CPU调度的根本单位,每个线程有自己的一个指令计数器、一组存放器和一个私有堆栈。而代码段、数据段以及操作系统的其它资源如翻开的文件是由一组线程共享的,这一组线程组成一个Task传统的进程,即heavyweight process相当于只有一个线程的Task。在许多方面,对线程的操作类似于进程:线程可处于就绪、阻塞、执行三种状态之一;线程可共享CPU,在单机系统中,任何时刻最多只能有一个线程处于执行状态;一个Task中的多个线程可并发执行。但与进程不同,一个Task中的多个线程并不互相独立,因为,所有线程均可所属Task的地址空间的任一单元,所以,一个线程读写其它线程的私有堆栈是十分容易的,即系统不提供线程间的保护。注:上面讲的Task是指一个完整的作业,其中可包括多个线程,与本课程设计中所讲的多任务中的任务系统中可并发执行的局部,如线程或进程含义不同,除此之外,本课程设计中所提到的Task或任务均代表后者。线程的切换只需切换存放器组的值,而不需做有关存管理方面的工作,实现起来也就比较简单。线程控制块与进程类似,基于线程的多任务系统中的任务,即线程,它不单是指静态的、可并发执行的程序段本身,其实也是一个动态的概念,是指可并发执行的程序段及其执行过程。因此,我们要用一个类似于进程控制块PCB的数据构造线程控制块TCB,来记录有关描述线程情况和控制线程运行所需的全部信息,具体来说,在一个TCB中主要应包括以下几方面的信息:1有关线程私有堆栈的信息在线程调度的过程中,为了保护线程的现场信息,每个线程都必须有自己的私有堆栈。我们把被切换线程的现场信息,包括目前各存放器的值和下一条指令的地址都保存在它的堆栈中,再从新线程的私有堆栈中恢复出一组新值来布置系统的存放器,并从私有堆栈中得到新线程的下一条指令地址。另外,每个线程中用到的局部变量也是存放在它自己的私有堆栈中的。因此,在TCB 中必须有线程的私有堆栈的信息,包括它在存的起始地址、堆栈的栈顶指针的段地址和偏移等信息。DOS中存的地址是20位的,而且DOS的存管理采用分段的方式,每个段的基址的低4位必须为0,指令和数据的逻辑地址可用两个16位的整数来描述,即:段地址seg和段偏移off,其中段地址seg中有段基址的高16位,故逻辑地址seg:off对应的物理地址为seg24+off。C语言经常用指针来描述一个地址,Turbo C提供了三个宏函数用来实现指针方式到段地址、偏移地址方式的相互转换:假设P为一个指针,则可通过FP_SEG(p)得到该地址的段地址,FP_OFF(p)得到该地址的段偏移;假设seg为一个地址的段地址,off为其段偏移,则可通过MK_FP(seg,off)得到对应的指针。2有关线程的状态的信息在基于线程的多任务系统中,一个线程的状态在它的生命周期中是在不断地变化的,在此,我们把线程的主要状态划分为:就绪、执行、阻塞和终止态。如果,一个线程拥有CPU,我们就说它处于执行态;如果它现在虽不在执行,但一旦获得CPU 就可执行,我们就说它处于就绪态;如果它在等待CPU以外的其他资源,则说它处于阻塞状态;如果线程所对应的程序段已运行完毕,则它处于终止状态。因此,在TCB中要设置一状态字段,用来记录各线程的现行状态。3线程的标识符线程标识符用于惟一地标识一个线程,与进程一样,通常一个线程有两个标识符:1外部标识符:它由创立者提供,通常是一个由字母、数字组成的字符串,记录在线程的TCB中。2部标识符,它通常是一个整数,由多任务系统在创立线程时设置。在本课程设计中,我们在多任务系统的初始化过程中,设置了一个struct类型的TCB数组来统一为各新建线程提供空白TCB,为了简单起见,我们可以隐含地用各线程所分配到的TCB在整个TCB数组中的下标来表示该线程的部标识符,所以不需要再专门记录在TCB中了。4其它信息 TCB中记录的信息量可随系统的复杂情况而变化,如当采用优先权算法进展调度时,在TCB中还必须设置优先权字段;当TCB要按*种方式排队时,在其中必须设置一指针字段;当必须唤醒因*种原因而阻塞的相关线程时,则必须设置阻塞原因字段;在使用消息缓冲队列机制实现线程通信时,则必须设置通信机制需要的字段,如接收线程的消息队列队首指针、消息队列的互斥信号量和资源信号量等。用C语言来描述,一个最简单的TCB的数据构造可以表示如下: /* 状态码常量定义 */ /* null0not assigned */ *define FINISHED 0 /*表示线程处于终止态或TCB是空白状态*/ *define RUNNING 1 /*表示线程处于运行态*/ *define READY 2 /*表示线程处于就绪态*/ *define BLOCKED 3 /*表示线程处于阻塞态*/ struct TCB unsigned char *stack; /* 线程堆栈的起始地址 */ unsigned ss; /* 堆栈段址 */ unsigned sp; /* 堆栈指针 */ char state; /* 线程状态,取值可以是FINISHED、RUNNING、READY、BLOCKED*/ char name10; /* 线程的外部标识符 */ tcbNTCB; /*NTCB是系统允许的最多任务数*/2.3 线程的创立和撤消线程的创立在创立一个新线程时,线程的创立者必需提供一些信息,如线程的外部标识符、线程所需的私有堆栈空间的大小、与线程所对应的程序段的入口地址的有关信息这里假设一个线程执行程序里的一个函数,所以创立者只需提供线程要执行的函数的函数名即可。1线程创立函数格式说明1函数申明原型:typedef int (far *codeptr)(void); /*定义了一个函数指针类型*/Int create(char *name,codeptr code,int stck) ;2函数功能描述:在main()函数中调用,创立一个新线程,让其执行code开场的代码。3输入:name:新创立线程的外部标识符;code:新创立线程要执行的代码的入口地址,此处用函数名作为传入地址;stck:新创立线程的私有堆栈的长度。 4输出:新创立线程的部标识符,假设创立失败,返回-1。2 函数实现的算法描述在创立一个线程时主要应完成以下工作:(1) 为新线程分配一个空闲的线程控制块TCB,该TCB 的数组下标即为新线程的部标识符。如果没有空闲的TCB,则返回-1,创立失败。(2) 为新线程的私有堆栈分配存空间因为同一进程的多个线程共享该进程的程序段和数据段空间,所以创立线程时不必象创立进程那样再为程序段和数据段分配存空间。(3) 初始化新线程的私有堆栈,即按CPU 调度时现场信息的保存格式布置堆栈,这一点是非常重要的,因为当CPU首次调度该线程运行时,CPU中的SS存放器和SP存放器将指向该线程的私有堆栈,并从该堆栈中获得线程运行的正确的指令地址和其它现场信息。新线程的首次执行是从对应函数的入口开场的;而且,执行时CPU的存放器ES、DS应置上恰当的值;Flags 存放器的允许中断位也应置上1,这样,线程执行过程中才允许硬中断如时钟中断发生并及时响应中断;其它存放器A*、B*、C*、D*、SI、DI、BP的值只在线程执行过程中才有意义,它们的初值可为任意值。初始化工作完成后堆栈中各信息项的值及其相应位置如图2-1b所示。为了方便堆栈的初始化工作,我们可以按照堆栈中的容设计一个以下的数据构造: struct int_regs unsigned bp,di,si,ds,es,d*,c*,b*,a*,ip,cs,flags,off,seg; ;然后用一个指向该数据构造的指针给堆栈赋值。(4) 初始化线程控制块,即填入线程的外部标识符,设置好线程私有堆栈的始址、段址和栈顶指针,将线程的状态置成就绪态READY,如图2-1a所示。 另外,如果线程调度算法是按优先权方式进展CPU调度,则需在TCB中置上新线程的优先权信息初始优先数可由用户提供;假设TCB的组织方式是按*种方式拉链,系统设置了线程就绪队列,则还需将新线程的TCB插入就绪队列;如果要实现通信,还需要将线程的消息队列队首指针设置为Null、消息队列的互斥信号量和资源信号量分别设置为1,Null和0,Null(5) 最后,返回新线程的部标识符。在Turbo C的small编译模式下,调用create(f1,(codeptr)f1,1024) 创立一个对应于函数f1()的线程后新线程的存映象如图2-1所示。图2-1对应函数f1()的新线程的存映像线程的撤消引起线程撤销的原因主要有两个:一是系统或用户要求撤销*个线程;二是当前线程所对应的函数已经运行完成。对于第一种情况比较简单,只需调用线程撤销原语将指定线程撤销即可;对于第二钟情况,首先必须自动调用线程撤销原语撤销当前已经运行完成的线程,然后还需要自动地重新进展CPU调度。1 线程撤销函数设计:1函数申明原型:void destroyint id;2功能:撤销部标识符为id的指定线程。3输入:id:将要被撤销的线程的部标识符。4输出:无5函数实现的算法描述:撤销线程所要完成的工作比较简单,主要是将线程所占据的资源归还给系统。在操作系统原理中已经介绍了线程本身根本不占据资源,它与同进程的其他线程共享该进程的代码段和数据段空间;但是线程作为一个可以独立调度和运行的根本单元也拥有一些必不可少的资源,如线程控制块TCB和私有堆栈。所以撤销线程所要做的事情主要就是两个:1将线程的私有堆栈所占的存空间归还给系统;2将线程控制块TCB的各成员变量进展初始化操作。2 撤销线程并重新进展调度前面提到如果是因为当前线程运行完成而引起线程撤消,则系统应能自动撤消该线程,并重新进展CPU调度。我们可以设置一个称为over的函数来完成这个工作,该函数需要顺序做两件事情:首先调用destroy撤销当前线程,然后重新进展CPU调度。所以现在关键的问题是在当前线程运行完成后CPU应能自动转去执行over,这可通过在创立线程时进展一些相关的处理来实现:在进展堆栈初始化时可预先将over() 的入口地址压入线程的私有堆栈中,如前面图2-3b所示;这样,当线程所对应的函数正常完毕时,over()函数的入口地址将作为函数的返回地址被弹出至CPU的CS、IP存放器,从而使CPU的控制权自动转向over()去执行。2.4 线程调度设计2.4.1 CPU调度中的关键问题CPU调度所要做的事情是保护旧线程的现场、找到新线程、恢复新线程的现场、并把处理机交给新线程让它执行。其中,找一新线程是比较容易实现的,只需按*种线程调度算法从所有处于就绪状态的线程中选择一个即可;剩余的问题旧线程的现场保护和新线程的现场恢复、CPU 控制权的转移才是CPU调度的关键,它们是通过堆栈的切换来实现的。在介绍堆栈切换的容之前,我们先来看看函数调用和进展中断处理时控制转移的情况。1函数调用时的控制转移情况在执行函数调用指令时,系统会自动地先将主调函数的下一条指令的地址在CS:IP中压入堆栈,然后把被调函数的入口地址装入CS和IP 存放器段函数调用只需压入和装配IP,控制就从主调函数转向被调函数;当执行函数返回指令时,系统将当前堆栈的栈顶的两个字主调函数下一条指令的地址弹出并送到IP和CS中段函数返回只需弹出一个字送到IP中,控制就从被调函数返回到主调函数。例如,我们编写了一个main()函数和一个f1()函数,在main()中调用f1()。程序的设计及调用返回关系如图2-2所示: 图2-2 函数调用及返回图在执行f1()函数的调用指令前的当前堆栈的情况如图2-3a所示。在执行函数调用指令时,系统首先将main中函数调用语句的下一条指令即“i=1;的地址在CS:IP中,这里用“返址1表示压入堆栈,此时堆栈容如图2-3b所示。然后将f1()函数的入口地址装入CS和IP 存放器,控制就从main()转向f1()。当执行f1()的最后一条指令“return函数返回指令时,系统将前面保存在堆栈中的返址1的偏移和返址1的段址弹出并送到IP和CS中,则控制就从f1()返回到main()了。 图2-3 函数调用前后堆栈容的变化2中断处理时的控制转移情况除了函数调用以外,中断也能实现控制的转移。当中断发生时,系统首先将标志存放器Flags的值压入堆栈,然后将装有被中断程序下一条指令地址的CS和IP存放器的容也分别压入堆栈,再从中断向量中获取中断效劳程序的入口地址并将它们装入CS和IP存放器,这样,控制就从被中断的程序转向中断效劳程序。中断返回时,系统自动从栈顶弹出返址1的偏移、返址1的段址和Flags并送到IP、CS和Flags存放器中,CPU就开场继续从断点处执行被中断程序。中断处理前后堆栈容的变化情况如图2-4所示:图2-4 中断处理前后堆栈容的变化3Interrupt类型函数的特殊作用在Turbo C中提供了一个特殊的函数类型说明符interrupt,我们可利用它将一个函数申明为中断处理函数。例如我们写了一个文件名为“aaa.c的C程序,其容如下:void interrupt fun(void);int i;main() i=0; fun(); i=1;void interrupt fun(void) i=2;用编译命令“tcc -S aaa得到以上C程序的汇编码: ifndef?version ?debugmacro endm endif ?debugS aaa.c _TE*Tsegmentbyte public CODE DGROUPgroup_DATA,_BSS assumecs:_TE*T,ds:DGROUP,ss:DGROUP _TE*Tends _DATAsegment word public DATA dlabelbyte dwlabelword _DATAends _BSSsegment word public BSS blabelbyte bwlabelword ?debugC E93B4F151F056161612E63 _BSSends _TE*Tsegmentbyte public CODE ;?debugL 3 _mainprocnear ;?debugL 4 movword ptr DGROUP:_i,0 ;?debugL 5 pushf callfar ptr _fun ;?debugL 6 movword ptr DGROUP:_i,1 1: ;?debugL 7 ret _mainendp ;?debugL 8 _funprocfar pusha*pushb* pushc* pushd* pushes pushds pushsi pushdi pushbp movbp,DGROUP movds,bp ;?debugL 10 movword ptr DGROUP:_i,2 2: ;?debugL 11 popbp popdipopsi popds popespopd* popc* popb* popa* iret _funendp _TE*Tends _BSSsegment word public BSS _ilabelword db2 dup (?) _BSSends ?debugC E9 _DATAsegment word public DATA slabelbyte _DATAends _TE*Tsegmentbyte public CODE _TE*Tends public_main public_fun public_i end从编译后的代码中可以看出,对于fun()函数,由于定义的类型是interrupt类型的中断处理函数,所以在使用tcc命令进展编译时,编译器将自动在fun()的开场参加一组push操作代码中第二组黑体字局部,以保存被中断程序的CPU现场环境信息;相对应地在fun()的代码最后自动参加一组pop操作代码中第三组黑体字局部,以便中断返回时恢复被中断程序的现场环境信息。从编译后的代码段中还可以看出,main()对fun()的调用是通过: pushf callfar ptr _fun /*代码中的第一组黑体字局部 */实现的,即先压入Flags存放器的容,再用call指令压入返回地址,并转去执行fun()函数。而在进入fun()时,系统首先将执行一组push操作,将A*、B*、C*、D*、ES、DS、SI、DI、BP 的值保存到堆栈中,再用fun()的数据段装配DS存放器,然后才执行fun()中的具体语句“i=2;。而在返回前,先要执行一组pop操作,从堆栈中恢复BP、DI、SI、DS、ES、D*、C*、B*、A* 的值,然后再用iret指令而不是ret指令返回,iret指令将从堆栈中弹出返址的偏移、返址的段址、flags到CPU的IP 、CS和Flags存放器中,从而使CPU继续从断点处执行被中断程序main()。fun()调用前后的堆栈容如图2-5所示,图中的“返址1是指main()函数中fun()函数调用指令的下一条指令“i=1的地址。 图2-5 fun调用前后的堆栈容的变化4 利用堆栈切换实现CPU切换从上面的描述可知,我们可以用函数或中断效劳子程序来处理CPU的切换,但关键仍在于堆栈的切换。例如,系统中有两个线程:线程和线程,它们的私有堆栈分别为 stack1 和stack2;线程目前拥有CPU,即线程正在执行,则系统的现行堆栈为线程1的私有堆栈stack1;在线程中调用一函数F(),函数的返回地址即线程的下一条指令的地址将压入到现行堆栈stack1中;假设在 F() 中,将系统的现行堆栈从stack1切换到stack2:保存现行堆栈的段址SS和栈顶指针SP到变量ss1、sp1中,并将线程的堆栈stack2的段址和栈顶指针装到CPU的SS与SP存放器中;如果stack2的栈顶有线程的下一条指令的地址,则从F()中返回时,将用现行堆栈stack2 的栈顶的字装配IP和CS,CPU就开场执行新线程,即线程。假设再用保存在变量ss1、sp1 中的容来装配SS和SP存放器,即将现行堆栈切换回线程的私有堆栈,则CPU 将被分配给线程,它将从原来的断点继续往下执行。考虑到CPU 切换时要进展现场保护,用中断效劳子程序来实现CPU的切换就显得更方便。下面我们用interrupt类型的函数swtch()来实现CPU在线程1和线程2间的切换,另外,必须注意的是,在堆栈切换过程中一定要做到操作的原子性,这一点我们可以通过关中断、开中断来到达。Swtch()的设计如下: void interrupt swtch(void) disable(); /*关中断*/ /* 保存现行堆栈的段址和栈顶指针供下次切换时用 */ ss1=_SS;/* ss1保存线程1的堆栈段址 */ sp1=_SP;/* sp1保存线程1的堆栈栈顶指针 */ /* 切换堆栈 */ _SS=ss2;/* ss2是线程2的堆栈段址 */ _SP=sp2;/* ss2是线程2的堆栈的栈顶指针 */ enable(); /*开中断*/ 上面代码中用到的_SS,_SP是Turbo C提供的伪变量。所谓的伪变量是一个和给定存放器相一致的简单的标识符,通过它们,我们可以在C 语言程序中直接相应的存放器。表2-1中给出了Turbo C可用的伪变量的完整列表、它们的类型、相应的存放器以及那些存放器通常的用处。表2-1 Turbo C 伪变量表伪变量类型存放器通常用处_A*无符号整型A*通用累加器_AL无符号字符型ALA*的低字节_AH无符号字符型AHA*的高字节_B*无符号整型B*通用变址器_BL无符号字符型BLB*的低字节_BH无符号字符型BHB*的高字节_C*无符号整型C*通用计数和循环_CL无符号字符型CLC*的低字节_CH无符号字符型CHC*的高字节_D*无符号整型D*通用存放数据_DH无符号字符型DHD*的高字节_CS无符号整型CS代码段地址_DS无符号整型DS数据段地址_SS无符号整型SS堆栈段地址_ES无符号整型ES附加段地址_SP无符号整型SP堆栈栈顶指针对SS的偏移_BP无符号整型BP基址指针_DI无符号整型DI用于存放器变量_SI无符号整型SI用于存放器变量由于swtch()是interrupt类型的函数,因此编译后的伪代码如图2-6b所示: 图2-6堆栈切换过程中CPU控制的转移情况假设线程1对应的程序段如图2-6a所示,线程2对应的程序段如图2-6c所示。又假设现在是线程1正在CPU上运行,则当前堆栈是线程1的堆栈stack1,其容如图2-7a所示。线程2目前处于就绪状态,其堆栈stack2的容如图2-8a所示。图2-7 CPU切换过程中线程1堆栈stack1的变化情况 图2-8 CPU切换过程中线程2堆栈stack2的变化情况下面我们来分析CPU从线程1切换到线程2的过程中控制的转移情况以及堆栈的变化情况。1线程1执行swtch()函数调用指令,系统首先将Flags、地址1的段址、地址1的偏移压栈,此时stack1的容如图2-7b所示。2然后CPU转去执行swtch(),首先执行一组push操作,由于此时的当前堆栈仍然是线程1的stack1,因此push操作执行完毕后stack1的容如图2-7c所示。3接下来swtch()进展堆栈切换:首先保存线程1的堆栈stack1的段址和栈顶指针到变量ss1和sp1中;然后将线程2的堆栈stack2的段址和栈顶指针装配到CPU的SS好SP存放器中,则从现在开场,系统的当前堆栈已经变成了stack2。4接着swtch()执行一组pop操作,将stack2中从BP开场到A*完毕的所有容弹出并装入CPU的相应存放器中,此时stack2的容如图2-8b所示。4最后swtch()执行中断返回指令“iret,从stack2中弹出线程2的偏移、线程2的段址和Flags并送到CPU的IP、CS和FLAGS存放器中,此时stack2的容如图2-8c所示。5CPU继续执行CS:IP存放器所指向的指令,即线程2的地址2这个位置的指令。于是CPU的控制权从线程1切换到线程2。DOS的不可重入性 在本课程设计题目中,要求采用基于优先权的时间片轮转调度算法,即当前线程时间片到时应重新进展CPU调度。则是不是线程的时间片到了就立即要重新进展调度呢.要答复这个问题,必须先理解“DOS的不可重入性的概念。在执行DOS的系统功能调用INT 21H时,DOS核先将A*、B*、C*、D*、SI、DI、BP、DS、ES九个存放器的容依次压入到用户堆栈中,并将用户堆栈当前双字指针SS:SP保存到DOS核的一个双字单元里,就不再使用用户堆栈,此时使用的是新建立的系统部堆栈,系统功能调用完毕后再舍弃系统部堆栈而恢复用户堆栈的使用。 DOS的系统部堆栈有三个,它们位于DOS操作系统空间的特定位置。当系统功能调用号为01-0CH、50H、51H之一,而且目前已有严重错误即INT 24H的执行标志为1时,使用第一个系统部堆栈也叫辅助堆栈;当功能调用号不属01-0CH、50H、51H之一,则使用第二个系统部堆栈也叫I/O堆栈;当功能调用号为01-0CH、50H、51H之一,但目前无严重错误发生时,使用第三个系统部堆栈也叫磁盘堆栈。如果在程序执行DOS系统功能调用的过程中,CPU被调度去执行程序,而后者又要执行一系统功能调用,而且这两个系统功能调用使用的是同一个系统部堆栈如磁盘堆栈,则程序的入栈数据将会把保存在该系统部堆栈中的程序的入栈数据覆盖掉,以后就无法再接着执行程序。由此可见,以上的这种情况原则上仅允许对DOS的一次性重入,而且要求先后两次功能调用使用不同的系统部堆栈,其它的对DOS 的重入都将导致错误。这就是DOS的不可重入性。从另一方面考虑,不仅DOS在上述情况下会出现问题,当硬盘磁头响应INT 13H的调用,正处在读写磁盘的过程中时,进展CPU的调度,而新的进程把磁头移到其它什么地方,这种重入问题虽对堆栈和可重复代码没什么影响,但将导致硬盘数据的混乱。假设按时间片进展CPU调度,时间片到时时,老线程很有可能是处于DOS的系统功能调用中,而调度后的新线程或调度程序本身又不可能完全不做系统功能调用。则,如何来解决这一问题呢.有两种可考虑的方法:(1) 如果发现INT 21H正在执行中,就推迟对处理机的切换。(2) 在进展CPU调度时,设法为老线程保存DOS的所有部信息包括三个部堆栈,并为新线程恢复DOS的所有部信息请参考有关“DOS的可对换数据区SDA的书。相比较而言,第一种方法更加简单。该方法的关键问题是如何确定DOS是否处于忙状态所谓DOS处于忙状态是指此时INT 21H正在执行中。在DOS的部有一字节,当系统没进入INT 21H时,该字节的值为0;当系统进入INT 21H时,DOS将该字节的值置成非0,而在退出INT 21H时,DOS又将它的值清0。这一字节被叫做DOS的平安标志,我们称它为INDOS标志,即当该标志的值为0 时,DOS不处于忙状态,可进展处理机调度;否则,DOS处于忙状态,需将处理机调度工作推迟。使用未公开的34h号系统功能调用,可得到INDOS标志的地址由ES:B*返回。由于此地址是在特定操作环境下的常量,所以,可将得到的INDOS 标志的地址保存起来供判断DOS是否处于忙状态时使用。另外,当DOS出现关键性错误时如用21H号系统功能调用随机读一个记录时发生的读盘错误,DOS的严重错误处理程序将置上严重错误标志DOS空间特定位置的一字节,并去除INDOS标志,再调用严重错误的中断处理程序INT 24H;而在INT 24H执行完后,又将严重错误标志清0,再将INDOS标志置上,然后根据用户输入的Retry、Ignore和Abort决定是重新读盘、还是忽略错误完毕系统功能调用、还是完毕整个应用程序。即在严重错误标志被置上时,虽然INDOS标志为0,但INT 21H可能仍没完毕,所以,此时也应推迟对处理机的调度。在MS-DOS 2.* 版本中,严重错误标志的地址在INDOS标志的后一个字节处,在MS-DOS 3.*中,此标志的地址位于INDOS标志的前一个字节处,而在MS-DOS 3.10及更高版本中,可通过5D06H 号系统功能调用获得此标志的地址。下面的C程序说明如何取得INDOS标志和严重错误标志的地址,并根据这两个标志判断DOS是否处于忙状态。 /* INDOS.C - Functions to manage DOS flags */ *include *include *define GET_INDOS 0*34 *define GET_CRIT_ERR 0*5d06 char far *indos_ptr=0; /*该指针变量存放INDOS标志的地址*/ char far *crit_err_ptr=0; /*该指针变量存放严重错误标志的地址*/ void InitDos(void); int DosBusy(void);/* InitDos()函数:功能是获得INDOS标志的地址和严重错误标志的地址 */ void InitDos(void) union REGS regs;/共用体数据类型 struct SREGS segregs;/什么意思. /* 获得 INDOS 标志的地址 */ regs.h.ah=GET_INDOS;/* intdos*() :Turbo C的库函数,其功能是调用DOS的INT21H中断*/ intdos*(®s,®s,&segregs);/* MK_FP():不是一个函数,只是一个宏。*/*其功能是做段基址加上偏移地址的运算,也就是取实际地址。 */ indos_ptr=MK_FP(segregs.es,regs.*.b*); /*获得严重错误标志的地址 */*代码中用到的_osmajor、_osminor是Turbo C的全程变量,其中前者为*/*DOS版本号的主要局部,后者为版本号的次要局部。*/ if (_osmajor3) crit_err_ptr=indos_ptr+1; else if (_osmajor=3 & _osminor=0)crit_err_ptr=indos_ptr-1;else regs.*.a*=GET_CRIT_ERR; intdos*(®s,®s,&segregs); crit_err_ptr=MK_FP(segregs.ds,regs.*.si); /* DosBusy():函数功能是获得Indos标志及严重错误标志的值,判断是否dos忙:*/ /* 如果返回值是1,表示dos忙;*/* 如果返回值是0,表示dos不忙;*/* 如果返回值是-1,表示还没有调用InitDos()*/ int DosBusy(void) if (indos_ptr & crit_err_ptr) return(*indos_ptr | *crit_err_ptr); else return(-1); /* InitDos() hasnt been called */ 调度程序的实现1调度算法设计本次课程设计题目要现基于优先权的时间片轮转调度算法,即当前线程时间片到时,应暂停当前线程的运行,重新选择一个优先级最高的就绪线程参加运行。为了简单,在我们给出的实例中没有使用优先权,也没有建立就绪队列,而只是用TCB 的数组下标隐含地把所有的线程排成一个循环队列,并使用简单的时间片轮转调度算法,即当前正在运行的线程时间片到时,就暂停它的执行,把它的状态从执行态变为就绪态,把它的现场信息压入它的私有堆栈,并从隐含队列中找出下一个就绪线程,恢复它的现场,让它执行。只要稍作改动,我们就可使用优先权高者优先的时间片轮转调度算法。为此,我们需在TCB中增加一优先数字段,并将优先数与优先权联系起来如:优先数大者优先权较低,优先数小者优先权较高,也可反之,我们也可使用*种原则修改优先数如:一个线程在就绪队列中等待一个时间片,则将它的优先数减去*个值a,以提高它的优先权而防止长期等待;一个线程执行完一个时间片,则将它的优先数加上*个值b,以降低它的优先权。另外,我们也可在TCB中加一字段,把所有就绪的线程按*种方式排成一显式就绪队列,如优先权从高到低的队列,这样寻找新线程参加运行时只要取出就绪队列的队首线程即可。2调度程序的实现思路引起CPU调度的主要原因有:时间片到时、线程执行完毕或正在执行的线程因等待*事件发生而不能继续执行。根据上述调度原因,在实例中的调度功能是通过两个函数来完成的:1void interrupt my_swtch(void):该函数主要解决两种原因引起的调度:线程执行完毕或正在执行的线程因等待*事件发生而不能继续执行。2void interrupt new_int8(void):该函数主要解决因时间片到时引起的调度,这可通过截取时钟中断int 08来完成。在有的系统中,调度程序只有一个。如在UNI*系统中,时钟中断效劳程序并不直接进展CPU调度,而只是设置需重新调度的标志;每当中断包括时钟中断或捕俘类似于PC 机的软中断完毕时,中断总控程序将检查重新调度标志,假设重新调度标志已置上且被中断的是用户态程序,则系统的中断总控程序将控制转向调度程序来切换CPU。3调度程序的具体设计1void interrupt my_swtch(void)该函数要完成的主要工作包括:首先保存当前线程的运行环境,包括私有堆栈的段址和偏移量;然后在系统的TCB数组中查找状态为就绪态的线程,恢复其运行现场,使其在cpu上参加运行。具体的程序设计流程图如图2-9所示。图中的current和timecount是实例中定义的两个全局变量,其中current记录当前正在运行的现场的部标识符,timecount记录当前线程从上次调度至今已经运行了多少时间,注意timecount的时间单位是一个时钟中断间隔1/18.2 秒,约等于55ms,比方timecount的值为1,则表示当前线程已经运行了约55ms。图2-9 my_swtch()的程序设计流程图2void interrupt new_int8(void)该函数要完成的主要工作包括:首先执行老的时钟中断处理程序的功能;然后判断当前线程的时间片是否到了,如果到了,且DOS不忙,则保存当前线程的运行环境,重新选择一个新的就绪线程,恢复其运行现场,使其在CPU上参加运行。由于该函数必须是自动地定期地运行,所以我们通过截取时钟中断int 08来完成。在PC机上,有一个定时器,每当它发一个时钟信号时一般情况下,它每秒发出18.2个信号,除非是对产生该中断的定时器芯片重新编程,系统就进入时钟中断处理程序INT 8)来完成系统计时、调用INT 1CH、关闭磁盘马达等工作。我们可设计新的时钟中断效劳程序,在其中来完成因时间片到时引起的CPU 调度。设计中要注意:新的时钟中断效劳程序不能太长,否则系统效率将大大下降甚至使系统无常工作;在新的时钟中断效劳程序里必须调用系统原来的INT 08H的功能,否则将影响磁盘马达的关闭和系统的计时,另外,我们还要依赖原来的INT 08H 向中断控制器发中断完毕指令EOI。在为INT 08H设置新的时钟中断效劳程序前,必须提前用Turbo C提供的getvect()函数获取系统原来的INT 08H 的中断效劳程序的入口地址并将它保存起来: void interrupt (*old_int8)(void); /*定义一个函数指针old_int8*/ old_int8=getvect(8);new_int8()具体的程序设计流程图如图2-10所示:图2-10 new_int8()的程序设计流程图为了观察不同的时间片对调度所起的影响,实例中用一符号常量TL来表示时间片的大小,单位与timecount变量一样为时钟中断的间隔1/18.2 秒;对于当前线程来说,在每次时钟中断发生时,timecount加。将timecount的值与TL 比较可判断当前线程的时间片是否到时,从而决定是否要进展CPU调度。current 是实例中的另一全程变量,始终等于正在执行线程的部标识符。最后有几个问题留待同学们思考:1在用my_swtch()进展CPU调度时,要不要判断DOS的状态.为什么.2在时间片调度时,时间片TL为什么不能太大或太小.2.5 根本实例程序的实现根据前面对设计思路的分析,我们可以编写一个较完整的实例:首先由main()函数进展一些初始化工作,然后直接创立0*线程对应于main()函数,再由0*线程调用create()创立1*、2*线程分别对应于函数f1()、f2(),最后,将系统的中断效劳程序设置成new_int8(),并把控制交给1*线程,启动多个线程的并发执行。不过在这个根本的实例程序中,我们没有实现线程的阻塞与唤醒、线程的同步与互斥、线程间的通信等容,请同学们在学习了后面2.6-2.8节的容后自行补充完成。0*线程是一个比较特别的线程,它在创立时没使用create(),而是在系统初始化后直接创立的,因它所对应的程序段为main函数中的一段,所以也直接使用整个系统的堆栈,而不在创立时为私有堆栈分配额外的空间;同样,撤消时也不需释放私有堆栈的空间,所以也没使用destroy(),而是直接撤消的,从这方面看,我们可将它看成是一个系统线程。另一方面,在启动多个线程的并发执行后,0*线程就
展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 管理文书 > 施工组织


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!