操作系统常见知识点总结(上)
一、操作系统基础
什么是操作系统?
- 操作系统(OS)是计算机的基石,负责管理硬件和软件资源。
- 操作系统是运行在计算机上的软件,管理硬件资源,确保应用程序能够使用系统内存、磁盘等资源。
- 操作系统屏蔽硬件层的复杂性,起到统筹硬件资源和管理软件运行的作用。
- 操作系统的内核(Kernel)是核心部分,负责内存管理、硬件设备管理、文件系统管理和应用程序管理,决定系统的性能和稳定性。
操作系统内核与CPU的区别
- 操作系统内核(Kernel)属于操作系统层面,而中央处理器(CPU)是硬件组件。
- CPU主要负责指令的运算与处理,内核负责系统管理和内存管理,屏蔽对硬件的直接操作。
应用程序、内核、CPU的关系
内核是连接应用程序和硬件的桥梁,处理系统的资源管理,CPU则执行计算任务。
操作系统的主要功能
进程和线程管理
管理进程的创建、撤销、阻塞、唤醒,以及进程间的通信。存储管理
管理内存的分配与回收,管理外部存储设备(如磁盘)的分配。文件管理
负责文件的创建、读取、写入、删除及目录管理。设备管理
管理输入输出设备和外部存储设备的请求、释放和启动。网络管理
管理计算机网络的配置、连接、通信及安全,确保网络服务的高效与可靠。安全管理
进行用户身份认证、访问控制、文件加密等,防止非法访问和操作系统资源。
常见的操作系统
Windows
目前最流行的个人桌面操作系统,界面简洁易用,软件生态丰富。适合个人用户,特别是玩游戏时。
Unix
最早的多用户、多任务操作系统,虽然已经逐渐退出主流,但对后来的 Linux 发展起到了重要影响。
Linux
免费且开源的类 Unix 操作系统,广泛使用于服务器和开发环境。Linux 是基于 Linux 内核的操作系统,很多发行版使用 Linux 内核,系统的其他部分由 GNU 工程提供。
Mac OS
苹果公司开发的操作系统,提供优秀的编程体验,用户界面和软件生态优于 Linux,适合日常开发和使用。
用户态和内核态
什么是用户态和内核态?
- 用户态 (User Mode): 用户态进程拥有较低的权限,主要处理用户程序的任务。访问硬件资源时需要通过系统调用请求操作系统的帮助,进入内核态。
- 内核态 (Kernel Mode): 内核态进程拥有高权限,可以访问计算机的所有资源,包括内存、设备、驱动等。操作系统内核在此模式下执行系统调用,并负责管理硬件资源。
为什么要有用户态和内核态?只有一个内核态不行么?
- 特权指令:一些操作,如内存分配、时钟设置、I/O 处理等,仅限于内核态执行。若所有进程都能执行这些危险指令,会导致系统崩溃。
- 资源共享:若仅有一个内核态,所有程序都将拥有相同的资源访问权限,可能导致资源冲突、系统不稳定、安全性降低。用户态和内核态的分离有助于保持系统的稳定性和安全性。
用户态和内核态是如何切换的?
用户态切换到内核态的方式包括:
- 系统调用 (Trap):用户进程主动请求内核执行特权操作(如磁盘读写),通过操作系统开放的中断机制实现切换。
- 中断 (Interrupt):外围设备完成任务后向 CPU 发出中断信号,CPU 会暂停当前任务,转而执行与中断相关的内核程序。
- 异常 (Exception):当用户程序执行中出现异常(如缺页异常),会触发进程切换到内核态进行异常处理。
在系统处理过程中,中断和异常都通过中断向量表找到对应的处理程序,区别在于,中断是由外部事件引发的,而异常是由指令执行结果引发的。
系统调用
什么是系统调用?
系统调用是用户程序请求操作系统内核执行特权操作的机制。用户程序无法直接访问系统资源(如文件管理、进程控制、内存管理等),因此需要通过系统调用来向操作系统提出服务请求,并由操作系统在内核态完成这些操作。
系统调用的分类
系统调用大致分为以下几类:
- 设备管理:设备的请求、释放、启动等操作。
- 文件管理:文件的读、写、创建、删除等操作。
- 进程管理:进程的创建、撤销、阻塞、唤醒,进程间通信等。
- 内存管理:内存的分配、回收及获取内存信息等操作。
系统调用由操作系统内核提供,运行在内核态,而普通库函数调用由函数库或用户自己提供,运行在用户态。
系统调用的过程
- 用户程序发起系统调用,触发中断(Trap),因为系统调用需要执行特权指令,用户程序权限不足。
- 中断发生后,当前程序的执行会中断,跳转到中断处理程序,内核开始执行系统调用。
- 内核处理完后,主动触发 Trap,再次发生中断,切换回用户态,继续执行用户程序。
通过系统调用,应用程序与操作系统之间能够进行有效的交互,访问底层资源如文件、设备、网络等。
二、进程和线程
什么是进程和线程?
进程 (Process)
进程是计算机中正在运行的一个程序实例。每个进程有自己的内存空间、资源和状态,彼此之间相互独立。
举例:你打开的微信应用就是一个进程。线程 (Thread)
线程是进程中的一个执行单元,也被称为轻量级进程。多个线程可以共享同一进程的资源(如内存空间、文件句柄、网络连接等),因此线程比进程更轻便。
举例:在微信进程中,可能有一个线程专门用来拉取新的消息,另一个线程用于处理用户的输入。
进程和线程的主要区别在于进程是资源分配的基本单位,而线程是程序执行的最小单位。
进程和线程的区别
资源分配
- 进程是操作系统进行资源分配的基本单位,每个进程拥有自己的独立内存空间和资源。
- 线程是进程的执行单位,多个线程共享进程的资源(如堆和方法区),但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
独立性
- 进程之间相互独立,一个进程的崩溃不会直接影响其他进程。
- 线程是同一进程内的执行单元,多个线程可以共享资源,因此线程之间可能相互影响。
开销与效率
- 创建和销毁线程的开销比进程小,线程的上下文切换效率更高。
- 进程拥有更好的资源管理和保护机制,但创建和销毁进程的开销较大。
总结
- 线程是进程的更小运行单位,一个进程可以有多个线程。
- 线程和进程最大的区别在于:进程是独立的,线程共享进程资源。线程的开销小但缺乏进程的资源管理和保护能力。
有了进程为什么还需要线程?
进程切换开销大
进程切换需要保存和恢复大量的上下文信息,开销较大。而线程切换的成本较低,因为线程在同一进程内共享资源,切换时只需要保存和恢复线程的上下文。线程更轻量
线程比进程更轻便,一个进程可以创建多个线程。多个线程在同一进程中运行,共享资源,不需要创建新的独立内存空间。并发处理任务
多个线程可以并发处理不同的任务,从而更高效地利用多核处理器。而进程只能在一个时间执行一个任务,如果遇到阻塞(如 I/O 阻塞),整个进程会挂起,直到操作完成。线程之间共享资源
同一进程内的线程共享内存、文件等资源,因此它们之间可以直接通信,而不需要通过操作系统的内核。这使得线程间通信更加高效。
线程相较于进程,能够更高效地管理资源并实现并发操作,因此在多任务处理和高性能计算中,线程提供了比进程更灵活的方式。
为什么要使用多线程?
从总体上来看:
计算机底层
线程是轻量级的进程,是程序执行的最小单位。线程切换和调度的成本比进程低得多。此外,在多核 CPU 的环境下,多个线程可以同时执行,从而减少了线程上下文切换的开销。互联网发展趋势
当前的系统常常需要处理百万级甚至千万级的并发请求,单线程无法满足这一需求。多线程并发编程是开发高并发系统的基础,能够显著提升系统的并发能力和整体性能。
深入计算机底层:
单核时代
在单核时代,多线程主要用于提高进程在 CPU 和 I/O 操作上的效率。例如,如果只有一个线程,且线程在执行 I/O 操作时被阻塞,整个进程会被阻塞。使用多线程后,即使一个线程被 I/O 阻塞,其他线程仍然可以继续执行,提升系统的整体效率。多核时代
在多核时代,多线程能够使进程利用多个 CPU 核心并行执行。例如,如果任务非常复杂,单线程只能使用一个核心,而多线程可以将任务拆分到多个核心上并行处理。这样,任务的执行效率会显著提高,接近于“单核时执行时间/CPU 核心数”。
总结
多线程能提高系统的并发处理能力,充分利用多核 CPU 的优势,尤其在需要高效处理大量并发任务的现代应用中至关重要。
线程间同步的方式
线程同步用于避免多个线程同时访问共享资源,导致资源冲突或不一致。以下是常见的同步方式:
互斥锁 (Mutex)
互斥锁机制确保只有一个线程能够访问共享资源。通过互斥锁,线程在访问共享资源时需要获取锁,只有获取锁的线程才能操作资源。Java 中的synchronized
关键字和Lock
接口(如ReentrantLock
)就是常见的实现。读写锁 (Read-Write Lock)
读写锁允许多个线程并发读取共享资源,但在写入资源时,只允许一个线程访问。通常用于读多写少的场景,提高读操作的效率。Java 中可以使用ReentrantReadWriteLock
来实现。信号量 (Semaphore)
信号量通过维护一个许可计数来控制资源的访问。它允许多个线程访问同一资源,但控制同时访问资源的线程数量。Java 中可以通过Semaphore
类来实现。屏障 (Barrier)
屏障同步用于确保多个线程在执行到某个点时同步阻塞,直到所有线程都到达该点后才继续执行。这通常用于需要多个线程一起完成某个任务的场景。Java 中的CyclicBarrier
就是一个常见的实现。事件 (Event) 和 Wait/Notify
通过wait()
和notify()
方法,线程可以在共享资源的状态改变时进行同步。wait()
会让线程释放锁并进入等待状态,直到其他线程通过notify()
或notifyAll()
唤醒它们。它常用于生产者-消费者模式等场景。
总结
线程同步的方式通过不同的机制来解决多个线程并发访问共享资源的问题,选择合适的同步机制可以提高系统的效率和稳定性。
PCB 是什么?包含哪些信息?
PCB(Process Control Block) 即进程控制块,是操作系统中用来管理和跟踪进程的关键数据结构。每个进程都有一个对应的 PCB,操作系统通过这个结构体来存储与进程相关的各种信息,并用它来进行进程调度和管理。可以把 PCB 看作是进程的“身份证”或者“大脑”。
PCB 主要包含以下几部分内容:
进程描述信息
- 进程名称
- 进程标识符(PID):操作系统为每个进程分配的唯一 ID。
进程调度信息
- 进程状态:进程的当前状态,如就绪、运行、阻塞等。
- 优先级:进程的优先级,用于决定调度顺序。
- 调度信息:进程阻塞的原因等调度相关信息。
资源需求信息
- CPU 时间:进程请求的 CPU 时间。
- 内存需求:进程所需的内存空间大小。
- I/O 设备信息:进程使用的输入/输出设备及其状态。
进程打开的文件信息
- 文件描述符:进程打开的文件编号。
- 文件类型和打开模式:文件的类型和当前的访问模式(读、写等)。
处理器状态信息
- 通用寄存器的内容:存储进程正在执行时的寄存器值。
- 程序计数器(PC):指向当前执行指令的地址。
- 程序状态字(PSW):包含处理器状态信息。
- 用户栈指针:指向当前用户栈的栈指针。
其他信息
- 进程的父进程 ID(PID),子进程等信息。
- 进程的通信和同步信息等。
总结
PCB 是操作系统进行进程管理和调度的核心数据结构,它包含了进程所需的所有信息,帮助操作系统在进程执行时进行状态保存、恢复和切换。
进程的状态
进程在其生命周期中会经历不同的状态,操作系统会根据进程的执行情况来调整其状态。一般来说,进程的状态主要有以下几种:
创建状态 (new)
- 当操作系统创建一个新的进程时,它进入创建状态。在这个阶段,进程还没有开始执行,操作系统分配资源并准备进程的执行环境。
就绪状态 (ready)
- 进程已经获得了除 CPU 外的所有资源,准备开始执行。此时,它等待 CPU 分配时间片,进入运行状态。就绪状态的进程并不在运行,只是等待 CPU 空闲。
运行状态 (running)
- 进程处于 CPU 上执行的状态。在单核 CPU 中,一次只能有一个进程处于运行状态。运行状态是进程执行的核心状态,处理器执行进程的指令。
阻塞状态 (waiting)
- 当进程因等待某些外部事件(如 I/O 操作、网络响应等)而不能继续执行时,进程会进入阻塞状态。在此状态下,进程无法继续占用 CPU,直到它等待的事件发生。
结束状态 (terminated)
- 当进程执行完成,或者由于异常、错误、或者操作系统强制终止,进程进入结束状态。结束状态的进程将被从系统中清除,释放所有占用的资源。
状态转换
进程的状态并不是固定的,而是可以相互转换的。例如,进程可以从创建状态进入就绪状态,然后从就绪状态转到运行状态,如果发生了阻塞(如等待 I/O),就会进入阻塞状态。一旦事件完成,进程就会回到就绪状态,直到再次获得 CPU 时间片。
最终,进程执行完毕或被终止时,它将进入结束状态,并被操作系统清理掉。
进程间的通信方式
进程间通信(IPC)是指不同进程之间交换数据或信息的方式。以下是常见的进程间通信方式:
管道/匿名管道 (Pipes)
- 简介:管道用于具有亲缘关系的父子进程或兄弟进程之间的通信。数据以字节流的形式在管道中传递。
- 特点:匿名管道没有名称,仅限于相关进程之间进行通信。它支持单向通信,即数据只能单方向流动(如从父进程到子进程)。
有名管道 (Named Pipes)
- 简介:与匿名管道类似,但它有一个明确的名字,使得无关的进程之间也可以通信。它以文件的方式存在于文件系统中,可以实现不同进程间的通信。
- 特点:遵循先进先出的原则(FIFO),可以在进程间进行双向或单向通信。与匿名管道的限制相比,有名管道允许更多的进程进行通信,且不一定需要亲缘关系。
信号 (Signal)
- 简介:信号是一种较为简单的进程间通信方式,常用于通知进程某个事件已经发生。它通常传递一个简单的通知,不携带复杂数据。
- 特点:信号可以中断进程的正常执行,通知进程做出响应,常用于进程的控制和管理,如
SIGINT
(中断信号)等。
消息队列 (Message Queuing)
- 简介:消息队列是内核提供的用于存储和传递消息的数据结构。它以队列的形式存储消息,支持先进先出(FIFO)原则,允许进程间异步交换信息。
- 特点:消息队列可以支持更复杂的通信协议,如支持优先级消息、随机查询等。消息不会丢失,除非显式删除或系统重启。
信号量 (Semaphores)
- 简介:信号量是一种用于多进程同步和互斥的机制。信号量是一个计数器,用来控制对共享资源的访问。
- 特点:通过控制对共享资源的访问,信号量帮助解决进程间同步问题,避免竞态条件。常用于资源的管理和控制。
共享内存 (Shared Memory)
- 简介:多个进程通过共享一块内存区域来进行通信。所有进程可以直接访问这块内存,不需要操作系统进行数据复制。
- 特点:共享内存提供了最快的通信方式,因为数据不需要通过内核复制。然而,它需要额外的同步机制(如互斥锁)来确保数据一致性。
套接字 (Sockets)
- 简介:套接字是进程间通信的基本操作单元,通常用于网络通信。它支持客户端和服务器之间的双向通信。
- 特点:套接字支持 TCP/IP 协议栈,可以在不同主机上的进程间进行通信。它不仅支持同一机器上的进程通信,还可以支持跨网络的通信。
总结
每种进程间通信方式都有其特定的应用场景,选择哪种方式取决于进程间通信的需求,如数据传输的复杂性、速度、是否需要跨网络等。对于高效的共享数据,通常使用共享内存,而需要更复杂的控制和同步时,可以使用消息队列、信号量等。
进程的调度算法
进程调度是操作系统中非常重要的一部分,它决定了CPU在多个进程之间的分配策略。不同的调度算法对进程的调度顺序和资源分配有不同的影响。以下是常见的几种进程调度算法:
先到先服务调度算法 (FCFS, First Come, First Served)
- 简介:该算法是最简单的一种调度算法,按照进程到达就绪队列的顺序依次分配CPU资源。第一个到达的进程最先执行,直到完成或被阻塞。
- 优点:实现简单,容易理解。
- 缺点:可能导致"长作业阻塞短作业"(即长时间的进程会导致后续进程等待过长的时间),造成较长的等待时间。
- 适用场景:适用于作业长度大致相等的情况。
短作业优先调度算法 (SJF, Shortest Job First)
- 简介:该算法选择预计执行时间最短的进程优先执行。根据进程预计的运行时间进行排序,时间最短的进程先执行。
- 优点:能够最小化平均等待时间。
- 缺点:需要事先知道每个进程的执行时间,而这种信息通常很难预测。此外,它可能导致“长作业饿死”问题,即长作业永远得不到执行。
- 适用场景:适用于能够准确估计进程运行时间的场景。
时间片轮转调度算法 (RR, Round-Robin)
- 简介:该算法将CPU时间划分为固定大小的时间片,每个进程被分配一个时间片,如果进程在时间片内没有完成,则将其挂起并放回就绪队列,让下一个进程执行。每个进程公平地轮流获取CPU时间。
- 优点:公平,所有进程都有机会执行,不会让一个进程独占CPU资源。
- 缺点:如果时间片设置过大,可能与FCFS相似;如果设置过小,则导致频繁的上下文切换,增加系统开销。
- 适用场景:适用于交互式应用,保证各进程之间的公平性。
多级反馈队列调度算法 (MFQ, Multi-level Feedback Queue)
- 简介:该算法结合了多个优先级队列,并根据进程的行为动态调整其所在队列的优先级。短作业优先被安排在高优先级队列中,而长作业则被逐渐调低优先级。当进程在高优先级队列中未完成时,它会被降到低优先级队列,从而使得短进程优先得到响应。
- 优点:兼顾了短进程和长进程的需求,能更公平地调度进程,并能适应不同工作负载的调度需求。
- 缺点:算法复杂,需要对进程进行动态监控和调优。
- 适用场景:适用于混合工作负载的系统,如UNIX操作系统中的调度策略。
优先级调度算法 (Priority Scheduling)
- 简介:为每个进程分配一个优先级,并优先执行优先级高的进程。优先级可以基于进程的需求(如内存、计算时间)或其它因素设定。优先级相同的进程会按照FCFS顺序调度。
- 优点:能够确保重要进程先执行。
- 缺点:可能导致“低优先级进程饿死”,即低优先级进程长时间无法获得CPU执行时间。
- 适用场景:适用于需要区别处理的不同进程,如实时系统。
总结
- FCFS:简单,但可能导致等待时间长。
- SJF:短作业优先,但依赖于准确的执行时间预测,可能导致长作业“饿死”。
- RR:公平且简单,但时间片过小会增加上下文切换开销。
- MFQ:综合了多个算法的优点,适合复杂工作负载,但实现较为复杂。
- Priority Scheduling:灵活,能够处理优先级不同的任务,但可能导致低优先级任务饥饿。
每种算法都有其适用场景,操作系统通常会根据实际需求选择或组合使用不同的调度算法。
僵尸进程与孤儿进程
在Unix/Linux系统中,进程的生命周期和资源管理是由操作系统内核控制的。当一个子进程结束后,虽然它的执行已经完成,但仍然需要将其状态信息传递给父进程。因此,进程结束时的状态管理可能会导致两种特殊情况:僵尸进程和孤儿进程。
僵尸进程
- 定义:僵尸进程是指子进程已经终止,但其父进程尚未通过
wait()
或waitpid()
等系统调用获取该子进程的终止状态信息,从而没有释放子进程占用的资源(如 PCB)。这时,子进程的 PCB 仍然存在于系统中,虽然它已不再执行任何操作。 - 产生原因:当一个子进程终止时,它的父进程需要通过
wait()
或waitpid()
来获取子进程的退出状态。如果父进程没有调用这些系统调用,那么子进程的 PCB 就不会被完全清理,导致它处于“僵尸”状态。 - 解决方式:父进程必须调用
wait()
或waitpid()
来回收子进程的资源,从而防止僵尸进程的产生。如果父进程不做回收,操作系统会在一定时间后自动清理这些僵尸进程。
孤儿进程
- 定义:孤儿进程是指一个进程的父进程已经终止,但该进程仍在运行。换句话说,孤儿进程的父进程已经不存在了。
- 产生原因:通常是由于父进程在子进程还未结束时退出,或者父进程没有及时处理子进程的终止状态。孤儿进程会失去与父进程的关联,成为系统中的“孤儿”。
- 处理方式:为了防止孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为
init
进程(进程号为 1)。init
进程是所有孤儿进程的新父进程,负责回收这些孤儿进程的资源,避免它们占用系统资源。
总结
- 僵尸进程:已结束的进程,父进程未回收其状态信息,导致进程的 PCB 依然存在于系统中。
- 孤儿进程:父进程已经终止,子进程继续运行,操作系统会将孤儿进程的父进程设置为
init
,由其回收资源。
如何查看是否有僵尸进程?
在 Linux 系统中,可以通过以下方法查看是否存在僵尸进程:
1. 使用 top
命令
- 运行
top
命令后,你可以在进程列表中看到每个进程的状态列 (STAT)。如果某个进程的状态是Z
,那么该进程就是一个僵尸进程。 - 另外,
top
命令中会显示系统中所有进程的概况,zombie
值表示当前存在的僵尸进程数量。如果该值为 0,则表示没有僵尸进程。
2. 使用 ps
命令查找僵尸进程
使用 ps
命令可以更加精确地查找僵尸进程及其父进程。以下命令可以帮助你查找所有状态为 Z
(僵尸状态)的进程:
ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'
解释:
ps -A
:显示系统中所有进程。-ostat,ppid,pid,cmd
:显示进程的状态 (STAT)、父进程ID (PPID)、进程ID (PID) 和命令。grep -e '^[Zz]'
:筛选出状态为Z
或z
的进程,即僵尸进程。
该命令会列出所有僵尸进程及其相关信息,包括其父进程 ID (PPID),你可以通过查看父进程来进一步分析和处理僵尸进程。
三、死锁
死锁
死锁(Deadlock) 是指在多进程或多线程环境中,两个或多个进程(或线程)相互等待对方释放资源,并且这种等待永远无法得到满足,从而导致系统中的这些进程(或线程)永久性地处于阻塞状态,无法继续执行。
死锁的四个必要条件
死锁的发生需要满足以下四个条件,通常称为死锁的四大必要条件:
互斥条件(Mutual Exclusion):
- 至少有一个资源必须处于“仅允许一个进程使用”的状态,即资源是不可共享的。若资源是可共享的,则不可能发生死锁。
占有并等待条件(Hold and Wait):
- 已经占有一个资源的进程在等待其他资源时,不会释放自己占有的资源。换句话说,进程在等待其他资源的同时,保持对已占有资源的占用。
非抢占条件(No Preemption):
- 一旦某个进程持有了某个资源,其他进程不能强行剥夺该资源,只能等待该进程主动释放资源。
循环等待条件(Circular Wait):
- 存在一个进程等待链,链中的每个进程都在等待下一个进程所持有的资源,并且链最后一个进程等待第一个进程的资源,形成闭环。
死锁的例子
假设两个进程 A 和 B,同时请求两个资源 R1 和 R2:
- 进程 A 获取了资源 R1,等待资源 R2;
- 进程 B 获取了资源 R2,等待资源 R1。
由于 R1 和 R2 是互斥的资源,且进程 A 和进程 B 都不能强制抢占对方的资源,它们将进入等待状态,并且永远不会释放占有的资源,从而造成死锁。
如何避免死锁
为避免死锁,常见的策略有:
- 资源分配图:使用资源分配图来检测系统是否处于死锁状态。
- 死锁预防:通过在系统中对资源分配进行控制,避免死锁的四个条件之一不成立,例如,要求进程在请求资源时必须一次性请求所有资源。
- 死锁检测与恢复:定期检查系统状态,发现死锁时通过撤销或强制释放某些进程的资源来解除死锁。
- 避免循环等待:例如规定资源的申请顺序,所有进程在请求资源时必须按照同样的顺序申请资源,从而避免循环等待。
死锁的实际例子
假设有两个进程 A 和 B,以及两个资源 X 和 Y,资源的分配和需求情况如下:
进程 | 占用资源 | 需求资源 |
---|---|---|
A | X | Y |
B | Y | X |
在这种情况下:
- 进程 A 占用了资源 X,并请求资源 Y。
- 进程 B 占用了资源 Y,并请求资源 X。
死锁发生的过程:
- 进程 A 获取到资源 X,开始执行,但此时它还需要资源 Y 才能继续执行。
- 进程 B 获取到资源 Y,开始执行,但它也需要资源 X 才能继续执行。
- 由于 进程 A 持有资源 X,并等待 Y,而 进程 B 持有资源 Y,并等待 X,导致它们形成了一个循环等待的关系。
- 两个进程都无法继续执行,因为它们都在等待对方释放资源。
此时,系统处于死锁状态,两个进程无法继续执行,无法释放资源,也无法退出。
死锁的四个条件:
- 互斥条件:资源 X 和 Y 是不可共享的,只能被一个进程占用。
- 占有并等待条件:进程 A 和进程 B 都在占用一个资源的同时等待另一个资源。
- 非抢占条件:进程 A 和进程 B 不会被抢占,它们只能自己释放资源。
- 循环等待条件:进程 A 等待进程 B 释放资源 Y,进程 B 等待进程 A 释放资源 X,形成了一个循环等待。
如何避免?
- 资源分配策略:为避免死锁,可以规定资源请求的顺序,确保进程请求资源时不可能形成循环等待。
- 请求资源时一次性全部申请:避免进程在中途释放资源而造成死锁。
产生死锁的四个必要条件
互斥(Mutual Exclusion):
- 资源必须处于非共享模式,即每次只有一个进程可以使用资源。如果另一个进程请求该资源,它必须等待直到该资源被释放。
占有并等待(Hold and Wait):
- 一个进程至少应该已经占有一个资源,并且在等待其他资源时,它不会释放已占有的资源。
非抢占(No Preemption):
- 资源不能被强制抢占。只有当进程完成任务并释放资源时,其他进程才能使用这些资源。
循环等待(Circular Wait):
- 存在一个进程等待链
{P0, P1, ..., Pn}
,其中每个进程都等待下一个进程持有的资源,且最后一个进程又等待第一个进程持有的资源,形成了一个闭环。
- 存在一个进程等待链
解释:
- 这些条件是死锁发生的 必要条件,即死锁必须满足这四个条件。如果这些条件中的任何一个不成立,系统就不可能发生死锁。
- 如果上述条件之一不满足,那么就不会形成死锁。例如,如果占有并等待条件不成立,那么进程在等待资源时会释放已占有的资源,从而避免了死锁。
注意:
- 这四个条件是死锁的必要条件,意味着如果系统发生了死锁,这四个条件一定都成立。
模拟死锁的代码示例
以下是一个简单的 Java 代码示例,模拟了两个线程因资源竞争而发生死锁的场景:
public class DeadLockDemo {
private static Object resource1 = new Object(); // 资源 1
private static Object resource2 = new Object(); // 资源 2
public static void main(String[] args) {
// 线程 1
new Thread(() -> {
synchronized (resource1) { // 获得资源 1 锁
System.out.println(Thread.currentThread() + " get resource1");
try {
Thread.sleep(1000); // 休眠 1 秒,模拟业务操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + " waiting get resource2");
synchronized (resource2) { // 尝试获得资源 2 锁
System.out.println(Thread.currentThread() + " get resource2");
}
}
}, "线程 1").start();
// 线程 2
new Thread(() -> {
synchronized (resource2) { // 获得资源 2 锁
System.out.println(Thread.currentThread() + " get resource2");
try {
Thread.sleep(1000); // 休眠 1 秒,模拟业务操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + " waiting get resource1");
synchronized (resource1) { // 尝试获得资源 1 锁
System.out.println(Thread.currentThread() + " get resource1");
}
}
}, "线程 2").start();
}
}
代码解释
资源锁定:
- 两个资源
resource1
和resource2
被两个线程共享。 - 线程 1 首先获取
resource1
锁,然后等待获取resource2
锁。 - 线程 2 首先获取
resource2
锁,然后等待获取resource1
锁。
- 两个资源
休眠模拟:
Thread.sleep(1000)
在每个线程中加入了 1 秒的休眠时间,这允许线程 2 在线程 1 获取到resource1
后能够及时获取resource2
,并且线程 1 在resource1
被占用的情况下能够获取resource2
。
死锁形成:
- 由于线程 1 和线程 2 都在等待对方的资源,并且两个线程互相持有对方需要的资源,形成了一个循环等待的情况,导致死锁。
输出结果
Thread[线程 1,5,main] get resource1
Thread[线程 2,5,main] get resource2
Thread[线程 1,5,main] waiting get resource2
Thread[线程 2,5,main] waiting get resource1
死锁解释
- 线程 1 在获得
resource1
锁后,进入休眠,等待resource2
锁。 - 线程 2 在获得
resource2
锁后,进入休眠,等待resource1
锁。 - 最终,两个线程都进入了等待状态,但因为都持有对方需要的资源,因此无法继续执行,从而导致死锁。
死锁产生的条件
- 互斥:
resource1
和resource2
是互斥资源,每次只有一个线程可以持有。 - 占有并等待:线程 1 占有
resource1
,等待resource2
;线程 2 占有resource2
,等待resource1
。 - 非抢占:资源不能被抢占,必须等待持有资源的线程释放。
- 循环等待:线程 1 等待线程 2 释放
resource2
,线程 2 等待线程 1 释放resource1
,形成一个循环等待。
通过这个代码示例,可以很清晰地看到死锁是如何发生的,以及如何满足死锁的四个必要条件。
解决死锁的方法
解决死锁问题通常有预防、避免、检测和解除四种方法,每种方法的应用场景和实现方式不同。以下是每种方法的详细介绍:
1. 死锁的预防
死锁预防的基本思想是通过限制并发进程对资源的请求,从而使得死锁的四个必要条件中的任何一个无法同时满足。
预防死锁的策略:
- 破坏互斥条件:使得资源可以被多个进程同时共享。虽然这种方法简单,但许多资源(如磁盘、打印机等)无法做到共享,因此在大多数情况下不可行。
- 破坏占有并等待条件:确保进程在执行前就获得它需要的所有资源,这样进程在执行过程中就不需要等待其他资源的释放。
- 破坏非抢占条件:使得资源可以被抢占。例如,如果一个进程持有某个资源并请求另一个资源,操作系统可以强制它释放当前持有的资源。
- 破坏循环等待条件:通过为资源建立顺序规则,要求进程按照一定顺序申请资源,避免形成环路。常见的做法有层次资源分配策略,要求进程按资源等级逐层申请和释放资源。
典型方法:
- 静态分配策略:进程必须在执行前申请所有需要的资源。进程要么获得所有资源并开始执行,要么不分配资源。缺点是资源利用率低,因为进程可能会占用一些实际上并不需要的资源。
- 层次分配策略:将资源划分为多个等级,要求进程按照从低到高的顺序申请资源,释放时也必须按照逆序释放。
2. 死锁的避免
死锁避免允许在系统中同时满足死锁的四个必要条件,但必须在每次资源请求时判断是否可能导致死锁。如果某次请求可能导致系统进入不安全状态,则不允许分配资源。
关键概念:
- 安全状态:系统中不存在死锁的状态,所有进程都能在有限的时间内完成。
- 不安全状态:系统中可能发生死锁的状态,进程的资源请求无法满足,导致死锁的发生。
银行家算法:
- 由Dijkstra提出,通过模拟资源的分配,判断系统是否处于安全状态。每次分配资源前,银行家算法会试探该资源是否会导致系统进入不安全状态。
- 如果分配后仍然处于安全状态,则实际分配资源;如果系统进入不安全状态,则该资源分配被撤销,进程继续等待。
3. 死锁的检测
与预防和避免不同,死锁检测不通过限制资源的请求来防止死锁,而是允许死锁发生,并通过定期检查系统状态来检测死锁的发生。
死锁检测的方法:
- 进程-资源分配图:死锁检测通常基于进程-资源分配图,该图描述了每个进程及其所占用的资源以及进程的资源请求情况。如果图中存在一个环路,并且每个资源只有一个实例,则表明系统已发生死锁。
死锁检测的步骤:
- 如果进程-资源分配图中无环路,则没有死锁。
- 如果存在环路,且每个资源类只有一个资源实例,则死锁发生。
- 如果存在环路,但资源类有多个资源实例,检查是否可以找到一个能释放资源的进程,如果能释放资源,则死锁解除;否则,死锁存在。
4. 死锁的解除
死锁检测后,如果系统处于死锁状态,需要采取措施解除死锁,常见的解除方法有以下几种:
解除死锁的策略:
- 立即结束所有进程的执行:这种方法简单直接,但会导致系统状态丢失,进程已完成的工作也会被丢弃,损失较大。
- 撤销死锁涉及的进程:可以撤销参与死锁的所有进程,通过撤销进程来打破死锁,但可能会导致部分计算结果丢失。
- 逐个撤销进程:通过撤销涉及死锁的进程,并回收其资源,直到所有死锁进程都被解除。
- 抢占资源:从涉及死锁的进程中抢占资源,并重新分配给其他进程,直到死锁解除。
总结
- 死锁的预防:通过对进程请求资源的限制,避免死锁条件的发生,通常会降低资源的利用率。
- 死锁的避免:系统动态检查资源请求情况,保证系统始终处于安全状态,从而避免死锁。
- 死锁的检测:允许死锁发生,定期检测系统是否处于死锁状态,并通过相应措施处理死锁。
- 死锁的解除:检测到死锁后,通过终止进程、撤销或抢占资源来解除死锁。
不同的策略适用于不同的场景,通常在实际操作系统中,采用的是死锁检测和解除的方法,辅以一定的死锁避免技术。