死锁的定义是指多个进程(或线程)在执行过程中,由于竞争资源或彼此通信而造成的一种阻塞现象,其中每个进程都持有某种资源并等待获取其他进程所占有的资源,导致所有进程都无法继续向前推进的状态。这种状态若无外力干预将永远持续。
死锁的四个必要条件是:
- 互斥条件:资源不能被多个进程共享,某一时刻只能由一个进程使用。
- 占有并等待:进程已保持至少一个资源,但又提出了新的资源请求,而该资源被其他进程占有,此时请求进程阻塞,但对自己已占有的资源保持不放。
- 非抢占条件:已分配给进程的资源,不能被其他进程强行剥夺,只能由该进程自行释放。
- 循环等待条件:存在一种进程资源的循环等待链,即P₁等待P₂占有的资源,P₂等待P₃占有的资源,……,Pₙ等待P₁占有的资源。
你提到的三个典型案例分别对应了这些条件的不同组合:
- 案例(1)进程推进顺序不当:体现了循环等待与占有并等待,是典型的“交叉请求”导致的死锁。
- 案例(2)同类资源分配不当:当系统无法满足所有进程的最大需求时,若分配策略不合理,容易触发“占有并等待”和“循环等待”,从而引发死锁。
- 案例(3)PV操作使用不当:信号量的错误使用破坏了同步机制,导致生产者与消费者都无法继续执行,属于同步逻辑错误诱发的死锁。
为避免死锁,可采取以下策略:
- 预防:破坏上述四个必要条件之一(如预分配资源破坏“占有并等待”)。
- 避免:使用银行家算法等动态策略确保系统始终处于安全状态。
- 检测与恢复:允许死锁发生,通过资源图算法检测,并采取终止进程或回滚等方式恢复。
- 忽略:某些系统(如Linux)选择忽略死锁,依赖程序员正确编写程序。
# 死锁示例模拟(简化版,仅示意)importthreadingimporttime# 定义两个锁lock_A=threading.Lock()lock_B=threading.Lock()defprocess_P1():print("P1: 尝试获取资源 A")lock_A.acquire()print("P1: 已获得资源 A")time.sleep(1)print("P1: 尝试获取资源 B")lock_B.acquire()# 等待 P2 释放 Bprint("P1: 已获得资源 B")lock_B.release()lock_A.release()defprocess_P2():print("P2: 尝试获取资源 B")lock_B.acquire()print("P2: 已获得资源 B")time.sleep(1)print("P2: 尝试获取资源 A")lock_A.acquire()# 等待 P1 释放 Aprint("P2: 已获得资源 A")lock_A.release()lock_B.release()# 启动两个线程模拟死锁t1=threading.Thread(target=process_P1)t2=threading.Thread(target=process_P2)t1.start()t2.start()t1.join()t2.join()print("程序结束")# 实际上可能永远不会执行到这里银行家算法是一种死锁避免策略,由艾兹格·迪杰斯特拉提出,用于多资源类型环境下的安全资源分配。其核心思想是:在每次资源分配前,系统进行“模拟分配”,判断此次分配是否会导致系统进入不安全状态。只有当分配后系统仍处于安全状态时,才真正分配资源,从而避免死锁。
1. 基本概念
设系统中有:
n个进程(P₁, P₂, …, Pₙ)m种资源类型(R₁, R₂, …, Rₘ),每种资源总数为Total[i]
定义以下数据结构:
- Allocation[i][j]:进程 i 已分配到的 j 类资源数量。
- Max[i][j]:进程 i 对 j 类资源的最大需求。
- Need[i][j] = Max[i][j] - Allocation[i][j]:进程 i 还需要的资源数。
- Available[j]:当前系统中 j 类资源的可用数量。
2. 安全状态与安全序列
一个系统处于安全状态,当且仅当存在一个安全序列——即所有进程的一个执行顺序{P₁, P₂, ..., Pₙ},使得每个进程都能在其后续资源请求中被满足(假设每个进程最终会释放所占资源)。
只要系统始终运行在安全状态下,就可避免死锁。
3. 银行家算法步骤(安全性检测算法)
defis_safe(available,max_claim,allocation):n=len(max_claim)# 进程数m=len(available)# 资源种类数need=[[max_claim[i][j]-allocation[i][j]forjinrange(m)]foriinrange(n)]work=available[:]finish=[False]*n safe_sequence=[]whilelen(safe_sequence)<n:found=Falseforiinrange(n):ifnotfinish[i]:# 检查该进程所需资源是否都 ≤ 当前可用资源ifall(need[i][j]<=work[j]forjinrange(m)):# 假设该进程获得资源并完成forjinrange(m):work[j]+=allocation[i][j]finish[i]=Truesafe_sequence.append(i)found=Truebreakifnotfound:returnFalse,[]# 不安全状态returnTrue,safe_sequence4. 资源请求处理流程
当某进程 Pi 发出请求 Request[j] 后,系统按如下步骤处理:
检查请求是否合法
若Request[j] > Need[i][j]→ 错误(超出最大需求)检查是否有足够资源
若Request[j] > Available[j]→ 进程等待尝试“预分配”并检测安全性
- 临时更新:
Available[j]-=Request[j]Allocation[i][j]+=Request[j]Need[i][j]-=Request[j] - 调用安全检测算法
- 若结果为安全 → 真正分配
- 若不安全 → 拒绝分配,恢复原状态,进程等待
- 临时更新:
5. 示例说明
假设有三个进程 P0、P1、P2,两类资源 A、B,总量为 [10, 8]
| 进程 | Max(A,B) | Alloc(A,B) | Need(A,B) |
|---|---|---|---|
| P0 | (7,5) | (0,1) | (7,4) |
| P1 | (3,2) | (2,0) | (1,2) |
| P2 | (9,5) | (3,0) | (6,5) |
Available = [10 - (0+2+3), 8 - (1+0+0)] = [5,7]
调用安全检测算法,发现可以找到安全序列(如 P1 → P0 → P2),因此当前状态安全。
若此时 P2 请求 (2,2),先预分配:
- New Alloc[P2] = (5,2),Need[P2]=(4,3),Available=(3,5)
- 再次运行安全检测,若仍能找到安全序列,则允许;否则拒绝。
6. 局限性
- 要求预先知道每个进程的最大资源需求(现实中难以准确预测)
- 进程数和资源类型需静态固定
- 开销较大,频繁调用安全检测影响性能
但它是理论上的重要模型,广泛应用于教学和某些嵌入式或高可靠性系统中。