目录
1)一个进程如何把信息传递给另一个?
2)确保两个或更多的进程在关键活动中不会出现交叉。
3)保持正确的顺序(如果该顺序是有关联的话)。
第一个问题对同一个进程中的线程而言比较容易,因为线程之间共用一个地址空间。后两个问题和解决办法同样适用于线程。
竞争条件
两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件。
调试包含有竞争条件的程序是一件很头痛的事。大多数的测试运行结果都很好,但在极少数情况下会发生一些无法解释的奇怪现象。不幸的是,多核增长带来的并行使得竞争条件越来越普遍。
临界区
实际上凡涉及共享内存、共享文件以及共享任何资源的情况都会引发竞争条件的错误,要避免这种错误,关键是要找出某种途径来阻止多个进程同时读写共享的数据。换言之,我们需要的是互斥(mutualexclusion),即以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。
前述问题的症结就在于,在进程A对共享变量的使用未结束之前进程B就使用它。为实现互斥而选择适当的原语是任何操作系统的主要设计内容之一。
我们把对共享内存进行访问的程序片段称作临界区域 (critical region) 或临界区 (critical section)。如果我们能够适当地安排,使得两个进程不可能同时处于临界区中,就能够避免竞争条件。
尽管这样的要求避免了竞争条件,但它还不能保证使用共享数据的并发进程能够正确和高效地进行
协作。对于一个好的解决方案,需要满足以下4个条件:
1)任何两个进程不能同时处于其临界区。
2)不应对CPU的速度和数量做任何假设。
3)临界区外运行的进程不得阻塞其他进程。
4)不得使进程无限期等待进入临界区。
忙等待的互斥(几种实现互斥的方案)
1)屏蔽中断
在单处理器系统中,最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要
离开之前再打开中断。屏蔽中断后,时钟中断也被屏蔽。CPU只有发生时钟中断或其他中断时才会进行进程切换,这样,在屏蔽中断之后CPU将不会被切换到其他进程。于是,一旦某个进程屏蔽中断之后,它就可以检查和修改共享内存,而不必担心其他进程介入。
这个方案不好!!!
因为把屏蔽中断的权力交给用户进程是不明智的。设想一下,若一个进程屏蔽中断后不再打开中断,其结果将会如何?整个系统可能会因此终止。而且,如果系统是多处理器 (有两个或可能更多的处理器), 则屏蔽中断仅仅对执行 disable 指令的那个 CPU 有效。其他 CPU 仍将继续运行,并可以访问共享内存。
2)锁变量
寻找一种软件解决方案。设想有一个共享(锁)变量,其初始值为0。当一个进程想进入其临界区时,它首先测试这把锁。如果该锁的值为0,则该进程将其设置为1并进入临界区。若这把锁的值已经为1,则该进程将等待直到其值变为0。于是,0就表示临界区内没有进程,1表示已经有某个进程进入临界区。
但是,这种想法也包含了与假脱机目录一样的疏漏。假设一个进程读出锁变量的值并发现它为0,
而恰好在它将其值设置为1之前,另一个进程被调度运行,将该锁变量设置为1。当第一个进程再次运行时,它同样也将该锁设置为1,则此时同时有两个进程进入临界区中。
可能读者会想,先读出锁变量,紧接着在改变其值之前再检查一遍它的值,这样便可以解决问题。但这实际上无济于事,如果第二个进程恰好在第一个进程完成第二次检查之后修改了锁变量的值,则同
样还会发生竞争条件。
3)严格轮换法

整型变量turn,初始值为0,用于记录轮到哪个进程进入临界区,并检查或更新共享内存。开始时,进程0检查turn,发现其值为0,于是进入临界区。进程1也发现其值为0,所以在一个等待循环中不停地测试turn,看其值何时变为1。
连续测试一个变量直到某个值出现为止,称为忙等待(busywaiting)。由于这种方式浪费CPU时间,所以通常应该避免。只有在有理由认为等待时间是非常短的情形下,才使用忙等待。用于忙等待的锁,称为自旋锁(spinlock)。
进程0离开临界区时,它将turn的值设置为1,以便允许进程1进入其临界区。假设进程1很快便离开
了临界区,则此时两个进程都处于临界区之外,turn的值又被设设置为0。现在进程0很快就执行完其整个循环,它退出临界区,并将turn的值设置为1。此时,turn的值为1,两个进程都在其临界区外执行。
突然,进程0结束了非临界区的操作并且返回到循环的开始。但是,这时它不能进入临界区,因为
turn的当前值为1,而此时进程1还在忙于非临界区的操作,过进程0只有继续while循环,直到进程1把turn的值改为0。这说明,在一个进程比另一个慢了很多的情况下,轮流进入临界区并不是一个好办法。
该方案要求两个进程严格地轮流进入它们的临界区,如假脱机文件等。任何一个进程都不可能在一轮中打印两个文件。尽管该算法的确避免了所有的竞争条件,但由于它违反了条件3:进程0被一个临界区之外的进程阻塞。
所以不能作为一个很好的备选方案。。。
4)Peterson解法

工作过程:一开始,没有任何进程处于临界区,现在进程0调用enter_region。它通过设置其数组元素(interested[0]=TRUE)和将turn置为0来标识它希望进入临界区。由于进程1并不想进入临界区,所以enter_region很快便返回(while语句循环0次,因为interested[1]=FALSE,注意,这里是一开始int interested[N]初始化为0,而不是进程1调用leave_region,它还没有进入过临界区呢。)。
如果进程1现在调用enter_region,进程1将在此处挂起直到interested[0]变成FALSE(进程1一直在while语句循环),该事件只有在进程0调用leave_region退出临界区时才会发生。
现在考虑两个进程几乎同时调用enter_region的情况。它们都将自己的进程号存入turn(只有turn会有冲突),但只有后被保存进去的进程号才有效,前一个因被重写而丢失。假设进程1是后存人的,则turn为1。当两个进程都运行到while语句时,进程0将循环0次并进入临界区,而进程1则将不停地循环且不能进入临界区,直到进程0退出临界区为止。
非常巧妙!!!
5)TSL指令
声明:这种方案需要硬件支持。
某些计算机中,特别是那些设计为多处理器的计算机,都有下面一条指令:TSL RX, LOCK
称为测试并加锁(test and set lock)。它将一个内存字lock读到寄存器RX中,然后在该内存地址上存一个非零值。读字和写字操作保证是不可分割的(就是前面的读到...存一个非零值),即该指令结束之前其他处理器均不允许访问该内存字。
执行TSL指令的CPU将锁住内存总线,以禁止其他CPU在本指令结束之前访问内存。
工作过程:为了使用TSL指令,要使用一个共享变量lock来协调对共享内存的访问。当lock为0时,任何进程都可以使用TSL指令将其设置为1,并读写共享内存。当操作结束时,进程用一条普通的move指令将lock的值重新设置为0。
这条指令如何防止两个进程同时进入临界区?

假设存在上面共4条指令的汇编语言子程序。第一条指令将lock原来的值复制到寄存器中并将lock(指共享变量lock而不是复制的值)设置为1(如果lock已被加锁,lock原来的值就是1),随后这
个原来的值与0相比较。如果它非零,则说明以前已被加锁,则程序将回到开始并再次测试。
经过或长或短的一段时间后,该值将变为0(当前处于临界区中的进程退出临界区时),于是过程返回,此时已加锁。要清除这个锁非常简单,程序只需将0存入lock即可,不需要特殊的同步指令。
有读者可能要问:假如两个进程同时调用第一条指令并且都读到lock值是0,这没防住呀!我一开始以为是上面提到的读字和写字操作保证是不可分割(即该指令结束之前其他处理器均不允许访问该内存字)会起作用,但如果两个处理器同时调用这条指令,不也会产生冲突?所以这里笔者也存在疑问。。。
6)XCHG指令(可替代TSL)
它原子性地交换了两个位置的内容,即寄存器和锁变量。 本质上和TSL指令一样,即将锁变量的内容弄到寄存器里,将寄存器提前赋的1赋给锁变量。