竞争条件
两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件。凡涉及到资源的共享时就easy发生这样的事情。
解决的办法是设立临界区,让进程相互排斥地訪问共享资源。
一个好的避免竞争条件的方案,必须满足4个条件:
- 不论什么两个进程不能同一时候处于临界区。
- 不应对CPU的速度和数量做不论什么如果。
- 临界区外运行的进程不得堵塞其他进程。
- 不得让进程无限期等待进入临界区。
忙等待相互排斥
1.屏蔽中断
进程在进入临界区时屏蔽中断(包括时钟中断),离开临界区时打开中断。这使得CPU无法切换到其他进程。
这样的方案的缺点在于,将中断屏蔽的进程可能不再将中断打开。导致CPU永远无法切换进程;并且屏蔽的仅仅是该进程相应的CPU的中断。其他没有被屏蔽中断的CPU仍然能够訪问共享资源。在多核系统中,这样的方法并不适用。
2、锁变量
进程进入临界区之前必须先持有锁,然后将锁占有,其他进程得不到所而在临界区外等待。但这样的方案的缺陷在于,进程在推断锁可用到占有锁之间可能会被调度,还有一个进程相同发现锁可用并进入临界区。这会导致两个进程同一时候进入临界区。
3、严格轮换法
进程等待某个变量被置位后才干进入临界区,例如以下图所看到的:
进程a在turn变为0之前循环等待。进程b在turn变为1之前循环等待。
这属于忙等待,非常显然浪费了CPU时间。
用于忙等待的锁称为自旋锁。
这样的方案的问题在于,两个进程必须依照严格的顺序交替进入临界区,这会减少速度较快的进程的运行效率。也就是违反了上述条件3.
4、Peterson算法
该算法非常easy且有效。仅由两个C函数构成:
关键的一点在于enter_region函数中的while循环,当两个进程同一时候进入enter_region函数时,能够确保先进入该函数的进程进入临界区,而后进入的在while循环出等待。
5、TSL/XCHG指令
TSL指令的形式例如以下:
TSL RX, LOCK # 将LOCK读入寄存器RX并将1写入LOCK所在内存,读和写是一个不可切割的原子操作
运行TSL的CPU将锁住总线,这使得其他不论什么CPU都无法訪问共享内存,相当于屏蔽中断的改进版。使用TSL指令的用法例如以下所看到的:
还有一条指令XCHG原子性地交换两个位置的内容,所以它可作为TSL指令的替代品控制进程进入临界区,原理实际上和上图是一样的,例如以下图所看到的。全部的Intel x86 CPU在底层同步中使用了XCHG指令。
休眠与唤醒
上述方案有一个共同的缺点就是进程在无法进入临界区时。处于忙等待状态。这浪费了CPU时间。要使进程在无法进入临界区时堵塞,而不是等待,能够使用进程间通信原语。比如:sleep、wakeup等。下面是另外一些进程间相互排斥、同步的方案。
1、信号量
由大神Dijkstra提出,它包括down(P,表示尝试)和up(V。表示添加)两种操作,所以又称PV操作:
- down:检查信号量的值是否大于0,大于0则减1并继续。等于0就休眠进程。整个操作不可切割。
- up:对信号量值增1,唤醒因为down操作而休眠的进程,使其继续运行未完毕的down操作。整个操作不可切割。
信号量可用于进程间的相互排斥和同步。
- 相互排斥:在同一时刻仅仅有一个进程能够进行操作。比如相互排斥地进入临界区。
- 同步:进程间的运行须要依照某种先后顺序。比如生产者发现缓冲区满时要停止;消费者发现缓冲区空时要停止。
使用信号量解决生产者-消费者的样例:
当中,empty和full是用来实现同步的信号量。mutex是用来实现相互排斥地信号量。
2、相互排斥量
无计数能力。是信号量的一个简化版本号。
相互排斥量包括两个状态:解锁(0)和加锁(1)。线程加锁和解锁函数mutex_lock、mutex_unlock的实现例如以下:
这里的mutex_lock和上面的enter_region的差别在于:调用mutex_lock的线程无法进入临界区时会释放CPU(thread_yield函数)运行另外的线程;而调用enter_region会不断循环測试(忙等待),直到临界区可用。
3、管程
使用信号量和相互排斥量存在一个问题:死锁。
比如上面的信号量部分代码中,如果将down(&empty)和down(&mutex)顺序对调,那么就有可能发生死锁。原因在于当生产者先锁住mutex,然后empty为0休眠后,消费者因为得不到mutex锁而休眠。这样两个进程将永远休眠下去。使用管程可解决这一问题。管程由过程、变量、数据结构等组成的一个模块,进程间必须相互排斥地訪问这个模块中的过程,例如以下图所看到的。管程的重要特性是,在任一时刻管程中仅仅能有一个活跃进程。
上图有一个名为example的管程,包括一个整型变量i、一个条件变量c和两个过程。管程为进程的相互排斥訪问提供了环境。接下来要解决的是同步问题。解决方法是使用条件变量:当管程中的过程发现自己无法继续运行时(比如生产者发现缓冲区满),会在某个条件变量身上运行wait操作堵塞自身并将其他进程调入管道。还有一个进程(如消费者)对缓冲区进行消费后。能够调用signal向条件变量发送信号以唤醒因调用wait而堵塞的进程(如生产者)。然后自身退出管程,被唤醒的进程进入管程。
4、消息传递
通过两条原语send和receive在进程间进行通信,send为发送消息而receive为接收消息(有可能发生堵塞)。也就是让消息称为共享资源的载体。使用消息传递机制解决生产者-消费者问题的代码例如以下:
5、屏障
一种应用于进程组的同步机制。它规定,全部进程都完毕了第n阶段,才干进入第n+1阶段。
也就是说。当有进程完了第n阶段而还有一些进程没有完毕第n阶段时,完毕的那些进程是须要堵塞等待未完毕进程的,例如以下图所看到的。
这能够在每一个阶段的末尾加入屏障来实现这一功能。
參考:
《现代操作系统》 P66-P82.