一、概述
(一)定义
-
操作系统是指控制和管理整个计算机系统的硬件和软件资源,并合理地组织调度计算机的工作和资源的分配;以提供给用户和其他软件方便的接口和环境;它是计算机系统中最基本的系统软件。
-
操作系统作为系统资源的管理者,提供的功能主要有处理机(CPU)管理,存储器管理、文件管理、设备管理。
-
操作系统的功能和目标是向上层即用户和应用程序提供简单易用的服务,操作系统把硬件功能封装成简单易用的服务,使用户能够更方便地使用计算机,用户无需关心底层硬件的原理,只需要对操作系统发出命令即可。
(二)操作系统提供的服务
- GUI:图形化用户接口
- 联机命令接口(交互式命令接口),例如windows的命令解释器,它的特点是用户说一句,系统就跟着做一句。
- 脱机命令接口(批处理命令接口),例如windows系统执行.bat文件,它的特点是用户一次性提出多个命令,系统一次性做多个操作。
- 程序接口:可以在程序中进行系统调用来使用程序接口,一系列系统调用组成程序接口。系统调用又被称为广义指令,系统调用类似于函数调用,是应用程序请求操作系统服务的唯一方式。普通用户不能直接使用程序接口,只能通过程序代码间接使用。例如在使用c语言输出hello world时,printf函数的底层就使用了操作系统提供的显示相关的“系统调用”,操作系统在收到系统调用请求,才会帮我们去操作硬件去进行输出hello world。命令接口和程序接口合起来称为用户接口,狭义的用户接口不包括GUI。
(三)操作系统的特征
操作系统的特征有并发、共享、虚拟和异步,并发和共享是两个最基本的特征,二者互为存在条件。
- 并发:指两个或多个事件在同一时间间隔内发生,这些事件在宏观上是同时发生,微观上是交替发生的。注意与并行区分开来,并行是指两个或多个事件在同一时刻同时发生。操作系统的并发性就是指在同一时间运行着多个程序,在宏观上是同时运行,微观上是交替运行。注意单核CPU同一时刻只能执行一个程序,各个程序只能并发地执行,多核CPU同一时刻可以同时执行多个程序,多个程序可以并行地执行。
- 共享:共享即资源共享,是指系统中的资源可供内存中多个并发执行的进程共同使用。资源共享有两种方式,一种是互斥共享,即这种资源虽然可以提供给多个进程使用,但是在一个时间段内只允许一个进程访问该资源。另一种是同时共享,即这种资源在一个时间段内可以有多个进程“同时”(宏观同时,微观可能是交替)地对该资源进行访问。
- 虚拟:虚拟是指把一个物理上的实体变为若干逻辑上的对应物。物理实体是实际存在的,而逻辑对应物是用户感受到的。如单核CPU的电脑可以同时运行多个程序,这在用户看来似乎有六个CPU在同时为自己服务。虚拟技术分为空分复用技术(如虚拟存储器技术)和时分复用技术(如虚拟处理器)
- 异步:在多个程序环境下,运行多个程序并发执行,但由于资源有限,进程的执行是走走停停的。多个程序同时运行可能会争抢资源。
(四)操作系统的发展和分类
- 手工操作阶段:程序员将程序转换成带小孔的纸带,再由计算机从纸带机中读取所要运行的程序,程序运行之后将结果输出到纸带,再由程序员来阅读。这个阶段的缺点主要是用户独占全机,并且人的速度要慢于计算机,这使得资源利用率极低。
- 批处理阶段:
单道批处理系统:这个阶段引入了脱机输入/输出技术,各个程序员可以将程序(纸带)放到纸带机上,然后由一个外围机将这些纸带上的程序及数据先存储到磁带上,然后计算机直接从磁带中读取数据,磁带的读取速度会比纸带机快得多。并且计算机上还运行着一个监督程序,会负责从磁带中读取数据并输出数据到磁带。这个阶段的优点是在一定程度上缓解了程序的人机速度矛盾。缺点是内存中仅能有一道程序在运行,只有程序运行结束之后才能调入下一道程序,CPU有大量的时间是在空闲等待I/O完成,资源利用率仍然很低。
多道批处理系统:这个阶段操作系统正式诞生,允许往内存读取多道程序,多道程序能到并发地运行。主要的优点是多道程序并发执行,共享计算机资源,资源利用率大幅提升。缺点是用户响应的时间长,没有人机交互功能,程序员提交了自己的程序之后,就无法调试程序。
-
分时操作系统:计算机以时间片作为单位轮流为各个用户/作业服务,各个用户可以通过终端(键盘鼠标等)与计算机进行交互。主要优点是用户的请求可以及时被响应,解决了人机交互问题。多个用户能够使用一台计算机,用户对计算机的操作相对独立,感受不到别人的存在。主要缺点是不能优先处理紧急任务,对各个用户/作业都是公平的。
-
实时操作系统:能够优先处理一些紧急任务,紧急任务不需要时间片排队。在实时操作系统的控制之下,计算机系统接收到外部信号后及时进行处理,并且要在严格的时限内处理完事件。实时操作系统的特点是及时性和可靠性。实时操作系统又分硬实时操作系统和软实时操作系统,硬实时操作系统必须严格遵守时间规定,软实时操作系统能够接受偶尔违反时间规定。
-
网络操作系统:能把网络中各个计算机有机结合起来,实现数据传送等。实现网络中各种资源的共享和各台计算机之间的通信。
-
分布式操作系统,主要特点是分布性和并行性,系统中的各台计算机地位相当,任何工作都可以分布在这些计算机上,由它们并行、协同地完成这个任务。
-
个人计算机操作系统
(四)操作系统的运行机制
- 一条高级语言的代码翻译过来可能会对应多条机器指令(二进制),程序运行的过程就是CPU执行一条一条机器指令的过程。
- 普通程序员编写的程序是“应用程序”,是跑在操作系统之上的;而一些程序员负责开发操作系统,他们编写的是“内核程序”,很多“内核程序”组成了操作系统,简称“内核”,内核是操作系统中最核心的部分,也是最接近硬件的部分。操作系统内核作为“管理者”,有时会让CPU执行一些“特权指令”比如内存清零指令,这些指令影响重大,只允许操作系统内核来使用。应用程序只能使用“非特权指令”如加法指令,减法指令等。CPU在执行一条指令前就能够判断其是否为特权指令。
- 为了让CPU能够区分目前正在运行的程序是应用程序还是内核程序,于是就有了CPU的两种状态:内核态(核心态、管态)和用户态(目态)。处于内核态时,说明此时正在运行的是内核程序,可以执行特权指令;处于用户态时,说明此时正在运行的应用程序。CPU中有一个寄存器叫程序状态寄存器(PSW),其中有一个二进制位,1表示内核态,0表示用户态。计算机刚开机时,CPU处于内核态,操作系统内核程序先上CPU运行,开机后需要运行应用程序时,操作系统内核会在让出CPU之前,用一条特权指令把PSW的标志位设置为用户态,完成状态的切换。当CPU处于用户态时,如果运行的应用程序要执行一条特权指令,那么会引发一个中断信号,当CPU检测到中断信号之后,会立刻变成内核态,并停止运行当前的应用程序,转而运行一个处理中断信号的内核程序,运行完之后才会将CPU使用权交给其他应用程序。
(五)中断
- “中断”会使CPU由用户态变为内核态,使操作系统重新夺回对CPU的控制权,如果没有中断机制,那么一旦应用程序在CPU上运行,CPU就会一直运行这个应用程序,也就没有并发了。
- 广义的中断分为内中断(异常、例外)和外中断(狭义的中断)。
内中断与当前执行的指令有关,中断信号来源于CPU内部,比如CPU在用户态时执行一条特权指令,这会使得CPU产生一个中断信号;有时候应用程序要请求操作系统内核的服务,此时会执行一条特殊的指令称为陷入指令(应用程序主动将CPU控制权还给操作系统内核),会引发一个内部中断信号。
外中断和当前执行的指令无关,中断信号来源于CPU外部。比如时钟中断(由时钟部件发来的中断信号,用来实现并发)、I/O中断(由输入/输出设备发来的中断信号)
- CPU在执行指令时会检查是否有异常(内中断)发生,在每个指令周期末尾,CPU还会检测是否有外中断信号需要处理
- 内中断(异常、例外)又分为陷阱(陷入)(由陷入指令引发、是应用程序故意引发的)、故障(由错误条件引发,可能会被内核程序修复,内核程序修复故障后会把CPU使用权还给应用程序,如缺页故障)、终止(由致命错误引起,内核程序无法修复故障,一般不再把CPU的使用权还给应用程序,而是直接终止应用程序,如整数除0、非法使用特权指令)
- 不同的中断信号,需要不同的中断处理程序来处理。当CPU检测到中断信号之后,会根据中断信号的类型去查询“中断向量表”,以此来找到相应的中断处理程序在内存中的存放位置。 中断处理程序是内核程序,需要运行在内核态。
(六)系统调用
- 系统调用是操作系统提供给应用程序(程序员/编程人员)使用的接口,可以理解为一种可供应用程序调用的特殊函数,应用程序可以通过系统调用来请求获得操作系统内核的服务。
- 操作系统向上提供系统调用,使得上层程序能够请求内核的服务。编程语言向上提供库函数,有时系统调用封装成库函数,以隐藏系统调用的一些细节,使程序员编程更加方便。普通应用程序可以直接进行系统调用,也可使用库函数,有的库函数涉及系统调用,有的不涉及系统调用。
- 如果两个进程可以随意、共享地使用打印机资源,那打印出来的东西可能会产生错乱。因此我们需要由操作系统对共享资源进行统一的管理,并向上提供系统调用,用户如果想要使用打印机这种共享资源,只能通过系统调用向操作系统内核发出请求,内核会对各个请求进行处理。
- 系统调用按功能分类可以分为设备管理(完成设备的请求、释放、启动等功能)、文件管理(完成对文件的读写、创建、删除等功能)、进程控制(完成进程的创建、撤销、阻塞、唤醒等功能)、进程通信(完成进程之间的消息传递、信号传递等功能)、内存管理(完成内存的分配、回收等功能)。
- 系统中的各种共享资源都由操作系统内核统一管理,因此凡是与共享资源有关的的操作(如存储分配、I/O操作、文件管理等),都必须通过系统调用的方式向操作系统内核提出服务请求,由操作系统内核代为完成。这样可以保证系统的稳定性和安全性,防止用户进行非法操作。
- 应用程序想要进行系统调用的大致过程是这样的:应用程序运行在用户态,然后要先使用传参指令给CPU的寄存器传递一些必要的参数;传递完参数之后,会执行一条陷入指令(trap指令、访管指令),这条指令会引发一个内中断,转入相应的中断处理程序,即系统调用的入口程序,此时CPU进入内核态。系统调用入口程序会根据寄存器中的参数判断用户需要哪种系统调用的服务,然后系统调用入口程序会调用与特定系统调用类型相对应的处理程序
(七)操作系统的内核
- 操作系统内核可以划分为时钟管理(实现计时功能)、中断处理(负责实现中断机制)、原语(是一种特殊的程序,具有原子性,即运行不可中断,运行时间短,调用频繁,处于操作系统最底层,是最接近硬件的部分)、对系统资源进行管理的功能(进程管理、存储器管理、设备管理)。前三部分是与硬件联系较为紧密的模块,最后一部分更多的是对数据结构的操作,不会直接涉及硬件,因此有的操作系统将前三部分包含在内核中,称为微内核,而有的操作系统将四部分都包含在操作系统内核中,称为大内核(宏内核、单内核)。操作系统内核需要运行在内核态,非内核功能运行在用户态,因此这两种操作系统内核的设计方式会对系统性能有一定的影响。
- 如果某一个应用程序想要请求操作系统的服务,而这个服务的处理同事涉及到进程管理、存储器管理、设备管理的话,由于大内核包含四个部分,只需要进行两次变态,由用户态变为内核态,提供服务结束后再变为用户态。而微内核的话,进程管理、存储器管理、设备管理这三个部分都可以在用户态下调用,但这三个部分也需要内核的支持,每使用一个功能都需要有两次形态转换,所以总共要有六次形态转换,频繁转换CPU形态会降低系统性能。
- 大内核(如Linux、Unix)优点是性能较高,缺点是代码庞大、结构混乱、难以维护;微内核(Windows NT)优点是结构清晰、方便维护,缺点是性能较低。
二、进程管理
(一)进程的概念
- 程序是静态的,是存放在磁盘里的可执行文件,是一系列的指令集合。进程是动态的,是程序的一次执行过程。进程是动态的,是程序的一次执行过程,同一个程序多次执行会对应着多个进程。
- 当进程被创建时,操作系统会为该进程分配一个唯一的PID(Process ID,进程ID),操作系统还会记录进程所属ID(UID),通过PID、UID操作系统可以区分每一个进程。操作系统还记录了给进程分配了哪些资源(如分配了多少内存,正在使用哪些IO设备,正在使用哪些文件),可用于实现操作系统对资源的管理。还记录了进程的运行情况(如CPU的使用时间,磁盘使用情况,网络流量使用情况等),可用于实现操作系统对进程的控制、调度。以上这些信息都会被统一保存在一个数据结构PCB(Process Control Block)中,即进程控制块。PCB是进程存在的唯一标志,当进程被创建时,操作系统为其创建PCB,当进程结束时,会回收其PCB。
- PCB包含的信息大致可以分为进程描述信息(PID、UID)、进程控制和管理信息(CPU、磁盘及网络流量使用情况统计、进程当前状态)、资源分配清单(正在使用哪些文件、哪些内存区域、哪些IO设备)、处理机相关信息(如PSW、PC等等各种寄存器的值,用于实现进程切换)。操作系统对进程进行管理工作所需的信息都在PCB中。
- 一个进程实体(进程映像,静态的,是动态的进程在某一时刻的一个快照,反应了某一时刻该进程的状态)的组成除了PCB之外,还由程序段(程序的代码、即指令序列)和数据段(运行过程中产生的各种数据,如程序中定义的变量)组成。PCB是给操作系统用的,而程序段和数据段是给进程自身使用的。比如在运行一个程序的时候,需要将这个程序读入内存才能运行,具体操作是这样的:操作系统会给这个程序创建相应的进程和PCB,并且将程序的指令读入内存(程序段),而程序中还可能定义了一些变量(数据段),也需要将这些变量或中间数据读取到内存中。
- 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。一个进程被调度,就是指操作系统决定让这个进程上CPU运行。运行同一个程序多次,他们的PCB和数据段是各不相同的,但程序段的内容是相同的。
进程具有动态性(最基本的特征、进程是程序的一次执行过程,是动态的产生、变化和消亡的)、并发性(内存中有多个进程实体,各进程可并发执行)、独立性(进程是能独立运行、独立获得资源、独立接受调度的基本单位)、异步性(各进程按各自独立的、不可预知的速度向前推进,操作系统要进行“进程同步机制”来解决异步问题)、结构性(每个进程都会配置一个PCB,从结构上看,进程由程序段、数据段、PCB组成)
(二)进程的状态与转换
- 进程正在被创建时,它的状态是“创建态”,在这个阶段操作系统会为进程分配资源,初始化PCB;当进程被创建完成之后,进入了“就绪态”,处于就绪态的进程已经具备运行条件,但由于没有空闲CPU,就暂时不能运行。当CPU空闲时,操作系统会选择一个处于就绪态的进程,让它上CPU运行,此时这个进程就处于“运行态”,CPU会执行该进程对应的程序(执行指令序列)。在进程运行的过程中,可能会主动请求等待某个事件的发生(如等待某种系统资源的分配,或等待其他进程的响应),在这个事件发生之前,进程无法继续往下执行,此时操作系统会让这个进程下CPU,并让它进入“阻塞态”,然后让另一个处于就绪态的进程上CPU运行。当事件发生之后,处于阻塞态的进程又会重新回到就绪态,等待CPU空闲再到CPU上运行。当某一个进程执行完毕之后,这个进程可以执行exit系统调用,请求操作系统终止该进程,此时该进程会处于“终止态”,操作系统会让该进程下CPU,并回收内存空间等资源,最后还要回收该进程的PCB。
- 进程可以主动从运行态转换到阻塞态,但不能直接由阻塞态转换到运行态,而是被动地由阻塞态转换到就绪态,再由就绪态转换到运行态。当处于就绪态的进程被调度时,就会由就绪态转换到运行态,当一个进程运行时间过长,时间片到或者CPU被抢占,这个进程就会由运行态回到就绪态。
- 运行态、就绪态、阻塞态是进程的三种基本状态,绝大部分时间进程都处于这三种状态,在单核CPU情况下,同一时刻只会有一个进程处于运行态,而在多核CPU情况下,可能会有多个进程处于运行状态。
- 在进程CPB中,会有一个state变量来表明当前进程所处的状态。
(三)进程的组织方式
- 链接方式,按照进程状态将PCB分为多个队列。操作系统拥有指向各个队列的指针;执行指针指向当前处于运行态的进程;有就绪队列指针指向当前处于就绪态的指针,通常会把优先级高的进程放在队头;有阻塞队列指针指向当前处于阻塞态的进程,很多操作系统还会根据阻塞原因的不同,再分为多个阻塞队列。
- 索引方式,操作系统会给各种状态的进程建立索引表,每个索引表的表项会指向各个进程的PCB,然后操作系统会持有指向各个索引表的指针。
(四)进程控制
- 进程控制的功能主要是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。进程控制需要用“原语”来实现,原语是一种特殊的程序,具有原子性,即运行不可中断,运行时间短,调用频繁,处于操作系统最底层,是最接近硬件的部分。进程控制即进程的状态转换需要一气呵成,如果不能一气呵成的话,就有可能导致操作系统中的某些关键数据结构信息不统一的情况,这会影响操作系统进行别的管理工作。比如处于阻塞队列中的某一个进程,它等待的事件已经发生,那么在操作系统中,负责进程控制的内核程序需要将进程的PCB的state设置为1,然后再将这个PCB放到就绪队列里;但是如果执行了一半就停止,就会出现state为1的PCB处于阻塞队列中,这是不正确的,容易出现问题。
- 原语的执行具有原子性,即执行期间不允许被中断,可以用“关中断指令”和“开中断指令”两个特权指令来实现原子性。正常情况下CPU每执行完一条指令都会例行检查是否有中断信号需要处理,如果有则暂停运行当前这段程序,转而执行相应的中断处理程序。而如果CPU执行了关中断指令之后,就不再例行检查中断信号,直到执行开中断指令之后才会恢复检查。这样的话,关中断和开中断之间的这些指令序列就是不可被中断的,这就实现了“原子性”。
- 如果要创建一个进程,要使用创建原语,这个原语所做的事情主要有:申请空白PCB、为新进程分配所需资源、初始化PCB并将PCB插入就绪队列。创建原语让一个进程从创建态变为就绪态。引起操作系统创建一个进程的事件有用户登录(分时系统中,用户登录成功,系统会为其建立一个新的进程)、作业调度(多道批系统处理中,外存中还有没投入运行的程序,当这些作业被放入内存时,会为其创建一个新的进程)、提供服务(用户向操作系统提出某些请求时,会创建一个进程处理该请求)、应用请求(由用户进程主动请求创建一个子进程)。
- 如果要终止一个进程,要使用撤销原语。使用了撤销原语,能够让一个进程由某一种状态变为终止态,并最终变为无。这个原语所做的事情主要有:从PCB集合中找到要终止的进程的PCB,若进程正在运行,则立刻剥夺CPU,将CPU分配给其他进程使用,并且终止该进程的所有子进程,将该进程的所有资源都归还给父进程或者操作系统,最后再删除PCB。引起进程终止的事件有正常结束(进程自己请求终止,通过exit系统调用)、异常结束(整数除以0,非法使用特权指令,被操作系统强行杀掉)、外界干预(电脑卡死,用户强制关闭进程)。
- 一个进程要从运行态变为阻塞态,这时操作系统就会执行一个阻塞原语,阻塞原语所要做的事情主要有:找到要阻塞的进程的PCB,保护进程运行现场,将PCB状态信息变为阻塞态,暂时停止其运行,然后将PCB插入相应事件的阻塞队列。引起进程阻塞的事件有需要等待系统分配某种资源、需要等待相互合作的其他进程完成工作。
- 一个进程要从阻塞态变为就绪态,这时操作系统就会执行一个唤醒原语,唤醒原语所做的事情主要有:在事件等待队列中找到PCB,将PCB从等待队列中移除,设置进程为就绪态,然后将PCB插入就绪队列,等待被调度。引起进程唤醒的事件是进程等待的事件已经发生。一个进程由什么原因被阻塞,就应该由什么原因被唤醒,阻塞原语跟唤醒原语应该成对使用。
- 执行切换原语能够让一个进程从运行态切换到就绪态,并且让另一个处于就绪态的进程转换成运行态。切换原语会让两个进程的状态发生改变,切换原语所做的事情主要有:将运行环境信息存入PCB,将PCB移入相应的队列中,选择另一个进程执行,并更新其PCB,同时还会根据该PCB恢复新进程运行所需要的环境。引起进程切换的事件有当前进程时间片到、有更高优先级的进程到达、当前进程主动阻塞、当前进程终止。
- CPU在从内存中取出指令进行执行时,CPU中会设置很多的寄存器,用来存放程序运行过程中所需的某些数据。比如存放程序状态的寄存器PSW(程序状态字寄存器)、PC(程序计数器、存放下一条指令的地址)、IR(指令寄存器,存放当前正在执行的指令)、通用寄存器(存放其他的一些必要信息)等等。
- 当进程切换的时候,当前进程的执行可能还没有结束,那么这个进程所存储在寄存器中的一些数据可能会被即将切换的进程所覆盖,为了解决这个问题,操作系统会在进程切换时先在PCB保留当前进程的运行环境(进程上下文)(保存一些必要的寄存器信息),这样当这个进程重新投入运行的时候就可以根据PCB来恢复它的运行环境。
- 无论哪个进程控制原语,所做的事情无非就是更新PCB信息(修改进程状态state,保存/恢复运行环境)、将PCB插入合适的队列、分配/回收资源。
(五)进程通信
- 进程通信就是指进程之间的信息交换,进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。为了保证安全,一个进程不能直接访问另一个进程的地址空间,为了保证进程之间的安全通信,操作系统提供了一些进程通信的方法,包括共享存储、消息传递、管道通信。
- 由于两个进程不能直接访问对方的内存空间,所以操作系统在内存中给两个进程分配一个共享空间,这两个进程之间的通信就可以通过这个共享空间来进行;这两个进程对共享空间的访问是互斥的(互斥通过操作系统提供的工具来实现)。
- 共享存储分为两种,一种是基于数据结构的共享,另一种是基于存储区的共享。基于数据结构的共享是规定是共享空间只能存放哪种固定的数据结构,这样两个进程的通信每次只能传递这样一个数据结构的数据,这种共享方式速度比较慢,限制比较多,是一种低级通信方式。基于存储区的共享,是在内存中画出一块共享存储区,数据的形式、存放位置都由两个进程来控制,而不是由进程来控制,这种共享方式速度更快,是一种高级通信方式。
- 管道通信。管道是指用于连续读写进程的一个共享文件,又名pipe文件。其实就是在内存中开辟一个大小固定的缓冲区。管道只能采用半双工通信,某一时间只能实现单向的传输,如果要实现双向同时通信,则需要设置两个管道;各个进程要互斥地访问管道。管道通信的实现是这样的:一个进程往管道中写数据(字符流形式),当管道满了之后另一个进程才可以开始读数据,而当管道的数据被读取完毕之后,原先的进程才能继续往管道里写数据;数据一旦被读取之后,就从管道被抛弃,因此读进程只能有一个,否则可能会出现读错数据的情况。
- 消息传递。进程间的数据交换以格式化的消息为单位,进程通过操作系统提供的“发送消息/接受消息”两个原语进行数据交换。格式化的消息包括消息头(包括发送进程ID、接受进程ID、消息类型、消息长度等)和消息体。消息传递的方式又分为两种,一种是直接通信方式,一种是间接通信方式。直接通信方式是通过发送原语把消息直接挂到接收进程的消息缓冲队列上,然后接收进程通过接收原语把消息缓冲队列上的消息依次接收。间接通信方式(信箱通信方式)是要先将消息发送到中间实体(信箱),操作系统会为各个通信的进程管理一个信箱,发送进程通过发送原语将消息发送到信箱,然后接收进程用接收原语将信箱中属于自己的消息取走。
(六)线程
- 在引入进程之前,各个程序只能串行执行,而在引入了进程之后,各个程序能够并发地执行。进程是程序的一次执行,而如果我们希望一个程序能够同时(并发)完成很多事情(比如聊天+视频+发文件),那么显然程序的一次顺序处理是没有办法完成这些事情的,传统的进程只能串行地执行一系列程序,因此引入了线程,来增加并发度。传统的进程中,CPU会轮流地为各个进程进行服务,这些进程就会并发地执行,传统的进程是程序执行流的最小单位。而引入了线程之后,CPU的服务对象就不再是进程,而是进程中的线程,一个进程中可能会包含着多个线程,而CPU会按照一定算法轮流地为这些线程服务,这样的话就能够实现我们希望的一个程序同时完成很多事情,并且在引入线程之后,线程成为了程序执行流的最小单位,是基本的CPU执行单元。引入线程之后,进程只作为除CPU之外的系统资源的分配单元(如打印机、内存地址空间等都是分配给进程的)。
- 在传统进程机制中,进程是资源分配、调度的基本单位,而引入线程后,进程是资源分配的基本单位,线程是调度的基本单位;在传统进程机制中,只能进程并发,引入线程后,各线程间也能够并发,提高了并发度;在传统进程机制中,各进程间并发,需要切换进程的运行环境,系统开销很大,而线程间并发,如果是同一进程间的线程切换,则不需要切换进程环境,系统开销小。
- 线程是CPU调度的单位;在多CPU计算机中,各个线程可占用不同的CPU,每个线程有一个线程ID和线程控制块(TCB);线程也有就绪、阻塞、运行三种基本状态;线程几乎不拥有系统资源,系统资源是分配给进程的;同一进程的不同线程共享进程的资源;由于共享内存地址空间,同一进程中的线程间通信无需系统干预;同一进程的线程切换不会引起进程切换,不同进程的线程切换会引起进程切换,切换同进程的线程系统开销会比较小,切换进程系统开销会比较大。
- 线程的实现方式分为两种,一种是用户级线程,另一种是内核级线程。
用户级线程(主要是早期的操作系统会用的线程实现方式)由应用程序通过线程库(很多编程语言提供了强大的线程库,能够实现线程的创建、销毁、调度等功能)实现,所有的线程管理工作都由应用程序负责(包括线程切换);用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预;在用户看来,是有多个线程,但是在操作系统看来,并意识不到线程的存在,用户级线程就是从用户视角能看到的线程;用户级线程的优点:用户级线程的切换在用户空间即可完成,不需要切换到内核态,线程管理的系统开销小,效率高。用户级线程的缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高;多个线程不可在多核处理机上并行运行,因为在用户级线程实现方式的情况下,CPU调度的基本单位还是进程,进程只能被分配一个核心。
内核级线程(大多数现代操作系统都实现了内核级线程)是由操作系统支持的线程。内核级线程的管理工作由操作系统内核来完成;线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在内核态才能完成;操作系统会为每个内核级线程建立相应的TCB(线程控制块),通过TCB对线程进行管理。“内核级线程”就是“从操作系统内核视角能看到的线程”。内核级线程的优点:当一个线程被阻塞之后,别的线程还能够继续执行,并发能力强。多线程可在多核处理机上并行执行;缺点是一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到内核态,因此线程管理的成本高,开销大。 - 在支持内核级线程的系统中,如果引入用户级线程,那么可以实现把若干个用户级线程映射到内核级线程,根据用户级线程和内核级线程的映射关系,可以划分为几种多线程模型:
一对一模型:一个用户级线程映射到一个内核级线程,每个用户进程有与用户级线程同数量的内核级线程,优点是当一个线程被阻塞之后,别的线程还能继续执行,并发能力抢。多线程可以在多核处理机上并行执行。缺点是一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到内核态,因此线程管理的成本高,开销大。
多对一模型:多个用户级线程映射到一个内核级线程,且一个进程只被分配到一个内核级线程。优点是用户级线程的切换在用户空间即可完成,不需要切换到内核态,线程管理的系统开销小,效率高。缺点是当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高;多个线程不可在多核处理机上并行运行。操作系统只看得见内核级线程,因此只有内核级线程才是处理机分配的单位。
多对多模型:n个用户级线程映射到m个内核级线程(n>=m),每个用户进程对应m个内核级线程。这种模型克服了多对一模型并发度不高的缺点(一个阻塞全体阻塞),又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。
(七)处理机调度
- 当有一堆任务需要处理,但是由于资源有限,这些事情没法同时处理,这就需要确定某种规则来决定处理这些任务的顺序,这就是“调度”研究的问题。
- 高级调度(作业(一个具体的任务)调度):按一定的原则从外存的作业后备队列中挑选一个作业调入内存,并创建进程,每个作业只调入一次,调出一次。作业调入时会创建PCB,调出时会撤销PCB。
- 低级调度(进程调度/处理机调度):按照某种策略从就绪队列中选取一个进程,将处理机分配给它。进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度,进程调度的频率很高,一般几十毫秒一次。
- 中级调度(内存调度):当内存不够时,可将某些进程的数据调出外存。等内存空闲或者进程需要运行时再重新调入内存,暂时调到外存等待的进程状态为挂起状态。被挂起的进程PCB会被组织成一个挂起队列。中级调度是按照某种策略决定将哪个处于挂起状态的进程重新调入内存。一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高。
- 暂时调到外存等待的进程状态为挂起状态,挂起态又可以细分为就绪挂起、阻塞挂起两种状态。当系统负载比较高,内存空间不够用时,可能会将某些处于就绪态的进程暂时调到外存中,这个进程就处于一个就绪挂起的状态,当内存空间空闲或者进程需要执行时这个进程就会被激活,把这个进程相关的数据放到内存当中。一个处于阻塞态的进程也有可能会被调到外存中,这个进程就处于一种阻塞挂起的状态,这个进程在需要的时候也会被激活重回阻塞态,除此之外,当引起阻塞的事件发生时,有的操作系统会使处于阻塞挂起状态的进程直接进入就绪挂起状态,然后再由就绪挂起状态转到就绪态。有的时候处于运行态的进程运行结束之后也会变回就绪挂起的状态,有的时候处于创建态的进程在创建进程结束之后,由于内存不足直接进入就绪挂起的状态。注意挂起和阻塞的区别,两种状态都是暂时不能获得CPU的服务,但挂起态是将进程映像调到外存中去了,而阻塞态进程映像还在内存中。有的操作系统会把就绪挂起、阻塞挂起分为两个队列挂起队列,甚至会根据阻塞原因不同将阻塞挂起进程进一步细分为多个队列。
七状态模型:
三层调度的对比:
(八)进程调度
进程调度(低级调度)就是按照某种算法从就绪队列中选择一个进程为其分配处理机。
-
需要进行进程调度和切换的情况主要有两种,一种是当前运行的进程主动放弃处理机,包括进程正常终止、运行过程中发生异常而终止、进程主动请求阻塞(如等待I\O);另一种是当前运行的进程被动放弃处理机,包括分享给进程的时间片用完、有更紧急的事需要处理(如I/O中断)、有更高优先级的进程就如就绪队列。
-
不能进行进程调度和切换的情况有:在处理中断的过程中,中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换;进程在操作系统内核程序临界区中;在原子操作过程(原语)中。
-
临界资源是指一个时间段内只允许一个进程内使用的资源,各进程需要互斥地访问临界资源。临界区是指访问临界资源的那段代码,各个进程需要互斥地进入临界区,即互斥地执行访问临界资源的那段代码。内核程序临界区一般是用来访问某种内核数据结构的,比如进程的就绪队列(由各就绪的进程的PCB组成)。
假如现在有一个进程处于内核程序临界区,并且这个临界区是要访问就绪队列的,那么在访问之前它会对就绪队列上锁,如果这个进程还没退出内核程序临界区的话,就要进行进程调度,但是由于进程调度相关的程序也需要访问就绪队列,且目前就绪队列是处于上锁状态的,这样进程调度就没法顺利完成。因此,内核程序临界区访问的临界资源如果不尽快释放的话,极有可能影响到操作系统内核的其他管理工作,在访问内核程序临界区期间不能进行调度和切换。
另一种状况,假如有一个进程在使用着打印机,在打印机完成之前,进程一直处于临界区内,临界资源不会解锁,但打印机又是慢速设备,如果不允许进程调度的话会导致CPU一直空闲,造成浪费。因此,由于普通临界区访问的临界资源不会直接影响操作系统内核的管理工作,因此在访问普通临界区时可以进行调度和切换。 -
进程调度的方式分为两种。一种是非剥夺调度方式(非抢占方式),即只允许进程主动放弃处理机,在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到进程终止或主动要求进入阻塞态。另一种是剥夺调度方式(抢占方式),当一个进程正在处理机上执行时,如果有一个更重要或者更紧迫的进程需要处理机,则暂停正在执行的进程,将处理机分配给更加紧迫的那个进程。非剥夺调度方式实现简单,系统开销小但是无法及时处理紧急任务,适合早起的批处理系统。剥夺调度方式可以优先处理更紧急的进程,也可以实现让各个进程按照一定的时间片轮流执行的功能(通过时钟中断),适合分时操作系统、实时操作系统。
-
狭义的进程调度是指从就绪队列中选出一个要运行的进程,这个进程可以是刚刚被暂停执行的进程,也可以是另一个进程,后一种情况就需要进程切换,进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程。广义的进程调度包含了选择一个进程和进程切换两个步骤。进程切换的过程主要完成对原来运行进程的数据的保存和对新的进程各种数据的恢复(根据保存在PCB中的各种信息)。进程切换是有代价的,如果频繁地进行进程切换和进程调度,必然会使得整个系统的效率降低。
(九)调度算法
调度算法的评价指标:
- CPU利用率:CPU忙碌的时间占总时间的比例
- 系统吞吐量:单位时间内完成作业的数量
- 周转时间:指从作业被提交给系统开始到作业完成为止的这段时间间隔。包括四个部分:作业在外存后备队列上等待作业调度(高级调度)的时间、进程在就绪队列上等待进程调度(低级调度)的时间、进程在CPU上执行的时间、进程等待I/O操作完成的时间。后三项在一个作业的整个处理过程中,可能发生多次。作业周转时间=作业完成时间-作业提交事件;平均周转时间=各作业周转时间之和/作业数;带权周转时间=作业周转时间/作业实际运行的时间。对于操作系统来说,更关心系统的整体表现,因此更关心所有作业周转时间的平均值即平均周转时间。对于周转时间相同的两个作业,实际运行时间长的作业在相同时间内被服务的时间更多,因此带权周转时间更小,用户满意度更高。对于实际运行时间相同的两个作业,周转时间短的带权周转时间更小,用户满意度更高。
- 等待时间:进程/作业等待处理机状态时间之和。对于进程来说,等待时间就是指进程建立之后等待被服务的时间之和,在等待I/O完成的期间其实进程也是在被服务的,所以不计入等待时间。对于作业来说,不仅要考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间。
- 响应时间:指从用户提交请求到首次产生响应所用的时间。
调度算法:
- 先来先服务(FCFS):主要从公平的角度考虑,按照作业/进程到达的先后顺序进行服务,当用于作业调度时,考虑的是哪个作业先到达后备队列;用于进程调度时,考虑的是哪个进程先到达就绪队列,一般是一种非抢占式的算法。优点是公平、算法实现简单,缺点是排在长作业/进程后面的短作业需要等待很长时间,带权周转时间很大,对短作业来说用户体验不好。这种算法不会导致饥饿(某进程/作业长期得不到服务),因为只要肯等待,作业/进程总会得到服务。
- 短作业优先(SJF,Shortest Job First):为了追求最少的平均等待时间,最少的平均周转时间,最少的平均带权周转时间,SJF的算法规则是所需服务时间最短的作业/进程优先得到服务,每次调度时选择当前已经到达的且运行时间最短的作业/进程进行服务,这个算法可用于作业调度,也可以用于进程调度(用于进程调度称为短进程优先,SPF,Shortest Process First)。SJF算法和SPF算法是非抢占式的算法。虽然SJF算法的平均等待时间、平均周转时间不一定最少,但相比于其他算法如FCFS,SJF依然可以获得较少的平均等待时间、平均周转时间,在所有进程同时可运行(所有进程几乎同时到达)的时候,采用SJF调度算法的平均等待时间、平均周转时间最少。SJF算法的优点是能够获得较少的平均等待时间和平均周转时间,缺点是不公平,对短作业有利,对长作业不利,当一直有更短的作业到来时,有可能会产生长作业饥饿的现象;另外,作业/进程的运行时间是由用户提供的,并不一定真实,不一定能够做到真正的短作业优先。
- SJF和SPF是非抢占式的算法,但是也有抢占式的版本,即最短剩余时间优先算法(SRTN,Shortest Remaining Time Next),每当有新进程进入就绪队列的时候就需要调度,如果新到达的进程剩余时间比当前运行的进程剩余时间更短,则由新进程抢占处理机,当前运行进程重新回到就绪队列,另外当一个进程完成时也需要调度。最短剩余时间优先算法SRTN的平均等待时间、平均周转时间最少。
- FCFS算法在每次调度时选择一个等待时间最长的作业/进程为其服务,但是没有考虑到作业的运行时间,导致了短作业不友好的问题;SJF算法是选择一个执行时间最短的作业为其服务,但是又没有考虑到各个作业的等待时间,因此导致了对长作业不友好的问题,甚至还会造成饥饿问题。为了能够综合考虑作业/进程的等待时间跟要求服务的时间,有了高响应比算法(HRRN,Highest Response Ratio Next),它的算法规则是在每次调度的时候计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务,响应比=(等待时间+要求服务时间)/ 要求服务时间;HRRN是一种非抢占式的算法,只有当前的作业/进程主动放弃处理机(正常/异常完成,或主动阻塞)时,才需要调度,才需要计算响应比。HRRN算法的优点是综合考虑了等待时间和运行时间,等待时间(要求服务时间)相同时,要求服务时间短的优先(SJF的优点),要求服务时间相同时,等待时间长的优先(FCFS的优点),对于长作业来说,随着等待时间越长,其响应比也会越来越大,从而避免了长作饥饿的问题。
以上几种调度算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心“响应时间”,也不区分任务的紧急程度,因此对用户来说交互性较差,这几种算法适合用于早起的批处理系统。
- 时间片轮转(RR,Round-Robin),公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。算法规则是按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片,若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列队尾进行重新排队。这个算法用于进程调度,因为只有当作业放入内存并建立相应的进程之后,才会被分配处理机时间片。该算法属于抢占式的算法,若进程未能在时间片运行完,将被强行抢占CPU使用权,由时钟装置发出时钟中断来通知CPU时间片已到。该算法常用于分时操作系统,更注重“响应时间”。如果时间片太大的话,使得每一个进程都可以在一个时间片内完成,则时间片轮转调度算法退化为先来先服务算法,并且会增大进程响应时间(如果一个进程在自己的时间片用完的时候发出一个命令,那么由于时间片太长,该进程等待响应的时间就会相应增加),因此时间片不能太大;另一方面,进程调度、切换是有时间代价的(保存、恢复运行环境),因此如果时间片太小,会导致进程切换频繁,系统开销增大,因此时间片也不能太小,一般来说设计时间片要让切换进程的开销占比不超过1%。RR的优点是公平、响应快,适用于分时操作系统,缺点是高频率的进程切换会造成一定的系统开销,并且不区分任务的紧急程度。该算法不会导致饥饿问题。
- 随着计算机的发展,特别是操作系统的出现,越来越多的场景需要根据任务的紧急程度来决定处理顺序,因此出现了优先级调度算法。算法规则是在调度时选择优先级最高的作业/进程。该算法既有抢占式的也有非抢占式的,非抢占式的除了当前进程主动放弃处理机时发生调度,当就绪队列发生改变的时候也需要检查是否会出现抢占。就绪队列未必只有一个,操作系统往往按照不同的优先级来组织这些就绪队列,并且把优先级高的进程排在更靠近队头的位置;根据优先级是否可以改变,可将优先级分为静态优先级(创建进程时确定,之后一直不变)和动态优先级(创建进程时有一个初始值,之后会根据情况动态地调整优先级)两种。一般来说系统进程优先级高于用户进程优先级,前台进程优先级高于后台进程优先级;操作系统会偏好执行I/O型进程(I/O繁忙型进程),因为I/O设备和CPU可以并行工作,如果优先让I/O繁忙型进程优先运行的话,则越有可能让I/O设备尽早地投入工作,则资源利用率、系统吞吐量都会得到提升。如果某进程在就绪队列等待了很长时间,可以适当提高它的优先级;如果某进程占用处理机运行了很长时间,可以适当降低它的优先级;如果某一个进程频繁地进行I/O操作,可以适当提升它的优先级。优先级调度算法的优点是可以用优先级区分紧急程度、重要程度,适用于实时操作系统,可灵活调整对各个进程/作业的偏好程度;缺点是如果有源源不断的高优先级进程到来,可能会导致饥饿问题。
- FCFS的优点是公平,SJF算法的优点是能够尽快处理完短作业,平均等待/周转时间较短,时间片轮转调度算法可以让各个进程得到及时的响应,优先级调度算法可以灵活调整各个进程被服务的机会,综合以上各个算法那的优点,又出现了一种多级反馈队列调度算法。该算法的规则是设置多级就绪队列,各级队列优先级从高到低,时间片从小到大,当新进程到达时先进入第一级队列,按FCFS原则分配时间片,若用完时间片进程还未结束,则该进程进入下一级进程队列队尾,如果已经是最低一级队列,则直接放到当前队列的队尾,当第k级队列为空时,才会为k+1级队列的进程分配时间片。该算法用于进程调度,属于抢占式的算法,在k级队列的进程运行过程中,如果更上级的队列进入了一个新进程,那么该新进程会抢占处理机,原来运行的进程会被放入所处队列的队尾。多级反馈队列调度算法的优点是结合了各个算法的优点,对各类型进程相对公平(FCFS优点),每个新到达的进程很快得到响应(RR的优点),短进程只用较少的时间就可以完成(SPF的优点),对并且不必实现估计进程的运行时间(SPF需要用户提供自己进程运行的时间长短,可能会存在用户造假,而该算法能够避免用户造假),可灵活调整各类进程对各类进程的偏好程度(优先级调度算法的优点)(如可以将因I/O而阻塞的进程重新放回原队列,这样I/O繁忙型的进程就能保持较高优先级),该算法的缺点是可能会导致饥饿问题,因为如果有源源不断的短进程到达的话,可能在第一级队列就被执行完毕,那么排在较低级队列的进程有可能一直得不到服务。
比起早期的批处理操作系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括分时操作系统、实时操作系统等)更注重系统的响应时间、公平性、平衡性等指标,而以上这几种算法能够较好地满足交互式操作系统的需求,因此这几种算法适用于交互式操作系统。
(十)进程同步和进程互斥
- 进程具有异步性的特征,即各并发进执行的进程以各自独立的、不可预知的速度向前推进。但是在某些应用场景下,我们需要某些进程按照一定的顺序来执行,也就是要解决这种异步性,这是进程同步要讨论的问题。同步也称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系,进程间的制约关系源于它们之间的相互合作。
- 进程的“并发”需要“共享”的支持,各个并发执行的进程不可避免地需要共享一些系统资源(比如内存,打印机、摄像头等I/O设备)。前面提到过在一个时间段内只允许一个进程使用的资源称为临界资源,摄像头、打印机等物理设备以及许多变量、数据、内存缓冲区都属于临界资源,对临界资源的访问必须互斥地进行。互斥也称为简介制约关系,进程互斥指当一个进程访问某临界资源时,另一个要访问临界资源的进程必须等待当前进程释放该资源后才能进行访问。对临界资源的访问,可以在逻辑上分为四个部分:进入区(负责检查是否可以进入临界区,如果可以进入,则会设置一个正在访问的标志,即上锁,以阻止其他进程同时进入临界区)、临界区(又称临界段,访问临界资源的代码)、退出区(负责解除正在访问临界资源标志,即解锁)、剩余区(做一些其他处理)。注意区分,临界区是进程中访问临界资源的代码段,而进入区和退出区是负责实现互斥的代码段。
- 为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:1. 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。2. 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待,3. 有限等待。对请求访问的进程,应保证其在有限时间内进入临界区,保证不会发生饥饿问题。4. 让权等待,当进程不能进入临界区时,应立即释放处理机,防止进程忙等待(进程不能继续往下推进但一直占用着处理机)。
进程互斥的软件实现方法:
- 单标志法:两个进程在访问完临界区之后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予。这样的话,两个进程就只能轮流访问临界区,这带来的问题是忽视了空闲让进原则,当一个进程访问完临界区之后,将访问权限交给了另一个进程,但如果另一个进程一直不访问临界区的话,那么权限就不会回到原来进程的手上,这样当原来的进程还想要访问临界区的时候,即使临界区空闲,原来的进程也无法访问临界区。
- 双标志先检查法:设置一个布尔型数组flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如flag[0]=true表明0号进程现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志flag[i]设置为true,之后开始访问临界区。该算法的问题是违反了忙着等待的原则,如果两个进程并发的运行,那么当第一个进程要表达自己想要访问临界区之前,另一个进程也想要访问临界区,由于两个进程是并发执行的,那么可能两个人都能到达表达自己想要访问临界区的意愿这一步,比如下图两端代码的执行顺序是152637,那么这两个进程就会同时访问临界区,这是不对的。原因在于,进入区的“检查”和“上锁”两个处理不是一气呵成的,“检查”后,“上锁”前可能发生进程切换。
- 双标志后检查法:双标志先检查法的改版。双标志先检查法的问题在于是先“检查”后“上锁”,但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到了先“上锁”后“检查”的方法,来避免上述问题。该算法虽然解决了忙则等待的问题,但是又违背了“空闲让进”和“有限等待”的原则,如果还是按照1526的顺序去并发执行两个进程的话,那么两个进程都无法进入临界区,产生了饥饿现象。
- Peterson算法:结合双标志法、单标志法的思想,如果双方都想争着进入临界区,那么进程会进行“谦让”。这个算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待这个原则,因为尽管某一个进程进入不了临界区,它仍然是卡在while循环内,还在CPU上执行,反复确认能否进入临界区。
bool flag[2];//表示进入临界区意愿的数组,初始值都是false
int turn = 0;//turn 表示优先让哪个进程进入临界区
P0进程:
flag[0] = true;//表明想要进入临界区
turn = 1;//表明可以先让1号进程进入临界区
while(flag[1] && turn ==1);//如果1号想进入临界区的话就1号先进入
critical section;
flag[0] = false;
remainder section;
P1进程:
flag[1] = true;/表明想要进入临界区
turn = 0;//表明可以先让0号进程进入临界区
while(flag[0] && turn == 0);//如果0号想进入临界区的话就0号先进入
critical sectoin;
flag[1] = false;
remainder section;
进程互斥的硬件实现方法:
- 中断屏蔽方法:利用“开/关中断指令”实现,与原语的实现思想一致,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况。在某进程访问临界区之前,执行关中断指令,即不允许当前进程被中断,也必然不会发生进程切换,直到当前进程访问完临界区后再执行开中断指令,才有可能有别的进程上处理机并访问临界区。该方法的优点是简单高效,缺点是不适用于多处理机,因为开/关中断指令只对执行该指令的处理机有用,对于其他处理机来说还是会正常切换进程的,因此就还是有可能出现多个进程同时访问临界区的情况;只适用于操作系统内核进程,不适用于用户进程,因为开/关中断指令只能运行在内核态,这组指令如果让用户随意使用会很危险。
- TestAndSet指令:简称TS指令,也称为TestAndSetLock指令,即TSL指令。TSL指令是由硬件来实现的,执行的过程不允许被中断,只能一气呵成。相比软件实现方法,TSL指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。它的优点是实现简单,无需像软件方法那样严格检查是否会有逻辑漏洞,适用于多处理机环境。缺点是不满足让权等待原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。
以下是C语言描述的逻辑:若刚开始lock是false,则TSL返回的old值为false,while循环条件不满足,直接跳过循环,进入临界区。若刚开始lock是true,则执行TSL后old返回的值是true,while循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行“解锁”。
- Swap指令,也称Exchange指令,或简称XCHG指令,Swap指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。优点是实现简单,无需像软件方法那样严格检查是否会有逻辑漏洞,适用于多处理机环境。缺点是不满足让权等待原则,暂时无法进入临界区的进程会占用CPU并循环执行Swap指令,从而导致“忙等”。
以下是C语言描述的逻辑:逻辑上看Swap和TSL并无太大差别,都是先记录下此时临界区是否被上锁(记录在old变量上),再将上锁标记lock设置为true,最后检查old,如果old为false则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。
(十一)信号量机制
- 用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便地实现了进程互斥、进程同步。信号量就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如系统中只有一台打印机,就可以设置一个初值为1的信号量。操作系统提供了wait(S)和signal(S)原语,又称为P、V操作,来对信号量进行改变。
- 整型信号量是用一个整数型的变量作为信号量,用来表示系统中某种资源的数量,与普通整型变量的区别是对信号量的操作只有三种,即初始化、P操作和V操作。
比如某计算机系统中有一台打印机:
int S = 1;//初始化整型信号量,表示当前系统可用的打印机资源数
void wait(int S){//wait原语,相当于进入区
while(S <= 0) ;//如果资源不够,就一直循环等待
S = S-1;//如果资源足够,则占用一个资源
}
void signal(int S){//signal原语,相当于退出区
S = S+1;//使用完资源后,在退出区释放资源
}
进程P0:
wait(S);//进入区,申请资源
使用打印机资源;//临界区,访问资源
signal(S);//退出区,释放资源
在整型信号量机制中,P、V操作和双标志先检查法做的事情其实基本是一致的,但双标志先检查法由于并发问题可能会导致“检查”跟“上锁”两个步骤不能一气呵成,使得两个进程同时访问临界区,但是wait是一个原语,使得“检查”跟“上锁”两个步骤一气呵成,从而避免了这个问题。整型信号量机制存在的问题是不满足让权等待原则,会发生“忙等”。
- 记录型信号量即用记录型数据结构表示的数据量。
//记录型信号量的定义
typdef struct {
int value;//剩余资源数
struct process *L;//等待队列
}semaphore;
//某进程需要使用资源时,通过wait原语申请
void wait(semaphore S){
S.value --;
if(S.value < 0){
block(S.L);//如果value<0,则说明剩余资源不够,使用block原语使进程从运行态进入阻塞态,并将其挂到信号量S的等待队列中
}
}
//使用完资源之后,通过signal原语释放
void signal(semaphore S){
s.value++;
if(S.value <= 0){
wakeup(S.L);//释放资源之后,若value小于等于0,表明还有别的进程在等待这种资源,则使用wakeup原语唤醒等待队列中的一个进程,该进程从阻塞态变为就绪态
}
}
当S.value<0时表示资源已经分配完毕,因此进程应调用block原语进行自我阻塞,主动放弃处理机,并插入该类资源的等待队列中,可见该机制遵循“让权等待”原则,不会出现“忙等”现象。
- 信号量机制实现进程互斥的步骤?
1.分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应该放在临界区)
2.设置互斥信号量mutex,初值为1,可理解为进入临界区的名额
3.在进入区申请资源——P(mutex)
4.在退出区释放资源——V(mutex)
//信号量机制实现互斥
//简易的信号量定义方式
semaphore mutex = 1;//初始化信号量
P1(){
...
P(mutex);//使用临界资源前需要上锁
临界区代码段
V(mutex);//使用临界资源后需要解锁
}
P2(){
P(mutex);//使用临界资源前需要上锁
临界区代码段
V(mutex);//使用临界资源后需要解锁
}
对不同的临界资源我们需要设置不同的互斥信号量。并且P、V操作必须承兑出现,缺少P操作的话就不能保证临界资源的互斥访问,缺少V操作的话会导致资源永不被释放,等待进程永不被唤醒。
- 信号量机制实现进程同步,进程同步是让各进程按照要求有序地进行推进。用信号量实现进程同步的步骤?
1.分析什么地方需要实现“同步关系”,即保证“一前一后“执行的两个操作
2.设置同步信号量S,初始为0
3.在“前操作”之后执行V(S)
4.在“后操作”之前执行P(S)(前V后S)
//信号量机制实现进程同步
semaphore S = 0;
P1(){
代码1;
代码2;
V(S);
代码3;
}
P2(){
P(S);
代码4;
代码5;
代码6;
}
若先执行到V(S)操作,则S++后S=1,之后执行到P(S)操作时,由于S=1,表示有可用资源,会执行S–,S的值变回0,P2进程不会执行block原语,而是继续往下执行代码4。若先执行到P(S)操作,由于S=0,S–之后S=-1,表示此时没有可用资源,因此P操作会执行block原语,主动请求阻塞。之后执行完代码2,继而执行V(S)操作,S++,使S变回0,由于此时有进程在该信号量对应的阻塞队列中,因此会在V操作中执行wakeup原语,唤醒P2进程,这样P2就可以继续执行代码4了。
- 信号量机制实现前驱关系
1.每一对前驱关系关系都是一个进程同步问题,为每一对前驱关系各设置一个同步量
2.在“前操作”之后对相应的同步信号执行V操作
3.在“后操作”之前对相应的同步信号量执行P操作
(十二)进程互斥和同步问题
PV操作题目分析步骤:
- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系
- 整理思路。根据各进程的操作流程确定P、V操作的大致顺序
- 设置信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初值要看对应资源的初始值是多少)
1 生产者消费者问题
系统中有一组生产者和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。生产者和消费者共享一个初始为空、大小为n的缓冲区。
- 分析两对同步关系,只有缓冲区没满时,生产者才能把产品放入缓冲区;只有缓冲区不空的时候,消费者才能从中取出产品,否则必须等待。
- 分析一对互斥关系,缓冲区是临界资源,各进程需要互斥地访问,否则会出现数据覆盖的情况。
- 设置两个同步信号量full和empty,初始值分别为0和n,还要设置一个互斥信号量mutex,实现对缓冲区的互斥访问。
semaphore mutex = 1;//互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n;//同步信号量,表示空闲缓冲区的数量
semaphore full = 0;//同步信号量,表示产品的数量,也即非空缓冲区的数量
producer(){
while(1){
生产一个产品;
P(empty);
P(mutex);
把产品放入缓冲区;
V(mutex);
V(full);
}
}
consumer(){
while(1){
P(full);
P(mutex);
从缓冲区取出一个产品;
V(mutex);
V(empty);
使用产品;
}
}
需要注意的是,实现互斥的操作一定要在实现同步的P操作之后,否则会产生死锁的情况,因为P操作会导致进程阻塞;而由于V操作不会导致进程阻塞,因此两个V操作顺序可以交换。
2 多生产者多消费者问题
桌子上有一个盘子,每次只能向其中投入一个水果,爸爸专门向盘子里放苹果,妈妈专门向盘子里放橘子,儿子专门等着吃盘子里的橘子,女儿专门等着吃盘子里的苹果。当盘子空时,爸爸或妈妈才能向盘子中放一个水果,仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。
- 把盘子视为大小为1,初始为空的缓冲区,爸爸和妈妈视为不同类型的生产者进程,儿子和女儿视为不同类型的消费者进程。
- 分析互斥关系,对盘子即缓冲区的访问要互斥地进行
- 分析同步关系。父亲将苹果放入盘子之后,女儿才能取苹果;母亲将橘子放入盘子中,儿子才能取橘子;只有盘子为空时,父亲或母亲才能放入水果。(盘子为空这个事件可以由儿子或女儿触发,事件发生后才允许父亲或母亲放入水果)
- 设置一个互斥信号量,设置同步信号量apple=0,orange=0,plate=1,表明初始时盘子有0个苹果,0个橘子,有一个空盘子。
semaphore mutex = 1;//实现互斥访问盘子(缓冲区)
semaphore apple = 0;//盘子中有多少个苹果
semaphore orange = 0;//盘子中有多少个橘子
semaphore plate = 1;//盘子中还可以放多少个水果
dad(){
while(1){
准备一个苹果
P(plate);
P(mutex);
把苹果放入盘子
V(mutex);
V(apple);
}
}
mom(){
while(1){
准备一个橘子
P(plate);
P(mutex);
把橘子放入盘子
V(mutex);
V(orange);
}
}
daughter(){
while(1){
P(apple);
P(mutex);
从盘子中取出苹果
V(mutex);
V(plate);
吃掉苹果
}
}
son(){
while(1){
P(orange);
P(mutex);
从盘子中取出橘子
V(mutex);
V(plate);
吃掉橘子
}
}
该题即使不设置专门的互斥信号量,也不会出现多个进程同时访问盘子的现象,因为本题的缓冲区大小为1,在任何时刻,apple、orange、plate三个同步信号量中最多只有一个是1,因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区。如果缓冲区的大小大于1的话,就需要设置专门的互斥信号量。
在本题中有一个难点,就是如果从单个进程来看这个题目的话,会出现以下情况:如果盘子里有苹果,那么一定要女儿取走苹果后父亲或母亲才能往盘子里放水果;如果盘子里有橘子,那么一定要儿子取走橘子后父亲或母亲才能往盘子里放水果。这就意味着有4对同步关系,那么就需要设置4个同步信号量来实现,但事实上我们从“事件”的角度来考虑,把上述4对进程的前后关系抽象为一对事件的前后关系,盘子变空事件引起放入水果事件,盘子变空事件可以由儿子或女儿引发,放水果事件可以是父亲或母亲执行,那么这样的话就可以用一个同步信号量来解决了。
3 抽烟者问题
假设一个系统有三个抽烟者进程和一个供应者进程,每个抽烟者不停地卷烟并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸、第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放在桌子上,拥有剩下的那种材料的抽烟者卷一根烟并抽掉它,并给供应者进程一个信号告诉完成了,供应者就会放另外两种材料在桌子上,这个过程一直重复(让三个抽烟者轮流抽烟)。
- 该题可视为可生产多种产品的单生产者和多消费者问题。
- 分析互斥关系,桌子可以抽象为容量为1的缓冲区,每次只能放入一种组合,组合一是纸+胶水,组合二是烟草+胶水,组合三是烟草+纸,需要互斥访问。
- 分析同步关系。桌子上有组合一,第一个抽烟者才能从桌子上取走东西;桌子上有组合二,第二个抽烟者才能从桌子上取走东西;桌子上有组合三,第三个抽烟者才能从桌子上取走东西;只有抽烟者抽完烟后发出信号,供应者才会将下一个组合放到桌子上。
- 设置同步信号量offer1=0,offer2=0,offer3=0,表明初始时桌子上没有三种材料组合,finish=0,表明初始时没有抽烟者进程发出完成的信号。
semaphore offer1 = 0;//桌子上组合1的数量
semaphore offer2 = 0;//桌子上组合2的数量
semaphore offer3 = 0;//桌子上组合3的数量
semaphore finish = 0;//抽烟是否完成
int i = 0;//用于实现三个抽烟者进程轮流抽烟
provider(){
while(1){
if(i==0){
将组合1放桌子上
V(offer1)
}else if(i==1){
将组合2放桌子上
V(offer2)
}else if(i==2){
将组合3放桌子上
V(offer3)
}
i = (i+1)%3;
P(finish);
}
}
smoker1(){
while(1){
P(offer1);
从桌子上拿走组合1,卷烟并抽掉
V(finish);
}
}
smoker2(){
while(1){
P(offer2);
从桌子上拿走组合2,卷烟并抽掉
V(finish);
}
}
smoker3(){
while(1){
P(offer3);
从桌子上拿走组合3,卷烟并抽掉
V(finish);
}
}
4 读者-写者问题
有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误,因此要求:1.允许多个读者同时对文件执行读操作;2.只允许一个写者往文件中写信息;3.任一写者在完成写操作之前不允许其他读者或写者工作;4.写者执行写操作前,应让已有的读者和写者全部退出。
- 分析互斥关系。写者进程跟写者进程需要互斥访问同一个文件,写者进程跟读者进程也需要互斥访问一个文件。
semaphore rw = 1;//用于实现对共享文件的互斥访问
int count = 0;//记录当前有几个读进程在访问文件
semaphore mutex = 1;//用于保证count变量的互斥访问
writer(){
while(1){
P(rw);//写之前“加锁”
写文件
V(rw);//写完了“解锁”
}
}
reader(){
while(1){
P(mutex);//各读进程互斥访问count
if(count == 0)//由第一个读进程负责
P(rw)//读之前加锁
count++;//访问文件的读进程数+1
V(mutex);
读文件
P(mutex);//各读进程互斥访问count
count--;//访问文件的进程数-1
if(count == 0)//由最后一个读进程负责
V(rw);//读完了解锁
V(mutex);
}
}
上述代码使用了count变量来记录当前有多少个读进程在访问文件,并且只对第一个读进程执行P操作,这样的话,写进程就不能跟读进程同时访问共享文件,而多个读进程可以同时访问共享文件。还设置了一个互斥信号量来使得count变量能够互斥访问,因为如果count变量不互斥访问的话,由于读进程是并发执行的,有可能在第一个读进程还没有执行count++之前,进程切换到另外一个读进程,这样的话两个读进程都执行了P操作,就会使得读进程之间没办法共同访问共享文件了。
上述代码仍然存在着一个问题,那就是如果有源源不断的读进程到来,那么写进程就会一直被阻塞,产生”饿死“现象。
在上述代码之上进行改进,增加一个互斥信号量w用于实现“写优先”(相对公平的读写,不是完全的写优先,也乘读写公平法),加上互斥信号量w之后,读者进程仍然可以共同访问共享文件,写进程之间仍然需要互斥访问共享文件,写进程跟读进程也需要互斥访问共享文件;并且能够发现,如果有一个读进程正在读文件时,另一个写进程要写文件,那么写进程会被阻塞,紧接着另一个读进程也要读文件,这个读进程也会被阻塞,写进程会在后来的读进程之前被唤醒,后来读进程会在写进程执行完毕之后才能被继续执行,这就解决了饥饿问题。另一种情况,如果一开始是一个写进程在访问文件,然后有一个读进程想要读文件以及另一个写进程想要写文件,那么读进程跟新的写进程都会被阻塞,然后等第一个写进程执行完之后会唤醒读进程,读进程执行完毕才会唤醒写进程。
semaphore rw = 1;//用于实现对共享文件的互斥访问
int count = 0;//记录当前有几个读进程在访问文件
semaphore mutex = 1;//用于保证count变量的互斥访问
semaphore w = 1;//用于实现“写优先”
writer(){
while(1){
P(w)
P(rw);//写之前“加锁”
写文件
V(rw);//写完了“解锁”
V(w)
}
}
reader(){
while(1){
P(w)
P(mutex);//各读进程互斥访问count
if(count == 0)//由第一个读进程负责
P(rw)//读之前加锁
count++;//访问文件的读进程数+1
V(mutex);
V(w)
读文件
P(mutex);//各读进程互斥访问count
count--;//访问文件的进程数-1
if(count == 0)//由最后一个读进程负责
V(rw);//读完了解锁
V(mutex);
}
}
5 哲学家进餐问题
一张圆桌上坐着五位哲学家,每两个哲学家之间的桌子上摆放一根筷子,桌子的中间是一碗米饭,哲学家们只会思考和进餐,哲学家在思考时并不影响他人,只有当哲学家饥饿时,才试图拿起左右两根筷子(一根一根地拿)。如果筷子已经在他人手上,则必须等待,饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕之后,放下筷子继续思考。
- 关系分析。系统中有五个哲学家进程,5个哲学家与左右邻居对其中间的筷子的访问是互斥的。
- 这个题目只有互斥关系,但与之前的问题不同,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免资源分配不当而造成死锁问题是关键。
- 设置互斥信号量数组chopsticks[5] = {1,1,1,1,1}用于实现对5个筷子的互斥访问,并对哲学家按0-4进行编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5
防止死锁的发生可以有三种处理方法:
- 可以对哲学家进程加一些限制条件,比如最多允许四个哲学家同时进餐,这样可以保证至少有一个哲学家是可以拿到两根筷子的。
semaphore chopsticks[5] = {1,1,1,1,1};
int count = 4;//还有多少个哲学家进程能够一起拿筷子
semaphore mutex = 1;//用于实现前4个哲学家能够一起拿筷子,最后一个哲学家要跟前4个哲学家互斥地拿筷子
Pi(){ //i号哲学家的进程
while(1){
if(count == 1 || count == 0)//第4个哲学家负责上锁,然后第5个哲学家如果想要拿筷子就会被阻塞在这里
P(mutex);
count --;
P(chopsticks[i]);//拿左
P(chopsticks[(i+1)%5]);//拿右
吃饭
V(chopsticks[i]);//放左
V(chopsticks[(i+1)%5]);//放右
count ++;
if(count == 1)//当还有一个哲学家进程可以一起拿筷子的时候就解锁
V(mutex);
思考
}
}
}
- 可以要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞,这就避免了占有一支筷子然后等待另一支筷子的情况。
semaphore chopsticks[5] = {1,1,1,1,1};
semaphore mutex = 1;//互斥地取筷子
Pi(){ //i号哲学家的进程
while(1){
P(mutex);
if(i % 2 == 1)
{
P(chopsticks[i]);//拿左
P(chopsticks[(i+1)%5]);//拿右
V(mutex);
吃饭
V(chopsticks[i]);//放左
V(chopsticks[(i+1)%5]);//放右
思考
}else if(i % 2 == 0)
{
P(chopsticks[(i+1)%5]);//拿右
P(chopsticks[i]);//拿左
V(mutex);
吃饭
V(chopsticks[i]);//放左
V(chopsticks[(i+1)%5]);//放右
思考
}
}
}
- 要求各哲学家拿筷子的事件需要互斥地进行,这样的话当有哲学家拿筷子被阻塞的时候,其他想要拿筷子的哲学家也会被阻塞,只有当正在吃饭的哲学家放下筷子时,被阻塞的哲学家才会被唤醒。
semaphore chopsticks[5] = {1,1,1,1,1};
semaphore mutex = 1;//互斥地取筷子
Pi(){ //i号哲学家的进程
while(1){
P(mutex);
P(chopsticks[i]);//拿左
P(chopsticks[(i+1)%5]);//拿右
V(mutex);
吃饭
V(chopsticks[i]);//放左
V(chopsticks[(i+1)%5]);//放右
思考
}
}
(十三)管程
- 信号量机制存在的问题是程序编写困难且容易出错,于是就出现了一种让程序员不需要关注复杂的PV操作,让写代码变得更加轻松,这就是管程,一种高级同步机制。
- 管程是用来实现进程的互斥和同步的。管程是一种特殊的软件模块,由这些部分组成:1.局部于管程的共享数据结构说明;2.对该数据结构进行操作的一组过程(函数);3.对局部于管程的共享数据结构设置初始值的语句;4.管程有一个名字。
- 管程的基本特征:1.局部于管程的数据只能被局部于管程的过程所访问;2.一个进程只有通过调用管程中的过程才能进入管程访问共享数据;3.每次仅允许一个进程在管程内执行某个内部过程。
- 定义了管程之后,会由编译器来负责实现各个进程互斥进入管程的过程,程序员不需要关心如何实现进程互斥,只需要调用管程中定义的方法即可。管程中还会设置一些条件变量和等待/唤醒操作,用来实现进程的同步问题。其实管程就是对一些复杂操作的封装,程序员只需要调用这些封装好的过程(方法、函数)即可实现进程同步跟进程互斥。
- 在java中,如果用关键字synchronized来描述一个函数,那么这个函数在同一时间内只能被一个线程调用。这跟管程十分类似。
(十四)死锁
-
死锁是指各个进程互相等待对方手里的资源,导致各进程都被阻塞,无法向前推进的现象。
-
饥饿是指由于长期得不到想要的资源,某进程无法向前推进的现象。
-
死循环是指在某进程执行的过程中一直跳不出某个循环的现象,有时是因为程序逻辑bug导致的,有时是程序员故意设计的。
-
死锁、饥饿、死循环的共同点和区别:
-
死锁产生的必要条件:
互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁
不剥夺条件:进程所获得的资源在未使用之前,不能由其他进程强行夺走,只能主动释放
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
(注意发生死锁时一定有循环等待,但是发生循环等待时未必死锁;即如果同类资源数大于1,则即使有循环等待,也未必会发生死锁,但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件了) -
什么时候会发生死锁?
1.对系统资源的竞争。各进程对不可剥夺的资源的竞争可能会产生死锁,对可剥夺的资源的竞争是不会引起死锁的。
2.进程推进顺序非法。请求和释放资源的顺序不当也有可能会产生死锁。
3.信号量的使用不当就会造成死锁。 -
死锁的处理策略?
1.预防死锁,破坏死锁必要条件中的其中一个。
2.避免死锁,用某种方法,防止系统进入不安全状态,从而避免死锁
3.死锁的检测和解除,允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁。
1 预防死锁的具体策略(静态策略)
- 破坏互斥条件。如果把只能互斥使用的资源改造为允许共享使用,则系统就不会进入死锁状态。比如利用SPOOLing技术,操作系统可以把独占设备在逻辑上改造成共享设备,比如将打印机改造成共享设备(将各个进程对打印机的请求传给一个输出进程,再由这个输出进程去使用打印机)。该策略的缺点是并不是所有的资源都可以被改造成可共享使用的资源,并且为了系统安全,很多地方都必须保护这种互斥性。
- 破坏不剥夺条件。有两种方案:一、当某个进程请求新的资源得不到满足时,它立即释放保持的所有资源,待以后需要时再重新申请,也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件;二、当某个进程需要的资源被其他进程所占用的时候,可以由操作系统协助,将想要的资源强行剥夺,这种方式一般需要考虑各进程的优先级。该策略的缺点是实现起来比较复杂,并且释放已获得的资源额能会造成前一阶段工作的失效,因此这种方法一般只适用于易保存和易恢复的资源,比如CPU;如果反复申请和释放资源会增加系统开销,降低系统的吞吐量;如果采用方案一,那么意味着只要暂时得不到某个资源,之前获得的资源就都需要放弃,以后再重新申请,如果一直发生这样的情况,就会导致进程饥饿。
- 破坏请求和保持条件。可以采用静态分配方法,即进程在运行前一次性申请完它所需要的全部资源,在它的资源未满足前不让它投入运行,一旦投入运行后,这些资源就一直归它所有,该进程就不会请求别的任何资源了。该方法的缺点是有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都保持着这些资源,那么就会造成严重的资源浪费,资源利用率太低,并且该策略也有可能会导致某些进程饥饿(比如a类进程只需要资源1才能投入运行,b类资源只需要资源2就能投入运行,而c类进程需要资源1和2才能投入运行,那么如果有源源不断的a类或b类进程,那么c类进程就会很难得到运行)。
- 破坏循环等待条件。可以采用顺序资源分配法,首先给系统中的资源编号,规定每个进程必须按编号递增的顺序来请求资源,同类资源(即编号相同的资源)一次性申请完。这样的话,一个进程只有在占有小编号资源的情况下,才有资格申请更大编号的资源,已经持有大编号的资源不可能逆向回来申请小编号的资源,这样就不会产生循环等待的现象。该策略的缺点是不方便增加新的设备,因为可能需要重新分配所有的编号;进程实际使用资源的的顺序可能和编号递增顺序不一致,会导致资源的浪费(比如进程需要先使用大编号的资源,然后才需要使用小编号的资源,但是由于需要按编号递增的顺序来请求资源,所以需要先申请小编号的资源,这就可能会使小编号的资源浪费);必须按规定次序申请资源,这就导致用户编程麻烦的问题(可能不同系统对相同资源的编号不一样,这样的话就需要改变编程代码)。
2 避免死锁的具体策略(动态策略)
- 所谓安全序列是指如果系统按照这种序列分配资源,则每个进程都能顺利完成,只要能找出一个安全序列,那么系统就是安全的,安全序列可能会有多个。如果分配了资源之后,在系统中找不出任何一个安全序列,那么系统就进入了不安全状态,这就意味着之后可能所有进程都无法顺序执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
- 如果系统处于安全状态,那么就一定不会发生死锁,如果系统处于不安全状态,那么就有可能会发生死锁。处于不安全状态未必会发生死锁,但发生死锁时一定是处于不安全状态。
- 在资源分配之前预先判断这次分配是否会导致系统进入不安全的状态,以此决定是否答应资源分配请求,这是“银行家算法”的核心思想。银行家算法是dijkstra为银行系统设计的,确保银行在发放贷款时不会发生不能满足所有客户需要的情况,后来该算法被用在操作系统中,用于避免死锁。
- 通过一个例子来了解银行家算法。
在系统中有五个进程P0-P4,3种资源R0-R2,初始数量为(10,5,7),则某一时刻的情况可以表示如下:
对这一时刻下已分配的资源进行加总,能够计算出当前剩余资源为(3,3,2),用剩余资源跟各进程最多还需要的资源数进行对比,若剩余资源数大于某进程最多还需要的资源数,则该进程一定能执行完毕,并且系统能够收回分配给它的资源,收回资源后更新剩余资源数,继续进行对比,重复执行下去如果能够让所有的进程执行完毕,那么这个系统就是安全的(找到了安全序列,找安全序列的过程称为安全性算法)。也就是说安全性算法的步骤是这样的:检查当前剩余的可用资源是否能满足某个进程的最大需求,如果可以就将该进程加入安全序列,并把该进程持有的资源全部回收;然后不断重复这个过程,如果最终所有进程能够加入到安全序列,那么这个系统就是安全的。
用代码实现的话是这样的:
假设系统中有n个进程,m种资源,每个进程在运行前先声明对各种资源的最大需求数,则可用一个nm的矩阵Max(二维数组)表示所有进程对各种资源的最大需求数,Max[i,j] = K表示进程Pi最多需要个资源Rj。同理,用一个nm的分配矩阵Allocation表示对所有进程的资源分配情况。Max-Allocation=Need矩阵,表示各进程还需要多少各类资源。另外,还要用一个长度为m的一维数组Available表示当前系统中还有多少可用资源。当某进程Pi向系统申请资源时,可用一个长度为m的一维数组Requesti表示本次申请的各种资源量。
可用银行家算法预判本次分配是否会导致系统进入不安全状态:
1.检查此次申请是否超过了之前声明的最大需求数。如果Requesti[j]≤Need[i,j](0≤j≤m)便转向2,否则认为出错,因为所需资源数已经超过了它所宣布的需要的最大值。
2.检查此时剩余的可用资源是否还能满足这次请求。如果Requesti[j]≤Available[j](0≤j≤m),便转向3,否则表示尚无足够资源,Pi需要等待。
3.试探分配资源,修改各数据结构数值。系统试探着把资源分配给进程Pi,并修改相应的数据(并非真的分配,修改数值只是为了做预判)。Available = Available - Requesti;Allocation[i,j] =
Allocation[i,j] + Requesti[j];Need[i,j] = Need[i,j] - Requesti[j];
4.用安全性算法检查此次分配是否会导致系统进入不安全状态。操作系统执行安全性算法,检查此次分配资源后,系统是否处于安全状态,若安全,才正式分配,否则恢复相应数据,让进程阻塞等待。
3 死锁的处理策略
- 为了能对系统是否已经发生了死锁进行检测,需要用某种数据结构来保存资源的请求和分配信息,并且还需要提供一种算法利用这些数据结构来检测系统是否已经进入了死锁状态。
- 利用的数据结构称为资源分配图,这种数据结构包含两种结点,分别是进程结点(对应一个进程,一般用圆表示)和资源结点(对应一类资源,可能有多个,一般用矩形表示一类资源,矩形中的小圆代表这类资源的数量)。这种数据结构还包含两种边,一种是由进程结点指向资源结点的边,称为请求边,表示进程想要申请多少个资源(每条边代表一个),另一种是由资源结点指向进程结点的边,表示已经为进程分配了几个资源(每条边代表一个)
-
如果资源分配图中的进程都能够被顺利执行,这种顺利执行的情况包括阻塞进程在其他进程执行完毕并归还资源后能够执行,那么可以将图的边完全消除,就称这个图是可完全简化的,此时一定没有发生死锁(相当于能够找到一个安全序列)。相反如果最终不能消除所有边,那么此时就是发生了死锁,最终还连着边的进程就是处于死锁状态的进程。
-
检测死锁的算法:
1.在资源分配图中,找出既不阻塞又不是孤点的进程Pi(即找出一条有向边与它相连(不是孤点),且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量(不阻塞))。消去它所有的请求边和分配边,使之成为孤立的结点。
2.进程Pi所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。 -
死锁的解除。
1.资源剥夺法。挂起(暂时存放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应该防止被挂起的进程长时间的得不到资源而饥饿。
2.撤销进程法(终止进程法),强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但付出的代价可能比较大,因为这些被终止的进程可能是运行了很长时间并且快结束的进程。
3.进程回退法,让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。
选择什么方法需要从几个方面入手:1.进程优先级;2.已执行时间;3.执行进度;4.进程使用了多少资源;5.进程是交互式的还是批处理式的。
三、内存管理
(一)前述知识
- 程序经过编译、链接后生成的指令中指明的是逻辑地址(相对地址,即相对于进程的起始地址而言的地址),如何将指令中的逻辑地址转换成物理地址(绝对地址)有三种策略:
1.绝对装入:在编译时,如果知道程序将放到内存中的哪个位置,编译程序将产生绝对地址的目标代码,装入程序按照装入模块中的地址,将程序和数据装入内存。也就是说,编译、链接后得到的装入模块的指令直接就使用了绝对地址。绝对装入只适用于单道程序环境(早期还没有操作系统的时候),灵活性较低。
2.静态重定位(可重定位装入):编译、链接后的装入模块的地址都是从0开始的,指令中使用的地址、数据相对于起始地址而言的逻辑地址。可根据内存的当前情况,将装入模块装入到内存的适当位置。装入时对地址进行“重定位”,将逻辑地址变换为物理地址(地址变换是在装入时一次完成的)。静态重定位的特点是一个作业装入内存时,必须分配其要求的全部内存空间,如果没有足够的内存,就不能装入该作业。作业一旦进入内存后,在运行期间就不能再移动,也不能再申请内存空间。
3.动态重定位(动态运行时装入):编译、链接后的装入模块的地址都是从0开始的。装入程序把装入模块装入内存后,并不会立即把逻辑地址转换成物理地址,而是把地址转换推迟到程序真正要执行时才进行。因此装入内存后所有的地址依然是逻辑地址,这种方式需要一个重定位寄存器的支持(重定位寄存器存放装入模块存放的起始位置)。采用动态重定位时允许程序在内存中发生移动,并且可将程序分配到不连续的存储区中,在程序运行前只需装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存,便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间。
- 编译:由编译程序将用户源代码编译成若干个目标模块(把高级语言翻译成机器语言)
- 链接:由链接程序将编译后形成的一组目标模块,以及所需库函数链接在一起,形成一个完整的装入模块
链接的三种方式:
1.静态链接:在程序运行之前,先将各目标模块及它们所需的库函数连接成一个完成的可执行文件(装入模块),之后不再拆开。
2.装入时动态连接:将各目标模块装入内存时,边装入边连接
3.运行时动态链接:在程序执行中需要该目标模块时,才对它进行链接,其优点是便于修改和更新,便于实现对目标模块的共享。
- 装入(装载):由装入程序将装入模块装入内存运行
(二)操作系统需要实现的功能
- 操作系统作为系统资源的管理者,需要对内存进行管理。
1.操作系统要负责内存空间的分配和回收
2.操作系统需要提供某种技术从逻辑上对内存空间进行扩充
3.操作系统需要提供地址转换功能,负责程序的逻辑地址与物理地址的转换(三种装入方式)
4.操作系统需要提供内存保护功能,保证各进程在各自存储空间内运行,互不干扰。内存保护可采取两种方法:一是在CPU中设置一对上、下限寄存器,存放进程的上、下限地址。进程的指令要访问某个地址时,CPU检查是否越界;方法二是采用重定位寄存器(基址寄存器)和界地址寄存器(限长寄存器)进行越界检查,重定位寄存器中存放的是进程的起始物理地址,界地址寄存器中存放的是进程的最大逻辑地址,如果CPU要访问某个逻辑地址的话,将这个逻辑地址与界地址寄存器中的最大逻辑地址进行比较,判断是否越界,如果不越界则与重定位寄存器中的起始物理地址进行一个相加,然后就能够得到实际想要访问的物理地址了。
(三)内存空间的扩充
- 早期的计算机内存很小,经常会出现内存大小不够的情况,后来人们引入了覆盖技术,用来解决“程序大小超过物理内存总和”的问题。覆盖技术的思想是将程序分为多个段(模块),常用的段常驻内存,不常用的段在需要时才调入内存。内存分为一个“固定区”和若干个“覆盖区”,需要常驻内存的段放在“固定区”中,调入之后就不再调出(除非运行结束),不常用的段放在“覆盖区”,需要时调入内存,用不到时调出内存。采用覆盖技术,按照程序自身逻辑结构,让那些不可被同时访问的程序段共享一个覆盖区,能够节省很多内存空间。覆盖结构必须由程序员来声明,由操作系统完成自动覆盖,这对用户不透明,增加了用户的编程负担,因此覆盖技术只用于早起的操作系统中。
- 除了覆盖技术还有一种内存扩充技术称为交换技术。交换(对换)技术的设计思想是:内存空间紧张时,系统将内存中的某些进程暂时换出外存,把外存中某些已具备运行条件的的进程换入内存(进程在内存与磁盘间动态调度)。前面所提到的中级调度就是为了实现交换技术而使用的一种调度策略,这里还涉及到前面所提到的七状态模型。
- 具有交换(对换)功能的操作系统中,通常把磁盘空间分为文件区和对换区两部分。文件区主要用于存放文件,主要追求存储空间的利用率,因此对文件区空间的管理采用离散分配方式;对换区空间只占磁盘空间的小部分,被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度,因此对换区空间的管理主要追求换入换出速度,因此通常对换区采用连续分配方式。总之,对换区的I/O速度比文件区的更快。
- 交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。换出的进程通常是阻塞进程、优先级低的进程(为了防止低优先级的进程刚被调入内存就被调出,有的操作系统会考虑进程在内存中的驻留时间)。注意进程被换出外存时,与进程相关的PCB仍然会常驻内存,不会调出外存,用来将进程重新调入内存。
(四)内存空间的分配与回收
- 内存空间的分配方式有两种,分别是连续分配管理方式和非连续分配管理方式。
1 连续分配管理方式
- 连续分配管理方式是指系统为用户进程分配的是一个连续的内存空间。连续分配管理方式又可以分为三种:分别是单一连续分配、固定分区分配、动态分区分配。
- 在单一连续分配方式中,内存被分为系统区和用户区,用户区通常位于内存的低地址部分,用于存放操作系统相关数据,用户区用于存放用户进程相关数据。采用单一连续分配方式的话,是不支持多个进程并发运行的,同一时间内存中只能有一道用户程序,用户程序进程独占整个用户区空间。这种分配方式的优点是实现简单,无外部碎片,可以采用覆盖技术来扩充内存,不一定需要采取内存保护;缺点是只能用于单用户、单任务的操作系统中,有内部碎片(分配给某进程的内存区域中,如果有些部分没有用上,就是内部碎片),存储器利用率极低。
- 随着支持多道程序的系统出现,为了能够在内存中装入多道程序,且这些程序之间又不会相互干扰,于是将整个用户空间划分为固定大小的分区,在每个分区中只装入一道程序,这就形成了最早、最简单的一种可运行多道程序的内存管理方式。固定分区分配还分为分区大小相等跟分区大小不等两种。分区大小相等的方式缺乏灵活性,但是很适用于用一台计算机控制多个相同对象的场合。分区大小不等的方式灵活性较强,可以满足不同大小的进程需求,可以根据常在系统中运行的作业大小情况对内存进行划分。操作系统需要建立一个分区说明表,来实现各个分区的分配与回收,每个表项对应一个分区,通常按分区大小排列,每个表项包括对应分区的大小、起始地址、状态(是否已分配),当某用户程序要装入内存时,由操作系统内核程序根据用户程序大小检索分区说明表,从中找到一个能满足大小、未分配的分区,将之分配给该程序,然后修改状态为“已分配”。这种分配方式是实现简单,无外部碎片;缺点是当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能;并且该分配方式会产生内部碎片,内存利用率低。
- 动态分区分配(可变分区分配)不会像固定分区分配方式一样预先划分内存分区,而是在进程装入内存时,根据进程的大小动态来建立分区,并使分区的大小正好适合进程的需要,因此系统分区的大小和数目是可变的。使用这种分配方式,系统常通过空闲分区表(每个空闲分区对应一个表项(一个数据结构,一个空闲分区表就是这个数据结构类型的数组),表项中包含分区大小、分区起始地址等信息)或空闲分区链(使用一个链表,链表中的每个元素是一个分区,每个分区的起始部分和末尾部分分别设置前向指针和后向指针,起始部分处还可记录分区大小等信息)来记录内存的使用情况;当有很多个空闲分区都能满足一个新作业的需求时,需要按照一定的动态分区分配算法从空闲分区表或空闲分区链中选出一个分区分配给该作业,并且要对空白分区表或空白分区链的状态进行更新;当某个进程执行结束后,要对内存进行回收,如果要回收的内存前面或后面有相邻的空闲分区,要将这些相邻的空白分区合并为一个,修改相应的表项或者空白分区链。这种分配方式没有内部碎片,但会产生外部碎片(内存中某些空闲分区太小而难以利用)。
- 如果内存中空闲空间的总和本来可以满足某进程的要求,但由于进程需要的是一整块连续的内存空间,因此这些“碎片”不能满足进程的需求,可以通过紧凑(拼凑)技术(将进程挪位,凑出一块连续的内存空间)来解决外部碎片。
- 动态分区分配方式应该使用前面三种装入方式中的动态重定位方式。
2 动态分区分配算法
- 在动态分区分配方式中,当有很多个空闲分区都能满足需求时,利用动态分区分配算法来选择分区进行分配。动态分区分配算法包括首次适应算法、最佳适应算法、最坏适应算法和邻近适应算法。
- 首次适应算法的思想是:每次从低地址开始查找,找到第一个能满足大小的空闲分区。实现方式是让空闲分区以地址递增的次序进行排列,然后每次分配内存的时候顺序查找空闲分区链或空闲分区表,找到大小能满足要求的第一个空闲分区进行分配,然后更新空闲分区表或空闲分区链。
- 最佳适应算法:由于动态分区分配是一种连续分配方式,为各进程分配的内存空间必须是连续的一整片内存空间,因此为了保证大进程到来时能够有连续的大空间,可以尽可能留下大片的空闲区,优先使用更小的空闲分区。实现方式是让空闲分区按照容量递增次序链接,每次分配内存时顺序查找空闲分区链或者空闲分区表,找到大小能满足要求的第一个空闲分区进行分配,然后更新空闲分区表或空闲分区链。这种算法有一种很明显的缺点,就是每次都选用最小的分区进行分配,这样会留下越来越多的难以利用的内存块,也就是说这种算法会产生很多的外部碎片。
- 最坏适应算法(最大适应算法):为了解决最佳适应算法产生过多外部碎片的问题,可以在每次分配的时候优先使用最大的连续空闲区,这样每次分配后剩余的空闲分区就不会太小,更方便利用。实现方式是让空闲分区按照容量递减次序链接,每次分配内存时顺序查找空闲分区链或空闲分区表,找到大小能满足要求的第一个空闲分区进行分配,然后更新空闲分区表或空闲分区链。这种算法的缺点是由于每次分配都是选择最大的分区进行分配,会导致较大的连续空闲区被迅速用完,如果有大进程到来的话,就没有内存分区可用了。
- 邻近适应算法:首次适应算法每次都是从链头开始查找的,这可能会导致低地址部分出现很多小的空闲分区,而每次分配查找时,都要经过这些空闲分区,增加了查找的开销,如果每次都从上次查找结束的位置开始检索,就能解决上述问题。实现方式是让空闲分区按照地址递增的顺序排列(可排成一个循环链表),每次分配内存时从上次查找结束的位置开始查找空闲分区链或者空闲分区表,找到大小能满足要求的第一个空闲分区进行分配。邻近适应算法的规则可能会导致无论低地址、高地址部分的空闲分区都有相同的概率被使用,也就导致了高地址部分的大分区更可能被使用,划分为小分区,导致最后无大分区可用(最坏适应算法的缺点)。
3 非连续分配管理方式
- 非连续分配管理方式包括基本分页存储管理、基本分段存储管理、段页式存储管理。
(1)基本分页存储管理
- 分页存储就是将内存空间分为一个个大小相等的分区,每个分区就是一个“页框”(页框=页帧=内存块=物理块=物理页面),每个页框有一个编号,即页框号(页框号=页帧号=内存块号=物理块号=物理页号),页框号从0开始。由于要将进程读取到内存当中,因此操作系统将进程的逻辑地址空间也分为与页框大小相等的一个个部分,每个部分称为一个页或者页面,每个页面也有一个编号,即页号,页号也是从0开始。操作系统以页框为单位为各个进程分配内存空间,进程的每个页面分别放入一个页框中,也就是说进程的页面和内存的页框有一一对应的关系。
- 为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表,页表通常存放在PCB中。进程的每个页面都会对应页表中的一个页表项,页表项中包括了页号和块号,这样的话页表就能够记录进程页面和实际存放内存块之间的映射关系。当知道内存大小和页面大小(或内存块大小)就能够知道内存会被分为多少个内存块,就能知道页表项中块号所需要表示的范围,然后就能知道至少需要多少个字节才能存储页表项中的块号。由于页表项是连续存储的,因此页号可以是隐含的,不占用存储空间(类比数组),知道页表项中存储块号所需字节,就能够找到页号为i的页表项(假设页表中的各页表项从内存地址为X的地方开始连续存放,块号所需要的存储空间是3B,那么i号页表项的存放地址=X+3i)。需要注意的是,页表项存储的是内存块号,而不是内存块的起始地址,j号内存块的起始地址 = j内存块大小。
- 将进程地址空间分页之后,操作系统如何实现逻辑地址到物理地址的转换?虽然各个进程的页面在内存中是离散存放的,但是页面内部都是连续存放的。因此如果要访问进程中的逻辑地址A,则需要确定逻辑地址A对应的页号P(逻辑地址A除以页面长度然后取整),然后找到P号页面在内存中的起始地址(需要查页表),然后确定逻辑地址A的“页内偏移量”W(逻辑地址对页面长度进行取余),最终得到逻辑地址A对应的物理地址=P号页面在内存中的起始地址+页内偏移量。
- 在计算机内部,地址是用二进制表示的,如果页面大小刚好是2的整数幂,则计算机硬件可以很快速的把逻辑地址拆分成(页号,页内偏移量)(下图1),而且还能很快速地得到逻辑地址对应的物理地址(下图2)
-
分页存储管理的逻辑地址结构可以分为两部分,一部分是页号,另一部分是页内偏移量W(或称页内地址),如果有k位表示页内偏移量,则说明该系统中一个页面的大小是2k个内存单元;如果有M位表示页号,则说明在该系统中,一个进程最多允许有2M个页面。
-
基本地址变换机构就是计算机中用于实现逻辑地址和物理地址转换的一组硬件机构。基本地址变换机构可以借助进程的页表将逻辑地址转换成物理地址,通常会在系统中设置一个页表寄存器(PTR),存放页表在内存中的起始地址F和页表长度M,进程未执行时,页表的起始地址和页表长度会放在PCB中,当进程被调度时,操作系统内核会把它们放到页表寄存器中。基本地址变换机构将逻辑地址转换成物理地址的大致过程是这样的:当进程需要被调度上处理机运行时,与进程切换相关的内核程序会根据PCB恢复进程运行环境,相关的信息会被放到一系列寄存器中,包括页表寄存器和程序计数器PC(指向下一条指令的逻辑地址),采用分页存储管理方式的系统会将逻辑地址分为页号和页内偏移量两部分(结构固定不变),利用页号跟页表寄存器中的页表长度就能够判断出该页号是否合法(不合法则会发出一个越界中断信号,内中断),如何页号是合法的则根据页号和页表寄存器中的页表起始地址进行计算找到与页号对应的页表项,得到与页号相对应的块号,再结合页内偏移量就能得到与逻辑地址相对应的物理地址了。
-
在分页存储管理(页式管理)的系统中,只要确定了每个页面的大小,逻辑地址结构就确定了。因此,页式管理中地址是一维的,即只要给出一个逻辑地址,系统就可以自动地算出页号、页内偏移量两个部分,并不需要显示地告诉系统这个逻辑地址中,页内偏移量占多少位。
-
对页表项大小的进一步探讨。
-
基本地址变换机构在访问逻辑地址时,需要对内存进行两次访问,分别是将逻辑地址转换成物理地址过程中访问内存中的页表和转换结束后访问物理地址。
-
具有快表的基本地址变换机构会让地址的转换变得更加快速。快表又称联想寄存器(TLB,translation lookaside buffer),是一种访问速度比内存快很多的高速缓存(TLB不是内存),用来存放最近访问的页表项的副本,可以加速地址变换的速度,与此对应,内存中的页表被称为慢表。快表是一种专门的硬件,当进程切换的时候,快表中的内容也会被清除。引入了快表之后,某进程要访问逻辑地址的过程是这样的:先将逻辑地址的页号与页表寄存器中的页表长度进行比对判断是否越界,不越界的话则访问快表,如果进程是第一次运行的话,那么快表为空,会接着去访问内存中的页表(慢表),找到对应的页表项后会复制一份存储到快表中,然后通过获得的内存块号和逻辑地址的页内偏移量找到与逻辑地址对应的物理地址,之后就可以访问逻辑地址对应的内存单元了;如果进程不是第一次运行,快表不为空,如果快表中有我们所需要的页表项,则可以直接根据这个页表项和逻辑地址的页内偏移量得到与逻辑地址相应的物理地址;如果快表中没有我们所需要的页表项,则仍然是访问慢表查找页表项,然后同时将页表项添加到快表中去,如果快表已满,则必须按照一定的算法对旧的页表项进行替换,然后根据查找的页表项和逻辑地址的页内偏移量找到相对应的物理地址,访问对应的内存单元。能够发现,如果快表中有我们所要找的页表项的话(命中),那么我们访问某个逻辑地址的过程只需要访问一次内存,这能够一定程度上提升访问速度。
-
因为局部性原理,一般来说快表的命中率可以达到90%以上。
时间局部性是如果执行了程序的某条指令,那么不久后这条指令很可能再次执行,如果某个数据被访问过,不久之后该数据很可能再次被访问(因为程序中存在着大量的循环)。
空间局部性:一旦程序访问了某个存储单元,在不久之后,其附件的存储单元也很有可能被访问,因为很多数据在内存中都是连续存放的。
因此,由于局部性原理,可能连续很多次查到的都是同一个页表项。 -
快表TLB和普通高速缓存的区别在于,TLB中只有页表项的副本,而普通的高速缓存可能会有其他各种数据的副本。
-
单级页表存在那些问题?
如果某计算机系统按字节寻址(地址按字节存储),支持32位的逻辑地址,采用分页存储管理,页面大小位4KB,页表项长度位4B,由于4KB=212B,因此页内地址要用12位表示,剩余20位表示页号。因此,该系统中用户进程最多有220页,相应的就是一个进程的页表中最多有220个页表项,所以一个页表最大需要220*4B=222B,需要222/212=210个页框存储该页表。前面所提到的根据页号查询页表的方法是要在所有的页表项都是在内存中连续存放的基础上才能使用的,这样的话需要210个页框大小的内存来存放页表,这显然是比较困难的,并且还丧失了离散存储方式的一个最大的优点。此外,根据局部性原理,很多时候进程在一段事件内只需要访问某几个页面就可以正常运行了,因此没有必要让整个页表都常驻内存。
由此得到单级页表存在的两个问题:一是页表必须连续存放,因此当页表很大的时候,需要占用很多个连续的页框;二是没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。 -
解决单级页表存在的第一个问题,我们可以模仿解决进程在内存中必须连续存储的问题的解决方法,把页表进行分组(二级页表),使每个内存块刚好可以放入一个分组,然后为离散分配的页表再建立一张页表,称为页目录表(外层页表,外层页表),建立了二级页表的页号(第几个二级页表)与内存块号对应的映射关系。
假设逻辑地址空间是32位,页表项大小为4B,页面大小为4KB,则页内地址占12位,即单级页表结构的逻辑地址结构中页内偏移量占12位,剩余20位用于存储页号。
两级页表结构中又将页号分为一级页号和二级页号。由于页面大小为4KB,页表项大小为4B,那么一个内存块中就可以存储4KB/4B=210个页表项,在单级页表结构中用20位表示页号,则一个进程最多有220个页表项,那么在两级页表结构中,最多可以分为220/210=210个二级页表,则需要10位来表示二级页号,剩余10位用来表示一级页号,一级页号是用来查找页目录表找到对应的二级页表的页号,二级页号是用来在二级页表中查找对应的页表项的。
二级页表结构实现地址变换的过程大致为:
1.按照地址结构将逻辑地址拆分为三部分
2.从PCB中读出页目录表始址,再根据一级页号查页目录表,找到下一级页表在内存中的存放位置
3.根据二级页号查表,找到最终想访问的内存块号
4.结合页内偏移量得到物理地址
-
解决单级页表存在的第二个问题,可以在需要访问页面时才把页面调入内存(虚拟存储技术)。可以在页表项中(页目录表和二级页表都要)增加一个标志位,用于表示该页面是否已经调入内存。若想访问的页面不在内存中,则产生缺页中断(内中断),然后将目标页面从外存调入内存。
-
若采用多级页表机制,则各级页表的大小不能超过一个页面。
-
如果没有快表机构,则两级页表访问某逻辑地址需要访问三次内存(n级页表要访问n+1次内存),第一次访问内存中的页目录表,第二次是访问内存中的二级页表,第三次是访问目标内存单元。两级页表虽然解决了单级页表存在的问题,但是代价是要多一次对内存的访问,访问逻辑地址的时候需要花费更多的时间。
(2)基本分段存储管理
- 基本分段存储管理与分页存储管理方式最大的区别就是离散分配时所分配的地址空间的基本单位不同
- 分段的含义是:进程的地址空间会按照自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员用段名来编程),每段从0开始编址。操作系统在进行内存分配时,是以段为单位来进行分配的,每个段在内存中占据连续空间,但各段之间可以不相邻。由于分段是按逻辑功能模块划分的,因此用户编程更加方便,程序的可读性更高。在用汇编语言写程序的时候,段名会被编译程序翻译成对应的段号,而单元会被编译程序翻译成段内地址。
- 分段系统的逻辑地址结构由段号(段名)和段内地址(段内偏移量)所组成。段号的位数决定了每个进程最多可以分为几个段,段内地址位数决定了每个段的最大长度是多少。
- 程序分为多个段后,各段离散地装入内存,为了保证程序能够正常运行,就必须能从物理内存中找到各段逻辑段的存放位置,为此,需要为每个进程建立一张映射表,简称为段表。段表和页表的作用十分类似,页表是记录了各个逻辑页面和实际的物理页框之间的映射关系,段表是记录了各个逻辑段到实际的物理内存存放位置的映射关系。段表中每个段对应一个段表项,其中记录了该段在内存中的起始位置(又称“基址”)和段的长度;各个段表项的长度是相同的;由于段表项长度相同,所以段号可以是隐含的,不占存储空间。
- 分段系统中,逻辑地址转换成物理地址的过程是这样的:当进程要上处理机运行时,与进程切换相关的内核程序会根据内存中的系统区中的与该进程相关的PCB来恢复进程运行环境,包括段表寄存器中数据(包括段表始址和段表长度)的恢复。当要访问进程的某逻辑地址时,系统会将逻辑地址转换成段号和段内地址,然后用段号跟段表寄存器中的段表长度进行比对判断是否越界(段号大于等于段表长度则会越界,因为段号是从0开始的),如果越界了则会产生越界中断,否则会继续执行;没有越界的话根据段号和段表始址来查询段表,找到对应的段表项(段表项存放地址=段表始址+段号*段表项长度),然后检查逻辑地址的段内地址是否是否超过段长,如果段内地址大于等于段长,则产生越界中断,否则继续执行,根据段表项得到的段基址以及逻辑地址的段内地址计算得到物理地址,然后去访问对应的内存单元。
-
分段、分页管理的对比:
1.页是信息的物理单位,分页的主要目的是为了实现离散分配,提高内存利用率,分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的。
2.段是信息的逻辑单位,分段的目的是更好地满足用户需求,一个段通常包含着一组属于一个逻辑模块的信息,分段的主要目的是更好地满足用户需求,一个段通常包含着一组属于一个逻辑模块的信息,分段对用户是可见的,用户编程时需要显式地给出段名。
3.页的大小固定且由系统决定,段的长度却不固定,取决于用户编写的程序。
4.分页的用户进程地址空间是一维的,程序员只需要给出一个记忆符即可表示一个地址,分段的用户进程地址空间是二维的,程序员在标识一个地址时,既需要给出段名,也需要给出段内地址。
5.分段比分页更容易实现信息的共享和保护。如果某个进程中的一个段的功能对其他进程也有帮助,那么可以在其他进程的段表中也添加上这一段对应的段表项,这就能够实现共享。(不能被修改的代码被称为纯代码或可重入代码(不属于临界资源),这样的代码是可以共享的,可修改的代码是不能共享的,因为如果一个代码段中有很多变量,各进程并发地同时访问可能会导致数据不一致)。如果采用分页的方式,就有可能一个页面包含了原本属于不同段的内容,那么不允许共享和允许共享的部分就可能混在一个页面,这样的话让这个页面与其他进程共享显然是不合理的。 -
分段方式访问逻辑地址的话会访问两次内存,第一次是查内存中的页表,第二次是访问目标内存单元。与分页系统类似,分段系统中也可以引入快表机构,将近期访问过的段表项放到快表中,这样可以减少一次访问,加快地址变换速度。
(3)段页式存储管理
- 分页存储管理方式的优点是内存空间利用率高,不会产生外部碎片,只有少量的内部碎片,但缺点是不方便按照逻辑模块实现信息的共享和保护;分段存储管理方式的优点是很方便按照逻辑模块实现信息的共享和保护,缺点是如果段长太大,为其分配很大的连续空间会很不方便,并且可能会产生外部碎片(虽然可以用“紧凑”来解决,但是要付出较大的时间代价)。结合这两种存储管理方式就产生了段页式存储管理方式。
- 段页式存储管理方式是将进程逻辑模块分段,再将各段分页,并且将内存空间分为大小相同的内存块(页框/页帧/物理块)
-
分段系统的逻辑地址结构由段号和段内地址(段内偏移量)组成,而段页式系统的逻辑地址结构由段号、页号、页内地址(页内偏移量)组成,其实也就是将段内地址进行再拆分为页号和页内地址。段号的位数决定了每个进程最多可以分为多少段,页号位数决定了每个段最多有多少页,页内偏移量决定了页面大小、内存块大小是多少。
在段页式系统中,“分段”对用户是可见的,程序员编程时需要显示地给出段号、段内地址。而将各段“分页”对用户是不可见的,系统会根据段内地址自动划分页号和页内偏移量,因此段页式管理的地址结构是二维的。 -
在段页式系统中,按照每个进程的逻辑模块将进程分为多个段,每个段对应一个段表项,每个段表项由段号、页表长度、页表存放块号(根据块号能够算出页表起始地址)组成,每个段表项长度相等,段号是隐含的。将每个段进行分页,每个页面对应一个页表项(包含页号、页面存放的内存块号,每个页表项长度相等,页号是隐含的),页表项存放在与该段对应的页表中,根据段表能够找到与这个段对应的页表在内存中的存储位置。一个进程会对应一个段表,但可能对应多个页表。
- 段页式存储管理方式中,进程上处理机上运行前,会从PCB中读取信息恢复进程运行环境,同时恢复段表寄存器中的数据(段表始址、段表长度)。当要访问进程的某逻辑地址时,系统会将逻辑地址拆分为段号、页号和页内偏移量,然后将段号和段表长度进行比对判断是否越界(如果段号大于等于段表长度则越界),如果不越界则继续执行;然后根据段号和段表始址查询段表项(段表项的存放地址为段表始址+段号*段表项长度),然后检查页号是否越界(因为各个段分成多少页是不固定的,所以要判断是否越界,如果页号大于等于页表长度,则发生越界中断),不越界则继续执行;根据段表项找到页表,再找到对应的页表项,根据页表项中的内存块号和逻辑地址的页内偏移量得到最终的物理地址,根据物理地址访问目标内存单元。
- 段页式存储管理方式如果不引入快表机构的话,需要三次访存,而如果引入快表机构,用段号也页号作为查询快表的关键字,若快表命中则仅需要访问一次内存。
(五)虚拟内存
-
虚拟内存也是内存扩充的一种方式。
-
传统的存储管理方式中,作业必须一次性全部装入内存后才能开始运行,这会造成两个问题:1.作业很大时,不能全部装入内存,导致大作业无法运行;2.当大量作业要求运行时,由于内存无法容纳所有作业,因此只有少量作业能运行,导致多道程序并发度下降。(一次性问题)
除此之外,传统的存储管理方式中,作业一旦被装入内存后,就会一直驻留在内存中,直至作业运行结束。事实上,在一个时间段内,只需要访问作业的一小部分数据即可正常运行,这就导致了内存中会驻留大量的、暂时用不到的数据,浪费了宝贵的内存资源。(驻留性问题)
为了解决这两个问题,出现了虚拟内存技术。 -
基于局部性原理,在程序装入时,可以将程序中很快会用到的部分装入内存,暂时用不到的部分留在外存,就可以让程序开始执行。在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。在操作系统的管理下,在用户看来似乎有一个比实际内存大得多的内存,这就是虚拟内存。
-
虚拟内存的三个特征:
1.多次性:无需在作业运行时一次性装入内存,而是允许被分成多次调入内存
2.对换性:在作业运行时无需一直常驻内存,而是允许在作业运行过程中,将作业换入、换出。
3.虚拟性:从逻辑上扩充了内存的容量,使用户看到的内存容量远大于实际的容量。 -
虚拟内存的实现需要建立在离散分配的内存管理方式基础上。在前面提到的三种非连续分配存储管理方式的基础上运用虚拟内存技术,就有了请求分页存储管理、请求分段存储管理、请求段页式存储管理。这几种存储管理方式与原来的存储管理方式最主要的区别在于:在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序(操作系统提供请求调页、请求调段功能)。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存(操作系统提供页面置换、段置换功能)。
1 请求分页存储管理方式
(1)相关介绍
- 请求分页存储管理方式与基础分页存储管理方式最主要的区别是:操作系统要提供请求调页功能,将缺失页面从外存调入到内存;操作系统要提供页面置换的功能,将暂时用不到的页面换出外存。
- 与基本分页存储管理相比,请求分页管理中,为了实现请求调页,操作系统需要知道每个页面是否已经调入内存;如果还没调入,那么也需要知道该页面在外存中存放的位置。当内存空间不够时,需要实现页面置换,操作系统需要通过某些指标来决定到底换出哪个页面;有的页面没有被修改过,就不用浪费时间写回外存;有的页面修改过,就需要将外存中的旧数据覆盖,因此,操作系统也需要记录各个页面是否被修改的信息。
- 基本分页存储管理方式的页表包含了页号和内存块号的信息,而请求分页存储管理方式的页表除了这两部分之外,还增加了状态位(是否调入内存,1位是0为否)、访问字段(记录最近被访问过几次,或记录上次访问的时间,供置换算法选择换出页面时参考)、修改位(标记页面调入内存后是否被修改过)、外存地址(页面在外存中存放的位置)四部分。
- 为了实现请求调页功能,系统中需要引入缺页中断机构。假设要访问进程某逻辑地址,在请求分页系统中,每当要访问的页面不在内存时,便产生一个缺页中断,然后由操作系统的缺页中断处理程序处理中断,此时缺页的进程阻塞,放入阻塞队列,调页完成后再将其唤醒,放回就绪队列。如果内存中有空闲块,就为进程分配一个空闲块,将所缺页面将入该块,并修改页表中相应的页表项。如果内存中没有空闲块,则有页面置换算法选择一个页面淘汰,若该页面在内存期间被修改过,则要将其写回内存,未修改过的页面不用写回内存。缺页中断是因为当前执行的指令想要访问的目标页面未调入内存而产生的,因此属于内中断。一条指令在执行期间,可能会访问到不同逻辑地址,而恰好这些逻辑地址属于不同的页面,就有可能会产生多次缺页中断。
- 访问逻辑地址时,与基本分页存储管理方式不同的是,在查找到页表项时,要根据状态位判断页面是否在内存中,如果不在的话需要进行请求调页,请求调页后如果内存有空闲的话就直接调入内存,否则的话还需要进行页面置换,并且需要修改页表中相应的页表项。注意请求分页存储管理方式也是可以引入快表机构的,快表会将最近访问过的页表对应的页表项放在其中,但当该页表调出外存时,快表中对应的页表项也要删除,否则可能访问错误的页面。
- 注意只有写指令才需要修改“修改位”,并且一般来说,只需要修改快表中的数据,只有要将快表项删除时才需要写回内存中的慢表,这样可以减少访存次数。
- 和普通的中断处理一样,缺页中断处理依然需要保留CPU现场。
- 换入/换出页面都需要启懂慢速的I/O操作,可见如果换入/换出太频繁,会有很大的开销。
- 页面调入内存后,需要修改慢表,同时也需要将表项复制到快表中。
- 在具有快表机构的请求分页系统中,访问一个逻辑地址时,若发生缺页,则地址变换步骤是:查快表(未命中)——查慢表(发现未调入内存)——调页(调入的页面对应的页表项会直接加入快表)——查快表(命中)——访问目标内存单元。
(2)页面置换算法
- 页面置换算法用于挑选暂时用不到的页面调入到外存,由于页面的置换开销很大,因此好的页面置换算法应该追求更少的缺页率(让换入换出的次数尽可能少)。页面置换算法包括最佳置换算法(OPT)、先进先出置换算法(FIFO)、最近最久未使用置换算法(LRU)、时钟置换算法(CLOCK)、改进型的时钟置换算法。
- 最佳置换算法每次选择淘汰的页面是以后永不使用或者在最长时间内不被访问的页面,这样可以保证最低的缺页率。实际上,只有在进程执行的过程中才能知道接下来会访问到的是哪个页面 ,操作系统无法提前预判页面访问序列,因此最佳置换算法是无法实现的。
- 先进先出置换算法每次选择淘汰的页面是最早进入内存的页面。实现方法是把调入内存的页面根据调入的先后顺序排成一个队列,需要换出页面时选择队头页面即可,队列的最大长度取决于系统为进程分配了多少个内存块。一般来说,当给进程分配越多内存块时,缺页率应该会下降,但是使用了先进先出置换算法,会产生Belady异常(当为进程分配的内存块数增加时,缺页次数不减反增,只有先进先出置换算法会产生该异常),先进先出置换算法虽然实现简单,但是该算法与进程实际运行时的规律不适应,因为先进入的页面也有可能最经常被访问,因此算法性能较差。
- 最近最久未使用置换算法每次淘汰的页面是最近最久未使用的页面。实现方法是赋予每个页面对应的页表项中,用访问字段记录该页面自上次被访问以来所经历的时间t,当需要淘汰一个页面时,选择现有页面中t值最大的,即最近最久未使用的页面。最近最久未使用置换算法的实现需要专门的硬件来实现,虽然算法性能好,但是实现困难,开销大。
- 时钟置换算法是一种性能和开销比较均衡的算法,又称最近未用算法(NRU)。简单的时钟置换算法实现方法:为每个页面设置一个访问位,再将内存中的页面通过链接指针链接成一个队列,当某页被访问时,其访问位置为1。当需要淘汰一个页面时,只需检查页的访问位,如果是0,则将该页换出,如果是1,则将它置为0,暂不换出,继续检查下一个页面,若第一轮扫描中所有页面都是1,则将这些页面的访问位依次置为0后,再进行第二轮扫描,第二轮扫描中一定会有访问位为0的页面,因此简单CLOCK算法选择一个淘汰页面最多会经过两轮扫描。
- 简单的时钟置换算法仅考虑到一个页面最近是否被访问过,事实上如果被淘汰的页面最近没有被修改过,就不需要执行I/O操作写回外存,只有被淘汰的页面被修改过才需要写回外存。因此,除了考虑一个页面最近有没有被访问过以外,操作系统还应考虑页面有没有被修改过,在其他条件相同时,应该优先淘汰没有修改过的页面,避免I/O操作,这就是改进型的时钟置换算法。修改位=0表示页面没有被修改过,修改位=1表示页面被修改过。该算法将所有可能置换的页面排成一个循环队列,然后进行扫描。第一轮扫描从当前位置查找第一个访问位和修改位为0的页面用于替换;如果第一轮扫描失败,则重新扫描,查找第一个访问位为0且修改位为1的页面进行替换,第二轮将所有扫描过的页面的访问位设为0;如果第二轮扫描失败,则重新扫描,查找第一个访问位和修改位均为0的页面用于置换;如果第三轮扫描失败,则重新扫描,查找第一个访问位为0且修改位为1的页面用于替换。由于第二轮扫描将所有页面的访问位都设置为0,因此经过第三轮、第四轮扫描一定会有一个页面被选中,因此改进型CLOCK置换算法选择一个淘汰页面最多会有四轮扫描。
(3)页面分配策略
-
驻留集:请求分页存储管理中给进程分配的物理块的集合。在采用了虚拟存储技术的系统中,驻留集大小一般小于进程的总大小。若驻留集太小,会导致缺页频繁,系统要花大量的时间来处理缺页,实际用于进程推进的时间很少;若驻留集太大,又会导致多道程序并发度下降,资源利用率降低,所以要选择一个合适的驻留集大小。
-
根据驻留集大小是否可变有两种分配方式。一种是固定分配:操作系统为每个进程分配一组固定数目的内存块,在进程运行期间不再改变,即驻留集大小不变。另一种是可变分配:先为每个进程分配一定数目的物理块,在进程运行期间做适当的增加或减少,即驻留集大小可变。
-
当发生缺页时进行置换有两种策略,一种是局部置换:发生缺页时只能选进程自己的物理块进行置换。另一种是全局置换,可以将操作系统保留的空闲物理块分配给缺页进程,也可以将别的进程持有的内存块置换到外存,再分配给缺页进程。
-
将两种分配方式和两种置换策略两两结合,可以得到四种方式,但是不存在固定分配且全局置换的方式(因为全局置换意味着一个进程拥有的内存块数必定会改变),所以只剩下三种方式。
-
固定分配局部置换:系统为每个进程分配一定的内存块,在整个运行期间都不改变,若进程在运行中发生缺页,则只能从该进程在内存中的页面中选出一页换出,然后调入需要的页面。这种策略的缺点是很难在开始就确定为每个进程分配多少个物理块。采用这种策略的系统可以根据进程大小、优先级或程序员给出的参数来确定为一个进程分配的内存块数。
-
可变分配全局置换:刚开始会为进程分配一定数量的内存块,操作系统会保持一个空闲内存块队列,当某进程发生缺页时,从空闲内存块中取出一块分给该进程;若无空闲内存块,则可以选择一个未锁定的页面换出外存(一些重要的页面内容不能被置换出内存,会被锁定),再将该内存块分配给缺页的进程。采用这种策略,只要某进程发生缺页时,都将获得新的内存块,仅当空闲内存块用完时,系统才会选择一个未锁定的页面调出,被选择调出的页可能是系统中任何一个进程中的页,因此这个被选中的进程的内存块会减少,缺页率会增加。
-
可变分配局部置换:刚开始会为每个进程分配一定数量的内存块,当某进程发生缺页时,只允许从该进程自己的内存块中选出一个进行换出外存。如果进程在运行中频繁缺页,系统会为该进程多分配几个内存块,直至进程缺页率趋势适当程度,反之,如果进程在运行中缺页率特别低,则可适当减少分配给该进程的物理块。
-
关于何时调入页面的问题,有两种策略。
第一种是预调入策略,根据局部性原理,一次调入若干个相邻的页面可能比一次调入一个页面更加高效,因此可以预测不久之后将要访问到的页面,将它们预先调入内存,但目前成功率只有百分之五十左右,所以这种策略主要用于进程的首次调入(运行前调入),由程序员指出应该先调入哪些部分。
第二种是请求调页策略:进程在运行期间发现缺页时才将所缺页面调入内存(运行时调入)。这种策略调入的页面一定会被访问到,但由于每次只能调入一页,且每次调页都需要磁盘I/O操作,因此I/O开销比较大。
一般两种策略会结合来使用。 -
关于从何处调入页面的问题。系统的外存一般分为对换区(读写速度更快,采用连续分配方式)和文件区(读写速度更慢,采用离散分配方式)。如果系统拥有足够的对换区空间,则页面的调入、调出都是在内存与对换区之间进行,这样可以保证页面的调入、调出速度很快。在进程运行前,需将进程相关的数据从文件区复制到对换区。当系统缺少足够的对换区空间,则凡是不会被修改的数据都直接从文件区调入,由于这些页面不会被修改,因此换出时不必写回磁盘,下次需要时再从文件区调入即可。对于可能被修改的部分,换出时写回磁盘对换区,下次需要时再从对换区调入。UNIX系统所采用的方式是:运行之前把进程有关的数据全部放在文件区,故未使用过的页面都可以从文件区调入,若被使用过的页面需要换出,则写回对换区,下次需要时从对换区调入。
-
如果刚刚换出的页面又要调入内存,刚刚换入的页面又要换出外存,这种频繁的页面调度行为被称为抖动或颠簸,产生抖动或颠簸的原因是进程访问的页面数目要高于可用的内存块数。抖动现象的发成需要系统花费大量的时间来处理进程的页面换入换出,实际用于进程执行的时间变少,需要尽量避免这种现象。为了研究应该为每个进程分配多少个内存块,出现了工作集的概念。工作集是指进程在某段时间间隔内,进程实际访问页面的集合。
2 请求分段存储管理方式
3 请求段页式存储管理方式
四、文件管理
(一)前述知识
-
一个文件有哪些属性?
1.文件名:由创建文件的用户决定文件名,主要是方便用户找到文件,同一目录下不允许有同名文件
2.标识符:一个系统内的各文件标识符唯一,对用户来说毫无可读性,因此标识符只是操作系统用于区分各个文件的一种内部名称
3.类型:指明文件的类型
4.位置:文件存放的路径(让用户使用)、外存中的地址(操作系统使用,对用户不可见)
5.大小:指明文件大小
6.保护信息:对文件进行保护的访问控制信息
还有创建时间、上次修改时间、文件所有者信息等等。 -
文件可以分为无结构文件(如文本文件)和有结构文件(如数据库表)。无结构文件由一些二进制字符流组成,又称“流式文件”。有结构文件由一组相似的记录(记录是一组相关的数据项(数据项是文件系统中最基本的数据单位)的集合)组成,又称“记录式文件”。
-
用户可以自己创建一层一层的目录,各层目录中存放相应的文件,系统中的各个文件就通过一层一层的目录合理有序地组织起来了。目录其实也是一种特殊的的有结构文件。
-
为了让用户或应用程序对文件进行管理,操作系统需要向上提供几个最基本的功能:创建文件(create系统调用)、删除文件(delete系统调用)、读文件(read系统调用)、写文件(write系统调用)、打开文件(open系统调用,读写文件之前调用)、关闭文件(close系统调用,读写文件之后调用)。使用这些基本的功能,可以用来实现一些更加复杂的功能,比如复制文件。
-
类似于内存分为一个个“内存块”,外存也会分为一个个块/磁盘块/物理块。每个磁盘块的大小是相等的,每块一般包含2的整数次幂个地址。同样类似的是,文件的逻辑地址也可以分为(逻辑块号,块内地址),操作系统同样需要将逻辑地址转换为外存的物理地址(物理块号,块内地址)的形式,块内地址的位数取决于磁盘的大小。与内存一样,外存也是由一个个存储单元组成的,每个存储单元可以存储一定量的数据,每个存储单元对应一个物理地址。操作系统以“块”为单位为文件分配存储空间,因此即使一个文件大小只有10B,但它依然需要占用1KB的磁盘块(假设一个磁盘块大小为1KB),外存中的数据读入内存时同样以块为单位。
(二)文件的逻辑结构
-
文件的逻辑结构是指在用户看来,文件内部的数据应该是如何组织起来的。而物理结构指的是在操作系统看来,文件的数据是如何存放在外存中的。
-
无结构文件内部的数据其实就是一系列字符流,没有明显的结构特点,因此也不用探讨无结构文件的逻辑结构问题。
-
有结构文件由一组相似的记录组成,又称为记录式文件,每条记录由若干个数据项组成,一般来说,每条记录有一个数据项可作为关键字。根据各条记录的长度(占用的存储空间)是否相等,又可分为定长记录和可变长记录两种。根据有结构文件中的各条记录在逻辑上如何组织,可以分为三类:顺序文件、索引文件、索引顺序文件。
-
顺序文件:文件中的记录一个接一个地顺序排列(逻辑上),记录可以是定长的或可变长的。各个记录在物理上可以顺序存储或链式存储。顺序存储在逻辑上相邻的记录在物理上也相邻(类似于顺序表);链式存储在逻辑上相邻的记录在物理上不一定相邻(类似于链表)。根据记录之间的顺序是否按关键字排列,又可以将顺序文件分为串结构(记录之间的顺序与关键字无关,通常按照记录存入的时间决定记录的顺序)和顺序结构(记录之间的顺序俺关键字顺序排列)。
-
假设顺序文件在物理上采用的是链式存储,则无论是定长还是可变长记录,都无法实现随机存储,每次只能从第一个记录开始依次往后查找。
如果顺序文件在物理上采用的是顺序存储,并且如果是可变长记录的话依然无法实现随机存储,每次只能从第一个记录开始依次往后查找,因为每一条记录的长度不一样,所以它们的地址并没有什么规律性,所以执行从第一条记录开始往后查找;如果是定长记录的话则可以实现随机存储,假设记录长度为L,则第i个记录存放的相对位置是i*L;若定长记录采用串结构,则无法快速找到某关键字对应的记录,若定长记录采用顺序结构,则可以快速找到某关键字对应的记录。
一般来说没有其他前提的话,顺序文件指的是在物理上采用顺序存储的顺序文件。 -
对于可变长记录,如果是顺序文件,要找到第i个记录的话,必须先顺序查找前i-1个记录,但是很多应用场景中又必须使用可变长记录,为了让可变长记录文件也能够随机访问,于是就有了索引文件。每个索引文件建立了一张索引表,用来加快文件检索速度,每条记录对应一个索引项,每个索引项大小都是相等的,索引表的各个表项在物理上需要连续存放,但文件中的记录在物理上可以离散地存放。索引表本身是定长记录的顺序文件(顺序存储方式),因此可以快速找到第i个记录对应的索引表。可以将记录的关键字作为索引号内容,若按关键字顺序排列,则还可以支持按照关键字折半查找。每当要增加或删除一个记录时,需要对索引表进行修改。由于索引文件有很快的检索速度,因此主要用于对信息处理的及时性要求比较高的场合。另外,可以用不同的数据项建立多个索引表。
-
索引文件的缺点是每个记录对应一个索引表项,这样的话索引表可能会很大,甚至可能会出现索引表项比文件本身的内容还大,这样对存储空间的利用率就太低了。因此出现了索引顺序文件,索引顺序文件是顺序文件和索引文件思想的结合。索引顺序文件中,同样会为文件建立一张索引表,但不同的是,并不是每个记录对应一个索引表项,而是一组记录(相当于一个顺序文件)对应一个索引表项。索引顺序文件的索引项不需要按关键字顺序排列,这样可以极大方便新表项的插入,也就是说,索引顺序文件的索引表是定长记录的串结构的顺序文件(顺序存储)。通过索引顺序文件去检索不定长记录的顺序文件,确实能够提高检索速度,同时还能解决索引文件索引表过大的问题。
-
如果文件记录数过多,采用索引顺序文件查找某个记录时,仍然会出现次数过多的情况。为了进一步提高检索效率,可以为顺序文件建立多级索引表,对于一个记录较多的文件,可以为该文件建立一张低级索引表,再将低级索引表进行分组,为其建立顶级索引表。这样可以进一步减少检索次数。
(三)文件目录
-
目录本身就是一种有结构文件,由一条条记录组成。每条记录对应一个存放在该目录下的文件。
-
每一个目录文件都会对应一个文件目录表,当我们双击打开目录文件下的一个文件或者一个目录文件的时候,操作系统就会在这个文件目录表中找到对应的目录项(也就是记录),根据目录项中的物理地址从外存中将信息读入内存,这样文件或者文件目录的信息就能够显示了。
-
目录文件中的一条记录就是一个文件控制块(FCB)(一个文件目录项),FCB的有序集合称为“文件目录”(上面提到的文件目录表)。一个文件会对应一个FCB。FCB中包含了文件的基本信息(文件名、物理地址、逻辑结构、物理结构等),存取控制信息(是否可读/可写,禁止访问的用户名单等),使用信息(如文件的建立时间、修改时间等)。FCB实现了文件名和文件存放地址之间的映射,使用户(用户程序)可以实现“按名存取“。
-
需要对目录进行哪些操作?
搜索:当用户要使用一个文件的时候,系统要根据文件名搜索目录,找到该文件对应的目录项。
创建文件:创建一个新文件,需要在其所属的目录中增加一个目录项。
删除文件:当删除一个文件时,需要在目录中删除相应的目录项。
显示目录:用户可以请求显示目录的内容,如显示该目录中的所有文件及相应属性。
修改目录:某些文件属性保存在目录中,因此这些属性变化时需要修改相应的目录项。(如文件重命名) -
早期操作系统并不支持多级目录,整个系统中只建立一张目录表,每个文件占一个目录项。单级目录实现了“按名存取“,但是不允许文件重名。在创建文件时,需要先检查目录表中有没有重名文件,确定不重名后才能允许建立文件,并将文件对应的目录项插入目录表中。显然,单级目录结构不适合多用户操作系统。
-
早期的多用户操作系统,采用两级目录结构,分为主文件目录(MDF)和用户文件目录(UFD)。主文件目录记录用户名及相应用户文件目录的存放位置。用户文件目录由该用户的文件FCB组成。这种目录结构允许不同用户的文件重名,文件名虽然相同,但是对应的其实是不同的文件。两级目录结构可以实现访问限制(检查此时登录的用户名是否匹配),但是两级目录结构依然缺乏灵活性,用户不能对自己的文件进行分类。
-
为了解决以上的问题,出现了多级目录结构(树形目录结构),每个目录下可以有更低一级的目录,并且不同目录下的文件可以重名。用户或用户进程要访问某个文件时要用文件路径名标识文件,文件路径名是一个字符串,各级目录之间用/隔开,从根目录出发的路径称为绝对路径。系统根据绝对路径一层一层找到下一级目录,刚开始从外存读入根目录的目录表,找到下一级目录的目录项后,根据对应的存放位置从外存读入对应的目录表,反复多次找到文件的存放位置。
-
很多时候,用户会连续访问同一目录内的多个文件,显然每次从根目录开始查找的话是很低效的,因此可以设置一个“当前目录”。当用户打开某个目录文件的时候,这个目录文件对应的目录表已经调入内存,那么可以把它设置为“当前目录”,当用户想要访问某个文件时,可以使用从当前目录出发的“相对路径”。引入“当前目录”和“相对路径”,会减少磁盘I/O的次数,这提升了访问文件的效率。
-
树形目录结构可以很方便地对文件进行分类,层次结构清晰,也能够更有效地进行文件的管理和保护。但是,树形结构不便于实现文件的共享,为此提出了“无环图目录结构”。在树形目录结构的基础上,增加一些指向同一节点的有向边,使整个目录成为一个有向无环图,可以更方便地实现多个用户间的文件共享。可以用不同的文件名指向同一个文件,甚至可以指向同一个目录,共享同一目录下的所有内容。需要为每个共享结点设置一个共享计时器,用于记录此时有多少个地方在共享该结点,用户提出删除结点的请求时,只是删除该用户的FCB、并使共享计数器减1,并不会直接删除共享结点。只有共享计数器减为0时,才删除结点。
-
其实在查找各级目录的过程中只需要用到“文件名”这个信息,只有文件名匹配时,才需要读出文件的其他信息,因此可以考虑让目录表减少存储的内容来提升效率。可以将除了文件名之外的文件描述都存储到一个索引结点中,每个文件都会有唯一一个索引结点,采用了索引结点这种机制之后,目录表所包含的东西就只有文件名和指向索引结点的索引结点指针。采用这种机制的话,由于目录项大小变小,一个磁盘块能够存储更多的目录项,因为每次磁盘I/O只读入一块磁盘块,这样检索目录项就可以在更少的磁盘块中进行检索,能够大大提升文件检索速度。当找到文件名对应的目录项时,才需要将索引结点调入内存,索引结点中记录了文件的各种信息,包括文件在外存中的存放位置,根据“存放位置”即可找到文件。存放在外存中的索引结点称为“磁盘索引结点”,当索引结点放入内存后称为“内存索引结点”。相比之下内存索引结点中需要增加一些信息,比如:文件是否被修改、此时有几个进程正在访问该文件等。
(四)文件的物理结构(文件的分配方式)
-
文件的物理结构/文件的分配方式要探讨的问题是对非空闲磁盘块的管理(存放了文件数据的磁盘块),也就是文件数据应该怎样存放在外存中?文件的分配方式分为连续分配、链接分配、索引分配,其中链接分配又分为隐式链接和显示链接。
-
类似于内存分页,磁盘中的存储单元也会被分为一个个块/磁盘块/物理块,在很多操作系统中,磁盘块的大小与内存块、页面的大小相同。而内存与磁盘之间的数据交换都是以块为单位的,这使得数据交换变得很方便。
-
在内存管理中,进程的逻辑地址空间被分为一个一个页面。同样的,在外存管理中,为了方便对文件数据的管理,文件的逻辑地址也被分为了一个一个的文件块,于是文件的逻辑地址也可以表示为(逻辑块号、块内地址)的形式。操作系统为文件分配存储空间都是以块为单位的。用户通过逻辑地址来操作自己的文件,操作系统负责实现从逻辑地址到物理地址的映射。如何将逻辑地址转换成物理地址是文件分配方式中的重点问题。
-
连续分配方式要求每个文件在磁盘上占有一组连续的块。使用这种分配方式,操作系统要将逻辑地址(逻辑块号,块内地址)转换成物理地址(物理块号,块内地址),只需要转换块号即可,块内地址保持不变。为了实现这种分配方式,要在文件目录记录存放的起始块号和长度(总共占用多少个块)。用户给出要访问的逻辑块号,操作系统检查逻辑块号是否合法,合法的话则找到该文件对应的目录项(FCB),然后操作系统可以算出物理块号(物理块号=起始块号+逻辑块号),因此连续分配支持顺序访问和直接访问(即随机访问)。
-
连续分配方式的优点:
由于读取某个磁盘块时,需要移动磁头,访问的两个磁盘块相隔越远,移动磁头所需的时间就越长,所以连续分配的文件在顺序读/写时速度最快。
连续分配方式的缺点:
由于连续分配方式要求文件在磁盘上占有连续的块,所以当文件要扩展存储空间的时候,如果刚好连着的磁盘块被其他文件占有的话,就需要把该文件所有的块迁移到有合适大小的磁盘空间,这种迁移操作花销很大,因此采用连续分配方式的文件不方便拓展。
如果采用连续分配方式,如果磁盘中剩余的空闲磁盘块都不是连续的,就不能为一个需要多个磁盘块的文件分配存储空间,这样的话这些空闲磁盘块就得不到使用。因此物理上采用连续分配方式,存储空间利用率低,会产生难以利用的磁盘碎片,虽然可以用紧凑来处理碎片,但需要耗费很大的时间代价。 -
链接分配采取离散分配的方式,可以为文件分配离散的磁盘块,然后用指针将这些磁盘块链接起来,分为隐式链接和显示链接。采用链接分配方式的话,需要在文件目录中记录文件存放的起始块号和结束块号,除了文件的最后一个磁盘块之外,每个磁盘块中都会保存指向下一个磁盘块的指针,这些指针对用户是透明的。采用这种分配方式是这样实现文件的逻辑地址到物理地址的转换的:用户给出要访问的逻辑块号i,操作系统找到该文件对应的目录项(FCB),从目录项中找到起始块号,将起始块读入内存,然后根据其中的指针找到下一逻辑块对应的物理块号,以此类推找到所有的磁盘块。读入i号逻辑块,总共需要i+1次磁盘I/O。
采用链式分配(隐式链接)方式的文件,只支持顺序访问,不支持随机访问,查找效率低。另外,指向下一磁盘块的指针也需要耗费少量的存储空间。
采用隐式链接的链式分配方式,很方便进行文件拓展,只需要随便找到一个空闲磁盘块,挂到文件的磁盘块链尾,并修改文件的FCB即可。另外,所有的空闲磁盘块都可以被利用,不会有碎片问题,外存利用率高。 -
链接分配还有一种显式链接的分配方式,跟隐式链接的区别在于把用于链接文件的各物理块显式地存放在一张文件分配表(FAT,File Allocation Table)中。采用显式连接的链接分配方式,文件目录中只需要记录文件的起始块号,文件分配表中会记录各磁盘块的链接顺序,根据起始块号以及文件分配表就可以知道每个文件所对应的磁盘块。一个磁盘仅设置一张FAT,开机时将FAT读入内存,并常驻内存,FAT的各个表项在物理上连续存储,且每一个表项长度相同,因此物理块号字段可以是隐含的。
采用显式链接的链式分配方式实现逻辑地址到物理地址的转换过程是这样的:用户给出要访问的逻辑块号i,操作系统找到该文件对应的目录项(FCB),从目录项中找到起始块号,若i>0,则查询内存中的文件分配表FAT,往后找到第i号逻辑块对应的物理块号,逻辑块号转换成物理块号的过程不需要读磁盘操作。
采用显示链接的链式分配方式的文件,支持顺序访问,也支持随机访问(想访问i号逻辑块时,并不需要依次访问之前的0~i-1号逻辑块),由于块号的转换不需要访问磁盘,因此相对于隐式链接来说,访问速度快很多。
显式链接也不会产生外部碎片,也可以很方便地对文件进行拓展。
显式链接的缺点是文件分配表需要占用一定的存储空间。
-
索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表,索引表中记录了文件的各个逻辑块对应的物理块(类似于内存管理中的页表),索引表存放的磁盘块称为索引块,文件数据存放的磁盘块称为数据块。采用索引分配方式,需要在文件目录中记录文件的索引块是几号磁盘块,索引表的表项是固定的,只要知道索引表的起始位置,就能够知道各逻辑块的位置,因此逻辑块号是隐含的。
采用索引分配方式的话,实现文件的逻辑块号到物理块号的转换过程是:用户给出要访问的逻辑块号i,操作系统找到该文件对应的目录项(FCB),从目录项可知索引表存放位置,将索引表从外存读入内存,并查找索引表即可知道i号逻辑块在外存中的存放位置。
索引分配方式是支持随机访问的,并且文件拓展也很容易实现,只需要给文件分配一个空闲磁盘块,并增加一个索引表项即可。 -
如果一个文件的大小较大,它的索引表的大小可能就会超过一个磁盘块的大小,这个问题有三种解决方案:链接方案、多层索引、混合索引。
-
链接方案:将多个索引表链接起来存放。当一个磁盘块存放不下索引表时,可以为一个文件分配多个索引块,在每个索引块中用一定的空间存储指向下一个索引块的指针。采用这种解决方案的话,文件目录只需要存储第一个索引块的存储位置。但是如果要访问的逻辑地址对应的索引项所在的索引块排在很后面,需要先顺序读入前面的索引块,这样才能知道后面的索引块的位置,这样的效率就十分低下。
-
多层索引:为了解决上面提到的效率低下问题,就有了多层索引的解决方案,原理类似于内存管理中的多级页表。使第一层索引块指向第二层索引块,还可根据文件大小的要求再建立第三层、第四层索引块。采用这种解决方案,文件目录只需要存储顶级索引块的存储位置。采用多层索引的话各层索引表大小不能超过一个磁盘块。采用多层索引的话,当需要访问一个逻辑地址的话,根据逻辑块号找到对应的目录项,然后根据目录项中顶级索引表的位置将顶级索引表调入内存,然后找到下一级索引表存放的位置读入内存,以此类推直到找到相应的索引表项,然后访问目标数据块。如果采用K层索引结构,且顶级索引表未调入内存,则访问一个数据块只需要K+1次读磁盘操作,相比链接方案来说度磁盘次数明显减少。但是这种多层索引的解决方案,对待那些较小的文件,仍然需要多次读磁盘的操作,这又降低了效率。
-
混合索引:为了解决多层索引方式存在的问题,又有了混合索引方式,混合索引方式是多种索引分配方式的结合。例如,一个文件的顶级索引表中,既包含直接地址索引(直接指向数据块),又包含一级间接索引(指向单层索引表)、还包含两级间接索引(指向两层索引表)。注意各级索引表也是不能超过一个磁盘块的大小。
采用混合索引方式,对于小文件来说,只需要较少的读磁盘次数就可以访问目标数据块。
(五)文件存储空间管理
- 文件的存储空间管理就是对空闲磁盘块的管理。
- 安装windows操作系统时,一个必经步骤是为磁盘分区(分为C、D、E盘等),这个将物理磁盘划分为一个个文件卷(逻辑卷、逻辑盘)的行为就称为存储空间的划分。对各个文件卷又要划分为目录区、文件区,目录区主要存放文件目录信息(FCB)、用于磁盘存储空间管理的信息,文件区用于存放文件数据,对文件卷的划分称为存储空间的初始化。有的系统支持超大型文件,可支持由多个物理磁盘组成一个文件卷。
- 对空闲磁盘块的管理方法有空闲表法、空闲链表法(包括空闲盘块链和空闲盘区链)、位示图法、成组链接法。
- 空闲表法就是建立一个空闲盘块表(跟内存管理的动态分区分配方法中的空闲表类似),记录了每一个空闲区间的起始位置和每一个空闲区间的长度。空闲表法适用于文件的物理结构是连续分配方式的情况。
如何分配磁盘块给文件?与内存管理中的动态分区分配很类似,为一个文件分配连续的存储空间,同样可采用首次适应、最佳适应、最坏适应等算法来决定要为文件分配哪个区间。
如何从文件回收磁盘块?与内存管理中的动态分区分配很类似,当回收某个存储区时有四种情况:
1.当回收区的前后都没有相邻空闲区,直接在空闲盘块表中增加一个表项。
2.当回收区的前后都是空闲区,需要将回收区跟前后两个空闲区合并,并且将两块空闲区的表项合并并更新表项,减少一个表项。
3.当回收区的前面是空闲区或者回收区的后面是空闲区时,将回收区跟空闲区合并,并且更新空闲区的表项,表项的多少不会改变。
- 空闲链表法可以进一步划分为空闲盘块链和空闲盘区链。空闲盘块链以盘块为单位组成一条空闲链,每一个空闲盘块中都会存储着指向下一个空闲盘块的指针;而空闲盘区链就是以盘区(由连续的空闲盘块组成)为单位组成一条空闲链,空闲盘区中的第一个盘块内记录了盘区的长度、下一个盘区的指针。
采用空闲盘块链的话,操作系统会保留着链头、链尾指针。当某文件申请k个盘块,则操作系统会从链头开始依次摘下k个盘块分配,并修改空闲链的链头指针。当回收某个文件的盘块时,将回收的盘块依次挂到链尾,并修改空闲链的链尾指针。采用空闲盘块链的管理方式,适用于文件的物理结构是离散分配的情况,为文件分配多个盘块时可能需要重复多次操作。
采用空闲盘区链的话,操作系统会保留着链头、链尾指针。当某文件申请k个盘块,则可以采用首次适应、最佳适应等算法,从链头开始检索,按照算法规则找到一个大小符合要求的空闲盘区,分配给文件。若没有合适的连续空闲区块,也可以将不同盘区的盘块同时分配给一个文件,注意分配后可能要修改相应的链指针、盘区大小等数据。当要回收某个文件的盘块时,若回收区和某个空闲盘区相邻,则需要将回收区合并到空闲盘区中。若回收区没有和任何空闲区相邻,将回收区作为一个单独的空闲盘区挂到链尾。采用空闲盘区链的存储管理方式,对文件的物理结构是离散分配或连续分配都适用,为一个文件分配多个盘块时效率更高。
- 位示图法就是用一个位示图来表示每个盘块的使用情况。每个二进制位对应一个盘块,0代表盘块空闲,1代表盘块已分配。位示图一般用连续的“字”来表示,字中的每一位代表一个盘块,因此可以用(字号,位号)来对应一个盘块号(盘块号=字长*字号+位号),某一盘块对应的字号为盘块号/字长,位号为盘块号%字长。
如何给文件分配空闲磁盘块?若文件需要k个块,则顺序扫描位示图,找到k个相邻或不相邻的0,根据字号和位号算出对应的盘块号,将相应的盘块分配给文件,将相应位设置为1。
如何回收分配给文件的磁盘块?根据回收的盘块号计算出对应的字号和位号,将相应二进制位设置为0。
采用这种存储管理方式的话,对于文件的物理结构是连续分配或离散分配都适用。
- 空闲表法、空闲链表法不适用于大型文件系统,因为空闲表或空闲链表可能过大,UNIX系统中采用了成组链接法对磁盘空闲块进行管理。将文件卷的目录区中专门用一个磁盘块作为“超级块”,当系统启动时需要将超级块读入内存,并且要保证内存与外存中的超级块数据一致。超级块中记录了下一组空闲盘块的数量,并且还记录这一组空闲盘块的盘块号,这一组空闲盘块中的第一个空闲盘块还需要记录下一组空闲盘块的信息(数量+盘块号),然后下一组空闲盘块的第一个空闲盘块还是记录下下组空闲盘块的信息,以此类推。当没有下一组空闲盘块时,在倒数第二个分组中将第一个空闲盘块设置一特殊值-1,表明没有下一组空闲盘块了。每组空闲盘块中的块号不一定是连续的,并且每一组的盘块数量有一个上限值。
采用这种分配方式,如何分配空闲盘块给文件?首先检查第一个分组中的块数是否足够(由超级块得知第一个分组有多少块),如果足够的话将第一个分组的空闲盘块分配给文件,并且修改超级块中的相应数据。如果文件需要的空闲块数刚好跟第一组的空闲块数一致的话,由于第一组空闲块的第一个空闲块存储了下一组空闲盘块的信息,所以要将这些信息复制到超级块中再将空闲盘块分配给文件。当文件需要的空闲盘块数超过第一组的空闲盘块数的话,可以将下下组的空闲盘块一起分配给文件,但是要注意如果是整组空闲盘块都分配出去的话,就需要先将那一组中的第一个空闲盘块记录的信息复制到超级块中。
如何回收空闲盘块?假设第一组的空闲盘块数没满的话,可以将回收到的空闲盘块插入到第一组中,然后修改超级块对应的数据。假设第一组的空闲盘块数已经满了,则可以将回收到的块作为一个新的分组,然后将超级块的信息复制到第一块中,再将超级块的信息修改为执行这一新的组,这样的话,原先的第一组就变成了第二组,新回收到的块就作为第二组。
(六)文件的操作
- 创建文件(create系统调用):
进行Create系统调用时,需要提供的几个主要参数:
1.所需的外存空间大小
2.文件存放路径
3.文件名
操作系统在处理Create系统调用时,主要做了两件事:
1.在外存中找到文件所需的空间(利用上一点所提到的存储空间管理策略)
2.根据文件存放路径的信息找到该目录对应的目录文件,在目录中创建该文件对应的目录项,目录项中包含了文件名、文件在外存中的存放位置等信息。
- 删除文件(Delete系统调用):
进行Delete系统调用时,需要提供的几个主要参数:
1.文件存放路径
2.文件名
操作系统在处理Delte系统调用时,主要做了几件事:
1.根据文件存放路径找到对应的目录文件,从目录中找到文件名对应的目录项。
2.根据该目录项记录的文件在外存的存放位置、文件大小等信息,回收文件占用的磁盘块(回收磁盘块时,根据存储管理策略的不同,需要做不同的处理)。
3.从目录表中删除文件对应的目录项。
- 打开文件(open系统调用):
进行Open系统调用时,需要提供的几个主要参数:
1.文件存放路径
2.文件名
3.要对文件的操作类型(如r、rw)
操作系统在处理open系统调用时,主要做了几件事:
1.根据文件存放路径找到相应的目录文件,从目录中找到文件名对应的目录项,并检查该用户是否有指定的操作权限。
2.将目录项复制到内存的“打开文件表”中,并将对应表目的编号(索引号、文件描述符)返回给用户,之后用户使用打开文件表的编号来指明要操作的文件。之后用户进程如要要再次操作已经打开的文件,就不需要每次都重新查目录了,这样可以加快文件的访问速度。
(打开文件表有两种,一种是系统的打开文件表,整个系统只有一张,另一种是用户进程的打开文件表,有多个;)
- 关闭文件(Close系统调用):
操作系统在处理Close系统调用时名主要做了几件事:
1.将进程的打开文件表相应表项删除
2.回收分配给该文件的内存空间等资源
3.系统打开文件表的打开计数器count减1,若count=0,则删除对应表项。
-
读文件(Read系统调用):进行读文件操作前,需要先打开文件,因此在进行读操作之前,该文件就已经在某个进程的打开文件表中了。进程使用read系统调用完成读操作。需要指明是哪个文件(在支持“打开文件”操作的系统中,只需要提供文件在打开文件表中的索引号即可),还需要指明要读入多少数据、指明读入的数据要放在内存中的什么位置。操作系统在处理read系统调用时,会从读指针指向的外存中,将用户指定大小的数据读入用户指定的内存区域中。
-
写文件(write系统调用):同样的,在进行写操作之前,该文件就已经在某个进程的打开文件表中了。进程使用write系统调用完成写操作,需要指明是哪个文件(在支持“打开文件”操作的系统中,只需要提供文件在打开文件表中的索引号即可),还需要指明要写出多少数据、写回外存的数据放在内存中的什么位置。
(七)文件共享
-
操作系统为用户提供了文件共享功能,可以让多个用户共享地使用同一个文件。文件共享的方式有两种,分别是基于索引结点的共享方式(硬链接),一种是基于符号链的共享方式(软链接)。
-
索引结点,是在前面所提到的一种文件目录瘦身策略。由于检索文件时只需要用到文件名,因此可以将除了文件名之外的其他信息放到索引结点中,这样目录项就只需要包含文件名、索引结点指针。基于索引结点的共享方式是在索引结点中设置一个链接计数变量count,用于表示链接到本索引结点上的用户目录项数,当count值大于1时,则说明有多个用户目录项链接到该索引结点上,或者说是有多个用户在共享此文件。若有用户要删除该文件,则只是把用户目录中与该文件对应的目录项删除,且索引结点的count值减一。若count值大于0,则说明还有别的用户要使用该文件,暂时不能把文件数据删除,否则会导致指针悬空,当count值等于时系统负责删除文件。
-
如果采用的是基于符号链的共享方式,用户会建立一个新的Link类型的文件(也是通过索引结点找到),该文件记录了用户想要共享使用的文件的存放路径。当用户访问这个Link类型的文件时,操作系统会根据其中记录的路径层层查找目录,最终在其他用户的目录表中找到对应表项,进而找到想要共享使用的文件的索引结点,然后就可以对文件进行访问。
即使软链接指向的共享文件已经被删除,但是Link型文件依然会存在,只是通过Link型文件中的路径去查找共享文件会失败(找不到对应目录项)。
由于用软链接的方式访问共享文件时要查询多级目录,会有多次磁盘I/O,因此用软连接访问文件比用硬链接访问文件来说会慢得多。
(八)文件保护
-
操作系统通过口令保护、加密保护、访问控制三种方法来保护文件数据的安全。
-
口令保护就是为文件设置一个“口令”,当用户请求访问该文件时必须提供“口令”。口令一般存放在与文件对应的FCB或者索引结点中,用户访问文件前需要先输入“口令”,操作系统会将用户提供的口令与FCB中存储的口令进行对比,如果正确则允许该用户访问文件。这种文件保护方式的优点是保存口令的空间开销不多,验证口令的时间开销也很小。缺点是正确的“口令”存放在系统内部,不够安全。
-
加密保护就是使用某个“密码”对文件进行加密,在访问文件时需要提供正确的“密码”才能对文件进行解密。也就是说存储的文件内容是加密后的内容。这种文件保护方式的优点是保密性强,不需要在系统中存储密码;缺点是加密/解密需要花费一定时间。
-
访问控制就是在每个文件的FCB(或索引结点)中增加一个访问控制列表(Access-Control List,ACL),该表中记录了各个用户可以对该文件执行哪些操作。有的计算机可能会有很多个用户,因此访问控制列表可能会很大,可以用精简的访问列表解决这个问题。精简的访问列表就是以组为单位,标记各组用户可以对文件执行哪些操作,当某用户想要访问文件时,系统会检查该用户所属的分组是否有相应的访问权限。如果对某个目录进行了访问权限的控制,那也要对目录下的所有文件进行相同的访问权限控制。这种文件保护方式实现灵活,可以实现复杂的文件保护功能。
(九)文件系统的层次结构
- 用户接口是最接近用户和应用程序的。文件系统需要向上层的用户提供一些简单易用的功能接口,这层就是用于处理用户发出的系统调用请求(Read、Write、Open、Close等系统调用)。
- 用户是通过文件路径来访问文件的,因此文件目录系统需要根据用户给出的文件路径找到相应的FCB或索引节点,所有和目录、目录项相关的管理工作都再文件目录系统完成。
- 为了保证文件数据的安全,还需要验证用户是否有访问权限,存储控制模块完成了文件保护相关功能。
- 用户指明了想要访问的文件的记录号之后,逻辑系文件系统与文件信息缓冲区将记录号转换为对应的逻辑地址。
- 物理文件系统把上一层提供的文件逻辑地址转换成实际的物理地址。
- 辅助分配模块负责文件存储空间的管理,即负责分配和回收存储空间。
- 设备管理模块直接和硬件进行交互,负责和硬件直接相关的一些管理工作。如分配设备、分配设备缓冲区、磁盘调度、启动设备、释放设备等。
(十)磁盘结构
- 磁盘表面由一些磁性物质组成,用来记录二进制数据,磁盘的盘面背划分为一个个磁道,一个磁道又被划分成一个个扇区,每个扇区就是一个“磁盘块”。由于最内侧磁道上的扇区面积最小,但各个扇区存放的数据量又相同,所以最内侧磁道上的数据密度最大。
- 在磁盘中读写数据,需要将磁头移动到想要读写的扇区所在的磁道,然后磁盘转动,让目标扇区从磁头下面划过,才能完成对扇区的读写操作。
- 一个磁盘由多个盘片组成,每个盘片有两个盘面,每个盘面都对应一个磁头,所有的磁头都连接在同一个磁臂上,它们只能共同进退。所有盘面中相对位置相同的磁道组成柱面。因此可以用(柱面号,盘面号,扇区号)来定位任意一个磁盘块。根据该地址可以这样读取一个块:(1)根据柱面号移动磁臂,让磁头指向指定柱面;(2)激活指定盘面的对应的磁头;(3)在磁盘旋转的过程中,指定的扇区会从磁头下面划过,这样久完成了对指定扇区的读写。
- 磁盘中磁头可以移动的称为活动头磁盘,磁臂可以来回伸缩来带动磁头定位磁道。而磁头不可移动的成为固定头磁盘,这种磁盘中每个磁道都有一个磁头,需要使用哪个磁头久激活哪个磁头。
- 磁盘中盘片可以更换的称为可换盘磁盘,盘片不可更换的称为固定盘磁盘。
(十一)磁盘调度算法
- 寻找时间(寻道时间)Ts:在读写数据前,将磁头移动到指定磁道所花的时间。寻找时间为启动磁头臂的时间加上移动磁头的时间。
- 延迟时间TR:通过旋转磁盘,使磁头定位到目标扇区所需要的时间,设磁盘转速r(转/秒,转/分),则1/r是转一圈需要的时间,找到目标扇区平均需要转半圈,因此TR = (1/2) * (1/r)= 1/2r。磁盘的转速越高,延迟时间越短。
- 传输时间Tt:从磁盘读出或向磁盘写入数据所经历的时间。假设磁盘转入为r,此次读/写数据的字节数为b,每个磁道上的字节数为N,则Tt = (1/r)* (b/N) = b/(rN)。
- 延迟时间和传输时间都与磁盘转速相关,则为线性相关,而转速是硬件的固有属性,因此操作系统也无法优化延迟和时间和传输时间。但是操作系统的磁盘调度算法会直接影响寻道时间。
- 先来先服务算法(FCFS):根据进程请求访问磁盘的先后顺序进行调度。先来先服务算法的优点是比较公平。如果请求访问的磁道比较集中的话,算法性能还算可以。缺点是如果有大量进程竞争使用磁盘的话,且请求访问的磁道很分散,则性能很差,寻道时间长。
- 最短寻找时间优先算法(SSTF):SSTF算法会优先处理的磁道与当前磁头最近的磁道,可以保证每次的寻道时间最短,但是不能保证总的寻道时间最短。(贪心算法的思想,只选择眼前最优,但总体未必最优)。算法的优点是性能比较好,平均寻道时间较短,缺点是可能会产生饥饿现象,如果一直有在磁头附近的磁道访问请求到来的话,磁头就会在一个小区域内来回地移动,较远的磁道可能就一直访问不到。
- 扫描算法(SCAN、电梯算法):在最短寻找时间优先算法的基础上,只有磁头移动到最外侧磁道的时候才能往内移动,移动到最内侧磁道的时候才能往外移动。该算法的优点是性能较好,平均寻道时间较短,不会产生饥饿现象。缺点是只有到达最边上的磁道时才能改变磁头移动方向,有可能会产生无意义的移动;且该算法对于各个位置磁道的响应频率不平均,假设磁头正向外移动,则越靠近最外侧的磁道会更快地被再次响应请求,而越靠里的磁道需要等待的时间越长。
- LOOK调度算法:为了解决扫描算法每次需要移动到最外侧或最内侧才能改变磁头移动方向,LOOK调度算法规定如果在磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向,边移动边观察所以称为LOOK。该算法的优点是,比起SCAN算法来说,不需要每次都移动到最外侧或最内侧才改变磁头方向,使寻道时间进一步缩短。
- 循环扫描算法(C-SCAN):为了解决SCAN算法对于各个位置磁道的响应频率不平均的问题,C-SCAN算法规定只有磁头朝某个特定方向移动时才处理磁道访问请求,而返回时直接快速移动至起始端而不处理任何请求。该算法的优点是,比起SCAN算法来说,对于各个位置磁道的响应频率比较平均。该算法的缺点是知道有到达最边上的磁道时才能改变磁头移动访问,可能会产生无意义的移动,并且磁头返回到另一端的磁道时,有时也可能会产生无意义的移动,因为下一个需要响应请求的磁道可能离边缘磁道有一些距离;另外比起SCAN算法,该算法的平均寻道时间更长。
- C-LOOK调度算法:C-SCAN算法的主要缺点是只有到达最边上的磁道时才能改变磁头移动方向,并且磁头返回时不一定需要返回到最边缘的磁道上。为了解决这个问题,C-LOOK算法规定如果磁头移动方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。该算法的优点是比起C-SCAN算法来说,寻道时间进一步缩短。
(十二)减少延迟时间的方法
- 磁头读入一个扇区数据后需要一小段时间处理,而盘片又在不停地旋转,因此如果逻辑上相邻的扇区在物理上也相邻,则读入几个连续的逻辑扇区,可能需要很长的延迟时间,因为无法一次性读取多个扇区数据,则需要等到可以读取下一个扇区数据且磁头到达下一个扇区的时机,这段时间可能盘片多旋转了一段时间。
- 为了解决上述问题,可以采用交替编号的方法,即让逻辑上相邻的扇区在物理上有一定的间隔,可以使读取连续的逻辑扇区所需要的延迟时间更小。
- 读取地址连续的磁盘块时,采用(柱面号,盘面号,扇区号)的地址结构可以减少磁头移动消耗的时间,而采用(盘面号,柱面号,扇区号)的话则不行。
- 减少延迟时间的第二个方法是错位命名。一般的话相邻的盘面相对位置相同处的扇区编号是相同的,当读取某盘面的最后一个扇区后要读取下一个盘面的第一个扇区,由于编号相同,读完第一个盘面的最后一个扇区后磁头就指向了下一个盘面的第一个扇区,但由于需要一些时间来处理数据,且盘面一直在转动,因此无法马上读取下一个盘面的第一个扇区,需要等待该扇区再次划过磁头才能读取。为了解决这种现象,将相邻盘面的扇区编号错开,留出处理的事件,能够减少延迟时间。
(十三)磁盘管理
- 磁盘初始化:(1)进行低级格式化(物理格式化),将磁盘的各个磁道划分为扇区。一个扇区通常可以分为头、数据区域、尾三个部分组成。管理扇区所需要的各种数据结构一般存放在头、尾两个部分,包括扇区校验码(如奇偶校验、CRC循环冗余校验等,用于校验扇区中的数据是否发生错误)。(2)将磁盘分区,每个分区由若干柱面组成。(3)进行逻辑格式化,创建文件系统,包括创建文件系统的根目录、初始化存储空间管理所用的数据结构(如位示图、空闲分区表)
- 计算机开机时需要进行一系列初始化的工作,这些初始化工作是通过执行初始化程序(自举程序)完成的,初始化程序可以放在ROM(只读存储器)中,ROM中的数据在出出厂时就写入了,并且以后不能再修改,ROM一般是出厂时就集成在主板上的。但是这种情况如果要更新自举程序的话,就会很不方便,因为ROM中的数据无法更改。所以现在的计算机ROM中只存放很小的“自举装入程序”,而完整的自举程序放在磁盘的启动块(即引导块),启动快位于磁盘的固定位置,开机时计算机先运行“自举装入程序”,通过执行该程序就可以找到引导块,并将完成的“自举程序“读入内存,完成初始化。一般拥有启动分区的磁盘称为启动磁盘或者系统磁盘(C盘)。
- 坏了、无法正常使用的扇区就是坏块,这属于硬件故障,操作系统是无法修复的,应该将坏块标记出来,以免错误地使用到它。
- 对于简单的磁盘,可以在逻辑格式化时(建立文件系统时)对真个磁盘进行坏块检查,标明哪些扇区是坏扇区,比如在FAT表上标明(这种方式中,坏块对操作系统不透明)。对于复杂的磁盘,磁盘控制器(磁盘设备内部的一个硬件部件)会维护一个坏块链表,在磁盘出厂前进行低级格式化时就将坏块链进行初始化,且磁盘会保留一些备用扇区,用于替换坏块,这种方案称为扇区备用。(这种处理方式中,坏块对操作系统透明)。
五、输入输出管理
(一)I/O设备
- I/O设备就是可以将数据输入到计算机,或者可以接受计算机输出数据的外部设备,属于计算机中的硬件部件。UNIX系统将外部设备抽为一种特殊的文件,用户可以使用与文件操作相同的方式对外部设备进行操作,比如Write操作就是向外部设备写出数据,Read操作就是从外部设备读入数据。
- 按使用特性分类,I/O设备可以分为人机交互类外部设备(鼠标、键盘、打印机等,数据传输速度慢)、存储设备(移动硬盘、光盘等,用于数据存储)、网络通信设备(调制解调器等,数据传输速度中等)。
- 按传输速率分配,I/O设备可以分为低速设备(鼠标、键盘等,传输速率为每秒几个到几百字节)、中速设备(如激光打印机等,传输速率为每秒数千至上万个字节)、高速设备(如磁盘等,传输速率为每秒数千字节至千兆字节)。
- 按信息交换的单位分配,可以分为块设备(如磁盘等,数据传输 基本单位是块,传输速率较高,可寻址,即对它可随机地读写任一块)和字符设备(鼠标、键盘等,数据传输的基本单位是字符,传输速率较慢,不可寻址,在输入输出时常采用中断驱动方式)。
(二)I/O控制器
- I/O设备由机械部件和电子部件组成,电子部件又叫做I/O控制器或设备控制器。I/O设备的机械部件主要用来执行具体I/O操作,I/O设备的电子部件通常是一块插入主板扩充槽的印刷电路板。CPU无法直接控制I/O设备的机械部件,因此I/O设备还要有一个电子部件作为CPU和I/O设备机械部件之间的“中介”,用于实现CPU对设备的控制。
- I/O控制器的功能有接受和识别CPU发出的命令(I/O控制器中会有相应的控制寄存器,用来存放CPU发送过来的命令和参数)、向CPU报告设备的状态(I/O控制器中会有相应的状态寄存器,用于记录I/O设备的当前状态)、数据交换(I/O控制器中会设置相应的数据寄存器,输出时,数据寄存器用于暂存CPU发来的数据,之后再由控制器传送设备。输出时。数据寄存器用于暂存设备发来的数据,之后CPU从数据寄存器中取走数据)、地址识别(类似于内存的地址,为了区分设备控制器中的各个寄存器,也需要给各个寄存器设置一个特定的“地址”。I/O控制器通过CPU提供的d指来判断CPU要读写的是哪个寄存器)。
- I/O控制器的组成包括:CPU与控制器的接口(用于实现CPU与控制器之间的通信,CPU通过控制线发出命令,通过地址线指明要操作的设备,通过数据线输入或输出数据)、I/O逻辑(负责接收和识别CPU的各种命令,并负责对设备发出命令)、控制器与设备的接口(用于实现控制器与设备之间的通信,可能会有多个)。
- 数据寄存器、控制寄存器、状态寄存器可能会有多个(如L每个控制/状态机器存起对应的一个具体的设备),且这些寄存器都要有相应的地址,才能方便CPU操作。有的计算机会让这些寄存器占用内存地址的一部分,称为内存映像I/O;另一些计算机则采用I/O专用地址,即寄存器独立编址。如果采用内存映像I/O的话,控制器中的寄存器与内存地址统一编址;如果采用寄存器独立编址的话,控制器中的寄存器使用单独的地址。
(三)I/O控制方式
- I/O控制方式即用什么样的方式来控制I/O设备的数据读/写,可以分为程序直接控制方式、中断驱动方式、DMA方式、通道控制方式。
- 程序直接控制方式,关键是轮询。这种方式CPU干预的频率会频繁,在I/O操作完成之前,完成之后需要CPU接入,并且在等待I/O完成的过程中CPU需要不断地轮询检查;这种方式数据传送的单位是字;数据的流向是I/O设备→CPU寄存器→内存(读操作)或内存→CPU寄存器→I/O设备(写操作);这种方式的优点是实现简单,在读/写指令之后,加上实现循环检查的一系列指令即可(因此才称为“程序直接控制方式”),缺点是CPU和I/O设备只能串行工作,CPU需要一直轮询检查,长期处于“忙等”状态,CPU利用率低。
- 中断驱动方式。由于I/O设备速度很慢,因此在CPU发出读/写命令后,可将等待I/O的进程阻塞,先切换到别的进程执行。当I/O完成后,控制器会向CPU发送一个中断信号,CPU检测到中断信号后,会保存当前进程的运行环境信息,转去执行中断处理程序处理该中断。处理终端的过程中,CPU从I/O控制器读一个字的数据传送到CPU寄存器,再写入主存。接着,CPU恢复等待I/O的进程(或其他进程)的运行环境,然后继续执行。
需要注意的是:(1)CPU会在每个指令周期的末尾检查中断;(2)中断处理过程中需要保存、恢复进程的运行环境,这个过程是需要一定时间开销的,可见如果中断发生的频率太高,也会降低系统性能。
相比程序直接控制方式,这种方式的CPU干预频率较低,只有在每次I/O操作开始之前,完成之后需要CPU介入;采用这种方式数据的传送单位是一个字;数据的流向是I/O设备→CPU寄存器→内存(读操作)或内存→CPU寄存器→I/O设备(写操作);这种方式的主要优点是I/O控制器会通过中断信号主动报告I/O已经完成,CPU不需要不停地轮询,CPU和I/O设备可并行工作,CPU利用率得到明显提升;主要缺点是每个字在I/O设备与内存之间的传输都需要经过CPU,而频繁的中断需要较大的时间代价。
- DMA方式即直接存储器存取,主要用于块设备的I/O控制。相比于中断驱动方式,这种方式数据的传送单位是块,且数据的流向是从设备直接放入内存,或者从内存直接到设备;仅在传送一个或多个数据块的开始和结束时,才需要CPU干预。
DMA控制器:
其中DR是数据寄存器,暂存从设备到内存,或从内存到设备的数据;MAR是内存地址寄存器,在输入时,MAR表示数据应放到内存中的什么位置,在输出时MAR表示要输出的数据放在内存中的什么位置;DC是数据计数器,表示剩余要读/写的字节数;CR是命令/状态寄存器,用于存放CPU发来的I/O命令,或设备的状态信息。
采用这种方式,CPU仅在传送一个或多个数据块的开始和结束时,才需要CPU干预;数据传送的单位是块(注意每次读写的只能是连续的多个块,且这些块读入内存后在内存中也必须是连续的);数据的流向是I/O设备→内存(读操作)或内存→I/O设备(写操作);这种方式的优点是数据传输以块为单位,CPU接入频率进一步降低,数据的传输不再需要经过CPU再写入内存,数据传输效率哪一步增加,CPU和I/O设备的并行性得到提升;缺点是CPU每发出一条I/O指令,只能读/写一个或多个连续的数据块,如果要读/写多个离散存储的数据块,或者要将数据分别写到不同的内存区域时,CPU要分别发出多条I/O指令,进行多次中断处理才能完成。
-
通道控制方式。通道是一种硬件,可以理解为弱化版的CPU,可以识别并执行一系列通道指令。与CPU相比,通道可以执行的指令很单一,并且通道程序是放在主机内存中的,也就是说通道与CPU共享内存。
采用这种方式,CPU干预的频率变得极低,通道会根据CPU指示执行相应的通道程序,只有完成一组数据块的读/写后才需要发出中断信号,请求CPU干预;数据传送的单位是一组数据块;数据的流向是I/O设备→内存(读操作)或内存→I/O设备(写操作);优点是CPU、通道、I/O设备可并行工作,资源利用率很高,缺点是实现复杂,需要专门的通道硬件支持。 -
各种方式的对比
(四)I/O软件的层次
- 用户层软件实现了与用户交互的接口,用户可直接使用该层提供的、与I/O操作相关的库函数对设备进行操作。用户层软件将用户请求翻译成格式化的I/O请求,并通过“系统调用”请求操作系统内核的服务。
- 设备独立性软件又称设备无关性软件,与设备的硬件特性无关的功能几乎都在这一层实现。主要实现的功能:(1)向上层提供统一的调用接口(如read/write系统调用);(2)实现设备的保护,原理类似于文件保护,设备被看作是一种特殊的文件,不同用户对各个文件的访问权限是不一样的,同理,对设备的访问权限也不一样;(3)差错处理,设备独立性软件需要对一些设备的错误进行处理;(4)设备的分配与回收;(5)数据缓冲区管理,可以通过缓冲技术屏蔽设备之间数据交换单位大小和传输速度的差异;(6)建立逻辑设备名到物理设备名的映射关系,根据设备类型选择调用相应的驱动程序。
用户或用户层软件发出I/O操作相关的系统调用时,需要指明此次要操作的I/O设备的逻辑设备名,设备独立性软件通过“逻辑设备表LUT(Logical Unit Table)”来确定逻辑设备对应的物理设备,并找到该设备对应的设备驱动程序。逻辑设备表存储了各逻辑设备名对应的物理设备名以及相应的驱动程序入口地址。操作系统可以采用两种方式管理逻辑设备表,第一种方式是he那个歌系统只设置一张逻辑设备表,这就意味着所有用户不能使用相同的逻辑设备名,因此这种方式只使用于单用户操作系统;第二种方式是为每个用户设置一张逻辑设备表,各个用户使用的逻辑设备名可以重复,适用于多用户操作系统,系统会才用户登录时为其建立一个用户管理进程,而逻辑设备表就存放在用户管理进程的PCB中。
- 不同设备的内部硬件特性不同,这些特性只有厂家才知道,因此厂家须提供与设备想对应的驱动程序,CPU执行驱动程序的指令序列,来完成设置设备i存期,检查设备状态等工作。
- 设备驱动程序只要负责对硬件设备的具体控制,将上层发出的一系列命令转化成特定设备能够识别的一系列操作,包括设置设备寄存器,检查设备状态等。
- 当I/O任务完成时,I/O控制器会发送一个终端信号,系统会根据终端信号类型找到相应的中断处理程序并执行。中断处理程序的处理流程如下:
(五)I/O核心子系统实现的功能
- I/O核心子系统实现的功能主要是I/O调度、设备保护、设备分配与回收、缓冲区管理(即缓冲与高速缓存)。有些书籍将假脱机技术(SPOOLing技术)也归为I/O核心子系统实现的功能,但是假脱机技术需要请求“磁盘设备”的设备独立性软件的服务,因此一般来说假脱机技术是在用户层软件实现的。
- I/O调度:用某种算法确定一个好的顺序来处理各个I/O请求。
- 设备保护:在UNIX系统中,设备被看作是一种特殊的文件,每个设备也会有对应的FCB。当用户请求访问某个设备时,系统根据FCB中记录的信息来判断该用户是否有相应的访问权限,以此实现“设备保护”的功能。
- 在早期,主机直接从I/O设备(纸带机)获得数据,由于设备速度慢,主机速度很快,人机速度矛盾明显,主机需要浪费很多时间等待设备。后来引入了脱机输入/输出技术,用一个外围控制机将纸带的数据传输到磁带上,然后主机再从较快速的磁带上读取或输出数据,这样的话速度就变快了很多。因为输入/输出操作是脱离主机的控制的,所以叫脱机操作。引入脱机技术后,缓解了CPU与慢速I/O设备的速度矛盾,另一方面,即使CPU在忙碌,也可以提前将数据输入到磁带;即使慢速的输出设备正在忙碌,也可以提前将数据输出到磁带。
- 假脱机技术又称SPOOLing技术,是用软件的方式模拟脱机技术。SPOOLing系统的组成如下:
内存会开辟一个输入缓冲区和输出缓冲区,在输入进程的控制下,输入缓冲区用于暂存从输入设备输入的数据,之后再转存到输入井中;在输出进程的控制下,输出缓冲区用于暂存从输出井送来的数据,之后再传送到输出设备上。
输入进程和输出进程必须和其他进程并发运行,才能够模拟脱机技术,因此实现SPOOLing技术,必须要有多道程序技术的支持。
使用SPOOLing技术,可以将打印机这种独占式设备改造成共享设备。当多个用户进程提出输出打印的请求时,系统会答应它们的请求,但是并不是真正把打印机分配给他们,而是由假脱机管理进程为每个进程做两件事:(1)在磁盘输出井中为进程申请一个空闲缓冲区,并将要打印的数据送入其中;(2)为用户进程申请一张空白的打印申请表,并将用户的打印请求填入表中(其实就是用来说明用户的打印数据存放位置等信息的),再将该表挂到假脱机文件队列上。当打印机空闲时,输出进程会从文件队列的队头取出一张打印请求表,并根据表中的要求将要打印的数据从输出井传送到输出缓冲区,再输出到打印机进行打印,用这种方式可一次处理完全部的打印任务。
-
设备分配时应该考虑的三个因素:设备的固有属性、设备分配算法、设备分配中的安全性。设备的固有属性可分为三种:独占设备、共享设备(可同时分配给多个进程使用,各进程往往是宏观上同时共享使用设备,而微观上交替使用)、虚拟设备(采用SPOOLing技术将独占设备改造成虚拟的共享设备,可同时分配给多个进程使用,如采用SPOOLing技术实现的共享打印机)。
从进程运行的安全性考虑,设备分配有两种方式:(1)安全分配方式,为进程分配一个设备后就将进程阻塞,本次I/O完成后才将进程唤醒。这种分配方式在一个时间段每个进程只能使用一个设备,优点是破坏了“请求和保持”条件,不会死锁,缺点是对于一个进程来说,CPU和I/O设备只能串行工作。(2)不安全分配方式,进程发出I/O请求后,系统为其分配I/O设备,进程可继续执行,之后还可以发出新的I/O请求,只有某个I/O请求得不到满足时才将进程阻塞。这种分配方式使得一个进程可以同时使用多个设备,优点是进程的计算任务和I/O任务可以并行处理,使进程迅速推进,缺点是有可能发生死锁。 -
一个通道可控制多个设备控制器,每个设备控制器可控制多个设备。
- 设备分配管理中的数据结构。
系统会为每个设备配置一张设备控制表DCT,用于记录设备情况。
每个设备控制器都会对应一张控制器控制表COCT,操作系统根据COCT对控制器进行操作和管理。
每个通道都会对应一张通道控制表CHCT,操作系统根据CHCT的信息对通道进行操作和管理。
操作系统中有一张系统设备表SDT,记录了系统中全部设备的情况,每个设备对应一个表目。
- 设备分配的步骤:(1)根据进程请求的物理设备名查找SDT;(2)根据SDT找到DCT,若设备忙碌则将进程PCB挂到设备等待队列中,不忙碌则将设备分配给进程;(3)根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中,不忙碌则将控制器分配给进程;(4)根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中,不忙碌则将通道分配给进程。只有设备、控制器、通道三者都分配成功时,这次设备分配才算成功,之后便可以启动I/O设备进行数据传送。
这种设备分配步骤的缺点是:(1)用户编程时必须使用武力设备名,底层细节对用户不透明,不方便编程;(2)若换了一个物理设备,则程序无法运行;(3)若进程请求的物理设备正在忙碌,则及时系统中还有同类型的设备,进程也必须阻塞等待。
改进方法:建立逻辑设备名与物理设备名的映射机制,用户编程时只需要提供逻辑设备名。
改进后的设备分配步骤:(1)根据进程请求的逻辑设备名查找SDT(用户编程时提供的逻辑设备名其实就是设备类型。);(2)查找SDT,找到用户进程指定类型的、并且空闲的设备,将其分配给盖设备,操作系统在逻辑设备表中新增一个表项;(3)根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中,不忙碌则将控制器分配给进程;(4)根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中,不忙碌则将通道分配给进程。
逻辑设备表建立了逻辑设备名与物理设备名之间的映射关系。某用户进程第一次使用设备时使用逻辑设备名向操作系统发出请求,操作系统根据用户进程指定的设备类型(逻辑设备名)查找系统设备表,找到一个空闲设备分配给进程,并在逻辑设备表中增加相应表项。如果之后用户进程再次通过相同的逻辑设备名请求使用设备,则操作系统通过逻辑设备表即可知道用户进程实际要使用的是哪个物理设备了,并且也能知道该设备的驱动程序入口地址。
- 缓冲区是一个存储区域,可以由专门的硬件寄存器组成,也可以利用内存作为缓冲区。使用硬件作缓冲区的成本较高,容量也较小,一般仅用在对速度要求非常高的场合(如如存储器管理中所用的联想寄存器)。一般情况下,更多的是利用内存作为缓冲区,设备独立性软件的缓冲区管理就是要组织管理好这些缓冲区。
在内存中可以开辟一小片区域作为缓冲区,CPU可以把要输出的数据快速地放入缓冲区,之后就可以做别的事情,然后慢速的I/O设备可以慢慢从缓冲区取走数据,在数据输入时也是类似的。因此缓冲区的一个作用就是缓和CPU与I/O设备之间速度不匹配的矛盾。
如果是字符型设备,则每输出完一个字符就要向CPU发送一次中断信号,如果利用缓冲区的话,可以减少对CPU的中断频率,放款对CPU中断响应时间的限制。
如果输出进程每次可以产生一块数据,而I/O设备每次只能输出一个字符,那么采用缓冲区将数据先存放到缓冲区中,再让I/O设备从缓冲区中读取数据,这样就解决了数据粒度不匹配的问题。
此外,很明显,采用缓冲区能够提高CPU和I/O设备之间的并行性。
- 缓冲区管理策略。
假设某用户进程请求某种块设备读入若干块的数据。若采用单缓冲的策略,操作系统会在主存中为其分配一个缓冲区,一般一个缓冲区的大小就是一个块。(当缓冲区数据非空时,不能往缓冲区冲入数据,只能从缓冲区把数据传出;当缓冲区为空时,可以往缓冲区冲入数据,但必须把缓冲区充满以后,才能从缓冲区把数据传出。)而用户进程的内存空间中,会分出一片工作区来接收输入/输出数据,一般工作区大小也与缓冲区相同。
假设某用户进程请求某种块设备读入若干块的数据。若采用双缓冲的策略,操作系统会在主存中为其分配两个缓冲区,一般一个缓冲区的大小就是一个块。
- 在两台机器之间通信时,可以配置缓冲区用于数据的发送和接受。两台机器如果配置单缓冲区的话,A机要发送的数据要先放入A机缓冲区中,等缓冲区满时将数据发出,此时B机的缓冲区用于接受数据,只有B机将缓冲区中的数据全部取走后,才能向A机发送数据。显然,若两个相互通信的机器只设置单缓冲区,在任一时刻只能实现数据的单向传输。若若两台机器设置双缓冲区,其中一个缓冲区用于存储要发送的数据,另一个缓冲区用于接受数据,则在同一时刻可以实现双向的数据传输。(管道通信中的管道其实就是缓冲区,要实现数据的双向传输,必须设置两个管道。)
- 很多时候两个缓冲区任然不能满足需要,操作系统可以给一个进程分配多个大小相等缓冲区,并让他们链接成一个循环队列,称为循环缓冲区。并且使用一个in指针指向下一个可以冲入数据的空缓冲区,使用一个out指针指向下一个可以去除数据的满缓冲区。
- 缓冲池由系统中共用的缓冲区组成。这些缓冲区俺使用状况可以分为:空缓冲队列,装满输入数据的缓冲队列(输入队列)、装满输出数据的缓冲队列(输出队列)。另外,根据一个缓冲区在实际运算中扮演的功能不同,又设置了四种工作缓冲区:用于收容输入数据的工作缓冲区(hin)、用于提取输入数据的工作缓冲区(sin)、用于收容输出数据的工作缓冲区(hout)、用于提取输出数据的工作缓冲区(sout)。
如果输入进程请求输入数据,则从空缓冲队列中取出一块作为收容输入数据的工作缓冲区(hin),充满数据后将缓冲区挂到输入队列队尾。
如果计算进程想要取得一块输入数据,则从输入队列中取得一块缓冲区作为提取输入数据的工作缓冲区(sin),缓冲区读空后挂到空缓冲区队列。
如果计算进程想要量准备好的数据冲入缓冲区,则从空缓冲队列中取出一块作为收入输出数据的工作缓冲区(hout),数据充满后将缓冲区挂到输出队列队尾。
如果输出进程请求输出数据,则从输出队列中取得一块充满输出数据的缓冲区作为提取输出数据的工作缓冲区(sout),缓冲区堵孔后挂到空缓冲区队列。