在计算机系统中,由于资源有限导致了进程之间的资源竞争与共享。
临界区和临界资源
首先看一个例子:设有两个计算机进程Pa和Pb共享内存MS,其中MS分为三个区域,即系统区,进程工作区和数据区。这里,系统区主要是堆栈s,其中存放那些空数据块的地址。数据区被划分大小相等的块,每个块中既可能放有数据,也有可能未放有数据。(该例子可以简单总结为压栈进程和出栈进程)
当进程Pa或Pb要求空数据块时,从堆栈最顶部(top指针所指的位置)取出所需数据块。当进程Pa或Pb释放数据块时,则把所释放数据块的地址放入堆栈顶部。令getspace为取空数据块过程,release(ad)为释放数据块过程,ad为待释放数据块的地址。如果堆栈s非空的话,进程Pa或Pb是可以用任意的顺序释放和获取数据块的,执行getspace就是获取一个空数据块,而执行release(ad)就是释放一个地址为ad的数据块。
getspace和release(ad)可进一步描述为:
其结果是调用getspace的进程取到的是h0+1中的一个未定义值,而调用release(ad)的进程把释放的空块地址ad重新放入了h0中。如何保证执行结果的正确性?比较合理的做法是:把getspace和release(ad)抽象为两个各以一个动作完成的顺序执行单位,那么执行结果的正确性是可以保证的。
在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。
当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被称为临界区(或把不允许多个并发进程交叉执行的一段程序称为临界区)。
临界区是由属于不同并发进程的程序段共享公用数据或公用数据变量而引起的,例如上例中的的临界区就是因为getspace和release(ad)共同访问栈s中的数据而引起的。因此临界区也可以被称为访问公用数据的那段程序。
进程互斥
显然,若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。如果几个进程共享同一临界资源,它们必须以互斥的方式使用这个临界资源。进程互斥也可以理解为,当一个进程进入临界区使用临界资源时,另一个进程必须等待。当占用临界资源的进程退出临界区后,这个等待的进程才能进入它的临界区访问临界资源。
对于临界区的使用应遵守如下规则:如果临界区空闲,就允许一个进程进入临界区;当临界K内有进程执行时,其他想进入者则需等待;任何一个进入临界K的进程必须在有限的时间内退出它的临界K;如果一个进程退出了临界K,就应允许一个等待进程进入临界区。
不允许两个以上的共享资源的并发进程同时进入临界区称为进程互斥。
进程互斥是进程之间的间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待。只有当使用临界资源的进程退出临界区后,这个进程才会解除阻塞状态。比如进程B需要访问打印机,但此时进程A占有了打印机,进程B会被阻塞,直到进程A释放了打印机资源,进程B才可以继续执行。不可能两个进程同时占用打印机来并发执行,因为两个打印进程打印的内容都不一样,会导致两个进程打印的内容都打印在同一张纸上。
互斥的加锁实现
某个进程调用访问临界资源的代码并执行,称为该进程进入关于临界资源的临界区。
对临界区加锁以实现互斥。当某个进程进入临界区之后,它将锁上临界区,直到它退出临界区时为止。并发进程在申请进入临界区时,首先测试该临界区是否上锁,如果该临界区已经被锁住,则该进程要等到该临界区开锁之后才有可能获得临界区。
设访问某临界资源的代码集合为s,s就是临界区,设锁定位key[s]:
lock(key[s])
<临界区>
unlock(key[s])
设key[s]=1时表示s临界区可用,key[s]=0时表示s临界区不可用。
实现unlock(key[s])只用一条语句即可实现:
key[s] = 1
实现lock(key[s])的方法:
lock(x)=begin local v
repeat //循环测试锁位
v = x //重复地把x赋值给v
until v=1 //不断判断 直到v等于1
x=0 //重新上锁
end
进程互斥例子:
一个地下停车场有300个车位,为了便于管理,停车场设置了一个空余车位显示系统。当有车进入停车场时,由P1进程实现计数器减1操作(表示车位减少一个)。当有车离开时,由P2进程实现计数器加1操作。
分析:因为在某一时刻停车场中的车出入是随机的,所以进程P1和P2并发执行。
设变量cwcount用于统计当前车位数,初值为300。
P1和P2进程的程序段描述如下:
通过分析得知两个进程是并发执行的,假设某一时刻cwcount=100,此时,有一辆车进入停车场,有一辆车则要离开停车场。那么,进程P1和P2并发执行,由Pl进程执行计数器减1操作,而P2进程执行计数器加1操作。在执行过程中,若进程P1和P2正常运行,一入一出后cwcount的值依然是100,结果正确。但是,由于进程具有异步性,并发执行的进程之间相互影响,Pl、P2进程在运行时叫能会发生执行顺序的改变,例如P1进程执行完al=cwcount语句后由于某种原因被中断,P2进程被调用执行,P2进程运行结朿后,P1进程又被恢复执行,最终运行的结果是cwcount=99,虽然P1和P2分别对cwcount进行了减1和加1的操作,但最终输出的结果是错误的。这是由于P1和P2使用了一个共享变量cwcount,对于cwcount操作的两个并发进程执行时出现了与时间有关的错误。可以把例子中的cwcount看作是临界资源,P1和P2访问它的那段程序即为各自的临界区。并发进程P1和P2必须互斥地访问临界资源才能输出正确的结果。加锁方式是通过加锁原语lock(w)和解锁原语unlock(w)来实现进程互斥。
加锁方式使用一个锁变量w来表示某种临界资源的状态,w=0表示资源空闲可用;w=l表示资源正在使用。当一个进程要进入临界区时应先测试w的值,若w=0,则允许进入临界区访问临界资源,否则就需要反复测试w的值,直到解锁为止。
用加锁方式实现临界区互斥非常简单,但处理机效率不高。为此,又引入了信号量机制和管程机制。
总结
虽然多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程所使用,我们把一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机等。此外,还有许多变量、数据等都可以被若干进程共享,也属于临界资源。
对临界资源的访问,必须互斥地进行,在每个进程中,访问临界资源的那段代码称为临界区。为了保证临界资源的正确使用,可以把临界资源的访问过程分成四个部分:
进入区。为了进入临界区使用临界资源,在进入区要检查可否进入临界区,如果可以进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区。
临界区。进程中访问临界资源的那段代码,又称临界段。
退出区。将正在访问临界区的标志清除。
剩余区。代码中的其余部分。
do {
entry section; //进入区
critical section; //临界区
exit section; //退出区
remainder section; //剩余区
} while (true)
互斥亦称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待, 当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。
例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时, 系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。
为禁止两个进程同时进入临界区,同步机制应遵循以下准则:
空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
有限等待。对请求访问的进程,应保证能在有限时间内进入临界区。
让权等待。当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。