一、进程同步的基本概念
在多道程序环境下,当程序并发执行时,由于资源共享和进程合作,使处于同一系统中的不同进程之间可能存在制约关系。分为两种:
1. 两种形式的制约
(1)间接相互制约关系
间接相互制约关系是指,在同一个系统中的进程,共享这相同的资源,一个进程占用,另一个进程就得等待。例如,进程A、B都要使用打印机,A占用打印机时,B就得等待,进入阻塞状态。
(2)直接相互制约关系
这种制约关系主要因为进程间的合作。例如输入程序A向缓存区输入数据,计算程序B从缓存区获取数据。当缓存区为空时,计算程序B无法获取数据,进入阻塞。当缓存区已满,输入程序A无法输入,进入阻塞。
2. 临界资源
第一章介绍过,就是类似打印机之类的资源,属于临界资源。进程间采用互斥的方式,实现对这类资源的共享。
生产者-消费者问题是一个著名的进程同步问题:
有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。
为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中;消费者进程可从一个缓冲区中取走产品去消费。
尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品。
虽然上面这两个进程(生产者和消费者)顺序执行是没错的,但是由于二者共享变量counter
在同步执行时就会出错。
例如:
按照这个流程执行结果就是错的。注意,在进程切换时,生产者的数据全部存放在了PCB中。
为了预防产生这种错误,解决此问题的关键是应把变量 counter 作为临界资源处理,亦即,令生产者进程和消费者进程互斥地访问变量counter。
3. 临界区
无论是硬件临界资源还是软件临界资源,多个进程必须互斥的访问它们。人们在每个进程中访问临界资源的那段代码被称为临界区。若是能够保证不同进程互斥地进入自己的临界区,就可以实现对临界资源的互斥访问。
因此在临界区前面增加一段用于检查的代码,这段代码称为进入区。在临界区后面也要加入一段代码,成为退出区,用于将临界区正在访问的标识改为未访问。
进程中除了进入区、临界区、退出区以外的代码称为剩余区。
4. 同步机制应遵循的规则
为实现进程互斥的访问临界区,同步机制应遵循以下四条规则:
(1)空闲让进。
就是临界区空闲允许一个请求进入临界区的资源进入。
(2)忙则等待。
已有进程进入临界区,其他进程想要进入临界区必须等待。
(3)有限等待。
对想要进入临界区的进程,应该在有限时间内进入自己的临界区,避免进入死等的状态。
(5)让权等待。
当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态。
二、信号量机制
1. 整型信号量
把整型信号量定义为一个用于表示资源数目的整型量S,它与一般整型量不同,除初始化外,仅能通过两个标准的原子操作wait(S)和signal(S)来访问。
很长时间以来,这两个操作一直被分别称为P、V操作。
Wait(S)和signal(S)操作可描述为:
wait(S): while S<=0 do no-op; S:=S-1;即P操作
signal(S): S:=S+1; 即V操作
wait(S)和signal(S)是两个原子操作,因此,它们在执行时是不可中断的。亦即,当一个进程在修改某信号量时,没有其他进程可同时对该信号量进行修改。此外,在wait操作中,对S值的测试和做S:=S-1操作时都不可中断。
更常见的称为 P、V操作,S 是信号量 P(S) V(S)
2. AND型信号量
AND同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。 只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它。亦即,对若干个临界资源的分配,采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。
由死锁理论可知,这样就可避免上述死锁情况的发生。为此,在 wait操作中,增加了一个“AND”条件,故称为AND同步,或称为同时wait操作,即Swait定义如下:
Swait(S1,S2,…,Sn)
if Si>=1 and … and Sn>=1 then
for i:=1 to n do
Si:=Si-1;
endfor
else
将本进程放在Si<1相关等待队列中。并将此过程的程序计数设置为 Swait 操作的开始
endif
Ssignal(S1,S2,…,Sn)
for i:=1 to n do
Si:=Si+1;
将与 Si 关联的队列中所有等待的进程删除到就绪队列中。
endfor;
三、信号量的应用
1. 利用信号量实现进程互斥(竞争)
为了让多个进程互斥访问某临界资源,只需为该资源设置一个互斥变量mutex(对应整型信号量中的S),设置其初始值为1。
然后把各个进程访问该临界区的代码置于 wait(mutex) 和 signal(mutex) 操作之间即可。
这样每个进程在进入该临界区之前必定要执行wait(mutex)。
若该资源此刻未被访问,本次wait操作必然成功,进程便可进入自己的临界区。
这时若再有其他进程也欲进入自己的临界区,此时由于对mutex执行wait操作定会失败,因而该进程阻塞,从而保证了该临界资源能被互斥地访问。
当访问临界资源的进程退出临界区后,又应对mutex执行signal操作,以便释放该临界资源。
利用信号量实现进程互斥的进程可描述如下:
2. 利用信号量实现前驱关系
设有两个并发执行的进程P1和P2。P1中有语句S1;P2中有语句S2。我们希望在S1执行后再执行S2。
为实现这种前趋关系,我们只须使进程P1和P2共享一个公用信号量S,并赋予其初值为0,将signal(S)操作放在语句S1后面;而在S2语句前面插入wait(S)操作。即:
在进程P1中: S1;signal(S);
在进程P2中: 用wait(S);S2;
这样,P2想要执行S2,若P1的S1未执行,S为0,进入阻塞状态。等S1执行完,执行signal(S)后,S2才可以执行。
例如:
S1,S2,S3,…,S6是最简单的程序段(只有一条语句)。他们之间的关系如下:
为使各程序段能正确执行,应设置若干个初始值为“0”的信号量。如为保证S1→S2,S1→S3的前趋关系,应分别设置 信号量a 和b。同样,为了保证S2→S4,S2→S5,S3→S6,S4→S6和 S5→S6,应设置信号量c,d,e,f,g。
即:
3. AND型信号量应用
上述的进程互斥问题,是针对各进程之间只共享一个临界资源而言的。
在有些应用场合,是一个进程需要先获得两个或更多的共享资源后方能执行其任务。
假定现有两个进程A和B,他们都要求使用打印机D和磁带机E,当然,D和E都应作为临界资源。为此,可为这两个数据分别设置用于互斥的信号量Dmutex和Emutex,并令它们的初值都是1。相应地,在两个进程中都要包含两个对Dmutex和Emutex的操作,即
若进程A和B按下述次序交替执行wait操作:
process A: wait(Dmutex); 于是Dmutex=0
process B: wait(Emutex); 于是Emutex=0
process A: wait(Emutex); 于是Emutex=-1 A阻塞
process B: wait(Dmutex); 于是Dmutex=-1 B阻塞
显然,发生了死锁。二者仅凭自身永远也无法挣脱。
这个时候使用上面讲述的AND型信号量即可,但是这种会造成资源浪费,不怎么常用。