深入解析死锁

一、死锁的基本概念

死锁:是指在多道程序系统中,一组进程中的每个进程都无限期地等待被该组进程中另一个进程所占有的资源,因而永远无法得到这些资源,导致所有进程都无法继续推进的一种僵持状态。例如,有进程 A 和进程 B,进程 A 占有资源 R1 且等待被进程 B 占有的资源 R2,而进程 B 占有资源 R2 且等待被进程 A 占有的资源 R1,这样它们就陷入了互相等待的死锁局面,谁都无法继续执行下去。

死锁产生必须同时满足以下四个必要条件:

  • 互斥条件:资源在某一时刻只能被一个进程所占有。比如打印机这类资源,同一时间只能有一个进程使用它来打印文档,不能多个进程同时使用。
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,且该请求的资源被其他进程占有,此时请求进程被阻塞,但对已获得的资源又不释放。例如,进程 P 已经获得了磁盘资源在进行读写操作,同时它又请求内存中的一块特定区域,但这块区域被另一个进程占用了,P 不释放磁盘资源,而是等待内存区域,就满足了这个条件。
  • 不可剥夺条件:进程所获得的资源在未使用完之前,不能被其他进程强行剥夺,只能由占有资源的进程自己主动释放。比如进程获得了一个文件的独占访问权限,在它没有完成文件操作前,其他进程不能强行获取该文件的访问权限。
  • 循环等待条件:存在一组进程,它们之间形成了一种头尾相接的循环等待资源关系。就像前面提到的进程 A 和进程 B 互相等待对方资源的情况,若有更多进程参与形成类似的循环等待链,也满足此条件。

二、死锁预防

死锁预防是通过破坏死锁产生的四个必要条件中的一个或多个,来确保系统不会进入死锁状态。

(一)破坏互斥条件

  • 思路:使资源可以同时被多个进程共享使用,但这对于很多物理设备资源(如打印机、磁带机等)来说很难实现,因为它们本身的特性决定了同一时间只能由一个进程使用。不过对于一些可重入的代码、数据资源等,可以通过设置合理的访问机制来实现共享,避免互斥限制。例如,对于一些只读的数据文件,多个进程可以同时以读的方式访问,不存在互斥问题。
  • 考研知识点:在考试中可能会考查哪些资源适合破坏互斥条件来预防死锁,以及相应的共享实现方式的原理和优缺点等。

(二)破坏请求和保持条件

  • 思路:采用静态分配资源的策略,即进程在运行前一次性申请它所需要的所有资源,只有当所有资源都申请成功后,进程才开始运行;或者要求进程在占有资源期间不能再申请新的资源,必须先释放已有的资源,然后再一次性申请新的资源需求。例如,一个数据库查询进程,在启动前就把它需要的内存空间、数据库连接等所有资源都申请好,运行过程中不再申请新资源,避免出现一边占着部分资源一边等待其他资源的情况。
  • 伪代码示例(采用静态分配资源策略):
// 假设资源有 R1、R2、R3,进程需要这三种资源
void process() {
    // 申请所有需要的资源
    if (request_all_resources(R1, R2, R3)) {  
        // 资源申请成功,开始执行进程任务
        execute_task();  
        // 任务执行完毕,释放所有资源
        release_all_resources(R1, R2, R3);  
    } else {
        // 资源申请失败,进入等待或报错处理
        wait_or_error_handling();  
    }
}
  • 考研知识点:考查对这种资源分配策略的理解,比如其适用场景、可能带来的资源利用率问题(因为一次性申请可能导致资源闲置等情况),以及与其他预防策略对比等方面。

(三)破坏不可剥夺条件

  • 思路:当一个进程请求的资源不能立即得到满足时,它目前占有的其他资源可以被系统强行剥夺,分配给其他需要的进程,待该进程后续需要这些资源时再重新分配给它。例如,在分时操作系统中,对于内存资源,如果一个进程长时间占有部分内存但又申请更多内存而无法满足时,系统可以把它已占有的部分内存回收,分配给其他更急需内存的进程,等它有足够资源可用时再分配回来。
  • 考研知识点:涉及到这种策略下的系统实现复杂度(因为要涉及资源的剥夺和重新分配管理等)、对进程执行的影响(可能导致进程执行中断等情况)等考点。

(四)破坏循环等待条件

  • 思路:对资源进行编号,规定进程必须按照资源编号递增的顺序申请资源,这样就不会出现循环等待的情况。例如,资源编号为 R1、R2、R3,进程如果先申请了 R1,之后若还需要申请其他资源,只能申请编号大于 R1 的,比如 R2 或 R3,不会出现申请 R1 等待 R3,而另一个进程申请 R3 等待 R1 的循环情况。
  • 伪代码示例(按资源编号顺序申请资源):
// 资源编号从 1 到 n,假设进程需要申请多个资源
void process() {
    int resource_needed[] = {2, 4, 5};  // 示例需要的资源编号
    sort(resource_needed);  // 对需要的资源编号排序,保证按顺序申请
    for (int i = 0; i < sizeof(resource_needed) / sizeof(resource_needed[0]); i++) {
        if (request_resource(resource_needed[i])) {
            continue;
        } else {
            // 资源申请失败,释放已申请的资源并等待或处理
            release_previously_requested_resources(resource_needed, i);
            wait_or_error_handling();
            break;
        }
    }
    // 所有资源申请成功,执行任务
    execute_task();
    // 任务执行完毕,释放所有资源
    release_all_resources(resource_needed);
}
  • 考研知识点:考查资源编号规则的制定合理性、对不同资源需求场景的适应性,以及这种策略在实际系统中如何有效实施等内容。

以下是死锁预防几种方法的对比图表:

对比项目破坏互斥条件破坏请求和保持条件破坏不可剥夺条件破坏循环等待条件
实现方式使资源可共享或改变资源特性静态分配或限制资源申请时机可强行剥夺进程已有资源按资源编号顺序申请资源
适用资源类型部分可共享的数据、代码等资源各类资源,但对资源使用模式有限制可灵活回收分配的资源各种有序编号的资源集合
系统复杂度较低(对可共享资源),高(改变资源特性)较高,涉及资源整体分配管理高,要处理资源剥夺和恢复等适中,需资源编号和申请顺序控制
资源利用率可能提高(共享资源),或受影响(改变资源)可能因闲置降低(静态分配)可动态调整,但有分配开销取决于编号规则和实际使用情况

三、死锁避免

死锁避免是在系统运行过程中,通过动态地检测资源分配状态,确保系统始终处于安全状态,避免进入死锁状态,而不是像死锁预防那样去破坏死锁产生的必要条件。

(一)安全状态与不安全状态

  • 安全状态:是指系统能按某种顺序(如〈P1, P2, …, Pn〉顺序)为每个进程分配资源,并且每个进程都能顺利完成,然后释放其占有的所有资源,使得后续进程也能继续获得资源完成任务,最终所有进程都能完成的状态。例如,有三个进程 P1、P2、P3,资源有 R1、R2、R3,系统现有资源数量和进程需求情况使得按照某种分配顺序(比如先满足 P1 部分需求让其能完成,P1 完成后释放资源再满足 P2 等),可以确保三个进程最终都能完成任务,此时系统就是安全状态。
  • 不安全状态:如果系统处于一种无论怎样分配资源,都无法保证所有进程最终都能顺利完成的状态,就是不安全状态,不过处于不安全状态并不意味着一定会发生死锁,但它有可能发展成死锁状态,需要避免进入这种状态。

(二)银行家算法(经典死锁避免算法)

  • 思路:银行家算法将操作系统看作银行家,把资源看作资金,进程看作客户。银行家拥有一定数量的资金(系统资源),客户(进程)会提出资金(资源)需求,银行家需要根据现有资金情况和客户的需求情况,动态地决定是否批准客户的贷款(资源分配)请求,以保证银行始终处于安全状态(系统不会进入死锁)。
  • 数据结构与初始化:
    • 可用资源向量 Available:表示系统中各类可用资源的数量,例如Available[m],其中m是资源种类数,初始化为系统初始时各类资源的总量。
    • 最大需求矩阵 Max:记录每个进程对各类资源的最大需求数量,例如Max[n][m],其中n是进程数量,m是资源种类数。
    • 分配矩阵 Allocation:记录当前每个进程已经分配到的各类资源数量,例如Allocation[n][m]。
    • 需求矩阵 Need:通过Need[n][m] = Max[n][m] - Allocation[n][m]计算得出,表示每个进程还需要的各类资源数量。
  • 资源请求处理(伪代码示例):
// 假设进程 Pi 发出资源请求向量 Request[i]
bool request_resources(int i, int Request[]) {
    // 检查请求是否超过了进程的需求
    for (int j = 0; j < m; j++) {
        if (Request[j] > Need[i][j]) {
            return false;  // 请求超需求,不合理,拒绝请求
        }
    }
    // 检查请求是否超过了系统可用资源
    for (int j = 0; j < m; j++) {
        if (Request[j] > Available[j]) {
            return false;  // 请求超可用资源,需等待,拒绝请求
        }
    }
    // 模拟分配资源,更新相关数据结构
    for (int j = 0; j < m; j++) {
        Available[j] -= Request[j];
        Allocation[i][j] += Request[j];
        Need[i][j] -= Request[j];
    }
    // 检查系统是否仍处于安全状态
    if (is_safe_state()) {
        return true;  // 系统安全,批准请求,实际分配资源
    } else {
        // 若不安全,回滚模拟分配的资源变更
        for (int j = 0; j < m; j++) {
            Available[j] += Request[j];
            Allocation[i][j] -= Request[j];
            Need[i][j] += Request[j];
        }
        return false;  // 拒绝请求,保持原状态
    }
}
  • 考研知识点:银行家算法是考研中的重点考查内容,包括其各个数据结构的含义、初始化方式、资源请求处理的详细流程及逻辑、如何判断安全状态(如通过安全序列查找等方法),以及算法的优缺点、适用场景等方面。

以下是死锁预防和死锁避免的对比图表:

对比项目死锁预防死锁避免
策略思路破坏死锁产生的必要条件动态检测确保系统处于安全状态
资源分配灵活性相对受限,按特定规则改变资源使用方式较灵活,根据系统实时状态分配资源
系统复杂度因方法不同有差异,部分方法复杂度较高较高,如银行家算法需复杂的状态检测和计算
资源利用率可能因限制条件而受影响理论上能更好利用资源,避免过度限制
对死锁的确定性尽力预防,确定性破坏条件避免死锁基于状态判断尽量避免进入死锁,非绝对阻止

四、死锁检测和解除

(一)死锁检测

  • 思路:系统运行过程中,定期或在特定时机(如资源分配出现频繁阻塞等情况时)去检查系统中是否存在死锁情况,通过分析资源分配图、进程等待关系等方式来判断。例如,构建一个资源分配图,节点表示进程和资源,边表示资源的分配和请求关系,如果图中出现了环且环中的资源都只有一个实例(不可共享),那么就存在死锁情况;如果资源有多个实例,则需要更复杂的算法来判断是否死锁,如通过化简资源分配图等方法,将没有阻塞的进程及其占有的资源逐步去除,看最终能否将图化简为空,如果不能则存在死锁。
  • 考研知识点:考查资源分配图的构建与分析方法、不同资源实例情况下死锁判断的具体算法流程,以及检测时机的选择依据等内容。

(二)死锁解除

一旦检测到死锁存在,就需要采取相应措施来解除死锁,使系统恢复正常运行。常见的解除方法有:

  • 资源剥夺法:从涉及死锁的进程中强行剥夺部分资源,分配给其他处于阻塞状态的进程,打破死锁的僵局。例如,在一个多进程并发访问数据库的系统中,若检测到死锁,可强行剥夺某个死锁进程占有的数据库连接资源,分配给其他等待该资源的进程,使其能继续运行,打破死锁。
  • 撤销进程法:直接撤销部分涉及死锁的进程,释放它们占有的资源,让其他进程能够获取资源继续运行。可以按照一定的策略来选择撤销哪些进程,比如撤销优先级最低的进程、撤销占用资源最少的进程或者撤销运行时间最短的进程等。例如,若系统中有多个死锁的打印任务进程,根据进程优先级,撤销优先级最低的那个打印进程,释放其占有的打印机资源,使其他打印进程可以继续打印。
  • 进程回退法:让参与死锁的进程回退到之前的某个安全状态(比如回退到上一次资源分配前的状态),重新进行资源分配,避免死锁情况出现。不过这种方法实现较为复杂,需要系统记录进程的详细执行历史和资源分配变化情况等信息。例如,在一个复杂的科学计算任务的多进程系统中,若发生死锁,通过记录的进程执行步骤信息,让相关进程回退到之前合理的资源分配状态,再重新尝试资源分配和任务执行。

以下是死锁检测和死锁解除的对比图表:

对比项目死锁检测死锁解除
目的发现系统中是否存在死锁情况在检测到死锁后,使系统摆脱死锁恢复运行
操作时机定期或按需检测,在系统运行中当检测到死锁后才执行相关操作
实现复杂度根据检测方法不同有差异,资源分配图分析等有一定复杂度较高,涉及资源剥夺、进程撤销或回退等复杂操作
对系统影响只是检测,一般不直接影响系统运行(除非检测操作频繁影响性能)可能影响部分进程执行,如进程撤销导致任务丢失等

通过对死锁相关内容从基本概念到预防、避免、检测及解除各方面的详细解析,我们能更好地理解和应对操作系统中这一重要的并发问题,在实际系统设计以及考研等相关学习考查中都有着重要意义。

更多推荐