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大类问题的解决思路,为什么要这么解决