二. 进程管理

1.1 进程的概念和特征

 

1 进程的概念

进程( Process)以便更好地描述和控制程序的并发执行 操作系统的并发性共享性(最基本的两个特性) 为了使参与并发执行的程序(含数据)能独立地运行,必须 为之配置一个专门的数据结构,称为进程控制块( Process Control Block,PCB).系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程。相应地,由程序段相关数据段PCB三部分构成了进程映像(进程实体).所谓创建进程,实质上是创建进程映像中的PCB:而撤销进程,实质上是撤销进程的PCB。值得注意的是,进程映像是静态的,进程则是动态的

从不同的角度,进程可以有不同的定义,比较典型的定义有:

1)进程是程序的一次执行过程。

2)进程是一个程序及其数据在处理机上顺序执行时所发生的活动。

3)进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。 在引入进程实体的概念后,我们可以把传统操作系统中的进程 定义为:进程是进程实体的运行过程是系统进行资源分配和调度的一个独立单位。这里的系统资源,指的是处理机、存储器和其他设备服务于某进程的“时间”,比如,把处理机资源理解为处理机的时间片才是准确的。因为进程是这些资源分配和调度的独立单位,即“时间片”分配的独立单位,这就决定了,进程一定是一个动态的、过程性的概念。

2 进程的特征 进程是由多程序的并发执行而引出的,它和程序是两个截然不同 的概念。进程的基本特征是 对比单个程序的顺序执行提出的,也是对进程管理提出的基本要求

1)动态性:进程是程序的一次执行,它有着创建、活动、暂停、 终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征

2)并发性:指多个进程实体,同存于内存中,能在一段时间 内同时运行,并发性是进程重要特征,同时也是操作系统重要特征。引入进程的的就是为了使程序能与其他进程的程序并发执行,以提高资源利用率。

3)独立性:指进程实体是一个能独立运行、独立获得资源和独立 接受调度的基本单位。凡未建立PCB的程序都不能作为一个独立 的单位参与运行。

4)异步性:由于进程的相互制约,使进程具有执行的间断性,即 进程按各自独立的、不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此,在操作系统中必须配置相应的进程同步机制。

5)结构性每个进程都配置一个PCB对其进行描述。从结构上 看,进程实体是由程序段, 数据段进程控制段三部分组成的。

1.2 进程的状态与转换

 

 通常进程有以下五种状态前三种是进程的基本状态

1)运行态进程正在处理机上运行。在单处理机环境下, 每一时刻最多只有一个进程处于运行状态。

2)就绪态进程已处于准备运行的状态,即进程获得了除 处理机之外一切所需资源,一旦得到处理机即可运行。

3)阻塞态,又称等待状态:进程正在等待某一事件而暂 停运行,如等待某资源为可用(不包括处理机)或等待输入 输出完成。即使处理机空闲,该进程也不能运行。

4)创建态进程正在被创建,尚未转到就绪状态。创建进 程通常需要多个步骤:首先申请一个空白的PCB,并向PCB 中填写一些控制和管理进程的信息;然后由系统为该进程分配 运行时所必需的资源; 最后把该进程转入到就绪状态

5)结束态进程正从系统中消失,这可能是进程正常结 束或其他原因中断退出运行。当进程需要结束运行时,系统 首先必须置该进程为结束状态,然后再进一步处理资源释放 和回收等工作,注意区别就绪状态等待状态(就绪状态是指 进程仅缺少处理机,只要获得处理机资源就立即执行;而等 待状态是指进程需要其他资源(除了处理机)或等待某一事 )。之所以把处理机和其他资源划分开,是因为在分时系 统的时间片轮转机制中,每个进程分到的时间片是若干毫秒。 也就是说,进程得到处理机的时间很短且非常频繁,进程 在运行过程中实际上是频繁地转换到就绪状态的:而其他 资源(如外设)的使用和分配或者某一事件的发生(如I/O操 作的完成)对应的时间相对来说很长,进程转换到等待 状态的次数也相对较少。这样来看,就绪状态和等待状态 是进程生命周期中两个完全不同的状态,很显然需要加以区分。

就绪态→运行态:处于就绪状态的进程被调度后,获得处理机资源(分派处理机时间片)于是进程由就绪状态转换为运行状态。

运行态→就绪态:处于运行状态的进程在时间片用完后,不得不让出处理机,从而进程由运行状态转换为就绪状态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程度将正执行的进程转换为就绪状态,让更高优先级的进程执行。

运行态→阻塞态:当进程请求某一资源(如外设)的使用和 分配或等待某一事件的发生(如IO操作的完成)时,它就从运行 状态转换为阻塞状态。

阻塞态→就绪态:资源分配到位,或等待的事件已发生(被动行为)。

创建态→就绪态: 系统完成创建进程相关的工作。

运行态→终止态: 进程运行结束,或运行过程中遇到不可修复的错误。

进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式阻塞状态→就绪状态:当进程等待的事件到来时,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞状态转换为就绪状态需要注意的是,一个进程从运行状态变成阻塞状态是一个主动的行为,而从阻塞状态变到就就绪状态是一个被动的行为,需要其他相关进程的协助。

1.3 进程控制

 进程控制的主要功能是对系统中的所有进程实施有效的管理,它 具有创建新进程、撤销已有进程、实现进程状态转换等功能。在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位。

1 进程的创建

允许一个进程创建另一个进程。此时创建者称为父进程,被创 建的进程称为子进程。子进程可以继承父进程所拥有的资源。当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程。此外,在撤销父进程时,也必须同时撤销其所有的子进程在操作系统中,终端用户登录系统、作业调度、系统提供服务、用户程序的应用请求等都会引起进程的创建。操作系统创建一个新进程的过程如下(创建原语):  

1)为新进程分配一个唯一的进程标识号,并申请一个空白的PCB (PCB是有限的).若PCB申请失败则创建失败。

2)为进程分配资源,为新进程的程序和数据,以及用户栈分配必 要的内存空间(在PCB中体现).注意:这里如果资源不足(比如内存空间),并不是创建失败,而是处于“等待状态或称为“阻塞状态”,等待的是内存这个资源。

3)初始化PCB,主要包括初始化标志信息、初始化处理机状态 信息和初始化处理机控制信息,以及设置进程的优先级等。 4)如果进程就绪队列能够接纳新进程,就将新进程插入到就绪 队列,等待被调度运行。

2 进程的终止

引起进程终止的事件主要有:正常结束,表示进程的任务已经 完成和准备退出运行。异常结束,表示进程在运行时,发生了某种异常事件,使程序无法继续运行,如存储区越界、保护错、非法指令、特权指令错、I/O故障等。外界干预是指进程应外界的请求而终止运行,如操作员或操作系统干预、父进程请求,父进程终止操作系统终止进程的过程如下(撤销原语)。

1)根据被终止进程的标识符,检索PCB,从中读出该进程的状态

2)若被终止进程处于执行状态,立即终止该进程的执行,将处理 机资源分配给其他进程。

3)若该进程还有子进程,则应将其所有子进程终止

4)将该进程所拥有的全部资源,或归还给其父进程或归还 给操作系统

5)将该PCB从所在队列(链表)中删除。  

3 进程的阻塞和唤醒

正在执行的进程,由于期待的某些事件未发生,如请求系统 资源失败、等待某种操作的完成。 数据尚未到达或无新工作可做等,则由系统自动执行阻塞 原语( Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。阻塞原语的执行过程是:

1)找到将要被阻塞进程的标识号对应的PCB;

2)若该进程为运行状态,则保护其现场,将其状态转为阻塞 状态,停止运行;

3) 把该PCB插入到相应事件的等待队列中,当被阻塞进程所期待的事件出现时,如它所启动的IO操作已完成或其所期待的数据已到达则由有关进程(比如,提供数据的进程)调用喚醒原语(Wakeup),将等待该事件的进程唤醒。

唤醒原语的执行过程是:

1)在该事件的等待队列中找到相应进程的PCB

2)将PBC从等待队列中移出,并设置相应的进程状态为就绪状态

3)把该PCB插入就绪队列中,等待调度程序调度。 需要注意的是,Block原语和 Wakeup原语是一对作用刚好 相反的原语,必须成对使用。 Block原语是由被阻塞进程 自我调用实现的,而 Wakeup原语则是由一个与被唤醒进 程相合作或被其他相关的进程调用实现的。

4 进程切换  

对于通常的进程,其创建、撤销以及要求由系统设备完成 的IO操作都是利用系统调用而进入内核再由内核中相应 处理程序予以完成的。进程切换同样是在内核的支持下实 现的,因此可以说,任何进程都是在操作系统内核的支 持下运行的,是与内核紧密相关的进程切换是指处理机 从一个进程的运行转到另一个进程上运行,这个过程中, 进程的运行环境产生了实质性的变化。进程切换的过 程如下:

1)保存处理机上下文,包括程序计数器和其他寄存器

2)更新PCB信息

3)把进程的PCB移入相应的队列如就绪、在某事件阻塞等队列

4)选择另一个进程执行,并更新其PCB

5)更新内存管理的数据结构。 6)恢复处理机上下文

注意,进程切换与处理机模式切换是不同的,模式切换时,处理机逻辑上可能还在同一进程中运行。如果进程因中断或异常进入到核心态运行,执行完后又回到用户态刚被中断的程序运行则操作系统只需恢复进程进入内核时所保存的CPU现场,无需改变当前进程的环境信息。但若要切换进程,当前运行进程改变了,则当前进程的环境信息也需要改变。

注意“调度”和“切换”的区别调度是指决定资源分配给哪个进程的行为,是一种决策行为, 切换是指实际分配的行为,是执行行为。一般来说,先有资源的调度,然后才有进程的切换

程序是如何运行的?

1.4 进程的组织

 进程是操作系统的资源分配和独立运行的基本单位。它 一般由以下三个部分组成:

1 进程控制块

进程创建时,操作系统就新建一个PCB结构,它之后就常驻内存,任一时刻可以存取,在进程结束时删除。PCB是进程实体的一部分,是进程存在的唯一标志。当创建一个进程时,系统为该进程建立一个PCB:当进程执行时,系统通过其PCB了解进程的现行状态信息,以便对其进行控制和管理;当进程结束时,系统收回其PCB,该进程随之消亡。操作系统通过PCB表来管理和控制进程PCB主要包括进程描述信息、进程控制和管理信息、资源分配清单和处理机相关信息等。各部分的主要说明如下:

1)进程描述信息 进程标识符标志各个进程,每个进程都有一个且是唯一的标识号用户标识符:进程归属的用户,用户标识符主要为共享和保护服务。

2)进程控制和管理信息 进程当前状态:描述进程的状态信息,作为处理机分配调度的依据 进程优先级:描述进程抢占处理机的优先级,优先级高的 进程可以优先获得处理机。

3)资源分配清单,用于说明有关内存地址空间虚拟地址空 的状况:所打开文件的列表和所使用的输入输出设备信息

4)处理机相关信息,主要指处理机中各寄存器值,当进程被切换时处理机状态信息都必须保存在相应的PCB中,以便在该进程重新执行时,能再从断点继续执行在一个系统中,通常存在着许多进程,有的处于就绪状态,有的处于阻塞状态,而且阻塞的原因各不相同。为了方便进程的调度和管理,需要将各进程的PCB用适当的方法组织起来。目前常用的组织方式有链接方式索引方式两种。 链接方式将同一状态的PCB链接成一个队列,不同状态对应不同的队列,也可以把处于阻塞状态的进程的PCB,根据其阻塞原因的不同,排成多个阻塞队列。 索引方式是将同一状态的进程组织在一个索引表中索引表的表项指向相应的PCB。不同状态对应不同的索引表,如就绪索引表和阻塞索引表等。

2 程序段

程序段就是能被进程调度到CPU执行的程序代码段。注意,程序可以被多个进程共享,就是说多个进程可以运行同一个程序。

3 数据段

一个进程的数据段,可以是进程对应的程序加工处理的原始 数据,也可以是程序执行时产生的中间或最终结果。

1.5 进程的通信

进程通信是指进程之间的信息交换。PV操作是低级通信方式,高级通信方式是指以较高的效率传输大量数据的通信方式。高级通信方法主要有以下三个类。

1.5.1 共享存储

1)设置一个共享空间

2) 要互斥的访问共享空间

共享的存储的两种方式:

基于数据结构的共享: 比如共享空间里只能放一个长度为10的数组。这种共享方式速度慢,限制多,是一种低级通信方式。

基于数据结构的共享: 在内存中画出一块共享存储区数据的形式,存放位置都由进程控制,而不是操作系统。相比之下,这种共享方式速度更快, 是一种高级的通信方式。

1.5.2 管道通信

1) 半双工通信,如果要实现双向通信,需设置两个管道

2) 各进程互斥访问。

3) 数据已字符流的形式写入,当管道写满时,写进程的wirte()系统调用将被阻塞。当数据被读走管道变空后,此时读进程read()系统调用将被阻塞

4) 没写满,就不允许读没读空,就不允许写

5) 数据被读出后就从管道中被抛弃,意味着读进程最多只能有一个。

1.5.3 消息传递

1) 传递结构化信息 (消息头/消息体)。

2) 系统提供 "发送/接收原语"。

两种方式:

直接通信方式: 消息直接挂到接收方的消息队列里。

间接(信箱)通信方式: 消息先发到中间体(信箱)。

1.6 线程概念和多线程模型

1.6.1 线程的基本概念

 

 引入进程的目的,是为了更好地使多道程序并发执行,以提高资源利用率和系统吞吐量,增加并发程度;而引入线程,则是为了减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能线程最直接的理解就是“轻量级进程”,它是一个基本的CPU执行单元,也是程序执行流的最小单元,由线程ID、程序计数器、寄存器集合和堆栈组成。

线程是进程中的一个实体,是被系统独立调度分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源但它可与同属一个进程的其他线程共享进程所拥有的全部资源。

一个线程可以创建和撤销另一个线程同一进程中的多个线程之间可以并发执行。

由于线程之间的相互制约,致使线程在运行中呈现出间断性线程也有就绪、阻塞和运行三种基本状态。

引入线程后,进程的内涵发生了改变,进程只作为除CPU以外系统资源的分配单元线程则作为处理机的分配单元。由于一个进程内部有多个线程,如果线程的切换发生在同一个进程内部,则只需要很少的时空开销。

1.6.2 线程与进程的比较

 

 

1)调度:

在传统的操作系统中,拥有资源和独立调度的基本单位都是进程。在引入线程的操作系统中,线程是独立调度的基本单位,进程是拥有资源的基本单位。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。

2)拥有资源:

不论是传统操作系统还是设有线程的操作系统,进程都是拥有资源的基本单位,而线程不拥有系统资源(也有一点必不可少的资源),但线程可以访问其隶属进程的系统资源。我们要知道,如果线程也是拥有资源的单位,那么,切换线程也需要较大的时空开销,线程这个概念的提出就没有意义了。

3)并发性:

在引入线程的操作系统中,不仅进程之间可以并发执行,而且多个线程之间也可以并发执行,从而使操作系统具有更好的并发性,提高了系统的吞吐量。

4)系统开销:

由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、IO设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程CPU环境保存及新调度到进程CPU环境设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。此外,由于同一进程内的多个线程共享进程的地址空间,因此,这些线程之间的同步与通信非常容易实现,甚至无需操作系统的干预。

5)地址空间和其他资源(如打开的文件):

进程的地址空间之间互相独立,同一进程的各线程间共享进程的资源,某进程内的线程对于其他进程不可见。

6)通信方面:

进程间通信(IPC)需要进程同步和互斥手段的辅助,以保证数据的一致性,而线程间可以直接读/写进程数据段(如全局变量)来进行通信。

1.6.3 线程的属性

 在多线程操作系统中,把线程作为独立运行(或调度)的基本单位,此时的进程,已不再是个基本的可执行实体。但进程仍具有与执行相关的状态,所谓进程处于“执行”状态,实际上是指该进程中某线程正在执行。线程的主要属性如下:

1)线程是一个轻型实体,它不拥有系统资源,但每个线程都应有一个唯一的标识符和一个线程控制块线程控制块记录了线程执行寄存器栈等现场状态。

2)不同的线程可以执行相同的程序,即同一个服务程序被不同的用户调用时操作系统为它们创建成不同的线程

3)同一进程中各个线程共享该进程所拥有的资源

4)线程是处理机的独立调度单位多个线程是可以并发执行的。在单CPU的计算机系统中各线程可交替地占用CPU;在多CPU的计算机系统中,各线程可同时占用不同的CPU,若各个CPU同时为一个进程 各线程服务则可缩短进程处理时间

5)一个线程被创建后便开始了它的生命周期直至终止,线程在生命周期内会经历阻塞态就绪态运行态等各种状态变化为什么线程的提出有利于提高系统并发性?可以这样来理解:由于有了线程,线程切换时有可能会发生进程切换,也有可能不发生进程的切换,那么平均下来,每次切换所需要的开销就小了,因而,就能够让更多的线程参与并发,也不会影响到响应时间等问题了。

1.6.4 线程的实现方式

线程的实现可以分为两类:

用户级线程( User-Level Thread,ULT)和内核级线程( Kernel-Lev Thread,KLT)。内核级线程又称为内核支持的线程

用户级线程中,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在,应用程序可以通过使用线程库设计成多线程程序。 通常,应用程序从单线程起始,在该线程中开始运行,在其运行的任何时刻,可以通过调用线程库中的派生例程创建一个在相同进程中运行的新线程

图(a)说明了用户级线程的实现方式在内核级线程中线程管理的所有工作由内核完成(一个用户线程对应一个内核线程),应用程序没有进行线程管理的代码,只有一个到内核级线程的编程接口。内核为进程及其内部的每个线程维护上下文信息,调度也是在内核基于线程架构的基础上完成。  

 图(b)说明了内核级线程的实现方式在一些系统中,使用组合方式的多线程实现。线程创建完全在用户空间中完成(多个用户线程对应一个内核线程),线程的调度和同步也在应用程序中进行。一个应用程序中的多个用户级线程被映射到一个内核级线程上。

 图(c)说明了用户级与内核级的组合实现方式。

1.6.5 多线程模型

有些系统同时支持用户线程和内核线程,由此产生了不同的多线 程模型,即实现用户级线程和内核级线程的连接方式

1)多对一模型

 将多个用户级线程映射到一个内核级线程,线程管理在用户空完成。此模式中,用户级线程对操作系统不可见(即透明)。

优点:线程管理是在用户空间进行的,因而效率比较高

缺点:当一个线程在使用内核服务时被阻塞,那么整个进 程都会被阻塞;多个线程不能并行地运行在多处理机上。

2)一对一模型

每个用户级线程映射到对应的一个内核级线程

优点:当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强。

缺点:每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。  

3)多对多模型

 将n个用户级线程映射到m个内核级线程上,要求m≤n。

特点:在多对一模型和一对一模型中取了个折中,克服多对一模型的并发度不高的缺点又克服一对一模型一个用户进程占用太多内核级线程,开销太大的缺点。又拥有多对一模型和一对一模型各自的优点,可谓集两者之所长。

1.7 处理机调度的概念、层次

 

1.7.1 调度的基本概念

在多道程序系统中,进程的数量往往多于处理机的个数,进程争用处理机的情况就在所难免。 处理机调度是对处理机进行分配,就是从就绪队列中,按照一定的算法(公平、高效)选择一个进程并将处理机分配给它运行,以实现进程并发地执行。

处理机调度是多道程序操作系统的基础,它是操作系统设计的核心问题。

1.7.2 调度的层次

作业从提交开始直到完成,往往要经历以下三级调度:

1) 高级调度(作业调度)

 

其主要任务是按一定的原则从外存上处于后备状态的作业中挑选一个(或多个)作业,给它(们)分配内存输入输出设备等必要的资源,并建立相应的进程,以使它(们)获得竞争处理机的权利。简言之,就是内存与辅存 (外存) 之间的调度。对于每个作业只调入一次、调出一次。 多道批处理系统中大多配有作业调度,而其他系统中通常不需要配置作业调度。作业调度的执行频率较低,通常为几分钟一次。

2) 中级调度(内存调度)

 引入中级调度是为了提高内存利用率和系统吞吐量。为此应使那些暂时不能运行的进程,调至外存等待,把此时的进程状态称为挂起状态。当它们已具备运行条件且内存又稍有空闲时,由中级调度来决定,把外存那些已具备运行条件的就绪进程再重新调入内存,并修改其状态为就绪状态,挂在就绪队列上等待

 3) 低级调度(低级调度)

 

其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。进程调度是操作系统中最基本的一种调度,在一般操作系统中都必须配置进程调度。进程调度的频率很高,一般几十毫秒一次 。  

1.7.3 三级调度的联系

作业调度从外存的后备队列中选择一批作业进入内存,为它们建立进程,这些进程被送入就绪队列,进程调度从就绪队列中选出一个进程,并把其状态改为运行状态,把CPU分配给它。 中级调度是为了提高内存的利用率,系统将那些暂时不能运行的进程挂起来。当内存空间宽松时,通过中级调度选择具备运行条件的进程,将其唤醒。

1)作业调度为进程活动做准备进程调度使进程正常活动起来,中级调度将暂时不能运行的进程挂起,中级调度处于作业调度和进程调度之间 。

2)作业调度次数,中级调度次数略,进程调度频率最高

3)进程调度是最基本的,不可或缺。

1.8 进程调度的时机、切换与过程、方式

1.8.1 进程调度的时机

进程调度和切换程序是操作系统内核程序。当请求调度的事件发生后,才可能会运行进程调度程序,当调度了新的就绪进程后,才会去进行进程间的切换。理论上这三件事情应该顺序执行,但在实际设计中,在操作系统内核程序运行时,如果某时发生了引起进程调度的因素,并不一定能够马上进行调度与切换。 现代操作系统中,不能进行进程的调度与切换的情况有以下几种情况:  

1)在处理中断的过程中:

中断处理过程复杂,在实现上很难做到进程切换,而且中断处理是系统工作的一部分,逻辑上不属于某一进程不应被剥夺处理机资源。

2)进程在操作系统内核程序临界区中:

进入临界区后,需要独占式地访问共享数据理论上必须加锁,以防止其他并行程序进入,在解锁前不应切换到其他进程运行,以加快该共享数据的释放。

3)其他需要完全屏蔽中断的原子操作过程中:

加锁、解锁、中断现场保护、恢复等原子操作。在原子操作过程中,连中断都要屏蔽,更不应该进行进程调度与切换。如果在上述过程中发生了引起调度的条件,并不能马上进行调度和切换,应置系统的请求调度标志,直到上述过程结束后才进行相应的调度与切换

应该进行进程调度与切换的情况有:

1)当发生引起调度条件,且当前进程无法继续运行下去时,可以马上进行调度与切换。如果操作系统只在这种情况下进行进程调度,就是非剥夺调度。 2)当中断处理结束或自陷处理结束后,返回被中断进程的用户态程序执行现场前,若置上请求调度标志,即可马上进行进程调度与切换。如果操作系统支持这种情况下的运行调度程序,就实现了剥夺方式的调度。 进程切换往往在调度完成后立刻发生,它要求保存原进程当前切换点的现场信息恢复被调度进程的现场信息。现场切换时,操作系统内核将原进程的现场信息推入当前进程的内核堆栈来保存它们,并更新堆栈指针内核完成新进程的内核栈装入新进程的现场信息更新当前运行进程空间指针重设PC寄存器等相关工作之后,开始运行新的进程

1.8.2 进程调度方式

 

所谓进程调度方式是指当某一个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要处理,即有优先权更高的进程进入就绪队列,此时应如何分配处理机。 通常有以下两种进程调度方式:

1)非剥夺调度方式,又称非抢占方式:

是指当一个进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或发生某种事件而进入阻塞状态时,才把处理机分配给更为重要或紧迫的进程。 在非剥夺调度方式下,一旦把CPU分配给一个进程,那么该进程就会保持CPU直到终止或转换到等待状态。这种方式的优点是实现简单、系统开销小适用于大多数的批处理系统,但它不能用于分时系统和大多数的实时系统。

2)剥夺调度方式,又称抢占方式:

是指当一个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程。 采用剥夺式的调度,对提高系统吞吐率和响应效率都有明显的好处。但“剥夺”不是一种任意性行为,必须遵循一定的原则,主要有:优先权短进程优先时间片原则等。

1.8.3 进程的切换与过程

进程切换的过程主要完成了:

1) 对原来运行进程各种数据的保存

2) 对新的进程各种数据的恢复

1.9 调度算法的评价指标

不同的调度算法具有不同的特性,在选择调度算法时,必须考虑算法所具有的特性。为了比较处理机调度算法的性能,人们提出很多评价准则,下面介绍这五种:

1.9.1 CPU利用率

 CPU利用率。CPU是计算机系统中最重要和昂贵的资源之一,所以应尽可能使CPU保持“忙”状态,使这一资源利用率最高。

 利用率 = 忙碌时间 / 总时间

1.9.2 系统吞吐量

 

系统吞吐量,表示单位时间内CPU完成作业的数量。长作业需要消耗较长的处理机时间,因此会降低系统的吞吐量。而对于短作业,它们所需要消耗的处理机时间较短,因此能提高系统的吞吐量。调度算法和方式的不同,也会对系统的吞吐量产生较大的影响。

系统吞吐量 = 总共完成的作业数 / 花费的总时间

1.9.3 周转时间

周转时间。是指从作业提交到作业完成所经历的时间,包括作业等待、在就绪队列中排队、在处理机上运行以及进行输入输出操作花费时间的总和。  

作业周转时间 = 作业完成时间 - 作业提交时间

平均周转时间 = 各作业周转时间之和 / 作业数

带权周转时间 = 作业周转时间 / 作业实际运行时间 = (作业完成时间 - 作业提交时间) / 作业实际运行时间

平均带权周转时间 = 各作业带权周转时间之和 / 作业数

1.9.4 等待时间

等待时间。是指进程 or 作业处于等处理机状态时间之和等待时间越长,用户满意度越低。处理机调度算法实际上并不影响作业执行或输入输出操作的时间,只影响作业在就绪队列中等待所花的时间。因此,衡量一个调度算法的优劣,常常只需简单地考察等待时间。  

1.9.5 响应时间

 响应时间。是指从用户提交请求到系统首次产生响应所用的时间。在交互式系统中,周转时间不可能是最好的评价准则,一般采用响应时间作为衡量调度算法的重要准则之一

1.10 FCFS、SJF、HRRN调度算法

在操作系统中存在多种调度算法,其中有的调度算法适用于作业调度,有的调度算法适用于进程调度,有的调度算法两者都适用。下面介绍几种常用的调度算法。

1.10.1 先来先服务(FCFS)调度算法

 

FCFS调度算法是一种最简单的调度算法,该调度算法既可以用于作业调度也可以用于进程调度。在作业调度中,算法每次从后备作业队列中选择最先进入该队列的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列在进程调度中,FCFS调度算法每次从就绪队列中选择最先进入该队列的进程,将处理机分配给它,使之投入运行,直到完成或因某种原因而阻塞时才释放处理机。

FCFS调度算法属于不可剥夺算法。从表面上看,它对所有作业都是公平的,但若一个长作业先到达系统,就会使后面许多短作业等待很长时间,因此它不能作为分时系统和实时系统的主要调度策略。但它常被结合在其他调度策略中使用。例如,在使用优先级作为调度策略的系统中,往往对多个具有相同优先级的进程按FCFS原则处理。FCFS调度算法的特点是算法简单,但效率低:对长作业比较有利,但对短作业不利(相对SJF和高响应比):有利于CPU繁忙型作业,而不利于I/O繁忙型作业。

1.10.2 短作业优先(SJF)调度算法

短作业(进程)优先调度算法是指对短作业(进程)优先调度的算法。短作业优先(SJF)调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法,则是从就绪队列选择一个估计运行时间最短的进程将处理机分配给它,使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机。

SJF调度算法也存在不容忽视的缺点:

1)该算法对长作业不利,SJF调度算法中长作业的周转时间会增加。更严重的是,如果有一长作业进入系统的后备队列,由于调度程序总是优先调度那些(即使是后进来的)短作业,将导致长作业长期不被调度“饥饿”现象,注意区分“死锁”.后者是系统环形等待,前者是调度策略问题).

2)该算法完全未考虑作业的紧迫程度,因而不能保证紧迫性作业会被及时处理。

3)由于作业的长短只是根据用户所提供的估计执行时间而定的,而用户又可能会有意或无意地缩短其作业的估计运行时间,致使该算法不一定能真正做到短作业优先调度。 注意,SJF调度算法的平均等待时间、平均周转时间最少。

1.10.3 高响应比优先调度算法HRRN

高响应比优先调度算法主要用于作业调度,该算法是对FCFS调度算法和SJF调度算法的种综合平衡,同时考虑每个作业的等待时间和估计的运行时间。在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行

2.1 时间片轮转、优先级、多级反馈队列

2.1.1 时间片轮转调度算法

 

 时间片轮转调度算法主要适用于分时系统。在这种算法中,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中第一个进程执行,即先来先服务的原则,但仅能运行一个时间片,如100ms.在使用完一个时间片后,即使进程并未完成其运行,它也必须释放出(被剥夺)处理机给下一个就绪的进程,而被剥夺的进程返回到就绪队列的末尾重新排队,等候再次运行。

在时间片轮转调度算法中,时间片的大小对系统性能的影响很大。如果时间片足够大,以至于所有进程都能在一个时间片内执行完毕,则时间片轮转调度算法就退化为先来先服务调度算法。如果时间片很小,那么处理机将在进程间过于频繁切换,使处理机的开销增大,而真正用于运行用户进程的时间将减少。因此时间片的大小应选择适当。时间片的长短通常由以下因素确定:系统的响应时间、就绪队列中的进程数目和系统的处理能力。

2.1.2 优先级调度算法

 

2.1.3 多级反馈队列调度算法

 

 

 多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合和发展,如图2-5所示。通过动态调整进程优先级和时间片大小,多级反馈队列调度算法可以兼顾多方面的系统目标。例如,为提高系统吞吐量和缩短平均周转时间而照顾短进程;为获得较好的I/O设备利用率和缩短响应时间而照顾I/O型进程;同时,也不必事先估计进程的执行时间。

多级反馈队列调度算法的实现思想如下:

1)应设置多个就绪队列,并为各个队列赋予不同的优先级,第1级队列的优先级最高,第2级队列次之,其余队列的优先级逐次降低。

2)赋予各个队列中进程执行时间片的大小也各不相同在优先级越高的队列中,每个进程的运行时间片就越小。例如,第2级队列的时间片要比第1级队列的时间片长1倍…第i+1级队列的时间片要比第i级队列的时间片长1倍。

3)当一个新进程进入内存后,首先将它放入第1级队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第2级队列的末尾,再同样地按FCFS原则等 待调度执行;如果它在第2级队列中运行一个时间片后仍未完成,再以同样的方法放入第3级队列如此下去当一个长进程从第1级队列依次降到第n级队列后,在第n级队列中便釆用时间片轮转的方式行。

4)仅当第1级队列为空时调度程序才调度第2级队列中的进程运行;仅当第1~ i-1级队列均为空时,才会调度第i级队列中的进程运行。如果处理机正在执行第i级队列中的某进程时有新进程进入优先级较高的队列(第1~ i-1任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i级队列的末尾

把处理机分配给新到的更高优先级的进程多级反馈队列的优势有以下几点:

1)终端型作业用户:短作业优先。

2)短批处理作业用户:周转时间较短。

3)长批处理作业用户:经过前面几个队列。

得到部分执行,不会长期得不到处理。

2.2 进程同步、互斥

涉及的相关概念如下:

1 临界资源

虽然多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程所使用,我们把一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机等。此外,还有许多变量、数据等都可以被若干进程共享,也属于临界资源。 对临界资源的访问,必须互斥地进行,在每个进程中,访问临界资源的那段代码称为临界区。

2 同步

 同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。

3 互斥

 

 

 

互斥亦称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时,系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。 为禁止两个进程同时进入临界区,同步机制应遵循以下准则:

1)空闲让进

临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。 2)忙则等待:

当已有进程进入临界区时,其他试图进入临界区的进程必须等待。

3)有限等待:

对请求访问的进程,应保证能在有限时间内进入临界区。

4)让权等待:

当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。

2.2.1 实现临界区互斥的基本方法

 

1 软件实现方法:

单标志法

 双标志法先检查

双标志法后检查

Peterson's Algorithm

 

 

 

2 硬件实现方法:

1)中断屏蔽方法

一个进程正在使用处理机执行它的临界区代码时,要防止其他进程再进入其临界区访问的最简单方法是禁止一切中断发生,或称之为屏蔽中断关中断。因为CPU只在发生中断时引起进程切换,这样屏蔽中断就能保证当前运行进程将临界区代码顺利地执行完,从而保证了互斥的正确实现,然后再执行开中断。其典型模式: ... 关中断临界区开中断; ... 这种方法限制了处理机交替执行程序的能力,因此执行的效率将会明显降低。对内核来说当它执行更新变量或列表的几条指令期间关中断是很方便的,但将关中断的权力交给用户则很不明智,若一个进程关中断之后不再开中断,则系统可能会因此终止。

2)硬件指令方法

  • TestAndSet指令
  • Swap指令

2.3 信号量

信号量是一种功能较强的机制,可用来解决互斥与同步的问题,它只能被两个标准的原语wait(S)和 signal(S)来访问,也可以记为“P操作”和“V操作”。

原语是指完成某种功能且不被分割不被中断执行的操作序列,通常可由硬件来实现完成不被分割执行特性的功能。如前述的“ Test-and-Set”和“Swap”指令,就是由硬件实现的原子操作。原语功能的不被中断执行特性单处理机时可由软件通过屏蔽中断方法实现。原语之所以不能被中断执行,是因为原语对变量的操作过程如果被打断,可能会去运行另一个对同一变量的操作过程,从而出现临界段问题。如果能 够找到一种解决临界段问题的元方法,就可以实现对共享变量操作的原子性。

2.3.1 整型信号量

 用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。

2.3.2 记录型信号量

整型信号的缺陷是存在 "忙等问题" ,因此人们又提出了“记录型信号量”,即用记录型数据结构表示信号量。

2.3.3 利用信号量实现同步,互斥,前驱关系

2.3.3.1 利用信号量实现同步

进程同步:让各并发的进程有序推进。

用信号量实现进程同步:

1)分析什么地方需要"同步关系", 即必须保证 "一前一后" 执行的两个操作(或两句操作)。

2)设置同步信号量S,初始为0。

3)在 "前操作" 之后执行 V(S)。

4)在 "后操作" 之前执行 P(S)。

2.3.3.2 利用信号量实现进程互斥

1) 分析并发进程的关键活动, 划定临界区。

2)设置互斥信号量mutex,值为1。

3) 在进入区 P(mutex) 申请资源。

4)在退出区 V(mutex) 释放资源。

2.3.3.3 利用信号量实现前驱

1)分析问题,画出前驱图,把每一对前驱关系都看成一个同步问题

2)为每一对前驱关系设置同步信号量,初值为0

3)在每个 “前操作” 之后执行V操作

4)在每个 “后操作” 之后执行V操作

2.4 经典同步问题

2.4.1 生产者-消费者问题

1. 问题描述:

 一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待:只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者个消费者从中取出消息。

2. 问题分析

 

该问题需要注意的几点:

  • 生产者,消费者共享一个初始为空,大小为n的缓冲区

  • 在缓冲区为空时,消费者不能再进行消费

  • 在缓冲区为满时,生产者不能再进行生产

  • 缓冲区是临界资源,各进程必须互斥访问

  • 在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步

  • 注意条件变量与互斥锁的顺序

由于前两点原因,因此需要保持线程间的同步,即一个线程消费(或生产)完,其他线程才能进行竞争CPU,获得消费(或生产)的机会。对于这一点,可以使用条件变量进行线程间的同步:生产者线程在product之前,需要wait直至获取自己所需的信号量之后,才会进行product的操作;同样,对于消费者线程,在consume之前需要wait直到没有线程在访问共享区(缓冲区),再进行consume的操作,之后再解锁并唤醒其他可用阻塞线程。

在访问共享区资源时,为避免多个线程同时访问资源造成混乱需要对共享资源加锁,从而保证某一时刻只有一个线程在访问共享资源。

3. 伪代码实现

假设在生产者和消费者之间的公用缓冲池具有n个缓冲区,可利用互斥信号量mutex实现诸进程的互斥使用;利用信号量empty和full分别表示缓冲池中空缓冲区和满缓冲区的数量。 又假设这些生产者和消费者互相等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池取走一个消息。

int in =0,out = 0;
item buffer[n];							//缓冲区列表	
semaphore mutex = 1,empty = n,full = 0;  //互斥信号量为1,起始缓冲区为空,没有产品
void producer(){			//生产者进程
	do{
		producer an item nextp;			
		...
		P(empty);  				//申请一个空闲缓存区
		P(mutex);				//申请使用缓冲池
		buffer[in] = nextp;		//向缓冲池投放产品
		in = (in+1)%n;			
		V(mutex);				//释放使用缓冲池的资源
		V(full);				//缓冲池的产品数量加1
	}while(true);
}
void consumer(){
	do{
		producer an item nextp;
		P(full);			//产品数量减1
		P(mutex);			//申请使用缓冲池
		nextc = buffer[out]; 
		out = (out+1)%n;    
		V(mutex);			//释放使用缓冲池的资源
		V(empty);			//缓冲池空闲区加1
		consumer the item in nextc;   //消费一个产品
		...
	}while(true);
}
void main(){
	cobegin
		producer();consumer();
	coend
}

2.4.2 多消费者问题

1. 问题描述:

 

假设有四个人:父亲、母亲、女儿、儿子,和一个空盘子,里面最多放一个水果。 父亲每次向盘子中放一个苹果,女儿只会吃苹果。 母亲每次向盘子中放一个橘子,儿子只会吃橘子。

这个问题可以被抽象为放水果事件和拿水果事件,用信号量 plate 来表示,当 plate = 1 时,表示盘子可以放一个水果;plate = 0 表示盘子不能放水果。信号量 apple 和 orange 表示盘子中苹果和橘子的数量。

2. 同步与互斥关系

该问题中,

有 3 对同步关系: (1)向盘子中放入水果。需要先拿走盘子中的水果,才能放入。 (2)拿走盘子中的苹果。需要先放入苹果,才能拿走。 (3)拿走盘子中的橘子。需要先放入橘子,才能拿走。

同步:在前事件发生后进行 V 操作,后事件发生前进行 P 操作。

有 1 对互斥关系: 四个人(n个进程)在同一时间去盘子拿水果必须只能有 1 人进行操作。对盘子 (临界资源) 的互斥访问,用互斥信号量 mutex 实现

互斥:在访问临界区之前进行 P 操作,访问之后进行 V 操作。

特殊的是,该问题中缓冲区大小为 1,因此可以不需要互斥信号量 mutex ;而如果缓冲区大小大于 1,就必须要互斥信号量 mutex

有mutex

 

 无mutex

3. 为什么有mutex和没有mutex一样呢?

  • 原因在于:本题中的缓冲区大小为1,在任何时刻,apple、 orange、 plate 三个同步信号量中最多只有一个是1。因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区…

如果有两个盘子plate

 

 

4. 知识总结与重要考点

总结:

在生产者_消费者问题中,如果缓冲区大小为1,那么有可能不需要设置互斥信号量就可以实现互斥访问缓冲区的功能。当然,这不是绝对的,要具体问题具体分析。

建议:

在考试中如果来不及仔细分析,可以加上互斥信号量,保证各进程一定会互斥地访问缓冲区。但需要注意的是,·实现互斥的P操作一定要在实现同步的P操作之后·,否则可能引起·“死锁”·。

2.4.3 读者-写者问题

1.问题描述:

2. 需要满足的条件:

1) 写进程与写进程之间必须互斥的写入数据(因为如果两个写进程同时对共享数据中的区域A中的数据进行写操作的话,会导致数据错误覆盖的问题)。

2) 写进程与读进程之间必须互斥的访问共享数据(因为写进程与读 进程如果同时访问共享数据,可能会导致数据不一致的问题。比 如:读进程A想要访问共享数据中的B数据,但是写进程C在读进程A访问B数据之前将B数据进行了更新,这就会导致读进程A读不到它想要读到的数据,从而出现数据不一致问题)。

3) 读进程与读进程之间可以同时访问数据,不需要实现互斥的访问共享数据(因为读进程读数据,并不会像之前的生产者消费者问题中的消费者那样改变数据或者是将数据清空,所以多个读进程可以同时访问共享数据)

3. 解题思路:

1) 第一步解决写进程与写进程之间必须互斥的写入数据写进程与读进程之间必须互斥的访问共享数据 两个问题。

int count=0;  //用于记录当前的读者数量
semaphore mutex=1;  //用于保护更新count变量时的互斥
semaphore rw=1;  //用于保证读者和写者互斥地访问文件
writer () {  //写者进程
    while (1){
        P(rw); // 互斥访问共享文件
        Writing;  //写入
        V(rw) ;  //释放共享文件
    }
}
 
reader () {  // 读者进程
    while(1){
        P(rw);  //阻止写进程写
        reading;  //读取
        V (rw) ;  //释放共享文件
    }
}

上面这种实现方式确实解决了了写进程与写进程之间必须互斥的写入数据写进程与读进程之间必须互斥的访问共享数据 这两个问题。但是, 假设读进程A正在访问共享数据,执行了P(rw) 和 读数据操作,还没有执行V操作解锁,此时读进程B也想访问共享数据,此时,读进程B会卡在P(rw)中的循环里面,也就是说进程B被阻塞了。 读进程与读进程之间也变成了必须互斥访问共享数据,并不满足题目读进程与读进程可以同时访问共享数据的要求!

2) 下面来实现多个读进程可以同时访问共享数据 解决方法: 引入count变量,用来记录当前有几个读进程在访问共享数据。

int count=0;  //用于记录当前的读者数量
semaphore rw=1;  //用于保证读者和写者互斥地访问文件
writer () {  //写者进程
    while (1){
        P(rw); // 互斥访问共享文件
        Writing;  //写入
        V(rw) ;  //释放共享文件
    }
}
 
reader () {  // 读者进程
    while(1){
        if (count==0)  //当第一个读进程读共享文件时
            P(rw);  //阻止写进程写
        count++;  //读者计数器加1
        reading;  //读取
        count--; //读者计数器减1
        if (count==0)  //当最后一个读进程读完共享文件
            V(rw) ;  //允许写进程写
    }
}

3) 上述实现方法表面上看达到了实现多个读进程可以同时访问共享数据的目的,但其实还是存在问题的。

存在的问题:

如果读进程A想要访问共享数据,并且执行了P(rw)“上锁”操作,此时,读进程B也想要访问共享数据,也会执行P(rw),但是因为进程A已经执行了“上锁”操作,所以进程B还是会被阻塞,无法访问共享数据。可见,仍然没有达到多个读进程可以同时访问共享数据的目的! 出现这种问题的原因: 对于count变量的检查与赋值操作无法“一气呵成”,可以被中断。 解决方法: 可以增加一个mutex互斥信号量来保证if判断语句 和 count++(count–) 能够“一气呵成”执行完,中间不会被打断,保证各进程对count的访问是互斥的。

int count=0;  //用于记录当前的读者数量
semaphore mutex=1;  //用于保护更新count变量时的互斥
semaphore rw=1;  //用于保证读者和写者互斥地访问文件
writer () {  //写者进程
    while (1){
        P(rw); // 互斥访问共享文件
        Writing;  //写入
        V(rw) ;  //释放共享文件
    }
}
 
reader () {  // 读者进程
    while(1){
        P (mutex) ;  //互斥访问count变量
        if (count==0)  //当第一个读进程读共享文件时
            P(rw);  //阻止写进程写
        count++;  //读者计数器加1
        V (mutex) ;  //释放互斥变量count
        reading;  //读取
        P (mutex) ;  //互斥访问count变量
        count--; //读者计数器减1
        if (count==0)  //当最后一个读进程读完共享文件
            V(rw) ;  //允许写进程写
        V (mutex) ;  //释放互斥变量 count
    }
}

4) 上述解决方案确实已经达到了多个读进程可以同时访问共享数据的目的,但此时,又出现了新的问题:只要又读进程在读取共享数据,写进程就要一直阻塞等待,这很可能导致写进程一直无法往共享数据中写入数据,也就是说写进程很有可能会被“饿死”。因此,这种算法中,读进程是优先的!下面我们来解决写进程可能会被“饿死”这个问题

解决方法: 设置变量semaphore w =1 ,用于实现“写优先”。然后分别为读进程、写进程增加关于信号变量w的P、V操作。

int count = 0;  //用于记录当前的读者数量
semaphore mutex = 1;  //用于保护更新count变量时的互斥
semaphore rw=1;  //用于保证读者和写者互斥地访问文件
semaphore w=1;  //用于实现“写优先”
 
writer(){
    while(1){
        P(w);  //在无写进程请求时进入
        P(rw);  //互斥访问共享文件
        writing;  //写入
        V(rw);  // 释放共享文件
        V(w) ;  //恢复对共享支件的访问
    }
}
 
reader () {  //读者进程
    while (1){
        P (w) ;  // 在无写进程请求时进入
        P (mutex);  // 互斥访问count变量
 
        if (count==0)  //当第一个读进程读共享文件时
            P(rw);  //阻止写进程写
 
        count++;  //读者计数器加1
        V (mutex) ;  //释放互斥变量count
        V(w);  //恢复对共享文件的访问
        reading;  //读取
        P (mutex) ; //互斥访问count变量
        count--;  //读者计数器减1
 
        if (count==0)  //当最后一个读进程读完共享文件
            V(rw);  //允许写进程写
 
        V (mutex);  //释放互斥变量count
    }
}

经过上面的“改造”,我们来验证一下是否达到了“写优先”的目的: 读进程A——>写进程a——>读进程B:

假设读进程A正在访问共享数据,那么读进程A肯定已经执行了P(w)、P(mutex)、P(rw)、V(mutex)、V(w)。此时,写进程a也想要访问共享数据,那么当读进程a执行P(w)时,不会被阻塞,但是执行到P(rw)时,由于读进程A还没有执行V(rw)“解锁”操作,所以,写进程a会被阻塞等待。而如果此时有第二个读进程B也想要访问共享数据,但由于之前第一个读进程A已经执行了P(w)“上锁”操作,所以当读进程B执行到P(w)操作时,也会被堵塞等待。

直到读进程A完成了读文件操作后,执行了V(rw)“解锁”操作,写进程a才会被“唤醒”。然后在写进程完成了写文件操作后,执行了V(w)“解锁”操作,读进程B才能被唤醒。

注意:这里为什么会先唤醒写进程a呢?

答:因为这里是写进程a比读进程B先想要访问共享数据,所以优先被唤醒。这里其实就是“先来先服务算法”

结论: 在这种算法中,连续进入的多个读进程,可以同时读文件;写进程和其他进程不能同时访问文件;写进程不会“饥饿”。但也并不是真正的“写优先”,而是遵循相对公平的先来先服务原则。有的也称这种算法为“读写公平法”。

4. 总结: 读者-写者问题为我们解决复杂的互斥问题提供了一个参考思路:核心思想在于设置了一个计数器count用来记录当前正在访问共享文件的读进程数。我们可以用count的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。如果是第一个进程则执行“上锁”操作,如果是最后一个进程则执行“解锁”操作。 另外,对count变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。 最后,还要认真体会我们是如何解决“写进程饥饿”问题的。 绝大多数的PV操作大题都可以用之前介绍的几种生产者-消费者问题的思想来解决,如果遇到更复杂的问题,可以想想能否用读者写者问题的这几个思想来解决。

要求:

1) 允许多个读者可以同时对文件执行读操作;

2) 只允许一个写者往文件中写信息;

3) 任一写者在完成写操作之前不允许其他读者或写者工作;

4) 写者行写操作前,应让已有的读者和写者全部退出。

2.4.4 哲学家进餐问题

1. 问题描述:

一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭.哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当晢学家饥饿的时候,才试图拿起左、右两根筷子(一根一根地拿起)如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,当进餐完毕筷子继续思考。

2.问题分析

由问题描述我们可以知道,一共有五个哲学家,也就是五个进程;五只筷子,也就是五个临界资源;因为哲学家想要进餐,必须要同时获得左边和右边的筷子,这就是要同时进入两个临界区(使用临界资源),才可以进餐。

3.信号量设置

因为是五只筷子为临界资源,因此设置五个信号量即可。

4.一个错误例子

首先我们根据我们之前学习的知识来解决问题,根据上面的分析,我们先给出伪代码:

semaphore mutex[5] = {1,1,1,1,1}; 		//初始化信号量

void philosopher(int i){
  do {
    //thinking			//思考
    P(mutex[i]);//判断哲学家左边的筷子是否可用
    P(mutex[(i+1)%5]);//判断哲学家右边的筷子是否可用
    //...
    //eat		//进餐
    //...
    V(mutex[i]);//退出临界区,允许别的进程操作缓冲池
    V(mutex[(i+1)%5]);//缓冲池中非空的缓冲区数量加1,可以唤醒等待的消费者进程
  }while(true);
}

我们来分析下上面的代码,首先我们从一个哲学家的角度来看问题,程序似乎是没有问题的,申请到左右两支筷子后,然后开始进餐。但是如果考虑到并发问题,五个哲学家同时拿起了左边的筷子,此时,五只筷子立刻都被占用了,没有可用的筷子了,当所有的哲学家再想拿起右边筷子的时候,因为临界资源不足,只能将自身阻塞,而所有的哲学家全部都会阻塞,并且不会释放自己手中拿着的左边的筷子,因此就会一直处于阻塞状态,无法进行进餐。

因为,为了解决五个哲学家争用的资源的问题,我们可以采用以下几种解决方法:

1) 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用餐完毕后能释放他占用的筷子,从而使别的哲学家能够进餐;

2 )当哲学家的左、右两支筷子可用时,才允许他拿起筷子;

3) 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反。

下面我们对每种方法给出哲学家进程的伪代码。

5. 解决哲学家进餐问题—方法一

对于方法一,至多只允许有四位哲学家同时去拿左边的筷子,我们可以简单的通过增加一个信号量实现,通过这个信号量限定哲学家并发去进餐的数量。

semaphore mutex[5] = {1,1,1,1,1}; 		//初始化信号量
semaphore count = 4;	//控制最多允许四位哲学家同时进餐

void philosopher(int i){
  do {
    //thinking		//思考
    p(count);		//判断是否超过四人准备进餐
    P(mutex[i]);	//判断缓冲池中是否仍有空闲的缓冲区
    P(mutex[(i+1)%5]);//判断是否可以进入临界区(操作缓冲池)
    //...
    //eat			//进餐
    //...
    V(mutex[i]);//退出临界区,允许别的进程操作缓冲池
    V(mutex[(i+1)%5]);//缓冲池中非空的缓冲区数量加1,可以唤醒等待的消费者进程
    V(count);//用餐完毕,别的哲学家可以开始进餐
  }while(true);
}

6.解决哲学家进餐问题—方法二

第二种方法,也就是使用AND型信号量,同时对哲学家左右两边的筷子同时申请, 仅仅当哲学家的左右两只筷子均可用时,才允许他拿起筷子。下面是伪代码:

semaphore chopstick[5] = {1,1,1,1,1}; 		//初始化信号量
semaphore mutex 1;
void philosopher(int i){
  do {
    //哲学家想进餐时,先拿左边的筷子,再拿右边
		wait(mutex);
		wait(chopstick[i]);
		wait(chopstick[(i+1)%5]);
		signal(mutex);
		...吃饭...
		//哲学家进餐完成后,先放下左边的筷子,再放下右边
		signal(chopstick[i]);
		signal(chopstick[(i+1)%5]);
  }while(true);
}

7.解决哲学家进餐问题—方法三

要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况:

semaphore chopstick[5]={1,1,1,1,1};		//初始化信号量

void philosopher(int i){
  do {
    if(i%2 == 0) { //偶数哲学家,先拿右边的筷子,再拿左边
        wait(chopstick[(i + 1)%5]);
        wait(chopstick[i]);
        ...吃饭...
        signal(chopstick[(i + 1)%5]);
        signal(chopstick[i]);
    } else { //奇数哲学家,先拿左边的筷子,再拿右边
        wait(chopstick[i]);
        wait(chopstick[(i + 1)%5]);
        ...吃饭...
            signal(chopstick[i]);
        signal(chopstick[(i + 1)%5]);
    }
  }while(true);
}

2.4.5 吸烟者问题

1. 问题描述:

 

  • 系统中有三个吸烟者进程一个供应者进程;

  • 每个吸烟者不停的卷烟并吸掉它,而卷烟需要三种材料:烟草、纸和胶水;

  • 三个抽烟者中,第一个拥有烟草,第二个拥有纸,第三个拥有胶水;

  • 而供应者每次将两种材料放在桌子上,拥有剩下的那种材料的吸烟者拿走桌子上的材料卷一根烟并吸掉它,并且发出完成信号告诉供应者进程自己完成了;

  • 然后供应者就会放另外两种材料在桌子上,这个过程一直重复;

  • 也就是可生产多种类型产品的单生产者-多消费者问题;

2. 问题分析

 

1) 关系分析

分析题中各个进程的互斥和同步关系

  • 首先将桌子视为大小为1的缓冲区;

  • 放在桌子上的东西有三种组合:组合一(纸+胶水),组合二(烟草+胶水),组合三(烟草+纸);

  • 互斥关系:由于缓冲区大小为1,因此即使不设置互斥信号量,也可以实现互斥访问;

同步关系1:桌子上有组合一,然后第一个抽烟者取走东西; 同步关系2:桌子上有组合二,然后第二个抽烟者取走东西; 同步关系3:桌子上有组合三,然后第三个抽烟者取走东西; 同步关系4:发出完成信号,然后供应者进程在桌子上放另一个组合;

2) 整理思路

根据各进程的操作流程,大致上确定P、V操作的执行顺序,即就是在每个同步关系中进行“前V后P”。

3) 设置信号量

  • 设置需要的信号量,并根据题目设置初值;

  • 互斥信号量初值一般为1;

  • 同步信号量初值根据对应资源的初始值确定;

3. 具体实现

 1)对于供应者进程

 

  • 使用一个变量 i 来实现轮流将三个组合放到桌子上

  • i 的初始值为0,每次放完东西都让i+1,根据i%3的值实现轮流循环的将各个组合放上桌子

  • 当供应者放完东西,对信号量finish执行P操作,由于此时还没有吸烟者进程拿走东西,因此供应者进程会阻塞到这里,直到有吸烟者进程拿走东西。

2)对于吸烟者进程

 

  • 吸烟者进程先检查自己对应的组合的信号量,如果桌子上没有该组合,则阻塞,如果有那就拿走该组合

  • 并且在拿走之后对信号量finish执行V操作,也就是发出了完成信号

2.5 管程

 1. 管程的定义

系统中的各种硬件资源和软件资源,均可用数据结构抽象地描述其资源特性,即用少量信息和对资源所执行的操作来表征该资源,而忽略了它们的内部结构和实现细节。管程是由一组数据以及定义在这组数据之上的对这组数据的操作组成的软件模块,这组操作能初始化并改变管程中的数据和同步进程。

2. 管程的组成

1)局部于管程的共享结构数据说明;

2)对该数据结构进行操作的一组过程;

3)对局部于管程的共享数据设置初始值的语句;

3. 管程的基本特性

1)局部于管程的数据只能被局部于管程内的过程所访问;

2)各进程/线程只有通过调用管程内的过程(管程提供的特定入口)才能进入管程访问共享数据;

3)每次仅允许一个进程在管程内执行某个内部过程;

由于管程是一个语言成分,所以管程的互斥访问完全由编译程序在编译时自动添加,无需程序员关注,而且保证正确

4.拓展1:用管程解决生产者消费者问题:

引入管程的目的无非就是要更方便地实现进程互斥和同步

1)需要在管程中定义共享数据(如生产者消费者问题的缓冲区)。

2)需要在管程中定义用于访问这些共享数据的“入口”——其实就是一些函数(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)。

3)只有通过这些特定的“入口”才能访问共享数据。

4)管程中有很多“入口”,但是每次只能开放其中一个“入口”,且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区 注意:这种互斥特性是由编译器负责实现的,程序员不用关心)。

5)可在管程中设置条件变量等待 / 唤醒操作解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出“入口”);可以通过唤醒操作等待在条件变量上进程或线程唤醒。

程序员可以用某种特殊的语法定义一个管程(比如 : monitor ProducerConsumer …… end monitor; ),之后其他程序员就可以使用这个管程提供的特定“入口”很方便地使用实现进程同步 / 互斥了。(封装思想)

4. 拓展二:Java 中类似于管程的机制:

2.6 死锁 

死锁,饥饿和死循环的区别:
死锁:至少是两个进程一起死锁,死锁进程处于阻塞态
饥饿:可以只有一个进程饥饿,饥饿进程可能阻塞也可能就绪
死循环:可能只有一个进程发生死循环,死循环的进程可上处理机
死锁和饥饿是操作系统要解决的问题,死循环是应用程序员要解决的问题

1. 死锁的定义

在多道程序系统中,由于多个进程的并发执行改善了系统资源的利用率并提高了系统的处理能力。然而,多个进程的并发执行也带来了新的问题—死锁。所谓死锁是指多个进程因竞争资源造成的一种僵局(互相等待),若无外力作用。这些进程都将无法向前推进。在计算机系统中也存在类似的情况。例如,某计算机系统中只有一台打印机一台输入设备,进程P1正占用输入设备同时又提出使用打印机的请求,但此时打印机正被进程P2所占用,而P2在未释放打印机之前又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

2. 死锁产生的原因

1)系统资源的竞争

通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

2)进程推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A发的消息可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

3. 死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生;

互斥条件:

进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

不剥夺条件:

进程所获得的资源在未使用完毕之前不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

请求和保持条件:

进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

循环等待条件:

存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

4. 死锁,饥饿和死循环的区别

死锁:至少是两个进程一起死锁,死锁进程处于阻塞态; 饥饿:可以只有一个进程饥饿,饥饿进程可能阻塞也可能就绪; 死循环:可能只有一个进程发生死循环,死循环的进程可上处理机; 死锁和饥饿是操作系统要解决的问题,死循环是应用程序员要解决的问题;

2.6.1 死锁的处理策略

2.6.1.1 预防死锁

设置某些限制条件,破坏产生死锁的四个必要条件中的一 个或几个,以防止发生死锁。

 

 1. 破坏互斥条件

如果允许系统资源都能共享使用,则系统不会进入死锁状态。但有些资源根本不能同时访问,如打印机等临界资源只能互斥使用。所以,破坏互斥条件而预防死锁的方法不太可行,而且在有的场合应该保护这种互斥性。

2. 破坏不剥夺条件

 

一个已保持了某些不可剥夺资源的进程,请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着,一个进程已占有的资源会被暂时释放或者说是被剥夺了,从而破坏了不可剥夺条件该策略实现起来比较复杂释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量。这种方法常用于状态易于保存和恢复的资源,如CPU的寄存器内存资源,一般不能用于打印机之类的资源。

若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿。

3. 破坏请求和保持条件

采用预先静态分配方法,即进程在运行前一次申请完所需要的全部资源,在它的资源未满足前,不把它投入运行。一旦投入运行后,这些资源就一直归它所有,也不再提出其他资源请求这样就可以保证系统不会发生死锁这种方式实现简单,但缺点也显而易见,系统资源被 严重浪费,其中有些资源可能仅在运行初期运行快结束时才使用,甚至根本不使用。而且还会导致“饥饿”现象,当由于个别资源长期被其他进程占用时,将致使等待该资源的进程迟迟不能开始运行

4. 破坏循环等待条件 

为了破坏循环等待条件,可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源同类资源一次申请完。也就是说,只要进程提出申请分配资源Ri,则该进程以后的资源申请中,只能申请编号大于Ri的资源

这种方法存在的问题是,编号必须相对稳定,这就限制了新类型设备的增加:尽管在为资源编号时已考虑到大多数作业实际使用这些资源的顺序,但也经常会发生作业使用资源的顺序与系统规定顺序不同的情况造成资源的浪费:此外,这种按规定次序申请资源的方法,也必然会给用户的编程带来麻烦

2.6.1.2 避免死锁

并非所有的不安全状态都是死锁状态,但当系统进入不安 全状态后,便可能进入死锁状态。反之,只要系统处于安 全状态,系统便可以避免进入死锁状态

1. 什么是安全序列

 2. 安全序列、不安全状态、死锁的联系

  • 所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个

  • 如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。

  • 【比如A 先归还了10亿,那么就有安全序列T→B → A】 如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态

  • 因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法”的核心思想。

判断“系统安全状态”法

在进行系统资源分配之前,先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程; 否则,让进程等待。

3. 银行家算法

  • 银行家算法是荷兰学者 Dijkstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁。

  • 核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。

实现步骤

以此类推,共五次循环检查即可将5个进程都加入安全序列中,最终可得一个安全序列。该算法称为安全性算法。可以很方便地用代码实现以上流程,每一轮检查都从编号较小的进程开始检查。实际做题时可以更快速的得到安全序列。

银行家算法示例(手算)

  • 手算(找到安全系列)

  •  手算(找不到安全系列)

 4. 代码实现

  • 假设系统中有 n 个进程,m 种资源;

  • 每个进程在运行前先声明对各种资源的最大需求数,则可用一个 n x m 的矩阵(可用二维数组实现)表示所有进程对各种资源的最大需求数。不妨称为最大需求矩阵 Max,Max[i, j]=K 表示进程 Pi 最多需要 K 个资源Rj。同理,系统可以用一个 n x m 的分配矩阵 Allocation表示对所有进程的资源分配情况。Max – Allocation =Need 矩阵,表示各进程最多还需要多少各类资源;

  • 另外,还要用一个长度为m的一维数组 Available 表示当前系统中还有多少可用资源;

  • 某进程Pi向系统申请资源,可用一个长度为m的一维数组 Request表示本次申请的各种资源量;

  • 数据结构: ①、长度为 m 的一维数组 Available 表示还有多少可用资源 ②、n x m 矩阵 Max 表示各进程对资源的最大需求数 ③、n x m 矩阵 Allocation 表示已经给各进程分配了多少资源 ④、Max – Allocation = Need 矩阵表示各进程最多还需要多少资源 ⑤、用长度为 m 的一位数组 Request 表示进程此次申请的各种资源数

  • 银行家算法步骤: ①、检查此次申请是否超过了之前声明的最大需求数 ②、检查此时系统剩余的可用资源是否还能满足这次请求 ③、试探着分配,更改各数据结构 ④、用安全性算法检查此次分配是否会导致系统进入不安全状态 安全性算法步骤: ①、检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收。 ②、不断重复上述过程,看最终是否能让所有进程都加入安全序列。

  • 系统处于不安全状态未必死锁,但死锁时一定处于不安全状态。系统处于安全状态一定不会死锁。

1) 申请的贷款额度不能超过银行现有的资金总额;

2) 分批次向银行提款,但是贷款额度不能超过一开始最大需求量的总额;

3) 暂时不能满足客户申请的资金额度时,在有限时间内给予贷款;

4) 客户要在规定的时间内还款;

2.6.1.3 死锁的检测和解除

如果系统中即不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法:

  • 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。

  • 死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。

1. 死锁的检测

为了能对系统是否已发生了死锁进行检测,必须:

  • 用某数据结构来保存资源的请求和分配信息;

  • 提供一种算法,利用上述信息来检测系统是否已进入死锁状态

1)数据结构资源分配图:

  • 两种结点

    进程结点:对应一个进程

    资源结点:对应一类资源,一类资源可能有多个

  • 两种边

    进程结点——>资源结点:表示进程想申请几个资源(每条边代表一个)

    资源节点——>进程结点:表示已经为进程分配了几个资源(每条边代表一个)

  • 如果系统中剩余的可用资源数足够满足进程的需求,那么这个进程暂时是不会阻塞的,可以顺利地执行下去。

  • 如果这个进程执行结束了把资源归还系统,就可能使某些正在等待资源的进程被激活,并顺利地执行下去。

  • 相应的,这些被激活的进程执行完了之后又会归还一些资源,这样可能又会激活另外一些阻塞的进程..

  • 如果按上述过程分析,最终能消除所有边,就称这个图是可完全简化的。此时一定没有发生死锁(相当于能找到一个安全序列) 如果最终不能消除所有边,那么此时就是发生了死锁

  • 可以消除所有边-说明未发生死锁

  • 不能消除所有边-说明发生了死锁

  • 最终还连着边的那些进程就是处于死锁状态的进程

2.检测死锁的算法

 

1)在资源分配图中,找出既不阻塞又不是孤点的进程Pi;

(即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量。如下图中,R1没有空闲资源,R2有一个空闲资源。若所有的连接该进程的边均满足上述条件,则这个进程能继续运行直至完成,然后释放它所占有的所有资源)。

消去它所有的请求边和分配边,使之称为孤立的结点。在下图中,P1是满足这一条件的进程结点,于是将P1的所有边消去

2)进程Pi所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。在下图中,P2就满足这样的条件。根据1中的方法进行一系列简化后,若能消去途中所有的边,则称该图是可完全简化的。

死锁定理:如果某时刻系统的资源分配图是不可完全简化的,那么此时系统死锁。

2. 死锁的解除

一旦检测出死锁的发生,就应该立即解除死锁;

补充:并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程;

1)解除死锁的主要方法有:

  • 资源剥夺法挂起(暂时放到外存上)某些死锁进程并抢占它的资源将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。

  • 撤销进程法(或称终止进程法)强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。

  • 进程回退法:让一个或多个死锁进程退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。

如何决定“对谁动手”

  • 进程优先级

  • 已执行多长时间

  • 还要多久能完成

  • 进程已经使用了多少资源

  • 进程是交互式的还是批处理式的

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值