操作系统中的死锁问题
死锁的概念
死锁(Deadlock)是指在计算机系统中,多个进程因竞争资源而陷入相互等待状态,导致所有进程都无法继续执行的一种现象。就像几个小朋友同时想要玩具,但每个人都握着别人的玩具不放手,结果大家都玩不了。😂
死锁的四个必要条件
- 互斥条件(Mutual Exclusion)✋:资源是独占的,一个时间点只能有一个进程使用。
- 占有并等待条件(Hold and Wait)⌛:进程已经持有至少一个资源,并且还在等待其他资源。
- 不剥夺条件(No Preemption)🚫:资源不能被强行剥夺,只能由持有的进程自己释放。
- 环路等待条件(Circular Wait)🔄:存在一个进程循环链,每个进程都在等待下一个进程所持有的资源。
死锁预防(破坏必要条件)
预防死锁的方法是通过破坏上述必要条件之一,来避免系统进入死锁状态。
-
破坏互斥条件:让资源可以共享。比如让小朋友们一起玩同一个玩具。不过这在很多情况下难以实现,因为有些资源必须独占使用。
- 优点:简单直接。
- 缺点:不适用于必须独占使用的资源。
- 实现方法:让资源可以共享,不再要求资源只能由一个进程独占使用。这可以通过设计共享资源池、引入可重入锁等方式实现。
- 可能的算法:信号量机制、共享内存、互斥对象池等。
-
破坏占有并等待条件:要求进程在开始时一次性请求所有需要的资源,或者在请求新的资源前释放掉已经占有的资源。就像小朋友们必须一次性拿到所有想要的玩具,否则就不能玩。
- 优点:有效防止资源等待。
- 缺点:可能会造成资源浪费,降低系统并发性。
- 实现方法:要求进程在开始时一次性请求所有需要的资源,或者在请求新的资源前释放掉已经占有的资源。可以引入预留资源、资源分配顺序规定等方式。
- 可能的算法:预分配资源、资源预留策略、资源分配顺序规定等。
-
破坏不剥夺条件:允许资源被强行剥夺。当一个进程请求资源时,如果资源不够,可以从其他占有资源的进程中剥夺。就像小朋友可以把别人的玩具拿过来玩。
- 优点:有效防止资源长时间占用。
- 缺点:可能导致进程频繁切换和状态保存,增加系统开销。
- 实现方法:允许资源被强行剥夺。当一个进程请求资源时,如果资源不够,可以从其他占有资源的进程中剥夺。可以实现资源抢占机制、优先级调整等方式。
- 可能的算法:资源抢占策略、优先级调度算法等。
-
破坏环路等待条件:对资源进行排序,进程必须按序请求资源,保证不会形成环路等待。就像小朋友们按顺序拿玩具,避免形成循环等待。
- 优点:简单有效。
- 缺点:需要对资源进行严格排序,实际操作中可能不便。
- 实现方法:对资源进行排序,进程必须按序请求资源,保证不会形成环路等待。可以利用图论中的拓扑排序算法对资源依赖关系进行排序,从而避免形成环路等待。
- 可能的算法:拓扑排序、资源分级分配、资源请求顺序规定等。
死锁避免(动态检查)
避免死锁的方法是通过动态检查系统状态,确保系统始终处于一个安全状态。最经典的算法是银行家算法。
银行家算法(Banker's Algorithm)- 找到能够运行的一个安全序列 - 赌徒放贷思想
核心概念
- 可用资源向量(Available):系统当前可以分配的资源数。
- 最大需求矩阵(Max):每个进程对资源的最大需求。
- 分配矩阵(Allocation):每个进程当前已经分配的资源数。
- 需求矩阵(Need):每个进程还需要的资源数。
核心思想
银行家算法的核心思想是模拟资源分配,确保系统始终处于一个安全状态。每次资源请求前,系统会检查模拟分配后的状态是否安全。如果分配后系统处于安全状态,则允许分配,否则拒绝请求。
安全状态和安全序列
安全状态:系统中存在一个进程序列,使得每个进程在最大需求得到满足前,系统都有足够的资源满足其他进程的需求。 安全序列:一个进程的排列顺序,使得每个进程可以依次获得资源并完成执行。
如何找到安全序列
- 初始化工作向量
Work为当前可用资源向量Available。 - 初始化完成向量
Finish为False。 - 查找一个
Finish为False的进程,且其需求小于等于Work。 - 如果找到该进程,则将
Work加上该进程的分配资源,并将Finish置为True。 - 重复步骤3,直到找到一个安全序列或者无法找到这样的进程。
银行家算法的变化过程
当一个进程请求资源时,系统会模拟分配资源,并检查分配后的系统状态。如果分配后系统仍处于安全状态,则实际分配资源;否则拒绝请求。
代码示例:
# 检查系统是否处于安全状态
def is_safe(avail, alloc, max_need):
n = len(alloc) # 进程数
m = len(avail) # 资源种类数
work = avail[:] # 可用资源向量
finish = [False] * n # 记录进程是否能完成
# 临时安全顺序
safe_seq = []
while len(safe_seq) < n:
allocated = False
for i in range(n):
if not finish[i]:
if all(max_need[i][j] - alloc[i][j] <= work[j] for j in range(m)):
for j in range(m):
work[j] += alloc[i][j]
finish[i] = True
safe_seq.append(i)
allocated = True
break
if not allocated:
return False, []
return True, safe_seq
# 资源分配请求
def request_resources(avail, alloc, max_need, req, pid):
n = len(alloc)
m = len(avail)
# 检查请求是否大于进程的最大需求
if any(req[j] > max_need[pid][j] - alloc[pid][j] for j in range(m)):
return False, "Request exceeds process's maximum claim."
# 检查请求是否大于可用资源
if any(req[j] > avail[j] for j in range(m)):
return False, "Request exceeds available resources."
# 尝试分配资源
for j in range(m):
avail[j] -= req[j]
alloc[pid][j] += req[j]
# 检查是否处于安全状态
is_safe_state, safe_seq = is_safe(avail, alloc, max_need)
if is_safe_state:
return True, "Resources allocated successfully."
else:
# 回滚分配
for j in range(m):
avail[j] += req[j]
alloc[pid][j] -= req[j]
return False, "Request leads to unsafe state."
好的,我们来扩展银行家算法的示例,展示一种选择了某个进程后无法找到安全序列的情况,并解释如何回到安全点继续寻找其他选择。
假设有4个进程(P1, P2, P3, P4)和3类资源(R1, R2, R3),资源数量分别为(10, 5, 7)。初始分配如下:
初始状态
需求矩阵 (Claim matrix, C)
| 进程 | R1 | R2 | R3 |
|---|---|---|---|
| P1 | 3 | 2 | 2 |
| P2 | 6 | 1 | 3 |
| P3 | 3 | 1 | 4 |
| P4 | 4 | 2 | 2 |
分配矩阵 (Allocation matrix, A)
| 进程 | R1 | R2 | R3 |
|---|---|---|---|
| P1 | 1 | 0 | 0 |
| P2 | 5 | 1 | 1 |
| P3 | 2 | 1 | 1 |
| P4 | 0 | 0 | 2 |
需求矩阵与分配矩阵的差 (C - A)
| 进程 | R1 | R2 | R3 |
|---|---|---|---|
| P1 | 2 | 2 | 2 |
| P2 | 1 | 0 | 2 |
| P3 | 1 | 0 | 3 |
| P4 | 4 | 2 | 0 |
资源向量 (Resource vector, R)
| 资源 | 总数 |
|---|---|
| R1 | 10 |
| R2 | 5 |
| R3 | 7 |
可用向量 (Available vector, V)
根据初始分配和资源总数,可以计算可用资源数:
Available = R - sum(Allocated)
可用向量计算如下:
R1: 10 - (1 + 5 + 2 + 0) = 10 - 8 = 2
R2: 5 - (0 + 1 + 1 + 0) = 5 - 2 = 3
R3: 7 - (0 + 1 + 1 + 2) = 7 - 4 = 3
所以初始可用向量为:
| 资源 | 可用数 |
|---|---|
| R1 | 2 |
| R2 | 3 |
| R3 | 3 |
P2运行到完成
P2运行完成并释放资源后的更新:
更新后的分配矩阵 (Allocation matrix, A)
| 进程 | R1 | R2 | R3 | |
|---|---|---|---|---|
| P1 | 1 | 0 | 0 | |
| P2 | 0 | 0 | 0 | <- P2释放资源 |
| P3 | 2 | 1 | 1 | |
| P4 | 0 | 0 | 2 |
更新后的可用向量 (Available vector, V)
R1: 2 + 5 = 7
R2: 3 + 1 = 4
R3: 3 + 1 = 4
所以新的可用向量为:
| 资源 | 可用数 |
|---|---|
| R1 | 7 |
| R2 | 4 |
| R3 | 4 |
尝试选择进程
尝试选择P1
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P1 | (2, 2, 2) | (7, 4, 4) |
P1的需求(2, 2, 2) ≤ 可用(7, 4, 4),分配资源给P1,P1完成后释放资源:
更新后的可用向量:Available = (7, 4, 4) + (1, 0, 0) = (8, 4, 4)
| 资源 | 可用数 |
|---|---|
| R1 | 8 |
| R2 | 4 |
| R3 | 4 |
尝试选择P3
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P3 | (1, 0, 3) | (8, 4, 4) |
P3的需求(1, 0, 3) ≤ 可用(8, 4, 4),分配资源给P3,P3完成后释放资源:
更新后的可用向量:Available = (8, 4, 4) + (2, 1, 1) = (10, 5, 5)
| 资源 | 可用数 |
|---|---|
| R1 | 10 |
| R2 | 5 |
| R3 | 5 |
尝试选择P4
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P4 | (4, 2, 0) | (10, 5, 5) |
P4的需求(4, 2, 0) ≤ 可用(10, 5, 5),分配资源给P4,P4完成后释放资源:
更新后的可用向量:Available = (10, 5, 5) + (0, 0, 2) = (10, 5, 7)
最终安全序列为:P2 → P1 → P3 → P4
尝试错误选择
假设我们在P2完成后,选择进程P4而不是P1:
尝试选择P4
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P4 | (4, 2, 0) | (7, 4, 4) |
P4的需求(4, 2, 0) ≤ 可用(7, 4, 4),分配资源给P4,P4完成后释放资源:
更新后的可用向量:Available = (7, 4, 4) + (0, 0, 2) = (7, 4, 6)
| 资源 | 可用数 |
|---|---|
| R1 | 7 |
| R2 | 4 |
| R3 | 6 |
尝试选择P1
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P1 | (2, 2, 2) | (7, 4, 6) |
P1的需求(2, 2, 2) ≤ 可用(7, 4, 6),分配资源给P1,P1完成后释放资源:
更新后的可用向量:Available = (7, 4, 6) + (1, 0, 0) = (8, 4, 6)
| 资源 | 可用数 |
|---|---|
| R1 | 8 |
| R2 | 4 |
| R3 | 6 |
尝试选择P3
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P3 | (1, 0, 3) | (8, 4, 6) |
P3的需求(1, 0, 3) ≤ 可用(8, 4, 6),分配资源给P3,P3完成后释放资源:
更新后的可用向量:Available = (8, 4, 6) + (2, 1, 1) = (10, 5, 7)
最后安全序列为:P2 → P4 → P1 → P3
然而,如果我们选择了一个不合适的进程,比如:
不合适选择的情况
假设我们在P2完成后,选择P3而不是P1或P4:
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P3 | (1, 0, 3) | (7, 4, 4) |
P3的需求(1, 0, 3) ≤ 可用(7, 4, 4),分配资源给P3,P3完成后释放资源:
更新后的可用向量:Available = (7, 4, 4) + (2, 1, 1) = (9, 5, 5)
此时:
尝试选择P1
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P1 | (2, 2, 2) | (9, 5, 5) |
P1的需求(2, 2, 2) ≤ 可用(9, 5, 5),分配资源给P1,P1完成后释放资源:
更新后的可用向量:Available = (9, 5, 5) + (1, 0, 0) = (10, 5, 5)
最后选择P4:
尝试选择P4
| 进程 | 需求 (C - A) | 可用 (V) |
|---|---|---|
| P4 | (4, 2, 0) | (10, 5, 5) |
P4的需求(4, 2, 0) ≤ 可用(10, 5, 5),分配资源给P4,P4完成后释放资源:
更新后的可用向量:Available = (10, 5, 5) + (0, 0, 2) = (10, 5, 7)
安全序列 P2 -> P3 -> P1 -> P4
银行家算法允许我们在选择进程后,验证整个系统是否在安全状态下运行,如果发现选择的某个进程会导致死锁或无法完成任务,则回到安全点,重新选择其他进程,从而确保系统安全运行。🎉