logo
导航

04. 死锁

死锁

基本概念

死锁定义

死锁:多个进程因竞争资源而造成的一种互相等待的现象,若无外力作用,这些进程都将无法向前推进。

死锁特征

  1. 互斥条件:资源不能被多个进程同时使用
  2. 请求和保持条件:进程在等待其他资源时,不释放已占有的资源
  3. 不可剥夺条件:资源不能被强制剥夺
  4. 循环等待条件:存在进程等待链,形成循环

死锁示例

经典死锁场景

进程A持有资源R1,请求资源R2
进程B持有资源R2,请求资源R1

资源分配图

    R1 -----> P1
    |         |
    |         v
    |         P2
    |         |
    R2 <------|

死锁预防

破坏互斥条件

策略:使用可共享的资源,避免互斥访问。

方法

  • 使用只读资源
  • 使用虚拟化技术
  • 使用资源池

示例

// 使用共享内存而不是互斥锁
shared_memory *data = get_shared_memory();
// 多个进程可以同时读取
read_data(data);

破坏请求和保持条件

策略:进程必须一次性申请所有需要的资源。

方法

  1. 资源预分配:进程开始执行前申请所有资源
  2. 资源释放:申请新资源前释放所有已占有的资源

示例

// 一次性申请所有资源
void process() {
    acquire_all_resources();
    // 执行任务
    release_all_resources();
}

破坏不可剥夺条件

策略:允许资源被强制剥夺。

方法

  • 设置资源优先级
  • 实现资源抢占机制
  • 使用超时机制

示例

// 资源抢占
void resource_preemption() {
    if (high_priority_process_needs_resource()) {
        preempt_resource_from_low_priority_process();
        allocate_resource_to_high_priority_process();
    }
}

破坏循环等待条件

策略:对资源进行编号,进程只能按编号顺序申请资源。

方法

  • 资源有序分配
  • 资源编号策略
  • 资源层次结构

示例

// 资源编号:R1=1, R2=2, R3=3
void ordered_resource_allocation() {
    // 只能按编号顺序申请
    acquire_resource(1);  // R1
    acquire_resource(2);  // R2
    acquire_resource(3);  // R3
    // 不能先申请R3再申请R1
}

死锁避免

银行家算法

原理:在分配资源前检查是否会导致不安全状态,只有在安全状态下才分配资源。

数据结构

int available[RESOURCES];     // 可用资源
int maximum[PROCESSES][RESOURCES];    // 最大需求
int allocation[PROCESSES][RESOURCES]; // 已分配
int need[PROCESSES][RESOURCES];       // 需求

安全状态检查

bool is_safe_state() {
    int work[RESOURCES];
    bool finish[PROCESSES];

    // 初始化
    for (int i = 0; i < RESOURCES; i++)
        work[i] = available[i];
    for (int i = 0; i < PROCESSES; i++)
        finish[i] = false;

    // 查找可以完成的进程
    for (int count = 0; count < PROCESSES; count++) {
        bool found = false;
        for (int i = 0; i < PROCESSES; i++) {
            if (!finish[i] && can_allocate(i, work)) {
                // 分配资源给进程i
                for (int j = 0; j < RESOURCES; j++)
                    work[j] += allocation[i][j];
                finish[i] = true;
                found = true;
            }
        }
        if (!found) return false; // 不安全状态
    }
    return true; // 安全状态
}

资源分配算法

bool request_resources(int process_id, int request[]) {
    // 检查请求是否超过需求
    for (int i = 0; i < RESOURCES; i++) {
        if (request[i] > need[process_id][i])
            return false;
    }

    // 检查是否有足够资源
    for (int i = 0; i < RESOURCES; i++) {
        if (request[i] > available[i])
            return false;
    }

    // 尝试分配资源
    for (int i = 0; i < RESOURCES; i++) {
        available[i] -= request[i];
        allocation[process_id][i] += request[i];
        need[process_id][i] -= request[i];
    }

    // 检查是否安全
    if (is_safe_state()) {
        return true; // 分配成功
    } else {
        // 恢复原状态
        for (int i = 0; i < RESOURCES; i++) {
            available[i] += request[i];
            allocation[process_id][i] -= request[i];
            need[process_id][i] += request[i];
        }
        return false; // 分配失败
    }
}

资源分配图算法

原理:使用有向图表示资源分配情况,检测是否存在环路。

图的构成

  • 节点:进程和资源
  • 边:资源分配和请求关系

检测算法

bool detect_deadlock() {
    // 简化资源分配图
    while (存在可以简化的节点) {
        // 找到可以简化的进程节点
        for (每个进程节点P) {
            if (P的所有请求都能满足) {
                // 删除P的所有边
                remove_all_edges_from(P);
                // 标记P为可简化
                mark_as_simplified(P);
            }
        }
    }

    // 检查是否还有边
    if (图中还有边)
        return true; // 存在死锁
    else
        return false; // 不存在死锁
}

死锁检测

资源分配图

图的表示

  • 圆形节点:进程
  • 方形节点:资源
  • 实线箭头:资源分配(资源 → 进程)
  • 虚线箭头:资源请求(进程 → 资源)

示例

    R1(1) -----> P1
    |             |
    |             v
    |             P2
    |             |
    R2(1) <------|

检测算法

简化算法

  1. 找到没有请求边的进程节点
  2. 删除该节点的所有分配边
  3. 重复步骤 1-2,直到无法简化
  4. 如果图中还有边,则存在死锁

实现

bool detect_deadlock_using_graph() {
    // 构建资源分配图
    build_resource_allocation_graph();

    // 简化图
    while (true) {
        bool simplified = false;

        // 查找可以简化的进程
        for (每个进程P) {
            if (P没有请求边) {
                // 删除P的所有分配边
                remove_allocation_edges(P);
                simplified = true;
            }
        }

        if (!simplified) break;
    }

    // 检查是否还有边
    return has_remaining_edges();
}

检测时机

检测策略

  1. 定期检测:每隔一定时间检测一次
  2. 按需检测:当进程请求资源时检测
  3. 资源耗尽检测:当某种资源耗尽时检测

检测开销

  • 时间复杂度:O(n²),n 为进程数
  • 空间复杂度:O(n×m),m 为资源类型数

死锁恢复

进程终止

策略:终止部分或全部死锁进程。

方法

  1. 终止所有死锁进程:简单但损失大
  2. 逐个终止死锁进程:选择代价最小的进程终止

选择标准

  • 进程优先级
  • 进程已运行时间
  • 进程剩余时间
  • 进程使用的资源数量

示例

void terminate_deadlocked_processes() {
    // 计算每个进程的终止代价
    for (每个死锁进程P) {
        cost[P] = calculate_termination_cost(P);
    }

    // 选择代价最小的进程终止
    while (存在死锁) {
        process = find_minimum_cost_process();
        terminate_process(process);
        release_resources(process);
    }
}

资源抢占

策略:从某些进程抢占资源,分配给其他进程。

方法

  1. 选择牺牲进程:选择代价最小的进程
  2. 回滚到安全状态:将进程回滚到某个检查点
  3. 重新分配资源:将抢占的资源分配给其他进程

回滚机制

void resource_preemption() {
    // 选择牺牲进程
    victim = select_victim_process();

    // 回滚到检查点
    rollback_to_checkpoint(victim);

    // 释放资源
    release_resources(victim);

    // 重新分配资源
    redistribute_resources();
}

检查点机制

原理:定期保存进程状态,发生死锁时可以回滚。

实现

// 保存检查点
void save_checkpoint(int process_id) {
    checkpoint[process_id] = {
        .registers = get_process_registers(process_id),
        .memory = get_process_memory(process_id),
        .resources = get_process_resources(process_id)
    };
}

// 回滚到检查点
void rollback_to_checkpoint(int process_id) {
    restore_process_state(process_id, checkpoint[process_id]);
}

死锁处理策略比较

策略选择

策略优点缺点适用场景
预防完全避免死锁资源利用率低实时系统
避免资源利用率较高需要预知资源需求批处理系统
检测资源利用率最高检测和恢复开销大通用系统
忽略实现简单可能导致系统挂起开发环境

实际应用

操作系统选择

  • Windows:主要使用死锁检测和恢复
  • Linux:主要使用死锁预防和避免
  • 实时系统:主要使用死锁预防

选择考虑因素

  1. 系统类型:实时系统、分时系统、批处理系统
  2. 资源特性:资源数量、资源类型、资源分配模式
  3. 性能要求:响应时间、吞吐量、资源利用率
  4. 可靠性要求:系统稳定性、故障恢复能力

死锁案例分析

案例 1:哲学家进餐问题

问题描述

  • 5 个哲学家围坐在圆桌旁
  • 每个哲学家需要两根筷子才能进餐
  • 筷子放在哲学家之间

死锁情况

所有哲学家同时拿起左边的筷子
然后等待右边的筷子
形成循环等待

解决方案

// 使用资源有序分配
void philosopher(int i) {
    int left = i;
    int right = (i + 1) % 5;

    // 总是先申请编号小的筷子
    if (left < right) {
        acquire_chopstick(left);
        acquire_chopstick(right);
    } else {
        acquire_chopstick(right);
        acquire_chopstick(left);
    }

    eat();

    release_chopstick(left);
    release_chopstick(right);
}

案例 2:数据库死锁

问题描述

  • 事务 A 持有锁 L1,请求锁 L2
  • 事务 B 持有锁 L2,请求锁 L1

解决方案

-- 使用超时机制
SET LOCK_TIMEOUT 5000; -- 5秒超时

-- 使用死锁检测
SET DEADLOCK_PRIORITY LOW; -- 设置死锁优先级

-- 使用资源有序分配
-- 总是按固定顺序申请锁
BEGIN TRANSACTION;
    UPDATE table1 SET column1 = value1 WHERE id = 1;
    UPDATE table2 SET column2 = value2 WHERE id = 1;
COMMIT;