2.1 进程与线程

第一节零碎知识比较多,关键在于进程状态的切换、进程线程的关系。

第一章中提到过的多道程序环境,由于程序的特点,不能让程序并发,所以引入了进程的概念,让进程来并发,从而实现了多道程序环境。可以说进程就是为了更好地描述和控制程序的并发执行,从而实现操作系统的并发性和共享性。

为了使参与并发执行的程序能够独立运行,必须为其配一个独立的数据结构,称为进程控制块(Process Control Block),系统利用PCB来描述近程的基本情况和运行状态,从而实现控制和进程管理。可以说PCB就是一个进程的代表,对进程的操作全部体现在对PCB的操作上。所谓创建进程就是创建进程的PCB,同理撤销进程就是撤销进程的PCB。

一般将进程定义为进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。进程是一个动态的、过程的概念。这里所提到的系统资源是处理器、存储器等设备。这里可以这样理解,处理机每次只能处理一个进程,所以一个进程对应着处理机的一次操作,从而使资源分配调度的基本单位。

进程具有如下的特征:
①动态性:进程是程序的一次执行,是动态的产生、变化、消亡的。
②并发性:多个进程实体同时存在与内存中,在一段时间内同时运行。
③独立性:进程实体是一个能独立运行、获取资源的基本单位,未建立PCB的程序都不能作为一个独立的单位参与运行。
④异步性:进程按照各自独立的、不可预知的速度前进,异步性会导致执行结果的不可再现性,因而需要配置相应的进程同步机制。
⑤结构性:进程实体是由程序段、数据段和PCB三部分组成的。

进程在其生命周期之内,一般会有五种状态,五种状态之间的转换是这一部分很重要的知识。
①运行态:指进程正在处理机上运行的阶段,每个时刻只能有一个进程处于运行态
②就绪态:进程获得了除处理器之外的所有资源,就只等着处理机便可以立即运行。一般处于就绪状态的进程可以有多个,一般排成一个队列,称为就绪队列。
③阻塞态:也称等待态,指的是进程正在等待某一事件而暂停运行,此时即使处理机空闲也不能运行。
④创建态:进程正在被创建,尚未转到就绪态,可以看作所有状态的起始。首先分配一个空白的PCB,之后向PCB内分配一些控制信息,最后分配必要的资源,最后转入就绪态。
⑤结束态:进程正从系统中消失,进程需要结束时必须先设置进程为结束态,然后才能进行相应的处理工作。

各种状态之间的转换是这一部分常考的内容,关系如下图:

图中有几个容易混的地方:
①就绪态和阻塞态区别在于前者是在等待处理机,而后者则是在等待除了处理机之外的其他资源
②进程得到处理机的时间是很短的,所以一个进程在运行过程中基本上都是在频繁切换的。正是因为切换速度很快,所以表面上看是许多个进程在同时进行,而实际上一个时间只有一个进程可以获得处理机。
③就绪态转换到运行态主要是靠进程得到处理机资源,运行态转换到就绪态是因为时间片耗尽不得不让出处理机,运行态转换为阻塞态是由于进程请求某一资源的使用或者等待某一事件的发生,阻塞态转换到就绪态主要是进程之前等待的事件发生了。
④运行态转换为阻塞态是主动的行为,阻塞态转换到就绪态是被动的行为。

进程创建时主要有以下过程:

进程终止时主要有三种情况:
①正常结束,进程的任务完成并且准备退出运行。
②异常结束,发生了某种异常事件导致程序无法继续运行。
③外界干预,受外界的请求而终止运行,比如操作系统干预或者父进程的干预。
具体步骤如下:

进程在执行过程中,会由于一些期待的事情没有发生而导致自己从运行态变为阻塞态,这种现象即阻塞,阻塞是一种主动行为,只有处于运行态的进程才能转为阻塞态。具体过程如下:

当被阻塞进程期待的事情发生了,有关进程就会将该阻塞进程唤醒,具体步骤如下:

进程之间的切换需要进入内核态才可以完成,是与内核紧密相联的。

进程作为一个独立运行、资源分配和调度的基本单位,主要是由三部分组成的:
①进程控制块(PCB)
PCB是进程实体的一部分,是进程存在的唯一标识。系统通过操作PCB来实现进程的管理和控制。系统只有通过PCB才能感知到进程的存在。可以将PCB看做学C语言入门时的结构体,里面很多的变量用来描述这个结构体的一些属性,PCB就相当于一个结构体,里面的量代表了一个进程的属性,对进程的操作都是靠修改这个PCB的内部值来实现的。

系统内通常存在很多进程的PCB,各个PCB处于不同的状态,为了方便管理,这些PCB可以有两种组织方式。链接方式将同一状态的PCB连接成一个队列,不同状态对应不同的队列。索引方式将同一状态的进程组织在一个索引表中,索引表的表项指向相应的PCB。
②程序段
即被进程调度程序调度到CPU中执行的程序代码段,可以理解为程序的代码。
③数据段
可以是进程对应的程序加工的原始数据,也可以是程序执行时产生的中间或者最终结果。

进程之间需要通过通信来实现信息的交流,通信有三种方式:
①共享存储:
在进程之间存在一块可以直接访问的共享空间,通过对这块共享空间进行读写操作来实现进程之间的信息交换。这种方式又分为两种:低级方式的共享是基于数据结构的共享,高级方式的共享是基于存储区的共享。
②消息传递:
进程之间以格式化的信息为单位进行通信,分为直接队列方式和间接通信方式。前者利用消息队列,进程之间直接通过消息缓冲队列来取得消息。后者需要将进程的消息发送到一个中间实体,由中间实体作为中转来取得信息。
③管道信息:
利用一个管道文件,连接一个读进程和一个写进程来实现通信。管道可以理解为共享存储的优化和发展,将原本的存储空间换成了缓冲区,只允许一边写入另一边读出,只要缓冲区有数据进程就能从缓冲区中读出。

前面提到的进程,是为了让程序能够并发起来,为了让程序并发的程度进一步提高,又引入了线程的概念。线程最直接的理解就是轻量级进程,是一个基本的CPU执行单元,也是程序执行流的最小单元。线程是进程中的一个实体,线程自己并不拥有系统资源,只有一点必不可少的资源,同属一个进程的多个线程共享拥有的全部资源。
引入了线程的概念之后,进程就只作为CPU之外的系统资源的分配单元,而线程才是处理机的分配单元。一个进程内部有许多个线程,所以线程之间的切换代价要远小于进程的切换代价。

对比进程与线程。线程是独立调度的基本单位,进程是拥有资源的基本单位。线程可以在一个进程内部切换也可以在不同的进程之间进行切换。进程拥有资源,线程只拥有一点点的必不可少的资源,线程可以访问所在进程的资源。引入线程后不仅进程之间可以并发执行,多个线程之间也可以并发执行。进程的切换需要分配回收资源,涉及的操作较多,所以系统的开销也很大,但线程之间的切换一般只需要保存少量信息,所以开销很小。进程的地址空间是相互独立的,但是同一进程的不同线程之间则是共享资源的,进程内的线程对其他进程不可见。进程之间通信一般需要一些手段的辅助,但是线程之间可以直接读写数据段来实现通信。

总的来说,线程是一个轻型实体,不同的线程可以执行相同的程序,同一个进程中的各个线程共享该进程所拥有的资源,线程是处理机调度的基本单位,多个线程也可以并发。

由于有了线程,在每次切换时就不需要每次都进程切换,从而平均而言降低了每次切换的开销

线程在实现时涉及到一个内核线程和用户线程的对应关系,主要有下面三种方式:

主要有三种方式:
①多对一模型:
多个用户级线程映射到一个内核线程上,优点是效率比较高,但是如果有一个内核线程在内核状态下被阻塞,整个进程都会被阻塞。
②一对一模型:
一个用户级线程映射到一个内核线程上,优点是并发能力强,但是开销比较大。
③多对多模型:
前面两种模型的折中,集二者之长。

多线程与多任务的区别在于,多任务是针对操作系统而言的,代表操作系统可以同时执行的程序个数,而多线程是针对一个程序而言的,代表一个程序可以同时执行的线程个数,而每个线程可以完成不同的任务。

2.2 处理机调度

这一节主要是讲调度算法,和计算机组成原理里面的调度有一定的交叉。

多道程序系统中,进程的数目往往多于处理机的个数,因此争用处理机是一定会出现的,相应的就需要一个算法来选择一个进程分配处理机资源。处理机调度是多道程序操作系统的基础,是设计的核心问题。

一般一个作业从提交到完成需要经过三次调度。
①作业调度
又称高级调度,其主要任务是按照一定原则从外存上处于后备状态的作业中选择一个作业,分配内存等资源使其具有争夺处理机的权利。即主存与外存之间的调度
②中级调度
又称内存调度,一些处于挂起态的进程,当已经具备运行条件时,如果内存有足够的空间,由仲基调度来决定将外存上那些已经就绪的进程重新调入内存,并修改为就绪态。即将选择阻塞态变成就绪态的调度
③进程调度
又称低级调度,主要任务是选择一个就绪的进程给它分配处理机。是最基本的一种调度,也是频率最高的调度。

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

进程的调度和切换程序属于内核程序,理论上只要有请求调度就会运行调度程序,但是有几种情况是不能进行调度的:
处理中断的过程
②进程处在内核程序临界区
③其他需要完全屏蔽中断的原子操作过程
这三种情况下如果发生了引起调度的条件,不能马上进行调度和切换。其余情况下进程切换往往在调度完成后马上发生,只需要保存当前进程的现场信息。

进程调度分为分多种方式。
根据是否可被抢占分为非剥夺调度方式和剥夺调度方式。

二者主要的区别在于已经得到资源的进程是否会被另一个进程打断,如果可以被打断就属于剥夺式,否则就是非剥夺式。

调度算法需要有几个考察因素:
①CPU利用率:
应该尽可能让CPU保持忙的状态,从而使资源的利用率尽可能的高。
②系统吞吐量:
单位时间CPU完成作业的数量。调度算法和方式不同,系统的吞吐量也会有很大的差别。
③周转时间:
周转时间是指从作业提交到完成所经历的时间。可以用完成时间减去提交时间来得到周转时间。在此基础上又引申出平均周转时间和带权周转时间,其中带权周转时间指的是作业周转时间和作业实际运行时间的比值。
④等待时间:
指的是进程处于等处理机状态的时间之和,这里需要注意,当进程不处在工作态时的时间都需要算在等待时间里面。
⑤响应时间
从提出请求到系统首次产生相应所用的时间,从用户角度看应该尽可能降低响应时间。

下面就是最重要的调度算法:
①先来先服务调度算法(First Come First Service)
算法每次从后备作业队列中选择最先进入该队列的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。
这种算法完全按照到达时间作为选择的标准,属于不可剥夺算法,表面上看起来很公平,但是对于短作业来说就需要等待前面的长作业完成才能轮到自己。所以常常与其它调度策略结合在一起使用。
优点是算法简单,缺点在于效率低对长作业有利但是对短作业不利,适合于CPU繁忙型作业不适合IO繁忙型作业。

②短作业优先调度算法(Short Job First)
调度算法从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它, 使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机。
对短作业优先调度,只考虑长短这一个标准。所以对长作业不利,甚至会出现饥饿的现象。而且不能保证紧迫性作业会被及时处理。此外,由于运行时间只是估计的,是由用户所提供的,所以算法不一定真的能够实现短作业优先调度。
但是这种调度算法的平均等待时间和平均周转时间最少

③优先级调度算法
优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行。
根据是否可以被抢占,又分为非剥夺式优先级调度算法和剥夺式优先调度算法。根据优先级在创建进程后是否可变又分为动态优先级和静态优先级。动态优先级指的是进程的优先级会根据需要动态变化,而静态优先级是在创建PCB时就确定的。动态优先级最典型的就是时间动态优先级,进程的优先级随着等待时间的增加而变高,从而使得低优先级的进程不至于出现饥饿。
一般来说,进程优先级的设置可以遵循以下规则:

④高响应比优先调度算法
在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行。
响应比指的是等待时间与要求服务时间的和除以要求服务时间。根据响应比的公式,可知作业的等待时间相同时,要求服务时间越短,响应比越高,有利于短作业。要求服务时间相同时,作业的响应比由其等待时间决定,等待时间越长,其响应比越高, 因而它实现的是先来先服务。对于长作业,作业的响应比可以随等待时间的增加而提高,等待时间足够长时,其响应比便可升到很高,从而也可获得处理机。因此,克服了饥饿状态,兼顾了长作业。

⑤时间片轮转调度算法
适用于分时系统,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中的第一个进程执行,即先来先服务的原则,但仅能运行一个时间片,在使用完一个时间片后,即使进程并未完成其运行,它也必须释放出处理机给下一个就绪的进程,而被剥夺的进程退回到就绪队列的末尾重新排队,等候再次运行。
时间片轮转调度算法中,时间片的大小选择十分重要,选的太大,以至于所有进程都可以在一个时间片内完成,这时时间片轮转调度算法就退化成了先来先服务调度算法。选的太小,就会导致处理机频繁切换进程,开销增大。
一般时间片的选择取决于响应时间、就绪队列中进程数目和系统的处理能力

⑥多级反馈队列调度算法
可以说是前面几种算法的集大成者。是时间片轮转调度算法与优先级调度算法的结合。

通过动态调整进程优先级和时间片大小,多级反馈队列调度算法可以兼顾多方面的系统目标。该算法设置多个就绪队列,给队列赋予优先级,每个队列内部的时间片大小各不相同,优先级高的队列中时间片小。一个进程来后,先放在第一队列的尾部,按照先来先服务原则等待时间片,如果轮到自己时一个时间片没有做完,就移动到下一个优先级队列的队尾开始排队,如此重复。整个算法只有当高优先级队列中没有进程等待时才会考虑低优先级的队列。

2.3 进程同步

这里是真正的难点,基本知识不难,但是题目可以出的特别难。

在多道程序环境下,进程并发执行,不同进程之间存在不同的制约关系,为了协调进程之间的相互制约关系,所以引入了进程同步的概念。

同步很大程度上是因为临界资源。临界资源指的是一次仅允许一个进程使用的资源,比如打印机、许多变量数据等。对这些临界资源的访问必须是互斥的。在每个进程中,访问临界资源的那段代码称为临界区。为了保证临界资源的正确使用,通常将访问临界资源的过程分为如下四部分:

进程之间的关系可以分为两类:同步和互斥。
同步指得是为了完成某种任务而建立的两个或者多个进程,需要在某些位置上协调工作次序、传递信息而产生的制约关系。就好比一条组装手机的生产线,只有上游将主板组装,中游才可以装电池,下游才可以装屏幕封胶。
互斥指的是当一个进程进入临界区访问临界资源时,另一个进程必须等待。这种情况多出现在对数据的访问上,比如读写同时进行,如果同时进行,可能导致读不到正确的数据,所以要求不能同时访问。
同步可以看做是进程之间的协调,而互斥则是一种争夺

为了进制两个进程同时进入临界区,必须保证:

实现临界区的互斥分为两类方法,软件方法和硬件方法。
软件方法通过在进入区设置一些标志来标明是否有进程在临界区中。具有以下几种算法:
①单标志法:
用一个公共变量来表示允许被进入的进程的编号,如果值为11标明允许编号为11的进程进入临界区。这种算法允许一个进程进入临界区,但是两个进程必须交替进入临界区,一个不进入会导致另一个也无法进入。这是因为变量需要两个进程相互来修改,如果一方不修改会导致这个和谐的交替过程终止。

从代码里面不难看出,整个交替的过程关键在于退出区的操作,将turn调整为下一个允许进入的进程,这才有了进程之间的切换,如果一方停止,另一方也会无法交替。
②双标志法先检查
该算法的基本思想是让每个进程在访问临界资源之前,先检查临界资源是否正在被访问,如果被访问了就需要让进程等待。需要用一个数组来记录是否进入了临界区。

这种操作的有点在于不需要交替进行,实现了连续使用,即使一方停止了也不会影响另一方进入临界区。但是程序在某种执行顺序时,可能会出现同时进入临界区的现象(1-2-3-4)
③双标志法后检查
是对算法二的修改,先修改自己的状态再检测对方的状态。

当两个进程同时想进入临界区时,都会先将自己的状态修改,从而导致死锁,根本就是一种错误的方法。
④皮特松算法
将前面的方法进行整合,同时设置turn和flag,以此来保证两个进程不会死锁也可以满足进入临界区的互斥。

代码关键在于理解,可以看着代码模拟这个过程来检验到底会不会出现死锁。

重点在于硬件的实现方法。通过硬件支持来实现临界区问题的方法称为低级方法,或者元方法。有以下几种具体的方式
①中断屏蔽方法
直接使用中断屏蔽,当一个进程进入临界区后,就直接关中断,从而不会出现进程的切换,从而也就没有临界区的冲突问题。但是这种方法会将关中断的权力交给用户,这种方式风险很大,所以并不明智。

②硬件指令方法
利用TestAndSet指令,这是一条原子指令,不会被中断,其作用是读出标志后把该标志设置为真,并返回原来的值。通过给每个临界资源设置一个共享布尔变量,用该指令来修改状态。

利用swap指令,也是一条原子指令,其作用是交换两个变量的内容,其使用和TestAndSet类似。

硬件方法适用于任意数目的进程,支持进程有多个临界区,只需要给每个临界区设置一个布尔变量。但是进入临界区时需要消耗处理机时间,存在饥饿的可能。

除了硬件和软件来实现临界区的互斥,信号量机制也是特别强大的方式。
信号量机制使用两个标准的原语:wait和signal,具体操作描述为:

只要信号量小于等于0,就会不断的重复循环,这种操作会让进程处于忙等(死循环)的状态。在此基础上对其做出改进就成了记录型信号量,需要用一个结构体来辅助。结构体内除了标准信号的变量,还需要一个队列来记录被挂起的进程。改进后的代码如下:


从代码不难看出,改进后当进程等待信号时,不会重复死循环,而是在第一次等不到信号后就进入队列等待,当另一进程发出信号后再将进入队列的进程拉出来。这种方式下,value的值大于零表示剩余的资源,value小于零时其绝对值表示正在等待信号的进程数目

信号量机制的强大在于可以解决同步和互斥两种问题。解决同步问题时,需要将信号量初始值设置为0,利用wait和signal来实现进程之间的同步。

解决互斥问题时,需要将信号量初始值设置为1,也是利用wait和signal来实现。

下面简单总结一下PV操作在同步互斥中的应用:在同步问题中,若某个行为要用到某种资源,则在这个行为前面P这种资源一下;若某个行为会提供某种资源,则在这个行为后面V这种资源一下。在互斥问题中,PV操作要紧夹使用互斥资源的那个行为,中间不能有其他冗余代码。

除了同步和互斥,信号量也可以用于解决前驱关系问题。即数据结构图论里面的关键路径,利用信号量,结合同步的思想,就可以将信号量用于前驱问题。简单来说就是等待前驱就用wait等待一个信号量,前驱做完之后就signal一个后序的信号量来告诉后面等待的进程自己做完了。

这部分经常考分析设计题,一定要仔细揣测题目的叙述,找出问题中的进程数目,确定进程之间是互斥还是同步关系,利用现有的典型例子来改写代码。比较容易混的是信号量初值的设置,主要看第一个来的进程是想让它做还是不想让它做,同步需要让后面进程等待,所以不能让它做,故初始值为0,互斥问题中第一个进程可以进入临界区,所以一定是设置为初始值1.

信号量机制中,需要大量的wait和signal操作,比较分散而且容易出现死锁,于是在此基础上又改进出了管程的工具。管程的特性保证了进程的互斥,从而不需要程序员来实现。
所谓管程实际上就是一个抽象的数据结构,或者说就是一个结构体,用少量的信息和对资源所执行的操作来表征该资源,不去考虑内部的实现细节
管程理论上分为四部分:管程的名称、内部共享数据结构的说明、对共享数据结构的操作、设置初始值的语句。完全可以用java里面对象的思想来理解,管程的名称即对象名,共享数据结构即属性,操作即方法,初始值赋值即构造函数。
管程将共享资源的操作封装起来,管程内的共享数据结构只能被内部的过程所访问,每次只允许一个进程进入管程,从而实现了互斥。
当一个进程进入管程后被阻塞,直到阻塞的原因解除时, 在此期间,如果该进程不释放管程, 那么其他进程无法进入管程。 为此,将阻塞原因定义为条件变量,利用条件变量将同一个原因而阻塞的进程挂在一起,相当于信号量机制里面的阻塞队列。条件变量本身是没有值得,只是在后面排队,而信号量是有值的,条件变量数值的功能由管程中剩余资源数来实现。

有一个常用的同步问题是这部分知识点很重要的,这些经典例子的代码可以直接改一改,换成分析题中的代码,重点在于掌握其思想。

①生产者消费者问题
这类问题的关键在于生产与消费,一方产出另一方消耗。生产者和消费者共享一个初始为空确定大小的缓冲区,缓冲区没满时生产者可以生产,缓冲区没空时消费者可以进行消费,否则都会等待。
这类问题中,生产者和消费者之间对缓冲区的访问是互斥的,同时二者之间也是同步关系,只有生产者生产了消费者才能消费。需要设置三个信号量,mutex用于作为互斥信号,full用于记录缓冲池中已经占用了的空间,初值为0,empty用于记录缓冲池中未使用的空间,初值为n。代码如下:

这个代码中有一个不好理解的点是先wait互斥信号还是先wait缓冲区空间,即先wait(full)还是先wait(mutux),这里可以这样去理解,如果先等待临界区,那么当一个消费者等到了临界区的信号,但是发现缓冲区里面没有可以消费的资源,就会等待,但是消费者会由于无法进入临界区进而导致无法生产,进而引起死锁,所以只能是先等待缓冲区资源,再等待临界区互斥信号。
这种涉及到一方产一方消耗的问题,都可以与生产者消费者问题关联,还有多生产者问题等类似的问题,都是一个类型的处理方法。

②读者写者问题
该问题有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。简而言之,读可以同时读,但是只要涉及写,不仅不能同时多个写,而且不能同时读写,这里可以参考数据库里面的知识,如果同时写,会导致数据被覆盖,如果同时读写,不一定能读到写之前的数据还是写之后的数据。
根据这个关系,可以发现读者和写者是互斥的写者和写者也是互斥的,而读者之间是不互斥的。所以设置一个count信号量来记录当前读者的数量,初值为0,互斥信号量mutex来保证对count访问的互斥,rw用于读者写者的互斥。
代码如下:

这个代码的关键在于count的使用,当count从0变成1时,需要检测是否有写者在写,而其余情况下是不需要这一步的,这是因为count从0变成1表示来了第一个读者,其余情况下是已经有读者在读了,说明后者根本没有写者在写,从而不需要考虑写者的问题。这种思路也可以延伸到一些过桥或者两方向排队问题,都用到count的方法。
但是如果长期有读进程,写进程可能会出现饥饿,可以增加一个信号量,让有写进程时,已经开始的读进程可以读,但是后面再来的读进程暂时挂起,从而防止了写进程的饥饿。

③哲学家进食问题
一张圆桌边上坐着5名哲学家,每两名哲学家之间的桌上摆一根筷子,两根筷子中间是一碗米饭,只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。若筷子已在他人手上,则需要等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,进餐完毕后,放下筷子继续思考。
这个问题中哲学家与其左右邻居对中间的筷子是互斥的,此时需要用一个互斥量数组来表示这个互斥关系,代码如下:

但是这个代码实际上是有问题的,如果每个哲学家都拿起了左手边的筷子,都会因为等不到另一只筷子而死锁,此时需要加一些额外的限制条件来保证不死锁,比如让最多四名哲学家进餐。这个问题如果用贪心的角度考虑,就会出现死锁,所以需要从整体角度去考虑,才能避免死锁,这才是哲学家进餐问题的核心。

2.4 死锁

所谓死锁,指的是多个进程进位争夺资源而形成的一种僵局,我需要的被别人占着,而别人又在等待我占据着的资源,两个人谁也不肯让步,就形成了一种互相等待的局面,即死锁。

死锁的产生主要有两种原因,一个是因为争夺系统的不可剥夺资源,另一个则是因为在程序运行过程中对请求和释放资源的顺序不正确从而导致的死锁,比如信号量机制中两个进程彼此等待互相的信号。

死锁的产生一定要满足四个必要条件,如果任何一个不满足就无法形成死锁,这四个条件也是后面死锁的避免的关键。四个条件如下:

为了应对死锁,主要有三种方法,即死锁的预防、避免、检测与解除
①死锁的预防
即破坏死锁的四个必要条件,以此来从根本上消除死锁
一般来说四个必要条件中,互斥是没法破坏的,这属于资源的属性,不能改变。剩下的三个必要条件都可以破坏。
破坏不剥夺,即当一个己保持了某些不可剥夺资源的进程请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着,一个进程己占有的资源会被暂时释放,或者说是被剥夺,或从而破坏了不剥夺条件。该策略实现起比较复杂,释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量。
破坏请求并保持条件,即进程在运行前一阶段将所需要的全部资源,在它的资源未满足前,不把它投入运行。一旦投入运行,这些资源就一直归它所有,不再提出其他资源请求。这样做显然会很浪费资源,一些只需要用一会的资源也会被占用很长一段时间。也有一种方式是分批申请资源,申请到的这批资源在用完之前完全属于这个进程,可以看作前面方法的改进版本。这类方式简单,但是会造成资源浪费,需要资源比较多的进程甚至有可能一直等待进而出现饥饿
破坏循环等待条件,可以采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源,则该进程在以后的资源申请中就能申请编号更大的资源。这种方法的问题是编号必须相对稳定,这就限制了新类型设备的增加,而且这种编号必然会给编程带来麻烦。

②死锁的避免
不去破坏必要条件,而是在运行过程中时刻检测,一旦发现有可能要出现死锁,不允许下一步操作。这种可能会出现死锁的状态叫做不安全状态,与之对应的是安全状态。安全状态指的是存在一个安全的解决方案让所有进程都能完成任务。如果没法找到一个安全序列,就说明处在不安全的状态。
这里需要区分,进入不安全状态并不是出现了死锁,只是可能会导致死锁。进入不安全状态可能会导进入死锁状态,反之只要处于安全状态就可以避免死锁,就不会出现死锁。

死锁避免的关键在于银行家算法,进程运行之前先声明对各种资源的最大需求量,当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过该进程声明的最大需求量。 若超过则拒绝分配资源,若未超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配。
银行家算法中有几个关键的量:

简单来说,available就是总共有的,max是进程总共需要的,allocation指的是已经分配出去的,need是还需要的,解题的第一步就是根据题目的叙述,把这几个量都写出来。
之后根据题目给出的申请,假设允许这次分配,看分配之后是否处于安全状态,只有处于安全状态才允许操作从而进行下一步操作。安全状态的检验主要是找一个安全序列,让剩下的进程全部完成,这个安全序列可能不止一个,只要有一个即可。

③死锁的检测与解除
允许发生死锁,但是发生后马上检测出并且破坏掉死锁。
死锁的检测主要利用资源分配图。资源分配图中,用圆圈表示进程,用方框表示资源,方框内的圆圈表示资源的个数,资源指向进程(分配边)表示资源被分配出去,进程指向资源(请求边)表示进程申请资源还没有得到。
检测的关键是用死锁定理,书上的介绍很长,简单来说,就是根据资源分配图,如果可以通过分配资源来让所有的进程完成任务,就称为图是可以完全简化的,此时是没有死锁的,反之就说图不可完全简化,死锁的条件是当且仅当资源分配图是不可完全简化的。
一旦发现死锁,就需要将其破坏。死锁的解除有三种方法
a.资源剥夺法:挂起一些死锁进程并抢占其资源,将这些资源分配给其它死锁的进程。
b.撤销进程法:强制撤销部分甚至全部死锁的进程并剥夺其占据的资源。
c.进程回退法:让一个或者多个进程回退到足以避免死锁的地步,进程回退时自愿释放资源而非被剥夺。

典型题


这类题目属于调度的经典题目,关键在于IO和计算是可以并行进行的,解题的最好方法是画运行的状态图,即甘特图,根据图中的用时,很容易就找出最少需要的时间。这道题画出的甘特图如下:


根据响应比的定义,高响应比优先算法在等待时间相同的情况下,作业执行时间越短,响应比越高,满足短任务优先。随着长作业等待时间的增加,响应比会变大,执行机会也会增大,因此不会发生饥饿现象。先来先服务和时间片轮转不符合短任务优先,非抢占式短任务优先会产生饥饿现象。故B项正确。


这道题考察的是临界区的定义,这里的临界区是指民问临界资源的那段代码。那么5个并发进程共有5个操作共享变量A的代码段。临界区是每个进程中有各自的,而不是许多进程抢一个临界区。

当进程退出临界区才置lock为FALSE,会负责唤醒处于就绪态的进程,选型A错误。等待进入,临界区的进程会一直停留在执行while(TSL(&lock))的循环中,不会主动放弃CPU,选项B正确。让权等待,即进程不能进入临界区时,应立即释放处理器,防止进程忙等。通过B选项的分析发现,上述伪代码并不满足让权等待的同步准则, 选项C错误。 while(TSL(&lock))在关中断状态下执行时,若 TSL(&lock)一直为true,不再开中断,则系统可能会到此终止,选项D错误。

这部分的大题难的直接想不出来,关键在于用好前面的模型。这道题看题目可以发现是与容量有关,其实是一个变形的生产者消费者问题,通过mutex来实现互斥,通过sa和sb来实现数量上的限制,即生产者消费者问题中的缓冲区容量,其初始值是通过取极限值来得到的,当B一个也没有的时候,A最大值可以取到M-1,同理sb的最大值是N-1,从而得到信号的初始值。


这道题关键在于利用好题目中的号,这个号并不是信号量,而是一个标志推进过程的量,在叫号实际上就是在协调进程,用两个互斥信号来保证对信号量的访问的互斥,其余的部分都是常规的信号量使用。但是用编号来协调的思想很是重要。


说实话,王道搞这种题实在是影响人心态,同时涉及了多个信号量问题的基本模型。首先,水桶属于资源共享的问题,小和尚取水和老和尚喝水必须要有桶,即同时进行的进程不能超过三个。其次,水缸内容量又属于生产者消费者问题,只有水缸中有水才能取水,水缸里面没满才能倒水进去。此外,水缸取水和水井取水又都是互斥的。所以是一个涉及很多量的问题,需要分开来分析。


这道题则类似于顺序协调的问题,通过信号量来实现顺序管理。首先输入设备是互斥的,而且数据的输入顺序是和三个进程对应的,这就要求读入数据时需要协调进程的顺序,通过设置信号量,让p1得到数据之后发出信号量让p2开始,同理p2再让p3进行。在数据计算部分,也需要用信号量来协调,只有p2完成输入之后x和y才可以开始计算,只有y运算完成后p3才可以开始计算。属于比较典型的一道题目。


这道题有点读者写者问题的味道,仔细分析其实和读者写者是殊途同归的。尤其是第二问,简单来说就是如果已经有从北向南的车开上桥了,那么后面来的从北向南的车可以接着开,从南向北也是同一个道理。根据读者写者的模板,选择一个countSN来表示当前从南向北的车的数目,当为0时表示没有车从南向北,此时从北向南的车一看桥空着,就可以占据桥从而开车上桥。
代码如下:



这道题目和上一道属于同一类型,都存在一个正在运行后序同类都可继续运行的现象,核心都是利用一个单独的计数量来表示是否有同类已经在进行了。代码如下:


不得不说,统考题就是比王道出的题靠谱,至少不偏,这道题目考查的是计算过程中的顺序,三个进程,进程1用到了x和y,进程2用到了y和z,进程3用到了y和z,所以对y的访问,进程2和3不能同时进行,进程1和3也不能同时进行,进程3存在了对临界资源的写,读写不能同时进行。所以需要互斥信号来辅助,一共三个,分别表示进程2和3关于y、z的互斥以及进程1和3关于y的互斥。


这道题主要是考察四个死锁中的概念。其中回退指的是死锁的解除过程中,将进程回退并且主动释放资源的过程。颠簸是在调页的过程中,由于页面调度不当当值频繁缺页频繁调度。饥饿则是指不断将自己已经占有的资源分配给别的进程,从而自己得不到运行。

这种题先画出资源分配图,根据资源的情况,一步一步化简即可,先分配给P4直接可以完成P4的任务,此时只剩下P1P2P3三个进程,当只有两个进程时,不难发现根本构不成死锁,申请的资源马上就可以分配,而三个进程时却很容易构成死锁,所以答案选C。这道题只要画出资源分配图就很好解决了。

看了两章操作系统看了一半了,实在不容易,越看越觉得上岸渺茫,难顶。

操作系统 第二章 进程管理相关推荐

  1. (王道408考研操作系统)第二章进程管理-第三节10:经典同步问题之哲学家进餐问题

    本文接: (王道408考研操作系统)第二章进程管理-第三节6:经典同步问题之生产者与消费者问题 ((王道408考研操作系统)第二章进程管理-第三节7:经典同步问题之多生产者与多消费者问题 (王道408 ...

  2. (王道408考研操作系统)第二章进程管理-第三节8:经典同步问题之吸烟者问题

    本文接: (王道408考研操作系统)第二章进程管理-第三节6:经典同步问题之生产者与消费者问题 ((王道408考研操作系统)第二章进程管理-第三节7:经典同步问题之多生产者与多消费者问题 文章目录 一 ...

  3. (王道408考研操作系统)第二章进程管理-第三节7:经典同步问题之多生产者与多消费者问题

    注意:生产者与消费者问题Linux系统编程专栏有案例讲解 Linux系统编程39:多线程之基于阻塞队列生产者与消费者模型 Linux系统编程40:多线程之基于环形队列的生产者与消费者模型 本文接:(王 ...

  4. 笔记篇:操作系统第二章 进程管理

    笔记篇:操作系统第二章 进程管理 目录 笔记篇:操作系统第二章 进程管理 2.1 进程的基本概念 2.1.1 程序的顺序执行及其特征 2.1.2 前驱图 2.1.3 程序的并发执行及其特征 2.1.4 ...

  5. 操作系统第二章 进程管理

    写在前面:本文参考王道论坛的 操作系统考研复习指导单科书 文章目录 第二章 进程管理 进程同步 读者写者问题 哲学家就餐问题 练习题 哲学家就餐:加碗(2019真题) 既是生产者又是消费者 和尚取水( ...

  6. 3 操作系统第二章 进程管理 进程定义、特征、组织、状态与转换

    文章目录 1 进程的定义和特征 2 进程的组织 3 进程的状态与转换 3.1 进程的状态 3.2 进程状态转换 1 进程的定义和特征 引入进程的原因 为了使程序能够并发执行,并且可以对并发执行的程序加 ...

  7. (王道408考研操作系统)第二章进程管理-第一节4:进程通信(配合Linux)

    文章目录 一:什么是进程通信 二:如何实现进程间通信及其分类 三:通信方式1-共享存储(共享内存) (1)课本基础内容 (2)补充-Linux中的进程通信 四:通信方式2-管道 (1)管道是什么 (2 ...

  8. (王道408考研操作系统)第二章进程管理-第一节3:进程控制(配合Linux讲解)

    文章目录 一:如何实现进程控制 二:进程控制原语 (1)进程创建 A:概述 B:补充-Linux中的创建进程操作 ①:fork() ②:fork()相关问题 (2)进程终止 A:概述 B:补充-僵尸进 ...

  9. 7 操作系统第二章 进程管理 进程同步与互斥

    文章目录 1 进程同步与互斥 1.1 进程同步 1.2 进程互斥 1.3 进程同步机制遵循的原则 1.3 进程同步.互斥小结 2 进程互斥实现方法 2.1 互斥的软件实现方法 2.1.1 单标志法 2 ...

最新文章

  1. PicoBlaze 设计实例
  2. Liunx下HPCC(HPC Challenge)的安装运行
  3. 微软MCITP系列课程(一)第一讲:部署虚拟机
  4. python set和frozenset 异同点学习记录
  5. 20191226每日一句
  6. 一个普普通通大四学生的2021
  7. windows server2012 R2 离线中文语言包下载与安装
  8. freenas搭建nas及san网络存储详解
  9. SQL注入漏洞--2
  10. YOLOV5 网络模块解析
  11. 全球最最可爱的的10种著名小型犬
  12. 【U盘检测】为了转移压箱底的资料,买了个2T U盘检测仅仅只有47G~
  13. (PADA)Partial Adversarial Domain Adaptation笔记
  14. 【机器学习】逻辑回归案例二:鸢尾花数据分类,决策边界绘制逐步代码讲解
  15. 【论文翻译】SHINE 一个用于特定领域实体与异构信息网络链接的通用框架
  16. linux下的SAMBA服务------SMB协议
  17. 字符串中空格符 空字符
  18. python连接ALM
  19. php随机生成手机号码
  20. Cesium开发:地下模式效果

热门文章

  1. 子图同构算法系列(1)
  2. ATL 开发 COM 过程中的一些经验、问题总结
  3. C#基于websocket-sharp实现简易httpserver(封装)
  4. maven私服的使用
  5. JAVA基础--IO输入输出(File使用)17
  6. ClassNotFoundException: javax.validation.ValidatorFactory
  7. JavaScript判断数组是否有重复值
  8. Linux逻辑卷(LVM)技术详解
  9. linux scp命令 不输入远程机器的密码,scp 命令无需输入密码完成 Linux 系统间远程拷贝...
  10. 子进程和父进程的结论_Python的多进程不是随便用滴!