操作系统-进程管理
操作系统-进程管理
xiaoyan操作系统基础
操作系统概述
操作系统定义与功能
操作系统(Operating System, OS)是计算机系统中至关重要的软件层,其主要职责是管理计算机硬件与软件资源,为应用程序提供统一的接口和服务。操作系统作为计算机的基石,确保了计算机系统的有效运行和资源的高效利用。
操作系统主要功能
- 硬件管理:操作系统负责管理计算机的硬件资源,包括但不限于中央处理器(CPU)、内存、输入输出设备等。通过硬件管理,操作系统能够合理分配和调度这些资源,以满足不同应用程序的需求。
- 软件资源管理:操作系统不仅管理硬件资源,还负责管理软件资源,如文件系统、进程和线程等。通过有效的资源管理,操作系统确保了多个应用程序能够同时运行,且互不干扰。
- 抽象与屏蔽复杂性:操作系统通过提供抽象层,屏蔽了底层硬件的复杂性,使得应用程序开发者无需深入了解硬件细节即可进行开发。这种抽象层包括文件系统、设备驱动程序等。
- 内核(Kernel):内核是操作系统的核心部分,负责执行最基本的任务,如内存管理、进程调度、文件系统管理以及硬件设备的管理。内核是操作系统与硬件之间的桥梁,确保了硬件资源的高效利用和应用程序的顺利执行。
内核与CPU的区别
- 内核:内核是操作系统的一部分,属于软件层面。它主要负责管理计算机硬件与软件资源,提供系统调用接口,使得应用程序能够访问硬件资源。
- CPU:中央处理器(CPU)是计算机的硬件核心,负责执行指令和进行算术逻辑运算。CPU是硬件资源的一部分,其功能由操作系统通过内核进行管理和调度。
用户态和内核态
用户态与内核态的概念
在操作系统中,进程的运行环境可以根据其访问系统资源的权限分为两种级别:用户态(User Mode)和内核态(Kernel Mode)。
- 用户态:在用户态下运行的进程仅能访问其自身的用户空间数据,权限较低。当应用程序需要执行某些需要更高权限的操作,如文件读写、网络通信、进程管理等,必须通过系统调用(System Call)向操作系统发起请求,从而进入内核态。
- 内核态:内核态赋予进程几乎完全的系统资源访问权限,允许其执行底层操作,如直接访问硬件、管理内存、调度进程等。当操作系统接收到来自用户态进程的系统调用请求时,会将该进程切换到内核态,执行相应的内核代码,完成后再次切换回用户态。
内核态的权限级别远高于用户态,因此能够执行更为底层的系统操作。然而,进入内核态涉及复杂的上下文切换和权限检查,开销较大。因此,应尽量减少内核态的切换次数,以提升系统性能和稳定性。
用户态与内核态的必要性
用户态与内核态的划分是操作系统安全性和稳定性的基石。其核心原因在于CPU指令集中的特权指令(Privileged Instructions)。
- 特权指令:某些CPU指令具有潜在的危险性,如直接操作硬件、修改内存映射、中断处理等。这些指令若被普通用户程序随意执行,可能导致系统崩溃或数据损坏。因此,操作系统将这些指令的执行权限限制在内核态,确保只有内核代码能够执行这些特权指令。
- 资源隔离与保护:如果所有进程都运行在内核态,那么每个进程都将拥有对系统资源的完全访问权限,这将导致资源竞争和潜在的冲突,严重影响系统的性能和效率。通过将进程分为用户态和内核态,操作系统能够有效隔离不同进程的资源访问,防止恶意或意外的操作对系统造成损害。
系统调用
什么是系统调用
在现代操作系统中,用户程序通常运行在用户态(User Mode),而操作系统内核运行在内核态(Kernel Mode)。当用户程序需要执行某些特权操作(如访问硬件资源、管理进程、操作文件系统等)时,由于用户态的权限限制,无法直接执行这些操作。此时,程序需要通过系统调用(System Call)向操作系统内核发起请求,由内核代为执行这些特权操作。
系统调用是用户程序与操作系统内核之间的接口,它提供了一种机制,使得用户程序能够安全、受控地访问内核提供的各种服务。根据其功能,系统调用大致可以分为以下几类:
- 文件管理:包括文件的读取、写入、创建、删除、重命名、权限管理等操作。
- 设备管理:涉及对硬件设备的请求、释放、配置等操作,如I/O设备的读写、设备的初始化等。
- 进程管理:包括进程的创建、终止、调度、同步、通信等操作,如fork()、exec()、wait()、signal()等。
- 内存管理:涉及内存的分配、回收、映射、保护等操作,如malloc()、free()、mmap()等。
系统调用与普通函数库调用(Library Call)在形式上相似,但本质上存在显著差异。普通函数库调用是在用户态下执行的,调用的是预先编译好的本地函数,而系统调用则是通过特定的指令(如x86架构中的int 0x80
或syscall
指令)触发,将控制权转移到内核态,由内核执行相应的操作。
系统调用过程
系统调用的执行过程涉及用户态与内核态之间的切换,具体步骤如下:
- 用户态发起系统调用:当用户程序需要执行特权操作时,它会通过特定的指令(如
syscall
)发起系统调用。此时,程序会传递系统调用号(System Call Number)以及必要的参数,这些参数通常通过寄存器传递。 - 中断(Trap):系统调用指令触发一个中断(Trap),导致CPU从用户态切换到内核态。中断发生后,当前用户程序的执行被挂起,CPU的控制权被转移到操作系统内核。
- 内核态处理系统调用:内核接收到系统调用请求后,根据系统调用号查找相应的内核函数(通常位于系统调用表中),并执行该函数。内核函数负责处理用户程序请求的操作,如访问文件、管理设备、调度进程等。
- 返回用户态:内核完成系统调用后,会通过特定的指令(如
iret
或sysret
)将控制权返回给用户程序。此时,CPU从内核态切换回用户态,用户程序继续执行,并从系统调用返回点恢复执行。 - 结果返回:系统调用的结果(如文件读取的数据、进程创建的状态等)通常通过寄存器或内存返回给用户程序。用户程序根据返回值判断系统调用是否成功,并进行相应的处理。
系统调用的过程涉及用户态与内核态之间的多次切换,这种切换带来了一定的开销,但同时也确保了操作系统的安全性和稳定性。通过系统调用,用户程序能够在受控的环境下访问内核资源,避免了直接操作硬件可能带来的风险。
进程与线程
什么是进程和线程
- 进程:进程是计算机程序正在运行的一个实例,是操作系统进行资源分配和调度的基本单位。例如,打开并运行的微信应用程序就是一个进程。每个进程都有独立的内存空间、文件描述符、网络连接等资源。
- 线程:线程是进程内的一个执行单元,也被称为轻量级进程。一个进程可以包含多个线程,这些线程共享进程的资源(如内存空间、文件句柄、网络连接等),但每个线程有自己的独立栈和寄存器状态。例如,在运行的微信应用程序中,可能有一个线程专门负责推送用户发出的信息,另一个线程负责接收和处理用户输入。
进程和线程的区别
- 资源分配:进程是操作系统中最小的资源分配单位,每个进程拥有独立的内存空间和系统资源。而线程是进程内的最小执行单位,共享同一进程的资源,如内存中的堆和元数据空间,但每个线程有自己的私有栈和寄存器状态。
- 切换开销:由于线程更加轻量,线程之间的上下文切换开销较小且较快。相比之下,进程之间的上下文切换开销较大,因为需要保存和恢复更多的状态信息,如页表、文件描述符等。
- 独立性:进程之间是相互独立的,一个进程的崩溃通常不会影响其他进程。而线程则不一定,因为同一进程下的线程共享资源,一个线程的错误可能会影响其他线程,甚至导致整个进程崩溃。
为什么有进程了还需要线程
- 切换开销:进程切换的开销较大,而线程切换的开销较小。通过使用线程,可以在同一进程内并发执行多个任务,减少上下文切换的开销。
- 并发执行:多线程可以并发执行任务,增加CPU的利用率。例如,一个进程可以同时处理用户输入、网络通信和后台任务,而无需等待某个任务完成后再处理下一个任务。
- 资源共享:同一进程下的线程可以共享进程的资源,如内存空间、文件句柄等。这使得线程间的通信更加高效,无需进行系统调用,减少了开销。
- 响应性:通过使用多线程,应用程序可以更好地响应用户操作。例如,一个线程可以处理用户界面更新,另一个线程可以处理后台计算任务,从而提高用户体验。
为什么要有多线程
多线程技术在现代计算机系统中扮演着至关重要的角色,其主要优势体现在以下几个方面:
提升系统整体使用效率
- 轻量级和低开销:线程是进程内的执行单元,相比于进程,线程更加轻量化,创建和销毁的开销较小。线程之间的上下文切换开销也远小于进程间的切换,这使得系统能够更高效地管理和调度多个任务。
- 资源共享:同一进程下的多个线程共享进程的资源,如内存空间、文件句柄、网络连接等。这种资源共享机制减少了资源复制的开销,提高了资源利用率。
- 并发处理:在单核CPU时代,多线程可以通过时间片轮转(Time-Slicing)机制实现并发执行,避免因单一线程阻塞而导致整个进程停滞。例如,当一个线程进行I/O操作时,CPU可以切换到其他线程继续执行,从而提高CPU的利用率。
- 多核利用:在多核CPU时代,多线程能够充分利用多核处理器的并行计算能力。通过将任务分配到不同的线程,系统可以在多个核心上并行执行任务,显著提升整体性能。
应对互联网发展带来的高并发
- 高并发处理:随着互联网的快速发展,现代应用系统需要处理大量的并发请求。例如,在电商平台的“双11”购物节期间,系统需要应对数百万级别的并发访问。多线程并发编程能够显著提高系统的并发处理能力,确保在高负载情况下系统仍能稳定运行。
- 响应性和用户体验:多线程技术使得应用程序能够更好地响应用户操作。例如,一个线程可以处理用户界面更新,另一个线程可以处理后台计算任务,从而提高用户体验。
- 分布式系统:在分布式系统中,多线程技术可以用于实现负载均衡和任务分发。通过将任务分配到不同的线程或进程,系统可以更高效地利用分布式资源,提高整体系统的吞吐量和可靠性。
线程间同步方式
线程同步是指在多线程环境中,确保多个线程能够有序地访问共享资源,避免出现资源冲突和竞态条件。线程同步机制是多线程编程中的关键技术,常见的同步方式包括互斥锁、读写锁、信号量、屏障和事件等。
1.互斥锁(Mutex)
互斥锁(Mutual Exclusion Lock)是最基本的同步机制之一,用于确保在同一时间段内只有一个线程能够访问共享资源。当一个线程获取到互斥锁后,其他试图获取该锁的线程将被阻塞,直到锁被释放。
在Java中,可以使用synchronized
关键字或Lock
接口来实现互斥锁。适用于需要独占访问共享资源的场景,如临界区保护。
2.读写锁(ReadWrite Lock)
读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读写锁适用于读操作远多于写操作的场景,可以提高并发性能。
在Java中,可以使用ReentrantReadWriteLock
类来实现读写锁。适用于读多写少的场景,如缓存系统、配置管理等。
3.信号量(Semaphore)
信号量是一种计数器,用于控制同时访问共享资源的线程数量。信号量可以允许多个线程同时访问共享资源,但需要限制最大并发数。
在Java中,可以使用Semaphore
类来实现信号量。适用于需要限制并发访问数量的场景,如连接池管理、资源池管理等。
4.屏障(Barrier)
屏障是一种同步原语,用于确保一组线程在某个点上同步。当一个线程到达屏障点时,它会被阻塞,直到所有线程都到达屏障点,然后所有线程继续执行。
在Java中,可以使用CyclicBarrier
类来实现屏障。适用于需要多个线程协同工作的场景,如并行计算、数据分片处理等。
5.事件(Event)
事件是一种通知机制,用于在多线程之间传递信号,实现线程间的同步。事件可以用于通知一个或多个线程某个条件已经满足,从而触发相应的操作。
在Java中,可以使用java.util.concurrent.CountDownLatch
或java.util.concurrent.Phaser
等类来实现事件通知机制。适用于需要线程间协作和通知的场景,如任务调度、事件驱动编程等。
PCB(Process Control Block)
PCB是什么
PCB(Process Control Block)即进程控制块,是操作系统中用于管理和跟踪进程的核心数据结构。每个进程在操作系统中都有一个唯一的PCB,用于存储与该进程相关的所有信息。PCB是操作系统进行进程调度和管理的基础,它记录了进程的当前状态、资源需求、调度信息等关键数据。
当操作系统创建一个新的进程时,会为该进程分配一个唯一的进程ID(PID),并为该进程创建一个PCB。随着进程的执行,PCB中的内容会不断更新,操作系统根据这些信息来管理和调度进程。
PCB包含的信息
PCB通常包含以下几类信息:
- 进程描述信息
- 进程ID(PID):唯一标识进程的整数。
- 进程名称:通常是可执行文件的名称。
- 用户ID(UID):标识进程所属的用户。
- 组ID(GID):标识进程所属的用户组。
- 进程调度信息
- 进程状态:标识进程当前的状态,如运行态(Running)、就绪态(Ready)、阻塞态(Blocked)等。
- 优先级:进程的调度优先级,用于决定进程在调度队列中的位置。
- 阻塞原因:如果进程处于阻塞状态,记录阻塞的原因,如等待I/O操作完成、等待信号量等。
- 调度队列指针:指向进程所在调度队列的指针,用于进程调度。
- 资源需求状态
- 内存需求:进程所需的内存空间大小及分配情况。
- CPU时间:进程已经使用的CPU时间以及剩余的CPU时间片。
- 设备资源:进程所需的设备资源,如文件描述符、网络连接等。
- 状态机的处理状态
- 寄存器状态:进程在切换时需要保存的寄存器值,如通用寄存器、浮点寄存器等。
- 程序计数器(PC):记录进程当前执行的指令地址。
- 程序状态字(PSW):记录进程的当前状态信息,如条件码、中断允许位等。
- 用户栈指针:指向进程的用户栈顶地址。
- 内核栈指针:指向进程的内核栈顶地址。
- 其他信息
- 信号处理信息:记录进程对信号的处理方式,如忽略、捕获、默认处理等。
- 文件描述符表:记录进程打开的文件描述符及其相关信息。
- 共享内存段:记录进程与其他进程共享的内存段信息。
- 线程信息:如果操作系统支持多线程,PCB中可能还包括线程的相关信息。
PCB是操作系统中用于管理和调度进程的关键数据结构,它包含了进程的所有重要信息,如进程描述信息、调度信息、资源需求状态、状态机的处理状态等。操作系统通过PCB来跟踪和管理进程的生命周期,确保进程能够正确地执行和调度。PCB的设计和实现直接影响操作系统的性能和稳定性,是操作系统内核的重要组成部分。
进程的状态
进程状态是指进程在其生命周期中所处的不同阶段。与线程状态类似,进程状态可以分为以下五种主要状态:
1. 创建状态(New):当一个进程被创建时,它首先进入创建状态。在这个阶段,操作系统为进程分配必要的资源,如内存空间、文件描述符等,并初始化进程控制块(PCB)。此时,进程尚未准备好执行,仍处于初始化阶段。
2. 就绪状态(Ready):当进程完成初始化并准备好执行时,它进入就绪状态。在就绪状态下,进程已经获得了除CPU之外的所有必要资源,等待操作系统调度器分配CPU时间片。一旦调度器选择该进程执行,它将进入运行状态。
3. 运行状态(Running):当进程被调度器选中并分配到CPU时间片时,它进入运行状态。在运行状态下,进程正在执行其任务,CPU正在处理该进程的指令。如果进程的时间片用完或需要等待某些资源(如I/O操作),它将退出运行状态,进入其他状态。
4. 阻塞状态(Blocked):当进程需要等待某些事件(如I/O操作完成、信号量释放等)时,它进入阻塞状态。在阻塞状态下,进程暂时停止执行,直到等待的事件发生。一旦事件发生,进程将重新进入就绪状态,等待再次被调度执行。
5. 结束状态(Terminated):当进程完成其任务或因某些原因(如异常、用户终止)需要退出时,它进入结束状态。在结束状态下,进程正在从系统中消失,操作系统会回收分配给该进程的资源,并销毁其PCB。进程的结束状态可以是正常退出(如任务完成)或异常退出(如崩溃、被终止)。
状态转换
进程状态之间的转换通常由操作系统内核管理和控制,具体转换如下:
- 创建状态 -> 就绪状态:进程初始化完成后,进入就绪状态,等待调度。
- 就绪状态 -> 运行状态:调度器选择就绪状态的进程执行,进程进入运行状态。
- 运行状态 -> 就绪状态:当进程的时间片用完或被抢占时,进程回到就绪状态。
- 运行状态 -> 阻塞状态:当进程需要等待某些事件时,进入阻塞状态。
- 阻塞状态 -> 就绪状态:等待的事件发生后,进程回到就绪状态。
- 运行状态 -> 结束状态:进程完成任务或被终止,进入结束状态。
进程通信(Inter-Process Communication, IPC)是指在操作系统中,不同进程之间进行数据交换和信息传递的机制。进程通信是多进程编程中的关键技术,常见的通信方式包括管道、有名管道、信号、消息队列、共享内存、信号量和套接字等。
进程通信方式
1. 管道(Pipe)/匿名管道
管道是一种半双工的通信方式,用于具有亲属关系的进程(如父子进程或兄弟进程)之间的通信。管道数据存放在内存中,遵循先进先出(FIFO)原则。
- 特点:半双工通信,数据单向流动;只能在具有亲属关系的进程间使用。
- 应用场景:适用于简单的父子进程间通信。
2. 有名管道(Named Pipe)
有名管道解决了匿名管道只能在具有亲属关系的进程间通信的限制,允许任意两个进程进行通信。有名管道通常存放在文件系统中,具有唯一的名称。
- 特点:全双工通信,数据双向流动;可以在任意两个进程间使用。
- 应用场景:适用于需要跨进程通信的场景。
3. 信号(Signal)
信号是一种异步通信机制,用于通知进程某个事件已经发生。信号通常用于处理异常情况或通知进程某些状态变化。
- 特点:异步通信,信号处理复杂;信号传递的信息量有限。
- 应用场景:适用于进程间的事件通知和异常处理。
4. 消息队列(Message Queue)
消息队列是一种消息的链表,存放在操作系统的内核中。消息队列具有特定的消息格式,并以特定的消息队列标识符标识。消息队列遵循先进先出原则,但与管道不同的是,消息队列可以传递结构化的消息。
- 特点:支持结构化消息传递;消息队列存放在内核中,生命周期较长。
- 应用场景:适用于需要传递复杂消息的进程间通信。
5. 共享内存(Shared Memory)
共享内存允许多个进程访问同一块内存区域,进程可以直接读写共享内存中的数据,从而实现高效的数据交换。共享内存需要引入锁机制来避免数据竞争。
- 特点:高效的数据交换;需要锁机制来避免数据竞争。
- 应用场景:适用于需要频繁交换大量数据的进程间通信。
6. 信号量(Semaphore)
信号量是一种计数器,用于控制多个进程对共享资源的访问。信号量通常用于实现进程间的同步,确保在同一时刻只有一个或多个进程访问共享资源。
- 特点:用于进程同步和资源控制;控制同时访问共享资源的最大进程数。
- 应用场景:适用于需要控制资源访问的进程间通信。
7. 套接字(Socket)
套接字是一种网络通信机制,用于在不同主机之间进行进程间通信。套接字支持多种协议(如TCP、UDP),可以实现端到端的通信。
- 特点:支持跨主机的进程间通信;支持多种协议。
- 应用场景:适用于分布式系统中的进程间通信。
进程的调度算法
进程调度算法是操作系统中用于决定哪个进程将获得CPU时间片的关键机制。不同的调度算法有不同的特点和适用场景,常见的调度算法包括先来先服务(FCFS)、短作业优先(SJF)、时间片轮转(RR)、优先级调度(Priority)和多级反馈队列(MFQ)等。
1. 先来先服务(FCFS)
先来先服务(First-Come, First-Served, FCFS)是一种简单的调度算法,按照进程进入就绪队列的顺序进行调度。最先进入就绪队列的进程将最先获得CPU资源,直到该进程执行完成或因某些事件阻塞放弃CPU占用。
- 特点:简单易实现;非抢占式调度;可能导致长作业长时间占用CPU,造成“饥饿”现象。
- 应用场景:适用于作业执行时间差异不大的场景。
2. 短作业优先(SJF)
短作业优先(Shortest Job First, SJF)是一种优先调度预计执行时间最短的进程的算法。从就绪队列中取出预计花费时间最短的任务,为其分配资源,直到任务执行完成或因某些事件阻塞放弃CPU占用。
- 特点:平均等待时间最短;非抢占式调度;可能导致长作业长时间等待,造成“饥饿”现象。
- 应用场景:适用于作业执行时间差异较大的场景。
3. 时间片轮转(RR)
时间片轮转(Round Robin, RR)是一种公平的调度算法,为每个进程分配固定的时间片(Time Slice),即允许进程运行的时间。当时间片用完后,进程被抢占并放回就绪队列的末尾,等待下一次调度。
- 特点:公平调度;抢占式调度;时间片大小影响调度性能。
- 应用场景:适用于需要公平分配CPU资源的场景。
4. 优先级调度(Priority)
优先级调度(Priority Scheduling)为每个进程分配一个优先级,优先级高的进程优先获得CPU资源。具有相同优先级的进程按照FCFS的方式执行。
- 特点:支持优先级调度;可能导致低优先级进程长时间等待,造成“饥饿”现象。
- 应用场景:适用于需要区分任务优先级的场景。
5. 多级反馈队列(MFQ)
多级反馈队列(Multi-Level Feedback Queue, MFQ)是一种复杂的调度算法,结合了优先级调度和时间片轮转的特点。系统维护多个就绪队列,每个队列具有不同的优先级。新加入的任务放在最高优先级的就绪队列,当分配的时间片耗完但任务仍未完成时,将该任务放入次优先级队列,依次降级,直到任务完成。
- 特点:支持优先级和时间片轮转;动态调整任务优先级;减少“饥饿”现象。
- 应用场景:适用于需要动态调整任务优先级的场景。
僵尸进程与孤儿进程
在Unix/Linux系统中,进程通过fork()
系统调用创建子进程,子进程是父进程的副本,拥有独立的进程控制块(PCB)。子进程与父进程相互独立,即使父进程终止,子进程仍然可以继续运行。
僵尸进程
当子进程调用exit()
系统调用结束自己的生命时,内核会释放子进程占用的资源,但子进程的PCB仍然保留在系统中。这些信息只有在父进程调用wait()
或waitpid()
系统调用时才会被释放,以便父进程了解子进程的退出状态。
僵尸进程是指子进程已经终止,但父进程没有调用wait()
或waitpid()
系统调用来获取子进程的退出状态,导致子进程的PCB没有及时释放。僵尸进程虽然不再运行,但其PCB仍然占用系统资源,可能导致资源泄漏。
孤儿进程
孤儿进程是指父进程已经终止,但子进程仍然在运行的进程。由于父进程已经终止,子进程无法通过父进程调用wait()
或waitpid()
系统调用,导致其PCB无法被回收。操作系统会检测到孤儿进程,并将其父进程设置为init
进程(进程ID为1),由init
进程负责回收孤儿进程的PCB。
如何检测僵尸进程
在Linux系统中,可以使用以下方法检测僵尸进程:
- 使用
top
命令:- 打开终端并输入
top
命令。 - 在
top
命令的输出中,S
(Status)列下标记为”Z”的进程即为僵尸进程。
- 打开终端并输入
- 使用
ps
命令:- 打开终端并输入
ps aux
命令。 - 在
ps
命令的输出中,STAT
列下标记为”Z”的进程即为僵尸进程。
- 打开终端并输入
- **使用
ps
命令结合grep
**:- 打开终端并输入
ps aux | grep Z
命令。 - 该命令会列出所有状态为”Z”的进程,即僵尸进程。
- 打开终端并输入
死锁
什么是死锁?
死锁是指在多线程或多进程环境中,两个或多个线程(或进程)因为资源被占用而陷入相互循环等待的状态,导致所有相关线程(或进程)都无法继续执行。死锁是一种常见的并发问题,可能导致系统停滞,影响系统的稳定性和性能。
死锁的必要条件
死锁的发生必须同时满足以下四个必要条件:
- 互斥(Mutual Exclusion):至少有一个资源必须处于非共享模式,即一次只能被一个线程(或进程)占用。如果另一个线程(或进程)请求该资源,它必须等待资源被释放。
- 非抢占性(No Preemption):资源不能被强制剥夺,只能由持有资源的线程(或进程)主动释放。其他线程(或进程)不能强行抢占资源。
- 持有等待(Hold and Wait):线程(或进程)在请求其他资源时,必须持有至少一个资源。即线程(或进程)在等待其他资源的同时,不会释放已持有的资源。
- 循环等待(Circular Wait):存在一组线程(或进程),每个线程(或进程)都在等待下一个线程(或进程)持有的资源,形成一个闭环。
死锁解决方案
死锁问题与其四个必要条件是充要关系,即只要破坏四个条件中的任意一个条件,就可以解除死锁状态。常见的死锁解决方案可以概括为预防、避免、检测和解除。
1. 预防死锁(Deadlock Prevention)
预防死锁是通过破坏死锁的必要条件来避免死锁的发生。具体方法包括:
- 破坏互斥条件:允许资源共享,但这通常不现实,因为某些资源天生就是独占的。
- 破坏非抢占条件:允许资源被强制剥夺,但这可能导致资源状态不一致,通常不推荐。
- 破坏持有等待条件:要求线程(或进程)在请求资源时必须释放所有已持有的资源,但这可能导致资源利用率降低。
- 破坏循环等待条件:对资源进行排序,要求线程(或进程)按照固定顺序请求资源,避免形成循环等待。
2. 避免死锁(Deadlock Avoidance)
避免死锁是通过动态分配资源,确保系统始终处于安全状态,避免进入死锁状态。常见的避免死锁算法包括:
- 银行家算法(Banker’s Algorithm):在分配资源前,预先检查分配是否会导致系统进入不安全状态,只有在安全状态下才进行资源分配。
3. 检测死锁(Deadlock Detection)
检测死锁是通过周期性地检查系统状态,判断是否存在死锁。常见的检测方法包括:
- 资源分配图(Resource Allocation Graph):通过构建资源分配图,检查是否存在环路,判断是否存在死锁。
- 等待图(Wait-For Graph):通过构建等待图,检查是否存在环路,判断是否存在死锁。
4. 解除死锁(Deadlock Recovery)
解除死锁是通过强制终止某些线程(或进程)或剥夺某些资源,打破死锁状态。常见的解除死锁方法包括:
- 进程终止:强制终止一个或多个陷入死锁的线程(或进程),释放其占用的资源。
- 资源抢占:选择一个线程(或进程),强制剥夺其占用的资源,分配给其他线程(或进程)。