挑战408——操作系统(9)——进程的同步与互斥

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/redRnt/article/details/83279782

操作系统中的并发进程有些是独立的有些需要相互协作,独立的进程在系统中执行不影响其他进程,也不被其他进程影响(因为他们没有共同需要一起用到的资源)。而另外一些进程则需要与其他进程共享数据,以完成一项共同的任务。
因此,为了保证操作系统的正常活动,使得程序的执行具有可再现性,保证执行结果的正确性。操作系统必须为这种协作的进程提供某种机制。
进程间的协作关系分为:互斥,同步,通信。(实习面试的时候,有个考官问过我这个问题)

进程间的协作关系

  • 互斥是指多个进程不允许同时使用同一资源。当某个进程使用某种资源的时候,其他进程必须等待。所以资源不能被多个进程同时使用。在这种情况下,程序的执行与其他进程无关。
  • 同步是指多个进程中发生的事件存在某种先后顺序。即某些进程的执行必须先于另一些进程(我们之前画的前驱图)。其任务是使得并发执行的诸多进程有效的共享资源和相互合作,从而使程序的执行具有可再现性。这种情况下,进程间接知道对方。
  • 通信是指多个进程间要传递一定的信息。这个时候进程直接得知对方。

临界资源和临界区

在计算机中,某段时间内只允许一个进程使用的资源称为临界资源,比如打印机,共享变量等等。而每个进程访问临界资源的那段程序代码称为临界区。
显然,几个进程共享一个临界资源,它们必须互斥使用。为此每个进程在进入临界区的时候,要对访问的临界资源进行检查,看它是否正在被访问,若是则不进入临界区,否则进入临界区,并设置标志,表明我这个进程正在访问临界资源,这段代码称为进入区。其余的代码称为剩余区。
在这里插入图片描述

当然,不是所有的程序都必须等待资源的释放,当满足下面几个条件的时候可以允许进入(具体的后面会解释,感觉配合案例好理解):
1. 空闲让进
2. 忙则等待
3. 有限等待
4. 让权等待

这里说说临界资源与共享资源的区别*:
临界资源是指某段时间内只允许一个进程使用的资源。(比如打印机)
共享资源是指某段时间内允许多个进程同时使用的资源。(比如磁盘,公用队列等,可以多个进程同时读取数据,但不能同时修改)
再注意一下临界区的概念:每个进程访问临界资源的那段程序代码称为临界区。 注意:临界区是进程的一段代码,有n个进程就会有n个临界区。

实现互斥的方法

实现互斥常用的方法有:软件实现法,硬件实现法,pv信号量法。其中软件实现方法虽然用的比较少,但是其算法的思想还是比较重要的,考试喜欢在选择题中考。硬件的实现了解一下,考的比较多。而PV操作即能实现同步又能实现互斥,所以是历年考查的重点,如果说大题年年考都不为过,这个放单独的章节写。

软件实现法

为了更好的理解思想,我们采用编程语言的思想去分析过程

单标志法

先看下面的两段代码:

/*进程p1*/                                 /*进程p2*/ 
while(turn != 0){                       while(turn != 1){
	什么也不做                                 什么也不做
}                                       }
/**p1的临界区**/                        /**p2的临界区**/
turn = 1;                                turn = 0;
/**剩余区**/                             /**剩余区**/

当采用单标志法的时候,我们设置公共的bool变量turn。对于进程P1而言,只有当turn =0的时候才允许它进入临界区,当它退出的时候,将turn置1,这个时候才轮到P2进入,因此实现了两个进程互斥进入临界区,保证了任何时刻都至多只有一个进程可以进入临界区
但是这种方法带来了新的问题:这种做法强制进程轮流进入临界区,而且这时候如果出现没有进程在访问临界资源的话,资源空闲,但是可以进入。不能保证空闲让进,也就是当资源空闲的时候,不能保证有进程进入。
举个例子:进程p1进入临界区并顺利执行后离开,并将turn 置1,按照正常流程应该轮到p2执行,但是如果p2迟迟不来呢?(比如某些原因阻塞了),那么临界资源空闲,而turn = 1一直成立,导致其他需要使用这个资源的进程必须等待。
所以不能做到空闲让进

双标志先检查法

那么既然前一种方式不能做到空闲让进,那么我们就设法克服这一点,看下面的代码:

/*进程p1*/                                 /*进程p2*/ 
while(flag[1]){ //(1)                     while(flag[0]){//(2)
	什么也不做                                 什么也不做
}                                     }
flag[0] = true; //(3)                     flag[1] = true;//(4)
/**p1的临界区**/                        /**p2的临界区**/
flag[0] = false;                       flag[1] = false;
/**剩余区**/                             /**剩余区**/

这样想,既然我们可以用一个标志位保证进程间只能允许一个进程访问临界资源,那么我们是否也可以再采用一个标志位来保证空闲让进呢?
我们设置一个BOOL类型的数组flag[2],初始值为false,用来表示一开始时所有进程都未进入临界区,若flag[0] = true,则表示p1允许进入临界区并执行。
与(1)不同,在进入之前,检查是否有进程在使用该资源。如果有,那么就等待,如果没有就进入。
我们来分上面两个进程:

while(flag[1]) //如果此刻p2正在使用临界资源,那么等待
flag[0] = true;//否则,将flag[0]置为true,进入临界区,也表明这个资源我在用。
flag[0] = false;//离开临界区,表明这个资源我用完了或者没在用

//再来看看P2:
 while(flag[0]) //如果此时p1正在使用临界资源,那么等待
 以下同上

这样进程在使用之前都检查一下是否有其他进程在使用临界资源,没有就进入,所以保证了空闲让进
潜在的问题:由于进程是并发执行的,所以执行的步骤或者说推进的速度都是不一致的,如果推进的速度是这样的:(1)->(2)->(3)->(4),那么(1)(2)步骤一开始都是false,继续执行(3)(4),发现这个时候flag[2]全部都是true,也就是双方的while循环都是对的,因此同时进入临界区。这样就违背了“忙则等待”。即有进程在访问临界资源的时候,其他进程必须等待。

双标志,先修改后检查

同样的,先看看方法(2)的问题所在,原因就是它先进行检查,却忽略了进程执行速度对检查的影响。那么我们在检查之前进修改一下呢?

/*进程p1*/                                 /*进程p2*/ 
flag[0 ] = true;                       flag[1] = true;
while(flag[1]){                        while(flag[0]){
   什么也不做                                 什么也不做
}                                     }
flag[0] = true;                        flag[1] = true;
/**p1的临界区**/                        /**p2的临界区**/
flag[0] = false;                       flag[1] = false;
/**剩余区**/                             /**剩余区**/

其实,对比上面的方法(2),只是先执行了赋值操作,我们分析一下:
flag[0 ] = true; //P1想进入临界区
while(flag[1]) //于是检查一下p2是否在使用临界资源,如果是,那么什么都不做
flag[0] = false;//否则进入临界区,并且离开的时候,置为false,表明资源使用完毕。这样,在获得进入临界区的权利后,准备进入之前,看看有没有进程也想进入,有的话就让给对方。克服了两个进程同时进入临界区的问题
但是,又考虑一种极端的情况
假设P1和P2都同时想进入临界区,并发执行的顺序同方法二,那么都将自己置为true,此时while循环执行,发现对方也想进入,于是相互谦让,导致谁都访问不了这个资源,产生饥饿现象。

Peterson’s算法

也称为先修改,后检查,后修改者等待算法。很拗口,但是理解起来不难,该算法可以看错做是方法(1)(3)的结合。用方法一中的turn标志实现临界资源的访问,用(3)的双标志个修改来维护临界区进入准则。

 /*进程p1*/                                 /*进程p2*/ 
 flag[0 ] = true;                       flag[1] = true;
 turn = 1;                                 turn = 0;
while(flag[1]&&turn ==1){          while(flag[0]&&turn ==0){
	什么也不做                                 什么也不做
}                                     }
/**p1的临界区**/                        /**p2的临界区**/
flag[0] = false;                       flag[1] = false;
/**剩余区**/                             /**剩余区**/

算法通过修改同一标志turn来描述标志修改的前后。我们同样分析一下p1的进程:
flag[0 ] = true; //表明P1想进入临界区
turn = 1;//设置标志位为1,
while(flag[1]&&turn ==1) //如果p2想用,并且p1的推进速度较慢,那么让出给p2.
flag[0] = false //否则进入临界区,并且离开的时候,置为false,表明资源使用完毕

为什么turn的值会受推进速度的影响?我们同样考虑之前的极端情况,按照顺序(1)(2)(3)(4),turn的值先从1,再变为0,说明P2的赋值语句turn = 0对比P1的赋值语句 turn = 1执行的较晚,根据后修改的进程等待的原则,这个时候给P1执行。

所以这个方法实现了“空闲让进”和"忙则等待"

硬件实现法

硬件的实现方法主要有两种:禁止中断和专用机器指令

  1. 禁止中断:
    这个方法其实很简单:
静止中断
/**临界区**/
开中断
/**剩余区**/

这种方式主要用于单处理机,因为单处理机中进程不能并发执行,所以只要保证一个进程不被中断即可。
但是这样子进程被限制只能交替进行。

  1. 专用机器指令(TS指令, swap指令)
    TS指令是指读出指定标记后记为true,设置一个bool变量lock,当lock为true时,代表资源正在使用,反之false表示空闲:
    在这里插入图片描述
    当进程进入临界区时,由于while循环的存在,于是不会主动放弃CPU。这种方法适用范围广,并且简单,同时支持多个临界区。

但是却不能做到“让权等待”,并且进程是随机选择的,可能造成某个进程饥饿。

猜你喜欢

转载自blog.csdn.net/redRnt/article/details/83279782