引言

在多道程序系统中,多个进程并发执行共享有限资源,若资源分配不当,就可能陷入死锁(Deadlock)——一组进程中的每个进程都在等待一个事件,而该事件只能由同组内的另一个进程触发,结果谁也动弹不得。死锁一旦发生,系统吞吐量骤降,甚至完全瘫痪。与死锁的斗争贯穿了操作系统设计的始终,从理论上的四个必要条件,到工程上的预防、避免、检测与恢复,形成了一套完整的武器库。本文将带你从理论到实践,用可运行的 Python 代码深入理解死锁检测与预防的核心机制。

一、死锁的四个必要条件

死锁的发生必须同时满足以下四个条件,缺一不可:

  1. 互斥条件(Mutual Exclusion):资源要么空闲,要么被一个进程独占,其他进程不能同时使用。
  2. 请求与保持条件(Hold and Wait):进程已经保持至少一个资源,但又提出了新的资源请求,而该资源被其他进程占有,此时请求进程阻塞,但对自己已获得的资源不释放。
  3. 不剥夺条件(No Preemption):进程已获得的资源在未使用完之前不能被剥夺,只能由进程自己释放。
  4. 循环等待条件(Circular Wait):存在一个进程等待环路,例如 P0 等待 P1 持有的资源,P1 等待 P2 持有的资源,……,Pn 等待 P0 持有的资源。

二、死锁处理策略概览

  • 预防死锁:破坏四个必要条件之一(如资源有序分配法破坏循环等待)。
  • 避免死锁:在资源分配前,通过某种算法判断此次分配是否会导致死锁,若会则拒绝;最著名的算法为 银行家算法(Banker's Algorithm)
  • 检测与恢复:允许死锁发生,但通过检测算法发现死锁,然后进行恢复(撤销进程或剥夺资源)。
  • 鸵鸟策略:假装死锁从没发生,交给用户手动处理(多数通用操作系统采用)。

下面我们重点通过代码实战来掌握避免死锁(银行家算法)死锁检测两种策略。

三、实战:银行家算法实现(避免死锁)

银行家算法由 Dijkstra 提出,它要求每个进程预先声明所需的最大资源量。系统在分配资源时,模拟分配后的状态,判断是否存在一个安全序列保证所有进程都能完成,如果存在则真正分配,否则让进程等待。

3.1 算法核心

  • Available 向量:系统当前可用的各类资源数量。
  • Max 矩阵:每个进程对各类资源的最大需求。
  • Allocation 矩阵:每个进程当前已分配的资源。
  • Need 矩阵:每个进程还需要的资源 = Max - Allocation。

安全状态判断:找到一个进程序列,使得每个进程的 Need ≤ 当前可用资源,执行后将它的 Allocation 全部释放累加到可用资源中,循环直到所有进程完成。若存在这样的序列,则系统处于安全状态。

3.2 Python 完整实现

#!/usr/bin/env python3
"""
银行家算法模拟:判断一次资源请求是否应被同意。
展示安全序列、分配后的状态变化。
"""

import numpy as np

class BankerAlgorithm:
    def __init__(self, available, max_demand, allocation):
        """
        :param available: 列表,系统初始可用资源,如 [3,3,2]
        :param max_demand: 二维列表,每个进程对每种资源的最大需求
        :param allocation: 二维列表,每个进程当前已分配的资源
        """
        self.available = np.array(available, dtype=int)
        self.max = np.array(max_demand, dtype=int)
        self.allocation = np.array(allocation, dtype=int)
        self.need = self.max - self.allocation
        self.n_processes = len(max_demand)

    def is_safe_state(self, available=None, need=None, allocation=None):
        """检查当前是否处于安全状态,并返回一个安全序列。"""
        if available is None:
            available = self.available.copy()
        if need is None:
            need = self.need.copy()
        if allocation is None:
            allocation = self.allocation.copy()

        work = available.copy()
        finish = np.zeros(self.n_processes, dtype=bool)
        safe_sequence = []

        # 最多分配 n_processes 轮,每轮找到一个可完成的进程
        for _ in range(self.n_processes):
            found = False
            for i in range(self.n_processes):
                if not finish[i] and all(need[i] <= work):
                    # 进程 i 可以完成,完成后释放其 allocation
                    work += allocation[i]
                    finish[i] = True
                    safe_sequence.append(i)
                    found = True
            if not found:
                break

        if all(finish):
            return True, safe_sequence
        else:
            return False, []

    def request_resources(self, process_id, request):
        """
        处理进程 process_id 的资源请求 request。
        返回: (是否同意, 原因或安全序列)
        """
        request = np.array(request, dtype=int)
        # 1. 检查请求是否超过声明 Need
        if any(request > self.need[process_id]):
            return False, "错误:请求超过声明的最大需求量!"
        # 2. 检查当前可用资源是否足够
        if any(request > self.available):
            return False, "当前可用资源不足,需等待。"
        # 3. 试探性分配
        self.available -= request
        self.allocation[process_id] += request
        self.need[process_id] -= request

        safe, sequence = self.is_safe_state()
        if safe:
            return True, f"同意分配!安全序列: {sequence}"
        else:
            # 恢复试探前的状态
            self.available += request
            self.allocation[process_id] -= request
            self.need[process_id] += request
            return False, "分配会导致死锁,请求被拒绝。"

    def __str__(self):
        return (f"Available: {self.available}\n"
                f"Max:\n{self.max}\n"
                f"Allocation:\n{self.allocation}\n"
                f"Need:\n{self.need}")

# ---------- 示例运行 ----------
if __name__ == "__main__":
    # 经典例子:5个进程,3种资源
    available_initial = [3, 3, 2]
    max_demand = [
        [7, 5, 3],
        [3, 2, 2],
        [9, 0, 2],
        [2, 2, 2],
        [4, 3, 3]
    ]
    allocation = [
        [0, 1, 0],
        [2, 0, 0],
        [3, 0, 2],
        [2, 1, 1],
        [0, 0, 2]
    ]
    banker = BankerAlgorithm(available_initial, max_demand, allocation)
    print("初始状态:")
    print(banker)
    safe, seq = banker.is_safe_state()
    print(f"初始安全状态: {safe}, 安全序列: {seq}")

    # 模拟 P1 请求 [1,0,2]
    print("\n--- 进程 P1 请求 [1,0,2] ---")
    ok, msg = banker.request_resources(1, [1,0,2])
    print(msg)
    print("请求后状态:")
    print(banker)

    # 模拟 P4 请求 [3,3,0] (应被拒绝)
    print("\n--- 进程 P4 请求 [3,3,0] ---")
    ok, msg = banker.request_resources(4, [3,3,0])
    print(msg)

运行结果解读:初始状态存在安全序列(如 [1,3,4,0,2]),系统安全。P1 请求 [1,0,2] 经试探后依旧安全,同意分配。P4 请求 [3,3,0] 超过可用资源或会导致死锁,被拒绝。

四、实战:死锁检测算法(资源分配图版)

当系统不采用预防或避免策略时,就需要定期运行死锁检测算法,及时发现环路,然后恢复。这里我们实现一种简化版:等待图(Wait-for Graph)的环检测。等待图是对资源分配图的简化,只保留进程到进程的等待边(P1→P2 表示 P1 在等待 P2 释放资源)。

4.1 原理

构造等待图:对每个进程,检查它正在请求的资源是否被其他进程占有,若是则添加一条从请求进程到占有进程的有向边。然后对等待图进行环检测(拓扑排序或 DFS)。如果存在环,则发生死锁,环上的所有进程处于死锁状态。

4.2 Python 实现

#!/usr/bin/env python3
"""
死锁检测:基于等待图的环检测。
输入:资源分配信息,输出死锁进程列表。
"""

from collections import defaultdict, deque

def detect_deadlock(allocation, request):
    """
    :param allocation: dict, {进程ID: {资源ID: 占有数量}}
    :param request: dict, {进程ID: {资源ID: 请求数量}}
    :return: 死锁进程列表(参与环的进程),无死锁返回空列表
    """
    # 构建等待图:进程 -> 等待的进程集合
    wait_graph = defaultdict(set)

    # 记录资源被哪些进程占有(总量)
    resource_owners = defaultdict(lambda: defaultdict(int))
    for proc, res in allocation.items():
        for r, amount in res.items():
            resource_owners[r][proc] += amount

    # 为每个请求构建等待边
    for proc, req in request.items():
        if not req:
            continue
        for r, need in req.items():
            if need <= 0:
                continue
            owners = resource_owners.get(r, {})
            # 只要资源有被其他进程占用,就有一条等待边
            for owner in owners:
                if owner != proc:
                    wait_graph[proc].add(owner)

    # 对等待图进行环检测(DFS)
    def dfs(node, visited, rec_stack):
        visited.add(node)
        rec_stack.add(node)
        for neighbor in wait_graph.get(node, set()):
            if neighbor not in visited:
                if dfs(neighbor, visited, rec_stack):
                    return True
            elif neighbor in rec_stack:
                return True
        rec_stack.remove(node)
        return False

    deadlocked = set()
    visited = set()
    for proc in list(set(list(allocation.keys()) + list(request.keys()))):
        if proc not in visited:
            rec_stack = set()
            # 使用一个临时标记找出环上的进程
            if dfs(proc, visited, rec_stack):
                deadlocked.update(rec_stack)

    # 精确找出环上的进程(可以 BFS 遍历一次)
    if deadlocked:
        # 可能存在多个死锁环,简单返回所有在环上的节点
        return list(deadlocked)
    else:
        return []

# ---------- 示例运行 ----------
if __name__ == "__main__":
    # 场景:3个进程,2种资源
    allocation = {
        'P0': {'R1': 1, 'R2': 0},
        'P1': {'R1': 0, 'R2': 1},
        'P2': {'R1': 0, 'R2': 0},   # P2 未占有任何资源,但请求资源
    }
    request = {
        'P0': {'R2': 1},               # P0 请求 1 个 R2
        'P1': {'R1': 1},               # P1 请求 1 个 R1
        'P2': {'R1': 1, 'R2': 1}
    }
    deadlock_procs = detect_deadlock(allocation, request)
    if deadlock_procs:
        print(f"检测到死锁!涉及进程: {deadlock_procs}")
    else:
        print("无死锁。")

在这个简单场景中,P0 占有 R1 并等待 R2(被 P1 占有),P1 占有 R2 并等待 R1(被 P0 占有),形成环,检测算法报告死锁。

五、常见问题与注意事项

1. 银行家算法的局限性
- 要求资源数量和最大需求预先已知且固定,在动态系统中难以满足。
- 算法开销与进程数和资源类型数乘积成正比,频繁调用影响性能。
- 假定了进程会主动释放资源,若进程崩溃未释放则打破模型。

2. 死锁检测的时机与频率
检测过于频繁消耗 CPU,过晚则死锁进程积累影响系统。通常采用定期检测或当资源利用率异常时触发。在实现中要注意检测算法的执行本身是否线程安全。

3. 恢复策略的选择
- 剥夺资源:从死锁进程中强制回收资源,代价是进程可能需要回滚甚至重新运行。
- 终止进程:选择代价最小的进程终止,但要考虑优先级、已执行时间、资源使用等。
- 多数数据库系统使用超时机制,结合死锁检测与事务回滚。

4. 现代开发中的启发
虽然操作系统内核很少实现银行家算法,但其思想广泛应用于 并发编程数据库锁 管理中。例如,编程中遵循加锁顺序一致(如总是先锁 A 再锁 B)可以预防循环等待;tryLock 方式则可实现非阻塞获取资源,打破请求与保持条件。

总结

死锁是多任务系统中永恒的话题。理解四个必要条件让我们能从源头思考解决方案;银行家算法展示了静态声明下优雅的避免策略;死锁检测与恢复则为无法避免的环境提供了安全网。本文的两段 Python 代码可直接运行,帮助你将抽象理论落地。掌握这些,无论面试、系统设计还是日常并发编码,你都能从容应对死锁挑战。

希望你能动手运行代码,修改资源矩阵,观察不同请求下的系统状态变化。只有亲手“制造”和“解决”一次死锁,才能真正吃透它!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐