竞争条件
竞争条件的含义是:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
如图,假设在打印机程序中,进程 A 和 B 同时决定将一个文件排队打印,A 读到 in=7(文件排队的位置)
,将 7 存在一个局部变量中,此时发生一次时钟中断,CPU 将进程切换到 B ,B 也读取到 in=7,并继续运行,将 7 加 1 变为 8 。而当进程 A 继续运行时,将需要打印的文件名保存在 7 的位置,直接覆盖了进程 B 保存的文件名,进程 B 将永远得不到任何打印输出。
临界区
要避免竞争条件,关键是要找出某种途径来阻止多个进程同时读写共享的数据。
我们把对共享内存进行访问的程序片段称作临界区域或临界区,如果能够适当地安排,使得两个进程不可能同时处于临界区中,就能够避免竞争条件。为保证使用共享数据的并发进程能够正确高效地进行协作,需要满足以下 4 个条件:
- 任何两个进程不能同时处于其临界区
- 不应对 CPU 的速度和数量做任何假设
- 临界区外运行的进程不得阻塞其他进程
- 不得使进程无限期等待进入临界区
忙等待的互斥
几种当一个进程在临界区中更新共享内存时,其他进程不会进入临界区,也不会带来任何麻烦的方案。
屏蔽中断
单处理器系统中,最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在要离开之前打开中断。屏蔽中断后,时钟中断也被屏蔽,CPU 只有发生时钟中断或其他中断时才会进行进程切换。
这种方案的缺点是,把屏蔽中断的权力交给用户进程是不明智的,另外,如果系统是多处理器,那么将是无效的。
锁变量
这种方案设想有一个共享变量(锁),初始值为 0 ,当一个进程想进入临界区,首先测试这把锁的值,如果为 0 ,怎将其设置为 1 并进入临界区,如果为 1,则等待其值变为 0 。但这种方案包含了打印机程序一样的疏漏,即当读出锁为 0 时,刚好发生进程切换。
严格轮换法
严格轮换法使用一个变量记录轮到哪个进程进入临界区,并检查或更新内存。进程使用忙等待的方式,连续测试这个变量,直到轮到该进程。该方案要求两个进程严格地轮流进入临界区,尽管这的确避免了所有的竞争条件,但这种方式浪费 CPU 的时间,并且违法了第三条(临界区外运行的进程不得阻塞其他进程),所以通常应该避免。
Peterson解法
TSL 指令
这是一种需要硬件支持的方案,执行 TSL 指令的 CPU 将锁住内存总线,以禁止其他 CPU 在本指令结束之前访问内存。与屏蔽中断不同的是,屏蔽中断无法阻止总线上的第二个处理器访问内存。
睡眠与唤醒
Peterson解法和TSL 指令都是正确的,但它们都将导致忙等待。本质上,它们的解法是,当一个进程想进入临界区时,先检查是否允许进入,若不允许,则该进程原地等待,直到允许为止。
优先级反转问题:假设当前有两个进程,H 进程优先级较高,只要处于就绪状态就会运行,L 优先级较低。某一时刻,当 L 进入临界区,此时 H 处于就绪状态,准备运行,开始忙等待。然而 L 一直不被调度,则无法离开临界区,H 将永远忙等待下去。
睡眠:sleep 是一个将引起调用进程阻塞的系统调用,即被挂起,直到另一个进程将其唤醒。
唤醒:wakeup 系统调用,传入要被唤醒的进程。
唤醒等待位:将一个 wakeup 信号发送给尚未睡眠的进程时,会导致信号丢失。加上一个唤醒等待位,当一个 wakeup 信号发送给一个清醒的进程时,将该位置 1 ,随后当该进程要睡眠时,如果唤醒等待位是 1,则将该位清除,该进程仍然保持清醒。
信号量
信号量是使用一个整型变量来累计唤醒次数,供以后使用。一个信号量的取值可以为 0(表示没有保存下来的唤醒操作)或者为正值(表示有一个或多个唤醒操作)。
互斥量
如果不使用信号量的计数能力,有时可以使用信号量的简化版本:互斥量。互斥量是一个可以处于两态之一的变量:解锁和加锁。
当一个线程(或进程)需要访问临界区时,如果该互斥量是解锁的,则调用线程可以自由进入该临界区。如果已经加锁,则调用线程被阻塞,直到在临界区中的线程完成并退出。如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。
管程
一个管程是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包。进程可以在任何需要的时候调用管程中的过程,但它们不能在管程之外声明的过程中直接访问管程的数据结构。
消息传递
消息传递适用于分布式系统中的进程通信,一个进程向另一个进程发送一条消息,接收者立马回复一条特殊消息,如果超过一定时间没有回复,则重新发送。通过在消息体中加序号标记是否是重复消息。
在生产者-消费者问题中,使用信箱这个数据结构,生产者向指定信箱发送消息,当信箱满时,生产者阻塞,消费者从信箱中获取消息。(类比消息队列)
屏障
屏障适用于进程组之间,需要同时执行过程时。在某一阶段末设置屏障,每当有一个进程执行完毕这一阶段的任务,到达屏障,则被挂起,直到所有进程执行完毕,再同时进入下一阶段。