MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

操作系统死锁的概念与成因

2024-06-046.0k 阅读

操作系统死锁的概念

在操作系统环境中,死锁是一个极为重要且复杂的问题。当系统中的多个进程(或线程)由于竞争资源或者彼此通信而形成一种永久阻塞的局面时,就发生了死锁。更具体地说,一组进程中的每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么该组进程是死锁的。

死锁一旦发生,这些进程将无法继续推进,它们所占用的资源也会被持续占用,无法释放,进而导致系统资源的浪费,甚至可能使得整个系统的性能严重下降,无法正常运行。

为了更好地理解死锁概念,我们可以通过一个日常生活中的例子来类比。假设有一条狭窄的单行道,只能允许一辆车通过。现在有两辆车 A 和 B,分别从道路的两端驶向中间。当 A 和 B 在道路中间相遇时,由于道路太窄,它们都无法继续前进。A 等待 B 后退让路,B 也等待 A 后退让路,双方都在等待对方的行动,这种情况就类似于操作系统中的死锁。

在操作系统中,死锁的进程就如同这两辆在单行道上僵持的车,它们各自占有一定的资源(如 CPU 时间、内存空间、I/O 设备等),同时又在等待获取其他进程所占用的资源,最终陷入一种相互等待且无法自行解脱的困境。

死锁的成因

资源竞争

资源竞争是导致死锁的一个主要原因。操作系统中的资源可分为两类:可剥夺资源和不可剥夺资源。可剥夺资源是指可以从拥有它的进程中强行剥夺而不会产生任何不良影响的资源,例如 CPU 时间片。而不可剥夺资源则是一旦分配给某个进程,就不能强行剥夺,只能在该进程主动释放后才能被其他进程使用,比如打印机、磁带机等设备。

当多个进程同时竞争不可剥夺资源时,如果分配策略不当,就很容易引发死锁。例如,系统中有两个进程 P1 和 P2,它们都需要使用打印机和磁带机这两种资源。假设系统先将打印机分配给 P1,磁带机分配给 P2。此时,P1 想要获取磁带机以便完成任务,而 P2 想要获取打印机继续工作。由于这两种资源都是不可剥夺的,P1 和 P2 就会相互等待对方释放自己所需的资源,从而导致死锁。

以下是一个简单的代码示例,用 Python 的多线程模拟资源竞争导致的死锁:

import threading

# 定义两把锁
lock1 = threading.Lock()
lock2 = threading.Lock()


def thread1():
    lock1.acquire()
    print("线程1获取锁1")
    lock2.acquire()
    print("线程1获取锁2")
    lock2.release()
    print("线程1释放锁2")
    lock1.release()
    print("线程1释放锁1")


def thread2():
    lock2.acquire()
    print("线程2获取锁2")
    lock1.acquire()
    print("线程2获取锁1")
    lock1.release()
    print("线程2释放锁1")
    lock2.release()
    print("线程2释放锁2")


if __name__ == "__main__":
    t1 = threading.Thread(target=thread1)
    t2 = threading.Thread(target=thread2)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

在上述代码中,thread1 先获取 lock1 再获取 lock2,而 thread2 先获取 lock2 再获取 lock1。如果两个线程同时运行,就很可能出现 thread1 获取了 lock1thread2 获取了 lock2,然后 thread1 等待 lock2thread2 等待 lock1 的死锁情况。

进程推进顺序不当

进程推进顺序不当也是造成死锁的关键因素之一。即使系统中的资源充足,如果进程以不合理的顺序请求资源,也可能引发死锁。

假设系统中有三个进程 P1、P2 和 P3,以及三种资源 R1、R2 和 R3。每个进程对资源的需求和请求顺序如下:

P1:请求 R1,请求 R2,释放 R1,释放 R2

P2:请求 R2,请求 R3,释放 R2,释放 R3

P3:请求 R3,请求 R1,释放 R3,释放 R1

如果按照以下顺序推进进程:

  1. P1 请求并获得 R1
  2. P2 请求并获得 R2
  3. P3 请求并获得 R3
  4. P1 请求 R2(由于 R2 被 P2 占用,P1 等待)
  5. P2 请求 R3(由于 R3 被 P3 占用,P2 等待)
  6. P3 请求 R1(由于 R1 被 P1 占用,P3 等待)

此时,三个进程都处于等待状态,且都在等待其他进程释放自己所需的资源,从而形成死锁。

死锁的必要条件

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

  1. 互斥条件:进程对所分配到的资源进行排他性使用,即在一段时间内某资源仅为一个进程所占有。如果此时还有其他进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。例如,打印机在某一时刻只能被一个进程使用,不能同时被多个进程共享打印。
  2. 占有并等待条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。比如一个进程已经获得了内存资源,还需要打印机资源,而打印机被其他进程占用,该进程就会在等待打印机的同时,不释放已占有的内存。
  3. 不可剥夺条件:进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。像磁带机这样的设备,一旦分配给某个进程,其他进程不能强行夺取,只能等该进程使用完毕主动释放。
  4. 循环等待条件:在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P1,P2,…,Pn} 中的 P1 正在等待一个 P2 占用的资源;P2 正在等待 P3 占用的资源,……,Pn 正在等待已被 P1 占用的资源。例如前面提到的 P1、P2、P3 进程对 R1、R2、R3 资源的请求导致的死锁情况,就形成了一个循环等待的环形链。

这四个条件是死锁发生的必要条件,只有当这四个条件同时满足时,死锁才会出现。因此,要预防或解除死锁,可以从破坏这四个条件中的一个或多个入手。

死锁的检测与处理

死锁检测算法

为了能够及时发现系统中是否存在死锁,需要使用死锁检测算法。常见的死锁检测算法有资源分配图算法和银行家算法的扩展等。

  1. 资源分配图算法:资源分配图是描述进程和资源之间关系的一种图形表示。在资源分配图中,节点分为两类:进程节点和资源节点。有向边分为两种:请求边和分配边。请求边是从进程节点指向资源节点,表示进程请求该资源;分配边是从资源节点指向进程节点,表示该资源已分配给该进程。

死锁检测的过程就是对资源分配图进行简化。如果经过一系列简化后,图中不存在任何不可简化的环,则系统没有死锁;否则,系统存在死锁,且环中的进程就是死锁进程。

具体的简化步骤如下: - 寻找一个既不阻塞又非孤立的进程节点 Pi。即 Pi 所请求的资源至少有一个是空闲的,并且 Pi 不是孤立节点(没有请求边和分配边与之相连)。 - 释放 Pi 所占有的所有资源,将与 Pi 相关的分配边全部删除,同时 Pi 变为孤立节点。 - 重复上述步骤,直到无法再找到这样的进程节点为止。

  1. 银行家算法的扩展:银行家算法原本是用于避免死锁的一种算法,但也可以通过扩展来检测死锁。在银行家算法的基础上,我们可以记录每个进程的资源请求历史。当检测死锁时,假设所有进程都突然同时请求它们所需的最大资源量,然后按照银行家算法的规则进行资源分配检查。如果发现无法满足所有进程的请求,且存在一组进程相互等待资源,那么就可以判断存在死锁。

死锁的处理策略

一旦检测到死锁,就需要采取相应的处理策略来解除死锁。常见的处理策略有以下几种:

  1. 资源剥夺法:从其他进程那里剥夺足够数量的资源给死锁进程,以解除死锁状态。例如,当检测到某个进程因等待打印机资源而陷入死锁时,可以强行剥夺其他占用打印机时间较长且优先级较低的进程对打印机的使用权,分配给死锁进程,使其能够继续运行。这种方法的关键在于如何选择剥夺资源的对象,要尽量减少对系统正常运行的影响,同时要考虑被剥夺资源进程的恢复问题。
  2. 撤销进程法:直接撤销死锁进程,释放它们所占用的全部资源。可以选择撤销优先级最低的进程,或者撤销已占用资源最多的进程等。例如,在一个多进程运行的系统中,检测到几个进程死锁,其中某个进程是后台的日志记录进程,优先级相对较低,就可以选择撤销该进程来解除死锁。但这种方法可能会导致已执行的部分工作丢失,特别是对于那些已经运行了很长时间且完成了部分重要任务的进程,撤销它们可能带来较大的损失。
  3. 进程回退法:让一个或多个死锁进程回退到足以避免死锁的某个状态,然后重新启动这些进程。这需要系统能够记录进程的运行状态和资源使用历史。例如,某个进程在执行一系列操作后进入死锁状态,通过回退到之前某个操作步骤,释放部分已占用的资源,然后重新执行,可能就可以避免死锁。这种方法相对较为复杂,需要精确地记录和控制进程的状态,同时要确保回退操作不会对系统造成其他不良影响。

死锁的预防

死锁的预防是通过破坏死锁产生的四个必要条件中的一个或多个来实现的。

破坏互斥条件

在某些情况下,可以通过资源共享的方式来破坏互斥条件。例如,对于一些原本需要独占使用的资源,可以通过虚拟化技术将其转化为可共享资源。以打印机为例,传统的打印机通常是独占设备,但通过网络打印服务器和打印队列技术,可以将打印机资源进行共享。多个进程可以将打印任务发送到打印队列,打印服务器按照一定的顺序依次处理这些任务,这样就避免了单个进程独占打印机导致的死锁问题。不过,并不是所有资源都能采用这种方式,对于一些必须保证数据一致性和完整性的资源,如数据库的排他锁,仍然需要满足互斥条件。

破坏占有并等待条件

可以采用以下两种方法来破坏占有并等待条件:

  1. 静态分配策略:进程在运行前一次性申请它所需要的全部资源,系统在资源满足其需求的情况下,才将所有资源分配给该进程,进程开始运行。在运行过程中,该进程不再请求其他资源。例如,一个大型的科学计算程序,在启动前就明确它需要的内存空间、CPU 时间片以及 I/O 设备等资源,系统在确认有足够资源后,一次性将这些资源分配给该程序,程序运行期间不会再请求新资源,从而避免了占有并等待的情况。但这种方法的缺点是资源利用率较低,因为有些资源可能在进程运行的后期才会用到,在前期就分配给它会造成资源闲置。
  2. 动态分配与释放策略:进程在请求新资源时,必须先释放它已经占有的所有资源,然后再重新申请所需的全部资源。例如,一个进程已经获得了部分内存资源,当它需要磁盘空间资源时,它必须先释放已占有的内存,然后同时申请内存和磁盘空间资源。这种方法可以提高资源利用率,但增加了进程管理的复杂性,而且在进程释放资源和重新申请资源的过程中,可能会因为资源状态的变化而导致进程无法及时获得所需资源,影响进程的执行效率。

破坏不可剥夺条件

对于一些关键资源,可以设置剥夺机制来破坏不可剥夺条件。例如,在 CPU 资源分配中,采用时间片轮转调度算法,当一个进程的时间片用完后,系统会剥夺它对 CPU 的使用权,分配给其他进程。对于其他资源,也可以根据资源的性质和系统的需求设计相应的剥夺策略。比如,当系统检测到某个进程因等待资源而陷入死锁,且该进程所占用的资源可以被剥夺时,系统可以强行剥夺这些资源分配给其他更需要的进程。但这种方法需要谨慎使用,因为对于一些资源,剥夺操作可能会导致数据不一致或其他不良后果,如正在进行写操作的文件资源,强行剥夺可能会导致文件损坏。

破坏循环等待条件

可以通过对资源进行排序,并规定进程按照一定的顺序请求资源来破坏循环等待条件。例如,将系统中的所有资源按照某种规则(如资源编号、资源类型等)进行排序。进程在请求资源时,必须按照从小到大的顺序依次请求。假设系统中有资源 R1、R2、R3,编号依次增大。一个进程如果需要使用 R2 和 R3,它必须先请求 R2,再请求 R3,不能先请求 R3 再请求 R2。这样就避免了进程之间形成循环等待的环形链,从而预防死锁的发生。这种方法实现相对简单,但可能会限制进程对资源的合理请求顺序,影响系统的性能。

综上所述,死锁是操作系统中一个复杂且重要的问题,了解死锁的概念、成因、检测与处理以及预防方法,对于操作系统的设计、优化和稳定运行具有至关重要的意义。通过合理的资源分配策略、进程调度算法以及死锁检测和处理机制,可以有效地避免死锁的发生,提高系统的资源利用率和性能。在实际应用中,需要根据不同的系统需求和场景,选择合适的死锁处理和预防方法,以确保操作系统的高效稳定运行。