235_用信号量实现进程互斥、同步、前驱关系
# 2.3_5_用信号量实现进程互斥、同步、前驱关系
各位同学大家好,在这个小节中我们会学习如何用信号量机制来实现进程互斥,进程同步和进程的前驱关系。
在之前的学习中我们已经介绍了进程互斥的几种实现方式,包括 4 种软件实现方式和 3 种硬件实现方式,但是之前介绍的那些方案当中或多或少都存在着一些大大小小的缺陷,而信号量机制比起之前介绍那些方法来说,相对来说要更完善一些,所以考研当中也比较容易考信号量机制,实现进程互斥的这种内容。
# 信号量机制实现进程互斥
在解决进程互斥问题之前,我们当然是首先需要区分到底哪一个部分是属于进程的关键活动的临界区。比如说如果有两个进程需要同时访问临界资源打印机的话,那么对打印机进行访问的那些代码就应该归属于所谓的临界区内。在区分了临界区之后,我们需要定义一个互斥信号量叫 mutex,这个名字其实可以是别的什么 ABCD 都可以,只不过习惯上人们都喜欢用 mutex 这个单词作为互斥信号量的名字,这个互斥信号量的初始值要设为一,因为在之前的介绍当中,我们知道信号量的初值其实表示的是系统当中某种资源的数量,而我们这提到的临界区,它其实同一个时间段内只允许一个进程对他进行访问,所以我们其实也可以把临界区理解为是一种特殊的资源,而这个资源只有一个,也就是说它只能被分配给一个进程使用,只有进程释放了这种特殊的资源之后,它才可以由另一个进程来使用。
所以和之前小节介绍的内容一样,如果说一个进程想要使用临界区这种特殊的系统资源的话,那么在使用它之前就需要对它所对应的信号量进行 p 操作,而使用之后需要对它再进行一个 v 操作,这个地方可能比较容易让大家蒙圈的是信号量的定义方式,咱们在之前介绍了记录型的信号量,我们把数据结构命数据结构的名字就命名为 semaphore,就是信号量的英文单词,但是在考试当中其实我们除非题目特别说明,不然我们只需要用这种方式来定义一个信号量就可以,这样可以大大节省我们书写的时间。我们具体来分析一下 p1 和 p2 这两个进程是如何互斥访问临界区代码的
首先假如说是 p1 先上处理及运行,然后它执行了它对信号量执行了 p 操作,所以这个信号量会由 1 变为 0,然后 p1 进程被分配了临界区特殊的系统资源,所以它就可以开始访问临界区系统资源,因此它可以顺利的执行临界区代码段。
但如果此时发生了进程切换,切换回了 p2 进程,此时他在对信号量进行 p 操作的时候,这个信号量又会从 0 变为-1。那么根据上一节小节的讲解,我们知道如果它变为负值的话,就证明此时其实已经没有这种这种系统资源可以分配给他了。因此 p2 会执行 block 原语,把自己阻塞起来,一直到 p1 使用完临界区,然后执行了 v 操作之后,释放了临界区特殊的系统资源,所以 p2 进程又会被唤醒,然后他又可以顺利的执行下去。
因此可以看到用初始值为一的这种信号量是可以实现对临界区的互斥访问的,这个地方我们需要注意,对于不同的临界资源,我们需要设置不同的互斥信号量,比如说 p1 和 p2 这两个进程要访要互斥访问的是打印机这种连接资源,那么就需要对这种临界资源设置一个叫做 mutex1 这样的一个信号量。
而如果 p3 和 p4 这两个进程,他要访问的是摄像头这种临界资源的话,那么就需要另外给它定义一个别的命名的信号量叫 mutex2,这两个信号量是不同的,这个地方稍微注意一下
那么 p 操作是对资源的申请,而 v 操作是对资源的释放,所以其实 pv 操作是必须成对出现的,如果我们缺少 p 操作的话,就不能保证两个进程或者说多个进程互斥的访问临界区这件事情。而如果我们缺少了 v 操作,也就是资源永远得不到释放的话,那么就会导致此时被阻塞的正在等待的进程可能永远不会被唤醒,而系临界区系统资源也永远不会被释放。以上就是如何用信号量机制实现进程互斥。
# 信号量机制实现进程同步
接下来我们再来看怎么用信号量机制实现进程同步。同步这个概念咱们已经好久没提了,可能有的同学有忘记,咱们再来简单的说一下,就是说假如说 p1 和 p2 这两个进程,他们俩并发执行,但是由于并发带来了异步性,所以这两个进程的相互切换,还有这些代码的推进顺序是我们不可预知的,有可能是代码 1 执行了,然后又切换回 p2 进程,执行了代码 4 代码 5,然后又切换回 p1 进程又执行了代码 2,代码 3 再切换回 p2,再执行代码 6,有可能是别的这种执行的顺序,所以由于异步性导致了这些代码的执行的先后顺序是我们不可预知的。
但是有的时候我们又必须保证代码的先后顺序,比如说代码 4,假如说它所需要进行的计算,必须要基于代码 1 和代码 2 的计算结果,那么代码 4 就必须在代码 1 和代码 2 之后才可以执行。在这种情况下就是我们进程同步要解决的问题,就是要让本来异步并发的进程可以互相配合,按照我们的期望有序的推进,也就是先执行代码 12,然后再执行代码 4。
我们来看一下用信号量机制,怎么实现刚才所说的这种进程同步。和进程互斥一样,在在解决这个问题之前,当然是先要分析我们到底有什么地方需要实现所谓的同步关系。而通过刚才的讲解,大家可能也会发现,所谓的同步关系其实就是要保证某些操作需要一前一后的执行,有一个操作一定要在前,而另一个操作一定要在后,我们可以设置一个同步信号量 s,把它的初始值设置为 0。和刚才一样,大家可以用 semaphore 这样简单的方式来进行声明。
怎么实现一前一后的这种顺序,我们需要在必须在之前进行的操作,完成了之后来执行一个 v 操作,而在必须执行在后面的操作执行之前来执行一个 p 操作
这个比较绕,我们具体来看一下 p1 和 p2 这两个进程。刚才咱们已经说过,代码 4 一定要在代码 2 之后才可以执行,所以代码 2 是执行在代码 4 之前的操作,所以在前操作完成了之后,也就是它的后面这个地方我们必须执行一个 v 操作,而代码 4 它是要在后面执行的一个操作,所以在他之前我们又需要执行一个 p 操作,我们来分析一下这样的设计是怎么实现保证一前一后的这种同步关系的。
首先来看,假如是 P1 这个进程先执行了 v 操作,那么 s 这个信号量本来它的值是 0,执行了 v 操作之后,它会进行一个加一的操作,s 变为一。p2 在只对 s 信号量执行 p 操作的时候,由于 s 此时的值已经是一了,也就是按照咱们之前的那种思路来说,就意味着系统当中有可用的资源,所以 p2 进程并不会被阻塞,它可以顺利的往下执行代码 4 代码 5。而在代码 4 执行之前,由于执行了 v 操作,也就意味着代码 2 肯定也是在微操作之前已经被执行了,因此是满足代码 4 必须在代码 2 之后执行这样一个要求的。
第二种情况,假如是 p2 进程,他先执行了 p 操作,由于 s 的初始值本来是 0,也就意味着它所对应的这种特殊的系统资源数量是 0,所以此时并没有资源可以分配给 p2。因此 p2 在执行 p 操作之后,他会自己主动的执行一个 block,原语把自己阻塞起来,一直到 p1 进程执行了代码 1 代码 2,继而又执行对信号量 s 的 v 操作。那么在 V 操作当中,他会发现此时有一个进程,它是处于等待 s 这种特殊的资源的等待队列里的,所以它会把进程唤醒,也就是会把刚才自动阻塞的 p2 进程给唤醒。那么当下一次再切换回 p2 进程的时候,他就可以顺利的开始往下执行代码 4 了。
所以可以看到,如果说用这样的设计的话,即使是 p2 先执行,那么它也会主动的阻塞,一直等到 p1 执行了这个 v 操作之后,这个阻塞才会被唤醒,然后 p2 才会才可以得以继续往下执行。所以这种情况也可以保证代码 4 一定是在代码 2 之后才可以执行的,所以我们刚才所提到的这种设计是正确的,把同步信号量 s 的初始值设为 0,然后在前操作之后执行 v 操作,在后操作之前执行 p 操作,经过刚才的讲解,大家应该已经对这两句话是什么意思,已经有一个更理性的认识了。
# 进程的前驱关系
接下来我们再来看一个更复杂的进程同步问题,在很多教材里把这个问题称作进程的前驱关系,直接来看一个例子,假如一个进程 p1 当中有一句代码 s1,然后 p2 当中有一句代码 s2,p3 有 s3,然后一直到 p6 进程,它有一句代码 s6。
那么假设我们对这些代码的执行顺序先后顺序有所要求,就像这个图所画的这样,所谓的前驱图是什么意思呢?首先是这是 s1 指向 s2,然后 s2 又分别指向了 s4 和 s5,这个意思就是说只有 s1 这一句代码执行了之后,才可以接着执行 s2 这个代码,而只有 s2 执行了之后才可以执行 s4 和 s5 这两个代码。
所以其实和刚才我们所说的同步关系是一样的,只不过它是多层的同步关系,看起来稍微更复杂一点,但是这种问题的解决思路其实和刚才的进程同步问题是一模一样的,我们无非就是要保证这些语句的执行,一个在前,一个在后,并且先后顺序不能颠倒。
那么根据刚才咱们分析的思路来看,我们可以为每一对这种一前一后的操作来设置一个同步变量。比如说 s1 到 s2 这种前后的关系,我们用一个同步信号量 a 来表示,当然它的初始值为 0,刚才咱们已经解释过了,然后其他的呃、bcdefg 反正每一个这种前后关系和前后关系,我们都需要设置一个与它对应的这种同步变量。
第二,在前操作之后要进行 v 操作,在后操作之前要进行 p 操作。比如说在 s1 这个语句一定是在 s2 之前要执行的,所以在前操作之后,我们需要执行一个 v 操作,对它所对应的同步信号量 a 进行一个 v 操作,
然后 s2 由于它一定要在 s1 之后执行所以在这个后操作之前,我们需要执行一个 p 操作,同样的 s1 和 s3 也会有一对 v 操作和 p 操作出现,大家只需要知道都是前 v 后 p 这样一个规律就可以了。
那么所有的这些进程,它的代码就是这个样子,p1 进程的代码 s1,当执行了 s1 这个语句之后,我们需要执行一个对 a 变量的 v 操作和对 b 信号量的 v 操作,就是这个样子。
然后在 s2p2 进程执行 s2 这个语句之前,它需要保证 s1 这个语句已经执行了,所以在它之前需要执行一个 p 操作,当然它之后又需要分别执行了对 c 和 d 这两个变量的 v 操作。假如说 p2 进程他先上处理机运行,先执行了 PA 操作,由于 a 原来的值是 0,也就意味着此时系统没有多余的资源可以分配给 p2,因此 p2 在执行 PA 这个操作的时候会进行自我的阻塞,从运行态、变为阻塞态并且挂到 a 信号量对应的等待队列当中,一直到操作系统在调度到 p1 进程,并且 p1 执行完了 s1 这一句之后又对 a 信号量进行了一个 v 操作。 p1 进程在对 a 进行 v 操作的过程中会发现,此时 a 这个信号量对应的等待队列当中有一个进程,p2 是在等待队列当中的,所以他又会在 v 操作当中执行一个 wake up 言语来唤醒 p2,以让 p2 从阻塞态又回到就绪态。然后一直到之后处理机调度,如果再调度到 p2 的话,那么它就可以开始顺利的往下执行 s2 这个语句了。所以这就保证了 s1 是在 s2 之前执行的,那么其他的这些关系大家也自己动手在稿纸上再来推一下。
# 小结
那么在这个小节当中,我们介绍了如何用信号量机制实现进程互斥,进程同步和进程的前驱关系。其中我们需要注意的是实现进程互斥的时候,我们需要把信号量的初始值设为一,因为我们可以把临界区看作是一种特殊的系统资源,而这个系统资源在每一个、时间段内只能分配给一个用户进程使用,所以我们需要把它的初始值设为一,
而与它不同的是实现进程同步的这种同步信号量,我们需要把它的初始值设为 0。另外我们还需要注意,在实现互斥和同步的时候,分别是需要在什么地方进行 p 操作,什么地方进行 v 操作。
另外我们还介绍了一种更复杂的进程同步问题,叫做进程的前驱关系,但是这个其实本质上和进程同步并没有太大区别,无非就是多设置几个同步信号量
除了进程互斥和进程同步的问题之外,我们还会在考试当中考察有多个系统资源的情况,比如说在上个小节咱们介绍的有两个打印机,那么有多少个系统资源,我们就要把这种系统资源对应的信号量的初始值给设为多少,并且在申请资源的时候进行 p 操作,然后释放资源的时候执行 v 操作。现在大家对 pv 操作在具体的题目当中到底要怎么使用,可能还没有一个很直观的体会,但是没有关系,咱们在之后的小节当中会讲一些很经典的这种 pv 操作的问题,在这个小节当中大家需要建立一个框架,需要知道在 pv 操作相关的题目当中,遇到的问题无非就是这 4 种,一个是用信号量实现进程互斥,另外是实现进程同步,再复杂一点就是实现一个更复杂的这种前驱关系。除此之外还会考察这种资源分配的问题,并且要能够理解这 4 大类问题的解决思路,为什么要这么解决