一、资源
把需要排他性使用的对象称为资源。资源可以是硬件也可以是软件,比如打印机或者数据库中的一个加锁记录。
资源可以分为两类:可抢占资源和不可抢占资源。
可抢占资源:可以从拥有它的进程中抢占而不产生副作用。
不可抢占资源:不引起相关的计算失败的情况下,无法把它从占有它的进程处抢占过来。
抢占这个词,在进程和线程调度时就提到了这个概念,那时是进程或者线程可以抢占CPU,即抢占式调度。存储器也可以抢占,如内存换页。
一般来说,可抢占资源不会引起死锁,可以在进程间重新分配资源而得到解决。
二、死锁
死锁的概念:如果一个进程集合中,每个进程都在等待只能由该集合中其他进程才能引发的时间,那么该进程集合就死锁的。
死锁并不仅仅发生在资源上,资源死锁只是一种。
资源死锁的四个必要条件:
(1)互斥条件。每个资源要么已经分配给了一个进程,要么就是可用的。
(2)占有和等待条件。已经得到了某个资源的进程可以再请求新的资源。
(3)不可抢占条件。已经分配给一个进程的资源不能被抢占,只能由占有它的进程显式地释放。
(4)环路等待条件。死锁发生时,系统中一定有由两个或以上的进程组成的一条环路,该环路中的每个进程都在等待着下一个进程所占有的资源。
可以自然地想到,可以用一个有向图来表示资源分配的情况。用圆形节点表示进程,方形表示资源。从资源节点到进程节点的有向边表示该资源被请求、并被进程占用。由进程到资源节点的有向边表示进程正在请求该资源,并且因为请求资源而导致进程被阻塞,处于等待该资源的状态。
这样,根据环路等待条件,一旦在某个时候有向图中出现了两个或两个以上进程组成的环路,就会导致死锁的发生。
下图是《现代操作系统》中的一个例子:
现在有三个进程A、B、C,它们需要R、S、T三个资源中的其中两个,具体参见图上方。操作系统可以选取任意一个就绪的进程执行。
我们可以看到,在第一种执行顺序中,每个进程都占有了一个资源,从而在请求第二个资源的时候,每个进程都进入了阻塞态,最后没有进程可以执行。同时注意到,资源图确实构成了环路。
在第二种执行顺序中,进程A率先获取了所有它需要的资源R和S,尽管C因为请求资源R而进入了阻塞态,但是A使用完资源后,释放了R和S,最终C可以继续执行下去,没有构成死锁。
处理死锁的策略:
(1)忽略该问题。一般称为鸵鸟算法,即躲起来视而不见。
(2)检测死锁并恢复。让死锁发生,检测它们是否发生,一旦发生死锁,就采取行动解决问题。
(3)仔细对资源进行分配,动态地避免死锁。也称为死锁避免。
(4)通过破坏引起死锁的四个必要条件,防止死锁的发生。也称为死锁预防。
三、死锁检测和恢复
采用这种办法的时候,系统并没有试图阻止死锁的发生,而是允许死锁发生,检测到死锁后,采取措施进行恢复。
检测:我们知道,死锁的一个必要条件是存在两个及以上进程组成的环路。通过检测有向图环路,是一种检测死锁存在的方法。
当每种类型存在多个资源时,检测可能会复杂许多。
恢复的方法:
(1)利用抢占恢复。死锁发生的必要条件,其中一个就是不可抢占。如果允许抢占,那么就可以破坏死锁条件。
(2)利用回滚:周期性对进程进行检查点检查,一旦发现了死锁,就回滚到一个较早的检查点上。
(3)通过杀死进程:这个方法是最显而易见的。杀死一个进程可以释放它占有的资源,如果仍然不行那么久继续杀死其他进程直到打破死锁。
四、死锁避免
1、资源轨迹图
如果直到了进程在各个阶段需要哪些资源,那么可以在图中进程标注。两个进程的交叠区域就是一个可能会造成死锁的区域。进程在图中只能向右或者向上前进,一旦进入了危险区,那么就有可能发生死锁。为了避免死锁,应当在合适的时间阻塞某个进程,使得运行避开这个区域。
2、银行家算法
在介绍银行家算法之前,先引入一个安全状态与不安全状态的概念。
如果没有死锁发生,并且即使所有的进程突然请求对资源的最大需求,也仍然存在某种调度次序,能使得每个进程运行完毕,则称该状态是安全的。
该图中,a状态下是安全的,因为按照b-e的顺序,整个过程能完成而不会产生死锁。只要系统仔细进行调度,就能够避免死锁。
该图中,a状态是安全的,但是b中把资源先分配给了A进程1个,导致后面可能会出现。注意,不安全状态并不一定是死锁,b中的状态,依然可以运行一段时间,B进程甚至可以运行完成,直到没有空闲资源才会发生死锁。如果A能主动释放资源,可能也不会出现死锁。
安全状态与不安全状态的区别是,从安全状态出发,系统能够保证所有的进程都能完成;而从不安全状态出发,没有这样的保证。
单个资源的银行家算法:对每一个请求进行检查,检查如果满足了这一需求是否会达到安全状态。如果能,那么满足该需求;如果不能,就推迟对这一请求的满足。
例如,上面图中,银行家可以只满足B客户的需求,拖延其他客户的需求,因而可以让B先完成,然后释放B的4个资源。有了这4个资源,就可以给B或者D分配所需要的资源。
不安全状态不一定产生死锁,不过如果一旦一个进程需要最大资源,就会导致死锁,银行家算法避免这种风险。
银行家算法可以推广到多个资源的情况,此时可以写成矩阵的形式,每次判断一行是否满足,即一个进程的多个资源都进行检查。
需要注意到,死锁避免是非常困难的,无论是资源轨迹图还是银行家算法,都需要事先知道进程运行的过程中需要的最大资源数,这几乎是不可能实现的!
五、死锁预防
死锁避免可以认为是在程序执行中动态地避免死锁发生,而死锁预防可以说是静态的方式,杜绝死锁发生的可能性。
只要能破坏死锁发生的四个必要条件之一,那么死锁就不会发生。
1、破坏互斥条件
尽量使得资源不被某个进程独占。比如打印机的假脱机打印,就是尽量避免一个进程独占打印机,而是把要打印的文件存入一个假脱机目录,然后通过一个守护进程管理打印机进行打印。
2、破坏占有和等待条件
即禁止已经持有资源的进程再等待其他资源。
一种方式是,在进程开始执行前请求所需的全部资源,如果不能满足,那么就不分配资源,进行等待。这种方式的问题在于,类似银行家算法,事先不知道需要多少资源,而且资源利用率不高。
另一种方式就是,当一个进程请求资源时,先暂时释放其当前所占用的所有资源,然后再尝试一次获取所需的全部资源。
3、破坏不可抢占条件
这个比较好理解,允许资源抢占即可。当然,有的时候资源应当是不可抢占的。
4、破坏环路等待条件
一种方法是对资源进行编号,进程在任何时候都可以请求资源,但是所有的请求必须按照资源编号的顺序(升序)提出。
六、其他问题
1、两阶段加锁
这是针对数据库的一种方法,第一阶段对所有需要更新的记录进行加锁,一旦某个记录已经被加锁,就释放之前的锁,从头进行重试。只有当第一阶段所有获取锁的行为都成功,才进行第二阶段的更新,否则放弃所有的锁。
2、通信死锁
这种情况其实是很常见的。当一个进程A向B发送信息后挂起,需要B进程的回复唤醒时,如果请求信息丢失,A就会被阻塞以等待回复,B会阻塞等待一个向其发送命令的请求,因而发生死锁。
通信死锁不涉及资源,不能通过合理调度资源来避免。一般通信协议会解决这种问题,包括超时重传等技术。
3、活锁
轮询(忙等待)的方式,在有时是有效的,因为挂起进程等待的开销很大。考虑如下程序:
enter_region()通过轮询获取资源,假设A获得了资源1,B获得了资源2,那么两个进程都不会阻塞,而是不停地进行轮询以获取资源。两个进程总是运行完系统分配的时间片,没有阻塞但是不会取得进展。
4、饥饿
饥饿的概念,其实与死锁和活锁差别比较大。考虑进程调度,基于优先级的调度中,如果总是有高优先级的进程就绪,那么一个低优先级的进程可能长时间无法上CPU运行。这就是饥饿现象,可以考虑通过动态优先级机制,可以动态提高长时间得不到运行的进程的优先级,从而使它可以运行。