操作系统 2.12-死锁处理
死锁产生的原因
举例分析
这张幻灯片展示了生产者-消费者问题中信号量使用不当导致死锁的情况。让我们详细分析一下幻灯片中的内容和问题。
信号量使用顺序
-
生产者:
Producer(item) {
P(empty); // 等待空槽
P(mutex); // 等待互斥锁
// 写入item到缓冲区
V(mutex); // 释放互斥锁
V(full); // 增加满槽计数
}
-
消费者:
Consumer() {
P(full); // 等待满槽
P(mutex); // 等待互斥锁
// 从缓冲区读出item
V(mutex); // 释放互斥锁
V(empty); // 增加空槽计数
}
问题描述
如果信号量的使用顺序被颠倒,例如生产者先申请 mutex
再申请 empty
,消费者先申请 mutex
再申请 full
,可能会导致死锁。
死锁示例
假设初始状态为:
-
empty = 0
(缓冲区满) -
full = 1
(缓冲区有一个满槽) -
mutex = 1
(互斥锁可用)
-
生产者执行:
-
P(mutex)
:mutex
从 1 变为 0,生产者获得互斥锁。 -
P(empty)
:empty
为 0,生产者阻塞。
-
-
消费者执行:
-
P(full)
:full
从 1 变为 0,消费者获得满槽。 -
P(mutex)
:mutex
为 0,消费者阻塞。
-
此时,生产者和消费者都处于阻塞状态,因为:
-
生产者需要
empty
信号量,但empty
为 0。 -
消费者需要
mutex
信号量,但mutex
为 0。 -
从而产生了死锁
系统分析
死锁是操作系统中的一个经典问题,它发生在多个进程互相等待对方持有的资源,导致所有进程都无法继续执行的情况。根据您提供的幻灯片内容,死锁的成因可以总结如下:
-
资源互斥使用:
-
资源不能被多个进程同时使用,即一个资源在某一时刻只能被一个进程占用。这导致一旦一个进程占有了某个资源,其他需要该资源的进程就无法使用它。
-
-
占有并等待:
-
一个进程已经持有了至少一个资源,但仍然等待获取其他资源。如果它等待的资源被其他进程占有,那么这些进程就可能因为无法获取所需资源而无法继续执行。
-
-
不可抢占:
-
已经分配给进程的资源不能被操作系统强制抢占。资源只能由占有它的进程自愿释放。
-
-
环路等待:
-
当存在一个进程等待链,其中每个进程都在等待下一个进程所占有的资源,就形成了一个等待环路。如果这个环路中的所有进程都在等待,那么这些进程就都无法继续执行,从而发生死锁。
-
幻灯片中的图示进一步形象化了死锁的成因,通过交通拥堵的例子来说明:
-
资源互斥使用:图中每个交叉路口只能同时通过一辆车,类似于资源的互斥使用。
-
占有并等待:一辆车已经占用了一个路口(资源),但停下来等待通过另一个路口(资源),类似于进程占有并等待其他资源。
-
环路等待:图中形成了一个环路,每个方向的车都在等待其他方向的车先通过,这类似于进程间的环路等待。
死锁产生的原因
幻灯片中列出了死锁的四个必要条件,这些条件是死锁发生的基础。以下是对这四个条件的详细解释和分析:
1. 互斥使用(Mutual exclusion)
-
定义:资源的固有特性,如道口,一次只能被一个进程使用。
-
影响:确保了资源在某一时刻只能被一个进程占用,防止了资源的并发访问,但也可能导致其他进程的等待。
2. 不可抢占(No preemption)
-
定义:资源只能自愿放弃,例如,一辆车必须完全离开道口后,其他车才能使用。
-
影响:一旦进程获取了资源,它必须完成工作并主动释放资源,操作系统不能强制回收资源,这可能导致资源长时间被占用。
3. 请求和保持(Hold and wait)
-
定义:进程必须占有资源,再去申请新的资源。
-
影响:进程在持有资源的同时请求新的资源,如果新资源不可用,进程将等待,同时保持已持有的资源,这可能导致其他进程无法获取所需资源。
4. 循环等待(Circular wait)
-
定义:在资源分配图中存在一个环路,即存在一组进程,每个进程都在等待下一个进程所占有的资源。
-
影响:形成循环等待后,所有涉及的进程都无法继续执行,因为没有进程能够释放资源来打破等待链。
处理死锁的方法
总览
幻灯片中概述了处理死锁的几种策略,这些策略可以类比为处理火灾的不同方法。以下是这些策略的详细解释和总结:
1. 死锁预防(Deadlock Prevention)
-
策略:类似于“no smoking”预防火灾,通过破坏死锁出现的条件来防止死锁的发生。
-
实施:确保系统不同时满足死锁的四个必要条件(互斥使用、不可抢占、请求和保持、循环等待)。
-
例子:资源有序分配,即所有进程按固定顺序请求资源。
2. 死锁避免(Deadlock Avoidance)
-
策略:类似于检测到煤气超标时自动切断电源,通过动态检测资源请求来避免死锁。
-
实施:在进程请求资源时,检查是否会导致死锁,如果会,则拒绝请求。
-
例子:银行家算法,通过模拟资源分配来检测死锁的可能性。
3. 死锁检测+恢复(Deadlock Detection and Recovery)
-
策略:类似于发现火灾时立刻拿起灭火器,通过检测系统状态来识别死锁,并采取措施恢复。
-
实施:定期检测系统状态,识别死锁,然后通过资源回滚或其他方法恢复系统。
-
例子:检测到死锁后,强制终止或回滚一些进程,释放资源。
4. 死锁忽略(Deadlock Ignoring)
-
策略:类似于在太阳上对火灾全然不顾,即不采取任何措施处理死锁。
-
实施:假设死锁发生的概率很低,或者系统可以承受偶尔的死锁。
-
例子:在某些系统中,如果死锁发生,简单地重启系统可能是最简单的解决方案。
死锁预防
幻灯片中介绍了两种死锁预防的方法,并解释了为什么使用这两种方法可以避免死锁的发生。以下是对这两种方法的总结和分析:
方法1:一次性申请所有需要的资源
-
策略:在进程执行前,一次性申请所有需要的资源,不会在占有资源后再去申请其他资源。
-
优点:通过确保进程在开始执行前就获取所有所需资源,避免了进程在执行过程中因请求资源而形成的等待环路,从而预防了死锁的发生。
-
缺点:
-
编程困难:需要预知未来,即在编程时就需要知道进程将需要的所有资源,这在实际中可能很难实现。
-
资源利用率低:许多资源可能在分配后很长时间后才被使用,导致资源在这段时间内被闲置。
-
方法2:对资源类型进行排序
-
策略:对资源类型进行排序,资源申请必须按顺序进行,不会出现环路等待。
-
优点:通过确保所有进程按照相同的顺序请求资源,避免了循环等待的情况,从而预防了死锁的发生。
-
缺点:仍然可能造成资源浪费,因为进程可能需要等待资源按照特定的顺序被释放。
死锁避免
安全状态和安全序列
在操作系统中,死锁避免的一个重要方法是通过银行家算法来判断系统是否处于安全状态。安全状态是指存在一个安全序列,使得所有进程都能按顺序完成执行而不会发生死锁。安全序列是指一个进程执行的顺序,按照这个顺序执行,系统不会进入死锁状态。
安全状态和安全序列的定义
-
安全状态:如果系统能够找到一个进程执行序列P1, P2, ..., Pn,使得每个进程Pi在执行时,其需要的资源都能得到满足,那么系统处于安全状态。
-
安全序列:上述的进程执行序列P1, P2, ..., Pn就是安全序列。
如何找安全序列
-
初始化:记录当前可用资源Available,以及每个进程的已分配资源Allocation和还需要的资源Need。
-
寻找可以执行的进程:在所有进程中,寻找一个进程P,使得P的Need <= Available。如果找到这样的进程P,那么P可以执行。
-
更新资源:将P的已分配资源Allocation加到Available中,表示P执行完毕后释放了资源。
-
重复步骤2和3:继续寻找可以执行的进程,直到所有进程都执行完毕,或者找不到可以执行的进程。
分析给定的表格
根据给定的表格,我们有以下数据:
-
Available:A=2, B=3, C=0
-
Allocation和Need如表格所示。
我们来分析选项,看看哪个是安全序列。
选项A: P1, P3, P2, P4, P0
-
P1:Need(0,2,0) <= Available(2,3,0),可以执行。执行后Available变为(5,3,2)。
-
P3:Need(0,1,0) <= Available(5,3,2),可以执行。执行后Available变为(7,4,2)。
-
P2:Need(6,0,0) <= Available(7,4,2),可以执行。执行后Available变为(10,4,2)。
-
P4:Need(4,3,1) <= Available(10,4,2),可以执行。执行后Available变为(10,5,3)。
-
P0:Need(7,4,3) <= Available(10,5,3),可以执行。
选项A是一个安全序列。
选项B: P0, P1, P2, P3, P4
-
P0:Need(7,4,3) > Available(2,3,0),不能执行。
选项B不是一个安全序列。
选项C: P3, P0, P1, P2, P4
-
P3:Need(0,1,0) <= Available(2,3,0),可以执行。执行后Available变为(4,4,2)。
-
P0:Need(7,4,3) > Available(4,4,2),不能执行。
选项C不是一个安全序列。
选项D: P3, P4, P1, P2, P0
-
P3:Need(0,1,0) <= Available(2,3,0),可以执行。执行后Available变为(4,4,2)。
-
P4:Need(4,3,1) <= Available(4,4,2),可以执行。执行后Available变为(4,5,3)。
-
P1:Need(0,2,0) <= Available(4,5,3),可以执行。执行后Available变为(7,5,3)。
-
P2:Need(6,0,0) <= Available(7,5,3),可以执行。执行后Available变为(10,5,3)。
-
P0:Need(7,4,3) <= Available(10,5,3),可以执行。
选项D是一个安全序列。
选项A和选项D都是安全序列。
银行家算法
银行家算法是一种避免死锁的资源分配策略,由艾兹格伯特·迪杰斯特拉(Edsger Dijkstra)提出。它通过模拟资源分配过程来检测系统是否会进入死锁状态。如果系统能够找到一个安全序列,即所有进程都能按某种顺序完成,那么系统就处于安全状态,否则可能会发生死锁。
算法介绍
银行家算法的核心思想是维护一个工作向量(Work),表示当前系统中可用的资源数量。算法开始时,工作向量等于系统中所有资源的总量。然后,算法尝试为每个进程分配资源,直到所有进程都能完成。
算法步骤
-
初始化:设置工作向量(Work)为系统中所有资源的总量,设置每个进程的完成状态(Finish)为假。
-
寻找安全序列:
-
进入一个无限循环,尝试为每个进程分配资源。
-
对于每个进程,检查其需求(Need)是否小于或等于当前的工作向量(Work)。
-
如果是,假设分配给它所需的资源,更新工作向量(Work = Work + Allocation[i]),并将该进程标记为完成(Finish[i] = true)。
-
如果所有进程都能按某种顺序完成,那么系统处于安全状态,算法结束。
-
如果在某个时刻,找不到可以继续执行的进程,则系统可能处于死锁状态。
-
-
死锁检测:如果在尝试过程中发现无法为任何进程分配资源,则算法终止并报告死锁。
伪代码
int Available[1..m]; // 每种资源剩余数量
int Allocation[1..n,1..m]; // 已分配资源数量
int Need[1..n,1..m]; // 进程还需的各种资源数量
int Work[1..m]; // 工作向量
bool Finish[1..n]; // 进程是否结束
Work = Available; // 初始化工作向量
Finish[1..n] = false; // 初始化完成状态
while (true) {
bool found = false;
for (i = 1; i <= n; i++) {
if (!Finish[i] && Need[i] <= Work) {
Work = Work + Allocation[i]; // 假设分配资源
Finish[i] = true; // 标记为完成
found = true;
break;
}
}
if (!found) break; // 如果没有找到可以执行的进程,退出循环
}
for (i = 1; i <= n; i++) {
if (!Finish[i]) return "deadlock"; // 检查是否有未完成的进程
}
时间复杂度
银行家算法的时间复杂度为 O(mn^2),其中 m 是资源类型的数量,n 是进程的数量。这是因为在最坏情况下,算法需要检查每个进程对每种资源的需求是否小于等于当前的工作向量。这个复杂度还是很高的。
银行家算法对于死锁的避免
银行家算法的工作原理
银行家算法维护四个关键的数组:
-
Available:表示系统中当前可用的资源数量。
-
Allocation:表示已经分配给每个进程的资源数量。
-
Need:表示每个进程还需要多少资源才能完成任务。
-
Work:表示当前可用资源的临时副本,用于模拟资源分配。
算法的步骤如下:
-
初始化
Work
为Available
的值。 -
设置所有进程的
Finish
标记为false
。 -
进入一个循环,尝试为每个进程分配资源,直到找到一个安全序列或确定系统处于不安全状态。
安全序列的寻找
在给定的资源分配表中,我们可以通过以下步骤找到一个安全序列:
-
初始时,
Work
设置为Available
的值[3, 3, 2]
。 -
检查每个进程的需求
Need
是否小于等于Work
。 -
如果是,假设分配资源给该进程,更新
Work
为Work + Allocation[i]
,并标记该进程为完成Finish[i] = true
。 -
重复步骤2和3,直到所有进程都被标记为完成或找不到可以继续执行的进程。
死锁忽略
死锁忽略
-
死锁出现不是确定的,即死锁发生的概率较低,因此可以通过系统重启来处理死锁问题。
-
大多数非专门的操作系统(如 UNIX、Linux、Windows)都采用死锁忽略策略,因为这些系统设计为通用操作系统,需要处理各种不同的应用场景,而不是专门为避免死锁而设计。