目录
死锁的基本概念
一组进程中,每个进程都无限等待被该组进程中另一进程所占有的资源,因而永远无法得到资源。
原因:资源数量有限、锁和信号量错误使用
必要条件
- 互斥使用:一个资源每次只能给一个进程使用
- 占有和等待:进程在申请新的资源的同时保持对原有资源的占有,已经得到了某个资源的进程可以再请求新的资源。
- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
- 循环等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
处理方法
主要有以下四种方法:
- 鸵鸟策略
- 死锁检测与死锁恢复
- 死锁预防
- 死锁避免
鸵鸟策略
把头埋在沙子里,假装根本没发生问题。
因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任何措施的方案会获得更高的性能。
当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。
死锁检测与死锁恢复
不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。
1. 每种类型一个资源的死锁检测
上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。
图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
2. 每种类型多个资源的死锁检测
上图中,有三个进程四个资源,每个数据代表的含义如下:
- E 向量:资源总量
- A 向量:资源剩余量
- C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量
- R 矩阵:每个进程请求的资源数量
进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。
算法总结如下:
每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。
- 寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。
- 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。
- 如果没有这样一个进程,算法终止。
3. 死锁恢复
- 利用抢占恢复,按照某种原则逐一抢占资源(资源被抢占的进程必须回退到之前的对应状态)
- 利用回滚再启动恢复
- 通过撤销所有死锁进程恢复
- 通过某种原则逐一撤销
死锁预防
在程序运行之前预防发生死锁。
1. 破坏互斥条件
假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
避免分配不是绝对必要的资源,让尽可能少的进程可以真正请求资源。
2. 破坏占有和等待条件
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源,即规定资源一次性的分配。
另一种实现方式是当一个进程请求资源时,先暂时释放其所占有资源,然后再尝试一次性请求所需全部资源。
3. 破坏不可抢占条件
当一个已经保持了某种不可抢占资源的进程提出新的资源请求不能被满足时,它必须释放已经保持的所有资源,以后需要时再去重新申请。
4. 破坏环路等待
系统给每类资源统一编号,进程只能按编号顺序来请求资源。
死锁避免
在程序运行时避免发生死锁。
1. 安全状态
图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。
定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。
安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。
2. 单个资源的银行家算法
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。
3. 多个资源的银行家算法
上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。
检查一个状态是否安全的算法如下:
- 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。
- 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
- 重复以上两步,直到所有进程都标记为终止,则状态时安全的。
如果一个状态不是安全的,需要拒绝进入这个状态。
哲学家就餐问题
问题描述:五个哲学家围坐在一圆桌旁,桌中央有一盘通心粉,每人面前有一只空盘子,每两人之间放一只筷子。为了吃通心粉,每个哲学家必须拿到两只筷子,并且每个人只能从自己的左边或者右边去取筷子
问题模型:应用程序中并发线程执行时,如何协调处理共享资源
将筷子当做信号量来处理,拿筷子时,对信号量采取 P 操作。
当每个哲学家都拿到了右边的筷子,在等待左边的筷子,就会出现死锁。
semaphore fork [5] = {1};
int i;
void philosopher (int i)
{
while (true) {
think();
P (fork[i]); //申请左边的筷子
P (fork [(i+1) mod 5]); //申请右边的筷子
eat();
V (fork [(i+1) mod 5]);
V (fork[i]);
}
}
void main()
{
parbegin (philosopher (0), philosopher (1),philosopher (2),
philosopher (3),philosopher (4));
}
第一种解决方案:最多允许4个哲学家坐在桌子周围
增加一个新的信号量 room,最大值为 4,用 P(room) 来表示他能不能做到桌子旁边,就实现了判断最多允许4个哲学家坐在桌子周围的条件。
semaphore fork[5] = {1};
semaphore room = {4};
int i;
void philosopher (int i)
{
while (true) {
think();
P (room);
P (fork[i]);
P (fork [(i+1) mod 5]);
eat();
V (fork [(i+1) mod 5]);
V (fork[i]);
V (room);
}
}
void main()
{
parbegin ( philosopher (0), philosopher (1),philosopher (2),
philosopher(3),philosopher (4) );
}
第二种解决方案:仅当哲学家的左右两只筷子都可用时,才允许他拿起筷子进餐
常规方案:
通过信号量 mutex 对 eat() 之前的取左侧和右侧筷子的操作进行保护,使之成为一个原子操作,这样可以预防死锁的出现。
semaphore mutex = 1;
semaphore chopstick[5] = {1};
void philosopher(int l)
{
while(true){
think();
wait(mutex);
wait(chopstick[l+1] % 5);
wait(chopstick[l]);
signal(mutex);
eat();
signal(chopstick[l+1] % 5);
signal(chopstick[l]);
}
}
*使用管程方案:
void philosopher[k=0 to 4]
/* the five philosopher clients */
{
while (true) {
<think>;
get_forks(k); /* client requests two forks via monitor */
<eat spaghetti>;
release_forks(k); /* client releases forks via the monitor */
}
}
monitor dining_controller;
cond ForkReady[5];
boolean fork[5] = {true};
void get_forks(int pid)
{
int left = pid;
int right = (++pid) % 5;
/*grant the left fork*/
if (!fork(left)
cwait(ForkReady[left]);
/* queue on condition variable */
fork(left) = false;
/*grant the right fork*/
if (!fork(right)
cwait(ForkReady(right);
/* queue on condition variable */
fork(right) = false:
}
void release_forks(int pid)
{
int left = pid;
int right = (++pid) % 5;
/*release the left fork*/
if (empty(ForkReady[left])
/*no one is waiting for this fork */
fork(left) = true;
else /* awaken a process waiting on this fork */
csignal(ForkReady[left]);
/*release the right fork*/
if (empty(ForkReady[right])
/*no one is waiting for this fork */
fork(right) = true;
else /* awaken a process waiting on this fork */
csignal(ForkReady[right])
}
第三种解决方案:规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;偶数哲学家则相反。
按此规定,4,5号哲学家竞争4号筷子,2,3号哲学家竞争2号筷子。即五个哲学家都竞争偶数号筷子,获得后再去竞争奇数号筷子,最后总有一个哲学家能够获得两只筷子并进餐。申请不到的哲学家进入阻塞等待队列。
死锁避免:最后一只筷子只能分配给手中已经有一只筷子的哲学家。
死锁预防:仅当哲学家的左右两只筷子都可用时,才允许他拿起筷子进餐;资源的有序分配:申请筷子时先拿编号大的筷子