进程
文章目录
程序是静态的,就是个存放在磁盘中的可执行文件,也就是一系列的指令集合。
进程是动态的,是程序的一次执行过程。同一个程序多次执行会对应多个进程。
概念
进程是计算机科学中的一个重要概念,它是操作系统管理和调度的基本单位,用于执行程序和任务。进程可以被认为是正在运行的程序的实例,具有自己的内存空间、代码、数据和系统资源的使用。以下是有关进程的概念的详细信息:
-
程序执行的实例:进程是一个正在运行的程序的实例。当您启动一个应用程序或运行一个脚本时,操作系统会创建一个进程来托管该程序的执行。每个进程都有自己的执行环境,包括内存和系统资源。
-
资源分配单位:进程是操作系统用来管理和分配计算机资源的基本单位。这些资源包括CPU时间、内存、文件句柄、网络连接等。操作系统负责确保多个进程能够共享这些资源,同时保持相互隔离,以防止彼此干扰或冲突。
-
独立性:每个进程都是相互独立的实体,它们不会直接干扰或影响其他进程的执行。这种独立性是通过操作系统的管理和保护来实现的,以确保一个进程的错误或崩溃不会影响整个系统。
-
并发执行:操作系统可以同时管理多个进程,使它们能够在同一计算机系统上并发执行。这允许多个任务在几乎同时执行,从而提高了系统的效率和响应能力。
-
状态管理:每个进程可以处于不同的状态,如运行、就绪、阻塞等。操作系统负责管理这些状态并根据需要进行状态转换,以有效地利用系统资源。
-
通信和协作:进程之间可以通过各种通信机制进行数据交换和协作。这种通信可以在不同的进程之间传递消息、共享数据或协调任务。
进程是操作系统中的核心概念,用于管理和执行计算机系统中的程序和任务。它们具有独立性、并发性、资源分配、状态管理和通信等特点,是操作系统的重要组成部分,对于实现多任务处理和系统资源的有效利用至关重要。
组成
进程是计算机系统中的基本执行单元,它包含多个组成部分,这些部分协同工作以执行程序或任务。包含下面主要组成部分:
-
程序段:进程的核心组成部分之一是程序代码,它是一系列指令或指令集,以二进制形式存储在计算机的内存中。程序代码定义了进程要执行的操作,包括算法、逻辑和计算任务。
-
数据段:进程需要处理的数据也是重要的组成部分。这些数据可以包括变量、数据结构、输入和输出等,它们存储在进程的内存空间中,用于程序的运行和操作。
-
程序计数器(Program Counter):程序计数器是一个特殊的寄存器,用于跟踪下一条要执行的指令的地址。它在程序执行期间不断更新,以指示下一步要执行的操作。
-
寄存器状态:进程的寄存器状态包括了一组寄存器的内容,这些寄存器存储了进程的执行上下文,如通用寄存器、栈指针、堆指针等。寄存器状态对于程序的执行和上下文切换非常重要。
-
堆和栈:堆和栈是进程内存的两个关键区域。堆用于动态分配内存,通常用于存储动态分配的数据结构,如对象和数组。栈用于存储函数调用和局部变量,以及控制函数的执行顺序。
-
进程控制块(Process Control Block,PCB) :PCB 是一个数据结构,包含了操作系统管理进程所需的信息。每个进程都有一个相关的 PCB,其中包括进程状态(用一个变量state来表示进程的当前状态)、进程标识符(P rocess ID,进程ID)、优先级、程序计数器、寄存器状态、资源分配信息等。PCB 允许操作系统管理进程的创建、调度、暂停、恢复和终止。
-
打开文件表(File Descriptor Table):进程通常需要访问文件和输入/输出设备。打开文件表存储了进程打开的文件和设备的信息,包括文件描述符、文件位置等。
-
其他资源:进程可能会使用其他资源,如网络连接、设备访问权限、信号处理器等。这些资源也可以包含在进程的组成部分中,以便操作系统管理和跟踪它们的使用。
这些组成部分协同工作,使进程能够执行程序或任务。操作系统负责管理这些部分,确保进程能够共享系统资源、执行程序代码并与其他进程协同工作。进程的组成部分可以根据操作系统和架构的不同而有所不同,但上述元素通常包括在进程的定义中。
特征
进程作为计算机科学中的一个基本概念,具有许多重要的特征,这些特征有助于定义和理解进程的性质和行为。以下是进程的主要特征:
-
动态性(Dynamic):进程是程序的一次执行过程,是动态地产生、变化和消亡的。动态性是进程最基本的特征。
-
并发性(Concurrency):进程允许多个进程在同一时间段内存在和执行。计算机系统通过交替执行这些进程来实现并发性,从而提高了系统的效率和响应能力。
-
独立性(Independence):每个进程都是独立的实体,它们不会直接干扰或影响其他进程的执行。这种独立性是通过操作系统的管理和保护来实现的,以确保一个进程的错误或崩溃不会影响整个系统。
-
异步性(Asynchrony):各个进程按照独立的、不可预知的速度向前推进,操作系统要提供"进程同步机制"来解决异步问题。
-
结构性(Structural):每个进程都会配置一个PCB。从结构上来看,进程由程序段、数据段、PCB组成。
这些特征使进程成为操作系统的基本组成部分,允许多任务处理、资源管理和程序协作,从而实现了复杂的计算和操作。进程的特征对于操作系统的性能、可靠性和安全性都具有重要影响。
状态与转换
进程在其生命周期中可以经历不同的状态,操作系统通过状态转换来管理和调度进程。以下是一些常见的进程状态以及它们之间的转换:
- 新建状态(New):当进程被创建但尚未开始执行时,它处于新建状态。在这个阶段,操作系统为进程分配必要的资源,如内存空间、进程标识符(PID)等。
- 就绪状态(Ready):一旦进程拥有了足够的资源,并且其所有前提条件都满足,它就会从新建状态转换为就绪状态。在就绪状态中,进程等待操作系统将其调度到CPU上执行。
- 运行状态(Running):当操作系统将进程从就绪状态调度到CPU上执行时,进程进入运行状态。在这个状态下,进程的指令正在执行,占用CPU时间。在单CPU情况下,同一时刻只会有一个进程处于运行态,多核CPU情况下,可能有多个进程处于运行态。
- 阻塞状态(Blocked或Waiting):当进程需要等待某些事件发生,如等待I/O操作完成、等待资源释放等,它会进入阻塞状态。在阻塞状态中,进程不会占用CPU时间,直到等待的事件发生。
- 终止状态(Terminated):进程完成了其任务或由于某种原因终止时,它会进入终止状态。在这个状态下,操作系统会释放进程占用的资源,但通常会保留一些信息以供进程的父进程或其他进程查询。
在进程整个生命周期中,大部分是都处于三种基本状态(运行态、就绪态、阻塞态)。
进程在这些状态之间进行转换,具体的状态转换可以由操作系统的调度器和各种事件触发:
-
新建状态到就绪状态:当进程被创建后,它会等待操作系统将其移到就绪状态,这通常由操作系统的调度器触发。
-
就绪状态到运行状态:调度器会选择一个就绪状态的进程,并将其移到运行状态,开始执行进程的指令。
-
运行状态到就绪状态:进程可能在运行时被中断或时间片用尽,然后被移到就绪状态,等待下一次执行。
-
运行状态到阻塞状态:进程在等待某些事件发生时,可能会被移到阻塞状态,直到事件发生并由操作系统触发将其移到就绪状态。
-
阻塞状态到就绪状态:一旦进程等待的事件发生,操作系统将其移到就绪状态,以等待再次执行。
-
运行状态到终止状态:当进程完成任务或由于错误或其他原因需要终止时,它会被移到终止状态。
-
阻塞状态到终止状态:如果进程在阻塞状态中终止,操作系统会将其直接移到终止状态。
这些状态和状态转换使操作系统能够管理和调度多个进程,以有效地共享计算机资源,提高系统的效率和响应能力。状态转换通常是由操作系统的内核和调度算法自动处理的,以确保进程按照合适的顺序执行。
组织方式
进程的组织方式一共有两种,一种是链式方式,另一种是索引方式。
链接方式
链式方式是一种进程组织方式,通常用于操作系统中管理和调度多个进程。在链式方式中,进程以链表的形式组织,每个进程都包含指向下一个进程的指针。这种方式通常用于实现一种特定的调度策略,例如循环调度(Round Robin Scheduling)。
每个进程都包含一个指向下一个进程的指针。这些进程按照链表的方式连接在一起,形成一个环形链表或线性链表。
当一个进程的时间片用尽时,它会被移到链表中的下一个进程。这种方式使得进程在一组进程中轮流执行,以达到公平共享CPU时间的目的。
链式方式的实现通常需要操作系统维护一个进程队列,其中包含了所有等待CPU时间的进程。调度器会选择队列中的第一个进程运行,然后将其移到队列的末尾或下一个位置,以等待下一次运行。
当然,对应的阻塞状态也会有阻塞队列形成的链表。
索引方式
通常用于操作系统中管理和跟踪多个进程。在索引方式中,每个进程都由一个唯一的索引或标识符引用,而不是按照线性或链式方式组织。这种方式允许快速的进程查找和访问,通常用于高效的进程调度和管理。
操作系统维护一个进程表或进程控制块(Process Control Block,PCB)表,其中包含了每个进程的索引号和相关信息。每个条目通常包含进程的状态、程序计数器、寄存器状态、资源分配信息等。
索引方式可以用于实现高效的进程调度。调度器可以根据进程的优先级或其他因素来选择下一个要执行的进程,并使用索引号直接访问该进程的信息。
进程控制
进程控制是操作系统中的一个关键任务,它涉及创建、管理和监控进程的各个方面。进程控制允许操作系统有效地管理多个进程,以便它们可以共享计算机资源、协同工作并按照预期执行任务。
进程控制的主要功能是对系统中的所有进程实施有效的管理,其具有创建新进程、撤销已有进程、实现进程转换等功能。
实现进程控制
可以使用原语来实现进程控制,因为原语的执行具有"原子性"(不可被中断的一个或一系列操作),可以一气呵成的完成。
为什么要保证进程控制一气呵成呢?
假设PCB中的变量state表示进程当前所处状态,1表示就绪态,2表示阻塞态。
可以画出上面的图。就绪状态的PCB就应该挂在就绪队列中,而阻塞状态的PCB应该挂在阻塞队列中。
如果此时进程2等待的事件发生了,则操作系统中,负责进程控制的内核程序至少需要做这样的两种事情:
- 将 PCB 2 的 state 设置为 1
- 将 PCB 2 从阻塞队列放到就绪队列中
此时,已经将 PCB 2 的 state 修改为 1,突然检测到一个中断信号,那么 PCB 2 的 state=1,但是它却被放在阻塞队列中。
此时就会导致 PCB 2 的state变量的值与其所处队列不匹配,会影响到操作系统进行别的管理工作。
以下是与进程控制相关的一些关键方面:
-
进程创建:
- 进程创建是指操作系统启动新进程的过程。通常,一个父进程可以创建一个或多个子进程。
- 创建进程涉及分配必要的资源,如内存空间、标识符(PID)、文件描述符等。
- 父进程可以使用特定的系统调用(例如
fork
和exec
)来创建子进程。
-
进程终止:
- 进程终止是指进程完成任务或由于错误或其他原因而结束。
- 操作系统需要释放进程占用的资源,回收内存、关闭文件描述符等。
- 父进程通常可以使用系统调用(如
wait
)来等待和监控子进程的终止状态。
-
进程状态管理:
- 操作系统维护进程的状态,通常包括就绪、运行、阻塞和终止状态。
- 进程可以在不同的状态之间转换,例如,一个就绪状态的进程可能会在被调度后进入运行状态,然后在等待I/O操作时进入阻塞状态。
-
调度和优先级:
- 操作系统使用调度算法来选择下一个要执行的进程。
- 进程通常具有不同的优先级,操作系统可以根据这些优先级来确定进程的执行顺序。
- 调度器负责决定哪个进程获得CPU时间,并在多任务系统中实现公平共享CPU资源。
-
进程间通信(IPC):
- 进程可能需要相互通信和协作。操作系统提供了各种IPC机制,如管道、消息队列、共享内存、套接字等,用于实现进程之间的数据交换和协作。
-
资源管理:
- 进程需要访问各种资源,如内存、文件、设备等。操作系统负责分配和管理这些资源,以确保它们被合理地共享和使用。
-
进程间关系:
- 进程可以有不同的关系,如父子关系、兄弟关系等。父进程通常可以创建子进程,并监控其状态。
- 进程之间的关系对于协作和数据共享非常重要。
-
错误处理:
- 操作系统需要处理进程中可能出现的错误,如内存访问冲突、非法指令、资源耗尽等。
- 错误处理包括进程终止、资源回收以及错误报告和日志记录等操作。
进程控制是操作系统中的一个核心功能,它负责管理、创建、终止、调度和监控进程,以确保计算机系统的有效运行。进程控制使得操作系统能够支持多任务处理、资源管理和进程间协作。
如何实现原语的"原子性"
原语的执行具有原子性,即执行过程必须一步到位,期间不允许被中断。可以使用"关中断指令"和"开中断指令"这两个特权指令实现原子性。
当CPU执行了关中断指令之后,就不在例行检查中断信号,直到执行了开中断指令之后才会恢复检查。
这样,关中断、开中断之间的这些指令序列就是不可被中断的,这就实现了"原子性"。
常见的两种进程控制的原语有两种,一种是创建原语(操作系统创建一个进程时使用的原语),一种是撤销原语。
创建原语在进程创建的时候执行:
- 申请空白的PCB
- 为新进程分配所需要的资源
- 初始化PCB
- 将PCB插入就绪队列
撤销原语在进程终止时执行:
- 从PCB集合中找到终止进程的PCB
- 若进程正在运行,立刻剥夺CPU,将CPU分配给其他线程
- 终止其所有子进程(进程间的关系是树形结构)
- 将该进程拥有的所有资源归还给父进程或者操作系统
- 删除PCB
阻塞原语在进程阻塞时执行:
- 找到要阻塞的进程对应的PCB
- 保护进程运行现场,将PBC状态信息设置为"阻塞态",暂时停止进程运行
- 将PCB插入相应事件的等待队列
唤醒原语在进程唤醒的时候执行:
- 在事件等待队列中找到PCB
- 将PCB从等待队列中移除,设置进程为就绪态
- 将PCB插入就绪队列,等待被调度
切换原语在进程需要切换的时候执行:
- 将运行环境信息存入PCB
- PCB移入相应队列
- 选择另一个进程执行,并更新其PCB
- 根据PCB恢复新进程所需要的运行环境
其中创建原语会使进程从创建态转变为就绪态,撤销原语使进程从就绪态/阻塞态/运行态转变为终止态,最后被操作系统删除。阻塞原语将进程从运行态转变为阻塞态,唤醒原语则是将进程从阻塞态转变为就绪态。切换原语会将当前进程从运行态转变为就绪态,然后再从就绪队列中找出一个就绪态的进程转变成运行态。
需要注意的是,进程因何事情被阻塞,就应该由何事唤醒。所以阻塞原语唤醒原语必须是成对使用的。
引起进程创建的事件有: ① 用户登录:分时系统中,用户登录成功,系统会为其建立一个新的进程。
② 作业调度:多道批处理系统中,当有新的作业放入到内存中时,会为其建立一个新的进程。
③ 提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求。
④ 应用请求:由用户进程主动请求创建一个子进程。
引起进程终止的事件有:① 正常结束(进程自己请求终止。例如exit系统调用)
② 异常结束(整数除以0、非法使用特权指令等,然后被操作系统强制杀掉)
③ 外界干预:用户选择杀掉进程(Ctrl + Alt + Delete)
引起进程阻塞的事件:① 需要等待系统分配某种资源
② 需要等待相互合作的其他进程完成工作
引起进程唤醒的事件:① 等待事件发生
引起进程切换的事件:① 当前进程的时间片到
② 有更高优先级的进程到达
③ 当前进程主动阻塞
④ 当前进程终止
进程通信(IPC)
进程之间的通信是指在计算机操作系统中,不同的进程之间通过特定的机制来交换数据、信息或信号的过程。进程通信通常用于实现多任务、协作任务或分布式计算等需求,使不同进程能够相互协作、协调工作,从而完成更复杂的任务。
进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。出于数据安全的考虑,两个进程之间是不能随意直接进行访问的(无论是数据的读取还是写入,都是不被允许的)
共享存储
虽然各个进程只能访问自己的存储空间,但是如果操作系统支持共享存储的功能,则这个进程可以申请一片共享存储区,这片共享存储区同样也可以被其他进程所共享。因此,当进程P像共享存储区存入数据时,则进程Q就可以共享这份数据。
基于存储区共享
操作系统在内存中划出一块共享存储区,数据的形式、存放位置都是由通信进程控制,而不是操作系统。这种共享方式速度很快,是一种高级通信方式。
为了避免出错,各个进程对共享空间的访问应该是互斥的。否则就会发生,多个进程向共享区的同一块区域进行写入,导致数据被覆盖等问题。各个进程可以使用操作系统提供的同步互斥工具(如P、V操作)。
基于数据结构的共享
假设操作系统给定的共享空间区域中,只能存放一个长度为3的数组,这种共享方式速度慢、限制多,是一种低级通信方式。
进程在对共享区域的读写操作的自由度远远没有基于存储区共享高。每次只能按照给定的数据结构的大小对数据进行读写。
消息传递
进程通过发送和接收消息来进行信息交流。这种通信方式通常用于分布式系统、并行计算和多进程环境中,允许不同的进程或线程之间传递数据、命令或通知。
进程间的数据交换以格式化的消息为单位。进程通过操作系统提供的"发送消息/接收消息"两个原语进行数据交换。
消息的结构如下图:
直接通信方式
在直接通信中,一个进程可以直接向另一个具体的进程发送消息,而不需要中间代理或共享数据结构。这种通信方式适用于明确的通信目标,消息发送者知道接收者的标识符或地址。
消息发送者主动地将消息发送给接收者,接收者也需要主动接收和处理消息。这种方式需要明确的发送和接收操作。
间接通信方式
间接通信通常使用消息队列、邮箱或共享数据结构作为中介,消息发送者将消息放置在队列或邮箱中(因此间接通信方式又被称为"信箱通信方式"),而消息接收者从中获取消息。消息队列可以支持多对多通信模式。
间接通信通常支持异步通信,即发送者可以将消息放置在队列中后继续执行其他任务,而不需要等待接收者立即处理消息。
管道通信
管道只能采用半双工通信,某一时间段内只能实现单向的传输。
如果要实现双向同时通信,则需要设置两个管道。
操作系统会实现各个进程互斥地访问管道。
当管道被写满时,写进程将会被阻塞,直到读进程将管道中的数据取走,即可唤醒写进程。
当管道中的数据一旦被读出之后,就会彻底消失。因此,当多个进程读同一个管道时,就可能会发生错乱。对此,通常会有两种解决方案
写进程往管道写入数据,即便管道没有被写满,只要管道没空,读进程就可以从管道读取数据。
读进程从管道读取数据,即便管道没有被读空,只要管道没满,写进程就可以往管道写入数据。
线程
在还没有引入进程之前,系统中各个程序只能串行执行。
在引入进程之后,进程是程序的一次执行。但是这些功能显然不可能是由一个程序顺序处理就能实现的。传统的进程是程序执行流的最小单位。
有的进程可能需要同时完成很多事情,而传统的进程只能串行地执行一系列程序。为此,引入了线程的概念,来增加并发度。引入线程之后,线程成为了程序执行流的最小单位。
一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件句柄等。线程之间可以更轻松地共享数据和通信,相比于进程,线程的创建、销毁和切换开销更小,因此在多任务处理中更为高效。不仅仅在进程之间可以并发,在进程内部的各个线程之间也可以并发,进一步提升了系统的并发度。
实现方式
线程的实现方式可以分为两大类:用户级线程(User-Level Threads)和内核级线程(Kernel-Level Threads)。每种方式都有其独特的特点和适用场景。
用户级线程
用户级线程是在应用程序中通过用户空间的库或框架来实现的线程。这些线程不依赖于操作系统内核的线程管理机制,而是由应用程序自己管理。以下是用户级线程的一些实现方式:
- 软件库:使用用户级线程库,例如POSIX线程库(pthread)、Windows线程库(Win32 Threads)或其他第三方线程库,来创建、调度和管理线程。这些库提供了API来创建和控制线程,通常使用线程控制块(Thread Control Block,TCB)等数据结构来管理线程的状态和上下文切换。
- 协作多任务:通过编写代码来实现协作多任务,例如使用协程或生成器函数。这种方式通常不涉及显式的线程创建和管理,而是通过函数调用和协作来实现并发执行。
- 用户级调度器:开发者可以编写自定义的用户级调度器,以实现特定的线程调度策略,例如轮转调度、优先级调度等。这需要更多的工作,但可以实现更高度的线程控制。
用户级线程的优点包括轻量级、快速线程创建和销毁,不需要切换到核心态,线程管理的系统开销小,效率高、灵活性高。
然而,当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高,多个线程不可在多核处理机上并行运行。
内核级线程
内核级线程是由操作系统内核直接管理的线程,每个线程对应于内核中的一个线程控制块(Thread Control Block,TCB)。以下是一些实现方式:
- 操作系统支持:现代操作系统通常提供原生的线程支持,例如Linux上的pthread、Windows上的Win32 Threads等。这些线程由操作系统内核管理,可以充分利用多核处理器和操作系统的多任务调度能力。
- 多线程库:开发者可以使用多线程库来创建和管理内核级线程。这些库提供了API,简化了线程的创建和管理,但底层仍然依赖于操作系统的线程管理机制。
内核级线程的优点包括能够充分利用硬件资源、更好的并发性能和稳定性。然而,线程的创建和销毁通常比用户级线程慢,因为涉及到系统调用和内核的介入。
内核级线程的优点有:
- 多核利用率高: 内核级线程由操作系统内核直接管理,能够充分利用多核处理器,实现真正的并行执行。每个内核级线程可以分配到不同的核心上执行,提高了应用程序的性能。
- 多任务调度: 操作系统内核能够进行多任务调度,因此内核级线程可以在多个进程之间进行切换,实现多任务并发。这使得应用程序能够更好地响应多个用户或处理多种任务。
- 稳定性: 内核级线程通常比用户级线程更稳定,因为操作系统可以监控线程的行为,防止线程之间的冲突和错误。此外,操作系统可以提供进程隔离,确保一个线程的问题不会影响到其他线程或进程。
- 资源管理: 内核级线程能够更好地管理系统资源,如内存、文件描述符等。操作系统可以分配和回收资源,确保线程能够正常运行。
- 阻塞操作: 内核级线程可以执行阻塞操作,如等待I/O完成,而不会导致整个进程的阻塞。这允许应用程序更有效地利用等待时间来执行其他任务。
内核级线程的缺点:
- 创建和销毁开销大: 内核级线程的创建和销毁通常较慢,因为它们需要进行系统调用,并涉及内核的介入。这会导致线程的创建和销毁开销相对较高。
- 资源消耗: 每个内核级线程都需要分配一定的内核资源,包括线程控制块和栈空间。因此,当创建大量线程时,会消耗较多的系统资源,可能会导致系统资源不足。
- 复杂性: 内核级线程需要操作系统支持,因此线程的创建和管理相对复杂。开发者需要使用多线程库或API来实现内核级线程,这可能需要更多的代码和工作。
- 不适合轻量级任务: 对于一些轻量级的任务,内核级线程的开销可能过大。在这种情况下,使用用户级线程可能更合适,因为用户级线程的切换开销较小。
上图也被称为一对一模型:一个用户线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。
多线程模式
一对一模型(内核级线程):一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。
优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
多对一模型(用户级线程):多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程。
优点:用户级线程的切换在用户空间即可完成,不需要切换到内核态,线程管理的系统开销小,效率高。
缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。
注意:操作系统只能识别到内核级线程,因此只有内核级线程才是处理机分配的单位。
多对多模型 :n 用户及线程映射到 m 个内核级线程( n ⩾ \geqslant ⩾ m)。每个用户进程对应 m 个内核级线程。
其克服了多对一模型并发度不高的缺点(即一个阻塞全体阻塞),又克服了一对一模型中用户进程占用太多内核级线程,开销太大的缺点。
状态与转换
线程的状态与转换和进程的状态与转换非常相似。线程的状态与转换主要关注阻塞 、就绪 、运行三种状态及其之间的转换,如下图
组织与控制
线程的组织和控制是多线程应用程序中的重要方面,它涉及到如何创建、启动、管理和协调线程以实现并发执行任务的目标。以下是线程的组织和控制的关键方面:
-
线程的创建: 创建线程是多线程应用程序的第一步。线程可以通过操作系统提供的线程库或编程语言提供的多线程API来创建。一般来说,线程的创建需要指定线程的入口函数和传递参数。此时线程会创建一个类似于进程控制块一样的线程控制块(TCB)。
-
线程的启动: 创建线程后,需要启动线程以开始执行。线程的启动通常由调用线程库或API中的特定函数来完成。线程启动后,它进入就绪状态并等待被调度。
-
线程的调度: 线程的调度由操作系统的线程调度器负责。调度器决定哪个线程在某一时刻执行,并分配 CPU 时间片给线程。线程的调度策略可以是抢占式的(即使一个线程的时间片用完,可以被剥夺CPU)或非抢占式的(直到线程主动放弃CPU控制权才会切换)。
-
线程的同步与互斥: 多个线程可能需要共享数据或资源,因此需要进行同步和互斥控制,以避免竞态条件和数据不一致性问题。常见的同步机制包括互斥锁、信号量、条件变量等。
-
线程的通信: 多个线程之间可能需要进行通信,以实现协作和数据传递。线程通信的方式包括共享内存、消息队列、管道等。通信机制依赖于线程库和编程语言。
-
线程的等待与唤醒: 线程可以等待某个条件满足或被唤醒。等待和唤醒通常使用条件变量或信号量来实现。线程等待特定事件或条件时,它会释放 CPU 控制权,等待其他线程唤醒它。
-
线程的终止: 线程可以正常完成任务后自行终止,或者被外部线程手动终止。线程的终止通常涉及资源的清理和状态的更新。
-
线程的控制和管理: 在多线程应用程序中,可能需要对线程进行控制和管理,例如设置线程的优先级、暂停和恢复线程、查询线程状态等。这些操作通常由线程库或API提供。
-
线程的错误处理: 多线程应用程序可能会面临线程崩溃、死锁和竞态条件等问题。因此,需要实现适当的错误处理机制来处理这些问题,以确保应用程序的稳定性和可靠性。
线程的组织和控制是多线程编程的关键部分,需要谨慎设计和管理,以确保线程能够协同工作并避免常见的多线程问题。正确的线程组织和控制可以提高应用程序的性能和并发能力,但也需要处理同步和互斥问题,以避免潜在的错误和竞争条件。
处理机调度
当有一堆任务需要进行处理,但是由于资源有限,这些事情无法同时进行处理。这就需要确定某种规则来决定处理这些任务的顺序。这就是调度。
概念
处理机调度是计算机操作系统中的一个重要概念,它涉及到如何有效地管理和分配计算机系统中的中央处理器(CPU)资源,以便多个进程(或任务)能够共享CPU,并且系统能够高效地运行。
以下是处理机调度的概念:
-
进程:进程是计算机系统中正在执行的程序的实例。每个进程都有自己的代码、数据和执行环境。处理机调度的主要任务之一是决定哪个进程将在CPU上执行。
-
调度策略:调度策略是指决定进程在CPU上执行顺序的算法或规则。不同的调度策略可以影响系统的性能和响应时间。常见的调度策略包括先来先服务(FCFS)、最短作业优先(SJF)、轮转调度(Round Robin)和优先级调度等。
-
就绪队列:就绪队列是存放处于就绪状态的进程的数据结构。就绪状态的进程已准备好运行,只需等待CPU资源。处理机调度程序从就绪队列中选择下一个要执行的进程。
-
阻塞队列:阻塞队列是存放处于阻塞状态的进程的数据结构。阻塞状态的进程通常在等待外部事件(如I/O操作完成)时被挂起,并且不能立即执行。一旦等待的事件发生,进程将从阻塞队列移至就绪队列。
-
上下文切换:上下文切换是当操作系统决定切换到另一个进程时,保存当前进程的执行状态(如寄存器的内容和程序计数器的值),然后加载下一个进程的状态,以便它可以继续执行。上下文切换会引入一些开销,因此调度策略的选择应考虑最小化上下文切换的次数。
-
周转时间和等待时间:周转时间是一个进程从提交到完成的总时间,包括等待时间和执行时间。等待时间是进程在就绪队列中等待的时间。
-
响应时间:响应时间是从用户提交一个任务到系统开始执行该任务的时间间隔。快速响应时间对于交互性应用程序非常重要。
不同的计算机系统和应用场景可能需要不同的处理机调度策略,以满足性能、响应时间和资源利用率的要求。处理机调度是操作系统的一个关键组成部分,可以帮助系统有效地利用CPU资源,提高系统的性能和可用性。
高级调度
高级调度,也称为长期调度或作业调度,是处理机调度中的一种重要策略,它负责从外部提交的作业队列中选择哪些作业进入系统执行。**每个作业只调入一次,调出一次。**作业调入时会建立PCB,调出时才会撤销PCB。
高级调度的主要目标是有效地利用系统资源,确保系统的吞吐量和性能。
当然内存空间是很有限的,有时无法将用户提交的作业全部放入内存中:
以下是高级调度的任务:
-
作业队列:高级调度管理着作业队列,这是一组等待执行的作业或任务。这些作业通常是由用户提交的,它们等待进入系统并分配给CPU。
-
选择作业:高级调度负责从作业队列中选择哪些作业将进入系统执行。选择的依据可以是多种因素,包括作业的优先级、资源需求、系统负载等。高级调度的目标是确保系统能够高效地运行,并且不过度拥挤。
-
预测系统负载:高级调度需要预测系统的负载情况,以便根据系统的当前状态来选择适当数量的作业进入执行。如果系统处于高负载状态,高级调度可能会选择较少的作业,以避免过度竞争CPU资源。
-
资源管理:高级调度需要考虑作业对系统资源的需求,例如内存、I/O设备等。它可能会根据系统资源的可用性来选择作业,以避免资源争用问题。
-
作业优先级:高级调度可以使用作业的优先级来确定哪些作业应该首先进入系统执行。通常,高优先级的作业会比低优先级的作业更早执行。
-
避免饥饿和死锁:高级调度需要确保所有作业都有机会进入系统执行,避免饥饿现象。此外,它还需要避免死锁情况,即系统中的作业相互等待资源,无法继续执行。
高级调度是一个重要的调度阶段,它在系统层面上决定哪些作业可以进入系统执行。它的目标是平衡系统的性能和资源利用率,确保系统在高效运行的同时不会过载。高级调度通常由操作系统的调度器(scheduler)或作业调度器(job scheduler)负责执行。与高级调度相对应的是低级调度(或进程调度),它负责在就绪队列中选择哪些进程将在CPU上执行。
中级调度
中级调度,也称为内存调度,它负责管理处于内存中的进程,决定哪些进程应该暂时被置于挂起状态,以便腾出内存空间供其他进程执行。中级调度在进程管理中起到了重要的作用,帮助系统有效地利用有限的内存资源,同时确保各个进程得到公平的执行机会。
按照某种策略决定将哪个处于挂起状态的进程重新调入内存。一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高。
以下是中级调度的一些任务:
-
进程状态管理:中级调度负责管理进程的状态,通常将处于就绪状态的进程从内存中暂时移除,将其置于挂起状态。这些进程的上下文信息被保存到外部存储(如硬盘)中,以释放内存空间。
-
内存管理:中级调度协助操作系统进行内存管理。通过将一些进程置于挂起状态,系统可以释放内存,以便为新进程提供足够的内存空间。这有助于避免内存过度占用和系统的死锁问题。
-
挂起和恢复进程:中级调度负责挂起(暂停)进程并将其状态保存到外部存储。当需要恢复进程时,中级调度负责将进程的状态重新加载到内存中,并将其放回到就绪队列,以便它可以继续执行。
-
资源释放:中级调度可以协助释放进程占用的资源,如内存、I/O设备等,以便这些资源可以被其他进程使用。
-
进程优先级:中级调度可以根据进程的优先级来决定哪些进程应该被挂起。通常,低优先级的进程更容易被挂起,以保留内存供高优先级的进程使用。
总之,中级调度是操作系统中进程管理的一个关键部分,它帮助系统维护内存中的进程集合,并确保系统能够高效地运行。通过在进程之间切换,并根据系统资源的可用性来挂起和恢复进程,中级调度有助于平衡系统的性能和资源利用率。与中级调度相对应的是高级调度(作业调度),负责选择哪些作业可以进入系统执行,以及低级调度(进程调度),负责在就绪队列中选择哪些进程将在CPU上执行。
低级调度
低级调度又称进程调度/处理机调度,按照某种策略从就绪队列中选取一个进程,将处理机分配给它。
进程调度时操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。进程调度的频率很高,一般几十毫秒就会触发一次。
低级调度,也称为进程调度或进程切换,是计算机操作系统中的一个关键组成部分,它负责从就绪队列中选择一个进程,将其加载到CPU并开始执行。低级调度控制了CPU资源的分配,以确保多个进程能够共享CPU,并决定哪个进程将在特定的时间片内执行。
以下是低级调度的任务:
-
进程切换:低级调度负责进程之间的切换。当一个进程的时间片用尽或它主动放弃CPU时,低级调度会选择下一个进程,将其上下文加载到CPU寄存器中,然后开始执行。
-
时间片管理:低级调度通常使用时间片轮转(Round Robin)或其他调度算法来管理每个进程被分配的时间片。时间片是一个小的时间段,通常在几毫秒到几十毫秒之间,它控制了每个进程在CPU上执行的时间。
-
上下文切换:在进行进程切换时,低级调度需要执行上下文切换操作。这包括保存当前进程的执行状态(寄存器值、程序计数器等),加载下一个进程的状态,以及切换内存映射等。
-
公平性和响应时间:低级调度的目标之一是确保各个进程获得公平的CPU时间,以避免某个进程占用CPU导致其他进程无法响应。同时,低级调度也需要保证系统对用户请求的快速响应时间。
-
调度算法:不同的操作系统可以使用不同的低级调度算法来选择下一个执行的进程。常见的算法包括轮转调度(Round Robin)、最短剩余时间优先(SRTF)、多级反馈队列等。
-
阻塞和唤醒:低级调度也需要处理进程的阻塞和唤醒。当一个进程在等待某些事件(如I/O操作完成)时被阻塞,低级调度会将其暂时移出执行队列。一旦等待的事件发生,进程被唤醒并重新加入队列。
总之,低级调度是操作系统中管理CPU资源分配的关键部分。它决定了哪个进程将在CPU上执行,以及每个进程执行的时间片。通过高效地执行进程切换和上下文切换操作,低级调度有助于确保系统能够高效地运行,同时平衡各个进程的执行需求。
调度时机
进程调度的时机是操作系统中的一个重要决策,它决定了何时以及哪个进程将被选择执行在CPU上。
需要进行进程调度与切换的情况:
-
当前进程执行完毕:当一个进程执行完了它的时间片(对于轮转调度)或者它的任务(对于其他调度算法),操作系统会触发进程切换,选择下一个进程执行。
-
当前进程主动让出CPU:有些进程可能会自愿放弃CPU的控制权,例如,当它需要等待某个事件发生时(如等待I/O操作完成),它可以请求操作系统将CPU分配给其他就绪进程。这个过程通常是通过系统调用(如
yield()
或sleep()
)来实现的。 -
进程被阻塞:如果一个进程在等待某个事件(如等待文件读取完成)时被挂起,操作系统会将其从CPU上移除,并将其状态置为阻塞。一旦等待的事件发生,操作系统会唤醒该进程,并将其移回就绪队列。
-
高级调度或中级调度:高级调度(作业调度)和中级调度(进程调度)也可以触发进程调度的时机。当新的作业或进程被提交到系统时,高级调度会决定何时将其加入到就绪队列。中级调度会管理内存中的进程,将某些进程置于挂起状态以腾出内存。这些操作都可能导致进程调度的发生。
-
中断和时钟中断:操作系统会周期性地接收时钟中断,这些中断可以用于触发进程调度。时钟中断发生后,操作系统可以检查当前运行进程的时间片是否已用完,如果是,则进行进程切换。
-
优先级调度:如果系统支持动态优先级调度,那么进程的优先级可能在运行时发生变化,当某个进程的优先级发生变化时,操作系统可能会根据新的优先级重新调度进程。
不能进行进程调度与切换的情况:
- 中断处理:当操作系统正在处理重要的中断(如硬件故障或异常中断)时,通常会禁止进行进程调度和切换,以确保中断处理程序能够迅速执行。这是为了保证系统的稳定性和可靠性。
- 关中断:在某些情况下,操作系统会关闭或禁用中断,以防止硬件中断干扰关键操作。在这种情况下,进程调度和切换可能会被暂时禁用,直到中断被重新启用。
- 原子操作:某些操作需要在一个原子操作中完成,即在执行期间不能被中断或切换到其他进程。在这种情况下,进程调度和切换可能会受到限制。
- 内核临界区 (并不是普通的临界区):内核代码中的临界区可能需要在不被中断的情况下执行,以避免竞争条件和数据不一致。在这些临界区域内,进程切换通常会被禁用。(
tips:
临界区为访问一段时间内只允许一个进程使用的资源的那段代码,而内核程序临界区一般是用来访问某种内核数据结构的,例如进程中的就绪队列) - 实时系统:实时操作系统需要满足特定的时间限制,以确保任务能够按时完成。在某些实时系统中,进程切换可能会被限制,以确保系统能够满足实时需求。
- 关键任务:某些任务可能对系统的性能和稳定性至关重要,进程调度和切换可能会受到限制,以确保这些任务能够获得足够的CPU时间。
调度方式
分为两种,一种是非剥夺调度方式 ,另一种是剥夺调度方式。
非剥夺调度方式:又称为非抢占式。即,只允许进程主动放弃处理机。在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态。该方式实现简单,系统开销小但是无法及时处理紧急任务,适合于早期的批处理系统。
剥夺调度方式:又称抢占方式。当一个进程正在处理机上执行时,如果有一个更重要或者更紧迫的进程需要使用处理机,则会立即暂停正在执行的进程,将处理机分配给更重要紧迫的进程。可以优先处理更紧急的进程,也可以实现让各进程按事件片轮流执行的功能(通过时钟中断)。适合分时操作系统、实时操作系统。
三层调度之间的对比与联系
这三种调度层次在管理和分配系统资源方面有不同的任务和职责,但它们共同协作以确保系统能够高效地运行。下面是它们之间的对比和关系:
-
高级调度(作业调度):
- 负责从外部提交的作业队列中选择哪些作业进入系统执行。
- 主要关注系统的吞吐量、资源利用率和作业优先级。
- 选择哪些作业可以进入系统,以及它们的执行顺序,以最大程度地满足用户需求。
- 决定哪些作业可以进入系统后,将它们放入就绪队列等待低级调度。
- 不涉及具体进程的执行,而是处理作业级别的任务。
-
中级调度(内存调度):
- 负责管理内存中的进程,确保内存资源得以充分利用。
- 控制哪些进程应该被暂时挂起(置于挂起状态),以腾出内存空间供其他进程执行。
- 将进程从内存中移动到外部存储(如硬盘),并在需要时将其重新加载到内存中。
- 与内存管理和挂起/恢复进程相关联,以确保系统能够有效地运行。
- 着眼于平衡内存资源的使用和系统性能。
-
低级调度(进程调度):
- 负责在就绪队列中选择一个进程,将其加载到CPU并开始执行。
- 控制CPU资源的分配,以确保多个进程能够共享CPU,同时确保公平性和响应时间。
- 使用调度算法(如轮转调度、最短剩余时间优先等)来选择下一个执行的进程。
- 处理进程的切换、上下文切换以及进程的阻塞和唤醒操作。
- 确保各个进程能够获得合理的CPU时间,并根据优先级来控制进程的执行。
三者的关系:
- 这三种调度层次在系统中协同工作,每个层次有自己的任务和职责,但它们相互关联以维护系统的平衡和高效性。
- 高级调度选择作业并将它们置于就绪队列,然后低级调度从就绪队列中选择进程进行执行。
- 中级调度通过管理内存中的进程,可以影响哪些进程在就绪队列中,从而间接影响低级调度的选择。
- 这些调度层次协同工作,以确保系统资源的合理分配、进程的公平执行和系统性能的最优化。
总结表格如下:
执行内容 | 发生地点 | 发生频率 | 对进程状态的影响 | |
---|---|---|---|---|
高级调度 (作业调度) | 按照某种规则,从后备队列中选择合适的作业将其调入内存,并为其创建进程 | 外存 => 内存 (面向作业) | 最低 | 无 => 创建态 => 就绪态 |
中级调度 (内存调度) | 按照某种规则,从挂起队列中选择合适的进程将其数据返回内存 | 外存 => 内存 (面向进程) | 中等 | 挂起态 => 就绪态 (阻塞挂起 => 阻塞态) |
低级调度 (进程调度) | 按照某种规则,从就绪队列中选择一个进程为其分配处理机 | 内存 => CPU | 最高 | 就绪态 => 运行态 |
高级、中级和低级调度是操作系统中不可或缺的组成部分,它们协同工作以管理作业和进程,确保系统能够高效地运行,并为用户提供合理的响应时间。不同的调度层次在不同的抽象级别上进行资源管理和调度,以实现整体系统的平衡和优化。
补充
暂时调到外存中等待的进程状态为挂起状态(挂起态)。挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态。
注意"挂起"和"阻塞"的区别,两种状态都是暂时不能获得CPU的服务,但挂起态是将进程映像调到外存去了,而阻塞态下进程映像还在内存中有的操作系统会把就绪挂起、阻塞挂起分为两个挂起队列,甚至会根据阻塞原因不同再把阻塞挂起进程进一步细分为多个队列。
调度程序
调度器(也称为调度程序)是操作系统的一个关键组件,负责管理和控制计算机系统中的进程和资源。调度器的主要任务是按照一定的规则和算法来分配CPU时间和其他系统资源,以使多个进程能够共享CPU,并确保系统运行平稳、高效。
以下是有关调度器的一些概念:
-
功能:
- 调度器的主要功能包括选择下一个要在CPU上执行的进程,以及确定进程的执行顺序。
- 它还管理进程的状态转换,包括从就绪状态到运行状态的切换,以及从运行状态到就绪状态或阻塞状态的切换。
- 调度器通常根据进程的优先级、调度算法和特定策略来做出决策。
-
调度算法:
- 调度器使用不同的调度算法来选择下一个要执行的进程。常见的调度算法包括先来先服务(FCFS)、最短作业优先(SJF)、轮转调度(Round Robin)、多级反馈队列调度(MLFQ)、最高响应比优先(HRRN)、优先级调度等。
- 不同的调度算法适用于不同的应用场景和系统需求。
-
上下文切换:
- 调度器负责执行上下文切换操作,这是将CPU从一个进程切换到另一个进程的过程。上下文切换包括保存当前进程的上下文(寄存器、程序计数器等)、加载下一个进程的上下文以及更新系统状态。
- 上下文切换会引入一定的开销,因此需要谨慎管理,以减少不必要的切换。
-
多核处理器:
- 在多核处理器系统中,调度器需要考虑如何有效地分配进程到不同的CPU核心,以充分利用多核架构的性能。
- 多核处理器调度也涉及到负载平衡和数据同步等问题。
-
实时调度:
- 对于实时系统,调度器需要确保任务能够在规定的时间限制内完成。实时调度器通常采用特殊的算法来满足实时任务的需求,如周期性调度和最早截止时间优先(EDF)等。
-
用户空间和内核空间:
- 调度器可以分为用户空间调度器和内核空间调度器。用户空间调度器通常由用户程序或用户态库实现,而内核空间调度器由操作系统内核管理。
- 内核空间调度器更加强大,可以管理系统的全部资源,而用户空间调度器仅能管理用户级别的进程。
在不支持内核级线程的操作系统中,调度程序的处理对象是进程:
支持内核级线程的操作系统,调度程序的处理对象是内核线程:
调度器是操作系统的一个关键组件,它负责管理和协调进程的执行,以实现多任务并发。调度器的设计和性能对于操作系统的整体性能和响应能力至关重要。
闲逛进程
"闲逛进程" 是操作系统中的一种特殊进程,通常也称为 "空闲进程" 或 "空闲任务"。它是为了充分利用系统资源而创建的一种进程。闲逛进程的主要目的是在系统没有其他任务需要执行时,使CPU保持繁忙状态,防止 CPU 等待或闲置。
闲逛进程的特点包括:
-
无实际工作:闲逛进程不执行任何有用的计算任务。它不负责运行用户应用程序或执行系统任务。其唯一目的是让 CPU 保持繁忙状态。
-
循环执行:闲逛进程通常包含一个无限循环,其内容可能是简单的指令序列,例如空的 while 循环。这使得 CPU 在不断执行指令,而不会退出或休眠。
-
低优先级:闲逛进程通常具有较低的优先级,这意味着当有其他进程需要执行时,它会被立即抢占,让其他任务优先执行。
-
资源利用:通过占用 CPU 时间,闲逛进程可防止系统进入完全空闲状态,从而确保 CPU 和其他资源保持活跃状态。
-
节能和热管理:在某些系统中,闲逛进程还可以用于节能和热管理。通过让 CPU 保持繁忙状态,可以防止 CPU 进入节能模式或降低频率,从而保持 CPU 温度和能耗在合理范围内。
需要注意的是,闲逛进程的存在是为了充分利用系统资源,而不是为了执行有意义的任务。在多任务操作系统中,当没有其他进程需要执行时,闲逛进程会被调度,以避免 CPU 闲置。
不同的操作系统可能使用不同的闲逛进程实现方式,但它们都具有相似的目标:保持系统资源的有效使用和响应能力。
调度算法的评价指标
该评价指标分为四个部分:CPU利用率、系统吞吐量、周转时间、等待时间、响应时间。
CPU利用率
指的是CPU忙碌的时间占总时间的比例。所以其利用率的算法为:
利用率 = 忙碌的时间 总时间 {\frac{忙碌的时间}{总时间}} 总时间忙碌的时间
系统吞吐量
衡量在单位时间内完成的进程数量。更高的吞吐量通常表示算法能够处理更多的进程。
系统吞吐量 = 总共完成作业数 花费的总时间 {\frac{总共完成作业数}{花费的总时间}} 花费的总时间总共完成作业数
周转时间
进程从提交给系统开始,到作业完成为止的平均时间。较低的平均周转时间通常表示算法能够更快地完成任务。
其包含四个部分:作业在外存后备队列上等待作业调度(高级调度)的时间、进程在就绪队列上等待进程调度(低级调度)的时间、进程在CPU上 的执行时间、进程等待I/O操作完成的时间。后三项在一个作业的整个处理过程中,可能发生多次。
周转时间 = 作业完成时间 - 作业提交时间
平均周转时间 = 各作业周转时间之和 作业数 {\frac{各作业周转时间之和}{作业数}} 作业数各作业周转时间之和
带权周转时间 = 作业周转时间 作业实际运行的时间 {\frac{作业周转时间}{作业实际运行的时间}} 作业实际运行的时间作业周转时间
平均带权周转时间 = 各作业带权周期时间之和 作业数 {\frac{各作业带权周期时间之和}{作业数}} 作业数各作业带权周期时间之和
等待时间
所有进程等待执行的时间。较低的平均等待时间通常表示算法能够提供更好的响应时间。等待时间越长,用户满意度越低。
对于进程来说,等待时间就是指进程建立后等待被服务的时间之和,在等待I/O完成的期间其实进程也是在被服务的,所以不计入等待时间。
对于作业来说,不仅要考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间。
与上面的指标类似,也会有一个平均等待时间来评价整体性能。
等待时间 = 周转时间 - 运行时间 (- I/O操作的时间)
如果针对纯计算型的进程(一个进程到达后要么在等待,要么在运行),则等待时间为周转时间减去运行实际那。如果既有计算、又有I/O操作的进程,则其等待时间就是周转时间减去运行时间,再减去I/O操作时间。
响应时间
进程从提交到首次获得CPU执行的时间。对于交互式应用程序,低响应时间至关重要。
调度算法
将从算法思想、算法规则、该调度算法是用于作业调度还是进程调度、是否为抢占式、优缺点、是否会导致饥饿现象(某进程/作业长期得不到服务)等这几方面进行分析学习。
先来先服务(FCFS)
- 算法思想:按照进程到达的顺序执行。
- 算法规则:按照队列中进程的顺序执行,不抢占正在执行的进程。
- 用于作业/进程调度:用于作业调度时,考虑的是哪个作业先到达后备队列;用于进程调度时,考虑的是哪个进程先到达就绪队列。
- 抢占式:非抢占式,一个进程开始执行后不会被中断。
- 优点:实现简单,公平。
- 缺点:排在长进程(作业)后面的短进程需要等待很长时间,其带权周转时间很大,对短作业的用户体验很不好。
- 饥饿现象:不会出现饥饿现象。
最短作业优先(SJF)
- 算法思想:选择执行时间最短的进程执行。追求最少的平均等待时间,最少的平均周转时间、最少的平均带权周转时间。
- 算法规则:选择最短估计执行时间的进程,不抢占正在执行的进程。
- 用于:主要用于作业调度,也可用于进程调度(短进程优先算法:SPF)。
- 抢占式 :SJF与SPF是非抢占式算法(每次调度时选择当前已经到达且运行时间最短的作业/进程)。但是也有抢占式的版本------最短剩余时间优先算法(SRTN,每当有进程加入就绪队列时就需要调度,如果新到达的进程剩余时间比当前运行的进程的剩余时间更短,则由新进程抢占处理机,当前运行进程重新回到就绪队列)
- 优点:最优的平均等待时间、平均周转时间(在所有进程都几乎同时到达时,采用SJF调度算法的平均等待时间、平均周转时间最少)。
- 缺点:需要准确估计执行时间,不适用于实时系统。对于短作业有利,长作业不利。
- 饥饿现象:可能发生,长任务会导致短任务等待。如果远远不断的短进程/作业到来,可能使长作业/进程长时间得不到服务。
SJF非抢占式算法:
SJF抢占式算法(SRTN最短剩余时间算法)
最高响应比优先(HRRN)
- 算法思想:综合考虑作业/进程的等待时间和要求服务的时间
- 算法规则:在每次调度时先计算各个作业进程/作业的响应比,选择响应比最高的进程/作业为其服务。
- 用于:主要用于进程调度,也可用于进程调度。
- 抢占式:非抢占式的算法。因此只有当前运行的进程/作业主动放弃处理机时,才需要调度,才需要计算响应比。
- 优点:综合考虑了等待时间和运行时间。当等待时间相同时,要求服务时间短的优先;当要求服务时间相同时,等待时间长的优先。对于长作业来说,随着等待时间越来越久,其响应比也会越来越大,从而避免了长作业饥饿的问题。
- 缺点:需要计算响应比,可能引入较大的计算开销。
- 饥饿现象:不会导致饥饿现象。
其中的响应比的计算公式为:响应比 = 等待时间 + 要求服务时间 要求服务时间 {\frac{等待时间+要求服务时间}{要求服务时间}} 要求服务时间等待时间+要求服务时间
时间片轮转调度(RR)
- 算法思想:按照时间片轮流执行进程。公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。
- 算法规则:将CPU时间分成时间片,进程轮流执行,可以抢占正在执行的进程。按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片。若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列队尾重新排队。
- 用于:用于进程调度(只有作业放入内存建立了相应的进程后,才能被分配处理机时间片)。
- 抢占式:通常是抢占式的,时间片用完会进行切换。若进程未能在时间片内运行完,将被强行剥夺处理机使用权。由时钟装置发出时钟中断来通知CPU时间片已到。
- 优点:公平,响应快,适用于分时系统。
- 缺点:可能引入上下文切换(高频率的进程切换)开销,不适用于长时间任务。不区分任务的紧急程度。(时间片不宜过大或过小,否则都会增加开销)
- 饥饿现象:不会导致饥饿现象。
当时间片大小为2时:
0时刻:0时刻只有P1到达就绪队列,让P1处理机运行一个时间片。
2时刻:2时刻时P2到达就绪队列,P1运行完一个时间片,被剥夺处理机,重新放到队尾。此时的P2排在队头,因此P2上处理机 (默认新到达的进程先进入就绪队列)
4时刻:3时刻时,P3到达,先插到就绪队尾,紧接着P2下处理机也插入到队尾。
5时刻:5时刻时,P4到达插入到就绪队尾(此时P1的时间片还没有用完,因此暂时不调度。另外,此时P1处于运行态,并不在就绪队列中)。
6时刻:6时刻时,P1的时间片用完,下处理机,重新放回就绪队尾,发生调度。
7时刻:虽然P3的时间片还没用完,但是由于P3只需要运行1个单位的时间,运行完成之后就会主动放弃处理机,因此也会发生调度。队头进程P2上处理机。
9时刻:进程P2时间片用完,并刚好运行结束,发生调度,P4上处理机。
11时刻:P4时间片用完,重新回到就绪队列,P1上处理机。
12时刻:P1运行结束,主动放弃处理机,此时就绪队列中只剩下P4,则P4上处理机。
14时刻:就绪队列为空,则此时让P4进程接着运行一个时间片。
16时刻:所有进程运行结束。
当时间片大小为5时:
从上面可以看出:
如果时间片太小,会导致进程切换过于频繁,系统会花费大量的时间来处理进程切换,从而导致实际用于进程执行的时间比例会减少。
如果时间片太大,使得每个进程都可以在一个时间片内完成,则时间片轮转调度算法就退化成先来先服务调度算法,并且会增大进程的响应时间。
一般来说,设计时间片时要让切换进程的开销占比不超过1%
优先级调度
- 算法思想:随着实时操作系统的出现,越来越多的应用场景需要根据任务的紧急程度来决定处理顺序。
- 算法规则:会按照每个进程/作业各自的优先级来选择调度优先级最高的进程/作业。
- 用于:既用于进程调度,也可以用于进程调度。
- 抢占式:非抢占式只需在进程主动放弃处理机时进行调度即可。而抢占式还需要再就绪队列变化时,检查是否会发生抢占。
- 优点:用优先级区分紧急程度、重要程度,适用于实时操作系统。可以灵活地调整对各种进程/作业的偏好程度。
- 缺点:若源源不断地有高优先级进程到来,则可能会导致饥饿。
- 饥饿现象:会导致饥饿现象。
非抢占式优先级调度:
抢占式优先级调度:
多级反馈队列调度算法
- 算法思想 :将进程按照不同的优先级划分成多个队列,每个队列的优先级不同,通常从高到低。高优先级队列中的进程具有更高的执行优先级,低优先级队列中的进程具有较低的执行优先级。
进程首先进入最高优先级队列,并根据一定的调度规则获得CPU时间。如果一个进程在其时间片内未完成,它将被移到较低优先级的队列中。反之,如果一个进程在较低优先级队列中执行完毕,它将重新回到较高优先级队列。
进程可以在不同队列之间循环移动,具体取决于其执行时间和行为。 - 算法规则 :
- 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大。
- 新进程到达时会优先进入第一级队列,按照FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时已经是最下级的队列,则重新放回该队列队尾。
- 只有第k级队列为空时,才会为k+1级队头的进程分配时间片。
- 用于:用于进程调度
- 抢占式:抢占式算法。在k级队列的进程运行过程中,若更上级的队列(1~k-1级)中进入了一个新进程,则由于新进程处于优先级更高的队列中,因此新进程会抢占处理机,原来运行的进程放回k级队列的队尾。
- 优点:适应性强,可以处理各种不同类型的任务,包括短作业和长时间任务。公平性较高,因为所有任务都有机会获得CPU时间。能够提供较好的响应时间,因为高优先级任务在较高优先级队列中有更多的执行机会。
- 缺点:实现相对复杂,需要管理多个队列和调整参数。
- 饥饿现象:如果有源源不断的高优先级任务进入高优先级队列,则会导致低优先级的任务可能很长时间得不到运行。
多级队列调度算法
先来先服务、最短作业优先、最高响应比优先这三种算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心"响应时间",也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。因此这三种算法一般适合用于早期的批处理系统。
比起早期的批处理操作系统来说,由于计算机造价大幅度降低,因此之后出现的交互式操作系统(如分时操作系统、实时操作系统等)更注重于系统的响应时间、公平性、平衡性等指标。而时间片轮转调度、优先级调度、多级反馈队列调度三种算法也能比较好地满足交互式系统的需求。因此这三种算法适合用于交互式系统(如UNIX使用的就是多级反馈队列调度算法)。
进程同步
进程的并发性带来了异步性(各个并发的进程独立的以不可预知的速度向前推进),而有的进程则需要有次序的相互配合来完成作业,所以有了进程同步。
进程同步是指多个进程在执行过程中按照一定的顺序和协调来共享和访问共享资源,以避免竞态条件(Race Condition)和确保数据的一致性。进程同步是多进程编程中的关键问题,特别是在多进程环境下,多个进程需要协作完成某个任务时。
-
竞态条件(Race Condition) :
竞态条件是指多个进程同时访问共享资源,导致不可预测的结果或数据不一致。竞态条件通常发生在以下情况下:多个进程试图同时读取和写入共享资源,而且执行顺序不确定。
-
临界资源 :
临界资源可以是内存中的变量、文件、数据库、网络连接等,多个进程需要对这些资源进行读取和修改。虽然多个进程可以共享系统中的各种资源,但其中许多资源一段时间内只能为一个进程所使用,我们把一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机等。
-
进程同步的目标 :
进程同步的主要目标是确保多个进程能够按照一定的顺序和规则访问共享资源,以避免竞态条件,并确保数据的一致性和正确性。
-
常见的进程同步机制:
a. 互斥锁(Mutex):互斥锁可以用来保护共享资源,只允许一个进程或线程在同一时刻访问资源。其他进程如果要访问资源,必须等待互斥锁被释放。
b. 信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量的值表示可用资源的数量,进程在访问资源前必须请求并获取信号量,然后在使用完资源后释放信号量。
c. 条件变量(Condition Variable):条件变量通常用于线程间的通信,它允许一个线程等待某个条件的发生,然后唤醒其他等待该条件的线程。条件变量可用于实现更高级的同步机制。
d. 屏障(Barrier):屏障用于确保多个进程在某个点上同时等待,然后一起继续执行。这在一些并行计算场景中非常有用。
-
实现进程同步的关键操作:
- 申请和释放互斥锁或信号量。
- 等待条件变量的发生和唤醒其他线程。
- 制定协议和规则,以确保进程之间的顺序和协调。
通过进程同步机制,可以确保多个进程按照一定的顺序访问共享资源,从而避免竞态条件和数据不一致问题,保证程序的正确性和可靠性。不同的编程语言和操作系统提供了不同的同步机制,但它们的核心概念和目标都是相似的。
进程互斥
因为对于临界资源的访问,需要互斥的进行。同一个时间段内只允许一个进程访问该资源。
临界资源的访问过程可以分为四个部分:
- 进入区:为了进入临界区使用临界资源,在进入区要检查可否进入临界区,如果可以进入临界区,则应该设置正在访问临界区的标志(例如加上互斥锁),以阻止其他进程同时进入临界区。
- 临界区(临界段):临界区是实际访问临界资源的部分。在这个部分,进程或线程可以执行与临界资源相关的操作,包括读取、写入或修改数据。这是临界资源的实际访问点。
- 退出区:进程或线程完成对临界资源的访问后,释放同步机制(例如释放互斥锁)以允许其他进程或线程进入临界区。在这个部分,通常还需要执行一些清理工作,确保资源状态正确。
- 残余区:指临界区之后的操作,即在退出临界区后,进程或线程执行的其他任务。这些任务不涉及对临界资源的访问,因此可以在不受同步机制的限制下执行。
互斥机制准则
为了禁止两个进程同时进入临界区,互斥机制应遵循以下的原则:
- 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
- 忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
- 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区。
- 让权等待:当进程不能进入临界区时,应立即释放处理器,防止进程忙等。
软件实现方式
实现进程互斥的软件实现方法有四种:单标志法、双标志先检查、双标志后检查、Peterson算法
单标志法
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。即每个进程进入临界区的权限只能被另一个进程赋予。
假设turn的初值为0,即刚开始只允许0号进程进入临界区(turn 变量背后的逻辑表示"谦让")。
若 P1 先上处理机运行,则代码会被一直卡在 ⑤。直到 P1 的时间片用完,发生调度,切换到 P0 上处理机运行。但代码 ① 不会卡住 P0,即 P0 可以正常访问临界区,在 P0 访问临界区期间即时切换回 P1 ,P1依然会卡在 ⑤。只有 P0 在退出区将 turn 改为1后,P1 才能进入临界区。
单标志法存在的主要问题是,违背了"空闲让进"原则。
双标志先检查法
算法思想 :设置一个布尔数组 flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如"flag[0] = true"意味着0号进程 P0 现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志 flag[i] 设为 true,之后开始访问临界区。
双标志先检查法主要的问题是,违背了"忙则等待"原则。原因在于,进入区的"检查"和"上锁"两个处理不是原子操作。在"检查"后,"上锁"前可能发生进程切换。
双标志后检查法
算法思想:是双标志先检查法的改版。前一个算法的问题是先"检查"后"上锁",但是这两个操作又无法一气呵成,因此会导致两个进程同时进入临界区的情况。因此,认门又想到先"上锁"后"检查"的方法,来避免上述问题。
双标志后检查法虽然解决了"忙则等待"的问题,但是又违背了"空闲让进"和"有限等待"原则,会因各个进程都长期无法访问临界资源而产生饥饿现象(两个进程都争着想要进入临界区,但是都上锁了,谁也不让谁,最后谁都无法进入临界区)。
Peterson算法
算法思想:结合双标志法、单标志法的思想。如果双方都争着想进入临界区,那可以让进程尝试"谦让"的方式,做一个有"礼貌"的进程。
c
bool flag[2]; // 表示进入临界区意愿的数组,初始化都是false
int turn = 0; // turn 表示优先让哪个进程进入临界区
// P0进程
flag[0] = true; // 表示本线程想进入临界区
turn = 1; // 可以优先让对方线程进入临界区
while(flag[1] && turn == 1); // 对方想进入临界区且自己"谦让",则本线程循环等待
critical section;
flag[0] = false; // 访问完临界区,表示自己已经不想访问临界区
remainder section;
// P1进程
flag[1] = true;
turn = 0;
while(flag[0] && turn == 0);
critical section;
flag[1] = false;
remainder section;
Peterson 算法用软件方式解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则。
硬件实现方法
进程互斥的硬件实现方法有三种:中断屏蔽方法、TestAndSet(TS指令/TSL指令)、Swap(XCHG指令)
中断屏蔽方法
利用"开/关中断指令"实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况)
优点:简单、高效
缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)。
TestAndSet指令
简称TS指令,也称为TestAndLock指令,或TSL指令。
TSL指令是用硬件实现的,执行过程不允许被中断,只能一气呵成。
若刚开始的lock是false,则TSL返回的old值为false,while 循环条件不满足,直接跳过循环,进入临界区。若刚开始lock是 true,则执行执行TLS后old返回的值为 true,while循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行"解锁"。
相比软件实现方式,TSL 指令把"上锁"和"检查"操作用硬件的方式变成原子操作。
优点:实现简单,无需像软件实现方式那样严格检查是否有逻辑漏洞;适合于多处理机环境。
缺点:不满足"让权等待"原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致"忙等"。
Swap指令
又称 Exchange 指令,简称 XCHG 指令。
Swap 指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成(原子操作)。
逻辑上 Swap 与 TSL 并无太大的区别,都是先记录下此时临界区是否已经被上锁(记录在 old 变量上),再将上锁标记 lock 设置为 true,最后检查 old,如果 old为false则说明之前没有别的进程对临界区上锁,则可以跳出循环,进入临界区。
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境。
缺点:不满足"让权等待"原则,暂时无法进入临界区的进程占用CPU并循环执行TSL指令,从而导致"忙等"。
互斥锁
互斥锁(Mutex,全名为 Mutual Lock)是一种用于多线程或多进程编程中的同步机制,它用于确保在任何给定时刻只有一个线程或进程能够访问临界资源,从而防止竞态条件和数据不一致性问题。互斥锁提供了互斥性,确保临界区内的代码只能被一个线程或进程执行,其他线程或进程必须等待锁的释放。
互斥锁的基本操作包括两个主要函数:锁定(Lock)和解锁(Unlock)。
- Acquire():线程或进程在进入临界区之前会尝试获取互斥锁。如果锁已经被其他线程或进程占用,那么当前线程或进程将被阻塞,直到锁可用。一旦获得了锁,线程或进程就可以进入临界区执行操作。
- Release():线程或进程在完成对临界资源的访问后应该释放互斥锁,以允许其他线程或进程进入临界区。释放锁后,其他线程或进程可以获得锁并访问临界资源。
每个互斥锁都有一个布尔变量 available,表示锁是否可用。如果锁是可用的,调用 acquire() 会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。
acquire() 或 release() 的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现。
互斥锁的主要缺点是忙等,当有一个进程在临界区中,任何其他进程在进入临界区时必须连续循环调用 acquire()。当多个进程共享同一CPU时,就浪费了CPU周期。因此,互斥锁通常用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行。
需要连续循环忙等的互斥锁,都可称为自旋锁(spin lock),如 TSL 指令、swap 指令、单标志法。
特性:
- 需要忙等,进程时间片用完才能下处理机,违反"让权等待"。
- 优点是等待期间不需要切换进程上下文,多处理系统中,若上锁的时间短,则等待的代价很低。
- 经常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区。不太适合单处理机系统(忙等的过程中不可能解锁)。
信号量机制
信号量(Semaphore)是一种用于进程或线程同步的抽象数据类型,用于管理共享资源的访问。信号量可以用于控制多个进程或线程之间的并发访问,以确保对临界资源的互斥访问,或者控制并发任务的执行顺序。信号量的主要目的是解决竞态条件、死锁和资源管理等问题。
信号量其实就是一个变量(即一个整数,又或者是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量。
原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。软件解决方案的主要问题是由"进入区的各种操作无法一气呵成",因此如果能把进入区、退出区的操作都用"原语"来实现,使这些操作都能"一气呵成"就能避免问题。
一对原语:wait(S)原语和signal(S)原语,可以把原语理解为自己所写的函数,wait与signal为函数名,括号中的信号量S其实就是函数调用时传入的一个参数。
wait、signal原语常简称P、V操作(荷兰语:proberen 和 verhogen)。因此也常将wait(S)、signal(S)两个操作分别写为P(S)、V(S)
信号量通常具有以下两个基本操作:
- P(Wait)操作 :
- 当进程或线程需要访问共享资源时,它执行P操作,尝试减小信号量的值。
- 如果信号量的值大于零,则P操作成功,进程或线程可以继续执行。
- 如果信号量的值等于零,则P操作将阻塞当前进程或线程,直到信号量的值变为非零。
- V(Signal)操作 :
- 当进程或线程完成对共享资源的访问时,它执行V操作,尝试增加信号量的值。
- 如果有其他进程或线程因等待资源而被阻塞,V操作会唤醒其中一个(或多个,具体取决于实现)等待的进程或线程。
信号量有两种主要类型:二进制信号量和计数信号量。
整形信号量
用一个整数型的变量作为信号量,用来表示系统中系统中某种资源的数量。
与普通整数变量的区别:对信号量的操作只有三种,即 初始化、P操作、V操作。
"检查"和"上锁"一气呵成,避免了并发、异步导致的问题。
存在的问题:不满足"让权等待"原则,会发生"忙等"
记录型变量
整形信号量的缺陷是存在"忙等"问题,因此人们又提出"记录型信号量",即用记录型数据结构表示信号量。
如果剩余资源数不够,使用block原语使进程从运行态进入阻塞态,并把其挂到信号量S的等待队列(即阻塞队列)中。
释放资源后,若还有别的进程在等待这种资源,则使用wakeup原语唤醒等待队列中的一个进程,该进程从阻塞态变为就绪态。
实现进程互斥
- 分析并发进程的关键活动,划定临界区(如:对临界区资源打印的访问就应放在临界区)
- 设置互斥信号量mutex,初值为1
- 在进入区P(mutex)申请资源
- 在退出区V(mutex)释放资源
注意:对于不同的临界资源需要设置不同的互斥信号量。
P、V操作必须成对出现。缺少P(mutex)就不能保证临界资源的互斥访问。缺少V(mutex) 会导致资源永不释放,等待进程永不被唤醒。
实现进程同步
- 分析需要实现"同步关系"的场景,即必须保证"一前一后"执行的两个操作(两行代码)
- 设置同步信号量S,初始化为0
- 在"前操作"之后执行V(S)
- 在"后操作"之前执行P(S)
若先执行到V(S)操作,则S++ 后S=1。之后当执行到P(S)操作时,由于S=1,表示有可用资源,会执行S--,S的值变回0, P2进程不会执行block原语,而是继续往下执行代码4。
若先执行到P(S)操作,由于S=0,S-- 后S=-1,表示此时没有可用资源,因此P操作中会执行block原语,主动请求阻塞。之后当执行完代码2,继而执行V(S)操作,S++, 使S变回0,由于此时有进程在该信号量对应的阻塞队列中,因此会在V操作中执行wakeup原语,唤醒P2进程。这样P2就可以继续执行代码4了
实现前驱关系
其实每一对前驱关系都是一个进程同步的问题:
- 要为每一个前驱关系各设置一个同步信号量。
- 在"前操作"之后对相应的同步信号量执行V操作。
- 在"后操作"之前对相应的同步信号量执行P操作。
所以实现的步骤/顺序为:
生产者消费者问题
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品(可以代指某种数据资源)放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。
生产者、消费者共享一个初始化为空、大小为n的缓冲区(假设n为5)
只有缓冲区未满时,生产者才能把产品放入缓冲区,否则必须要等待。
只有缓冲区非空时,消费者才能从中取出产品,否则必须等待。
缓冲区是临界资源,各个进程之间必须互斥地访问(成为互斥关系)。因为如果为非互斥关系,可能两个生产者进程随机选中缓冲区中的同一块区域进行写入(如上图),则会致使数据覆盖,导致数据丢失。
注意 :实现互斥的P操作一定要在实现同步的P操作之后,否则会造成生产者和消费者循环等待被对方唤醒,出现"死锁"情况。而V操作不会导致进程阻塞,因此两个V操作顺序可以交换。
吸烟者问题
吸烟者问题(The Smoker's Problem)是一个经典的同步问题,用于展示多进程或多线程之间的协作和同步。问题的描述如下:
在一个场景中,有三个吸烟者(Smoker)和一个代理者(Agent)。每个吸烟者都有以下两个特征:
-
每个吸烟者都有一个不同的烟草配料,例如,一个吸烟者有烟草、另一个有纸、第三个有火柴。
-
每个吸烟者都需要这三种配料才能开始吸烟。
供应者的任务是:
-
供应者周期性地生成两种随机的烟草配料,并将它们放在一个桌子上。
-
供应者等待一个吸烟者拿起这两种烟草配料并开始吸烟。
-
一旦一个吸烟者开始吸烟,供应者会再次生成两种随机的烟草配料,等待下一个吸烟者。
吸烟者的任务是:
-
每个吸烟者需要等待桌子上的两种烟草配料中有自己缺少的那种。
-
一旦满足条件,吸烟者会拿起两种烟草配料,开始吸烟。
-
吸烟者吸烟结束后,需要通知代理者,代理者再生成两种新的烟草配料。
吸烟者问题的目标是通过协调代理者和吸烟者的行为,确保每个吸烟者都能够获取到自己所需的两种烟草配料,然后开始吸烟。
同步关系(前V后P):
桌上有组合一 => 第一个抽烟者取走东西
桌上有组合二 => 第二个抽烟者取走东西
桌上有组合三 => 第三个抽烟者取走东西
供应者:
解决吸烟者问题通常涉及使用同步机制,如互斥锁、条件变量、信号量等,以确保代理者和吸烟者之间的正确协作。不同的编程语言和操作系统可以有不同的实现方式,但问题的本质是协作和同步。这个问题对于理解多进程或多线程编程中的同步概念非常有用。
读-写者问题
读者-写者问题(Reader-Writer Problem)是一个经典的同步问题,用于展示多进程或多线程之间的协作和同步。问题的描述如下:
在一个共享资源(通常是一个数据结构,如数据库、文件等)上,有多个读者和写者同时访问。读者是只读操作,写者是写操作。问题的目标是确保在读者和写者之间的并发访问时不会引发数据不一致性、竞态条件和死锁等问题。
具体来说,读者-写者问题有以下要求:
-
多个读者可以同时访问共享资源,但不能同时写入。
-
如果有写者在写入,其他读者和写者都必须等待写者完成。
-
多个写者不能同时写入,且当写者在写入时,其他读者和写者都必须等待写者完成。
解决读者-写者问题通常需要使用同步机制来确保正确的访问顺序和互斥性。一种常见的解决方法包括使用互斥锁(Mutex)和信号量(Semaphore)。以下是一个通用的解决方案:
-
使用两个互斥锁:一个用于控制读者的访问,另一个用于控制写者的访问。
-
使用一个计数信号量来跟踪当前有多少读者在访问共享资源。
-
写者在写入前必须获得写者互斥锁,并且在写入结束后释放它。
-
读者在读取前必须获得读者互斥锁,然后在读取结束后释放它。
-
在读者和写者访问共享资源前,需要对读者数量进行计数信号量的P操作,以确保写者不会在读者存在时写入。
-
在读者和写者访问共享资源后,需要对读者数量进行计数信号量的V操作,以通知其他读者或写者可以访问。
这个通用解决方案确保了读者和写者之间的正确协作,防止了数据不一致性和竞态条件。需要注意的是,具体的实现可能因编程语言、操作系统或应用场景而异,但核心原则是相似的。读者-写者问题对于理解同步和互斥概念以及多线程或多进程编程中的并发问题非常有用。
哲学家进餐问题
哲学家进餐问题(Dining Philosophers Problem)是一个经典的多进程同步问题,用于展示多个进程之间的并发互斥和资源分配问题。问题的场景描述如下:
有五位哲学家坐在一张圆形餐桌周围,每位哲学家都在思考和吃饭。在每个哲学家之间放置一只叉子,共有五只叉子。每位哲学家的行为如下:
-
哲学家可以思考,此时他不需要叉子。
-
哲学家可以尝试进餐,但需要两只相邻叉子,一只放在左侧,另一只放在右侧。
问题的目标是设计一个算法,确保哲学家在思考和进餐之间的正确协作,以避免死锁(所有哲学家都在等待叉子)和资源争夺(多个哲学家尝试同时获取叉子)等问题。
解决哲学家进餐问题的一种经典方法是使用互斥锁和条件变量。以下是一个通用的解决方案:
-
为每个叉子创建一个互斥锁,以确保每次只能有一个哲学家拿起或放下叉子。
-
为每位哲学家创建一个状态变量,表示他们的状态(思考、饥饿或进餐)。
-
当哲学家想要进餐时,他必须获取两只相邻叉子的锁。如果其中一只叉子已被其他哲学家占用,他需要等待。
-
如果哲学家成功获取两只相邻叉子的锁,他可以开始进餐,并将状态变为进餐状态。
-
进餐结束后,哲学家将放下叉子并将状态变回思考状态,然后通知左右两侧的哲学家,以便他们有机会进餐。
-
哲学家进入饥饿状态时,他会尝试获取叉子,如果成功获取,他进入进餐状态,否则他将等待。
这个解决方案确保了哲学家之间的正确协作,避免了死锁和资源争夺问题。需要注意的是,具体的实现方式可能因编程语言、操作系统或应用场景而异,但核心原则是相似的。哲学家进餐问题对于理解并发编程中的互斥和条件同步非常有用。
管程
信号量机制存在的问题是编写程序困难、容易出错。
管程是一种特殊的软件模块(面向对象中的类),有这些部分组成:
- 局部于管程的共享数据结构说明(属性)。
- 对该数据结构进行操作的一些过程(函数)。
- 对局部于管程的共享数据设置初始值的语句(构造方法)。
- 管程有一个名字。
管程的基本特征:
- 局部于管程的数据只能被局部于管程的过程所访问。
- 一个进程只有通过调用管程内的进程才能进入管程访问共享数据。
- 每次仅允许一个进程在管程内执行某个内部过程。
由编译器负责实现各进程互斥地进入管程中的过程。
引入管程的目的:更加方便地实现进程互斥和同步。
-
需要在管程中定义共享数据(如生产者消费者问题的缓冲区)
-
需要在管程中定义用于访问这些共享数据的"入口"-- -其实就是一些函数(如生产者消费者问题中,可以定义一一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)
-
只有通过这些特定的"入口"才能访问共享数据
-
管程中有很多"入口",但是每次只能开放其中一个"入口",并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。注意:这种, 互斥特性是由编译器负责实现的,程序员不用关心)
-
可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出"入口");可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。
死锁
死锁(Deadlock)指的是在多个进程或线程之间,每个进程都在等待某个资源被释放,而同时又持有其他资源,导致它们都无法继续执行下去的状态。这种情况下,系统进入了一种僵局,无法自行恢复,需要通过干预来解决。
死锁通常涉及多个资源,例如内存、文件、打印机、网络连接等,不同进程或线程试图获取这些资源,但由于资源之间的竞争条件,它们陷入了相互等待的状态。
死锁的产生是因为以下四个条件同时满足(只要其中任何一个条件不满足,死锁都不会发生):
-
互斥条件(Mutual Exclusion):每个资源只能被一个进程或线程占用,其他进程或线程必须等待资源释放。
-
占有和等待条件(Hold and Wait):进程或线程至少持有一个资源,并且在等待获取其他资源。
-
不可抢占条件(No Preemption):资源不能被强制抢占,只能在进程或线程自愿释放后才能被其他进程或线程获取。
-
循环等待条件(Circular Wait):存在一个进程或线程的资源请求链,形成一个循环,每个进程或线程都在等待下一个资源,直到最后一个又在等待第一个进程或线程释放资源。
注意 :发生死锁时一定有循环等待,但是发生循环等待时未必死锁(即循环等待时死锁的必要不充分条件)。如果同类资源数大于1,即使有循环等待,也未必发生死锁。但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件。
为了避免死锁的发生,通常采用以下策略:
-
资源分配策略:确保系统在分配资源时不会导致死锁。这可以通过先分配所有必要资源,然后才允许进程或线程执行,或者采用其他资源分配算法来实现。
-
超时和重试:在等待资源时,设定一个超时限制,如果超过限制还未获取资源,则释放已经占有的资源,并尝试重新获取。
-
死锁检测和恢复:定期检测系统是否进入了死锁状态,如果检测到死锁,采取措施来中断其中一个或多个进程,以解除死锁。
-
避免死锁:通过仔细设计进程或线程的资源请求顺序,以避免循环等待条件的发生(使用银行家算法)。
-
使用信号量、互斥锁等同步机制:正确使用同步机制可以减少死锁的发生。
死锁、饥饿、死循环的区别
死锁:各进程互相等待对方手里的资源,导致各进程都发生阻塞,无法向前推进的现象。
饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象。例如:在SPF(短进程优先)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而导致长进程"饥饿"。
死循环:某进程执行过程中一直无法跳出某个循环的现象。有时是因为程序逻辑bug导致的,有时则是程序员故意设计的。
预防死锁(静态策略)
预防死锁只需要让形成死锁的四个条件中的任何一个不成立即可。
破坏互斥条件
**互斥条件 **:只有对必须互斥使用的资源的争抢才会导致死锁。
破坏互斥条件以预防死锁,有以下几种方法:
- 共享资源:将原本互斥的资源设计为可共享的资源。这意味着多个进程或线程可以同时访问或使用这些资源,而不会发生冲突。这通常需要在资源的设计和实现阶段进行考虑,以确保资源可以被多个实体同时访问。
- 使用副本:如果资源无法被共享,可以考虑使用资源的副本而不是原始资源。每个进程或线程可以获得自己的资源副本,这样互斥条件不再适用于副本,从而避免了死锁的发生。但要注意,这可能会增加系统开销和资源消耗。
- 虚拟化资源:将资源虚拟化,使多个进程或线程看起来好像拥有它们自己的资源,但实际上它们共享一个资源池。这种虚拟化可以通过技术如线程池、进程池或连接池来实现,以降低资源的独占性。
- 资源管理策略:实施资源管理策略,例如资源预分配,将资源分配给进程或线程之前,确保没有其他进程或线程正在使用它们。这可以减少资源竞争,降低死锁的可能性。
值得注意的是,破坏互斥条件可能会引入性能和复杂性方面的问题,因为多个实体同时访问资源可能会导致冲突和竞争条件。因此,在考虑破坏互斥条件时,需要权衡系统性能和死锁预防的需求。
此外,应该根据具体的应用场景和系统需求来选择合适的死锁预防策略。死锁预防是系统设计中的一个重要方面,需要综合考虑多个因素,以确保系统能够在高效性和可靠性之间取得平衡。
破坏不抢占条件
不抢占条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
要破坏不抢占条件以预防死锁,可以考虑以下方法:
- 抢占资源:允许系统在某些情况下抢占资源,即使进程或线程尚未自愿释放资源。这种策略需要慎重考虑,因为强制抢占可能导致数据一致性和应用程序完整性问题。通常,只有在确保抢占不会引发更大问题的情况下才能采用这种方法。
- 资源超时释放:引入资源的超时释放机制,即当一个进程或线程持有资源一段时间后,系统会自动释放资源,即使进程或线程没有自愿释放。这可以通过设置资源的最大占用时间来实现。
- 资源占用限制:规定资源的占用限制,即资源只能被占用一定时间,之后就会自动释放。这种方法可以强制进程或线程定期释放资源,降低死锁的发生概率。
- 资源预留:在资源分配时,进程或线程可以预留所需的资源,但不立即占用。只有当它确实需要资源时才占用。这可以减少资源的持有时间,从而减少死锁的可能性。
- 动态资源分配:采用动态资源分配策略,只有在进程或线程能够安全获取所需资源的情况下才进行分配。如果无法满足条件,就等待或回退。这有助于确保资源分配是安全的,不会导致死锁。
该策略的缺点:
- 实现起来会比较复杂。
- 释放已经获得的资源可能会造成前一阶段工作的失效。因此这种方式一般只适用于易保存和恢复状态的资源(例如,CPU)。
- 反复地申请和释放资源会增加系统开销,降低系统吞吐量。
破坏占有和等待条件
占用和等待:进程已经保持了至少一个资源,但是又提出了新的资源请求,而该资源又被其他进程占用,此时请求进程被阻塞,但又对自己已有的资源保持不放。
要破坏占有和等待条件以预防死锁,可以考虑下面的方法:
- 资源预分配:在进程启动时,要求它获取所有需要的资源,而不是逐个请求。这可以通过让进程在开始执行前获取所有资源,或者通过一次性获取资源池中的所有资源来实现。这样可以避免在执行过程中等待其他资源的情况。
- 资源动态分配:只有在进程能够同时获取所有需要的资源时,才进行资源分配。如果无法满足条件,就不分配资源,而是让进程等待。这种方式可以防止进程在持有某些资源的情况下等待其他资源。
- 资源释放和重新请求:如果一个进程在持有某些资源的同时需要其他资源,它可以释放已持有的资源,然后重新请求所有需要的资源。这可以减少占有和等待条件的出现。
- 资源优先级:为资源分配和请求定义优先级,确保高优先级的进程可以更容易地获取资源。这可以减少低优先级进程占用资源而导致高优先级进程等待的情况。
- 资源占用限制:规定每个进程可以占用的资源数量或总资源数量的上限。这可以防止进程过多地占用资源,降低死锁的可能性。
- 资源管理策略:实施资源管理策略,确保资源的分配和释放是有序的,避免资源分配导致死锁的情况。
破坏循环等待条件
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已经获取的资源都同时被下一个进程所请求。
以下是一些破坏循环等待条件的方法:
-
资源排序:为系统中的资源分配一个全局唯一的编号或标识符,并要求进程按照一定的顺序请求资源。这样可以减少循环等待的可能性,因为资源的分配顺序变得可预测。
-
资源申请策略:采用资源分配策略,要求进程只能同时申请一类资源,而不是多种资源。这可以减少资源之间的交叉等待,从而减少死锁的概率。
-
资源分级:将资源分为不同的等级或层次,要求进程只能获取相同或更低级别的资源。这样可以确保资源请求不会形成循环等待。
-
资源组合:将多个资源组合成一个资源组,进程只能请求整个资源组而不是单独的资源。这可以降低循环等待的可能性,因为资源组的请求是原子的。
-
超时机制:引入超时机制,当一个进程请求资源时,如果在一定时间内无法获得资源,就会释放已经占有的资源,以避免死锁。这需要注意超时值的设置,以确保不会误解资源的真实可用性。
-
银行家算法:银行家算法是一种死锁避免策略,它使用资源分配和进程请求的信息来判断是否分配资源会导致死锁。只有在分配资源不会导致死锁的情况下才进行分配。
-
资源释放:要求进程在完成任务后立即释放占有的资源,而不是等待其他资源。这可以减少资源持有时间,减少循环等待的机会。
该策略的第一种方法的缺点:
- 不方便增加新的设备,因为可能需要重新分配所有的编号。
- 进程实际使用的顺序可能和编号递增顺序不一样,会导致资源的浪费。
- 必须按照规定次序申请资源,用户编写困难。
避免死锁(动态策略)
安全序列是一组进程的执行顺序,使得系统不会进入死锁状态。在安全序列中,每个进程都按照它的资源请求顺序逐一获取资源,执行完任务后释放资源。在整个执行过程中,没有出现资源竞争导致的死锁,系统能够正常运行并顺利完成进程。
如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
如果系统处于安全状态,就一定不会 发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
银行家算法
银行家算法(Banker's Algorithm)是一种死锁避免算法,用于管理多个进程对有限资源的竞争和分配,以避免死锁的发生。这个算法最初由荷兰计算机科学家 Edsger W. Dijkstra 于1965年提出。原本该算法是 Dijkstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用于操作系统中。
银行家算法的详细解释:
1. 进程和资源: 银行家算法涉及到多个进程和多个资源。每个进程都需要一定数量的不同类型的资源来执行任务,而这些资源可能是有限的。
2. 资源的种类: 每种资源都有一个固定的数量,表示为"总数"。这些资源可以是CPU时间、内存、文件句柄、打印机等。
3. 进程的资源需求: 每个进程在开始执行之前必须指定其所需的各种资源数量。这些需求被表示为一个向量,称为最大需求(Maximum Demand)向量。
4. 系统资源状态: 银行家算法维护一个当前资源状态的向量,表示为可用资源(Available Resources)向量。这个向量的每个元素代表相应资源的可用数量。
5. 安全状态: 在银行家算法中,系统会定期检查是否存在安全状态。一个系统被认为处于安全状态,如果存在一种资源分配序列,使得所有进程都能够成功完成,而不会导致死锁。
6. 分配资源的规则: 银行家算法遵循以下规则来分配资源:
- 请求资源:当一个进程请求资源时,系统检查是否有足够的资源可用。如果有足够的资源,则分配资源给进程;否则,进程将被阻塞直到资源可用。
- 分配资源:当资源分配给一个进程时,系统将从可用资源中减去相应数量的资源。
- 释放资源:当进程完成任务并释放资源时,系统将释放的资源添加到可用资源中。
7. 安全性检查: 在每次资源分配之后,系统会检查是否存在安全状态。安全性检查通常涉及模拟进程执行和资源释放,以验证是否存在一种资源分配序列,使得所有进程都能够成功完成而不会导致死锁。
8. 如果系统处于安全状态: 如果安全性检查确定系统处于安全状态,那么资源分配是合法的,进程可以继续执行。否则,系统将等待资源,直到安全状态恢复。
银行家算法的主要目标是确保资源分配不会导致死锁。它通过以下步骤来实现这一目标:
-
初始化:
- 确定系统中的资源种类数量和每种资源的总数。
- 对每个进程,确定它的最大资源需求(Maximum Demand)和已分配资源数量(Allocation)。
- 计算每个进程的需求矩阵(Need = Maximum - Allocation)。
- 初始化可用资源向量(Available),它表示当前系统中每种资源的可用数量,最初等于总资源数量。
-
请求资源:
- 当一个进程请求分配资源时,系统首先检查请求的资源数量是否小于等于可用资源向量中的相应资源数量。如果是,则继续;否则,进程必须等待资源。
-
分配资源:
- 如果系统确定分配资源不会导致死锁(通过安全性检查,下一步详细描述),则分配资源给进程。这涉及将请求的资源数量从可用资源向量中减去,并将这些资源添加到进程的已分配资源中。
-
安全性检查:
- 在每次资源分配后,系统都会进行安全性检查以确保系统仍然处于安全状态。安全状态意味着存在一种资源分配序列,使得所有进程都能够成功完成而不会导致死锁。安全性检查通常涉及以下步骤:
a. 创建一个工作向量(Work),它是可用资源向量的拷贝。
b. 创建一个完成标志向量(Finish),初始化为false,表示所有进程都没有完成。
c. 重复以下步骤,直到所有进程都完成或没有进一步的进程可以分配资源:
i. 对于每个未完成的进程i,检查其需求矩阵是否小于等于工作向量。如果是,则进程i可以完成,将它的已分配资源添加回工作向量,并将进程i标记为完成。
ii. 如果找不到任何未完成的进程满足条件,系统认为当前状态不安全。 - 如果循环结束后,所有进程都已完成,则系统认为当前状态是安全的,可以继续分配资源。否则,系统会等待,直到有新的资源可用。
- 在每次资源分配后,系统都会进行安全性检查以确保系统仍然处于安全状态。安全状态意味着存在一种资源分配序列,使得所有进程都能够成功完成而不会导致死锁。安全性检查通常涉及以下步骤:
-
资源释放:
- 当一个进程完成任务时,它会释放已分配的资源。这些资源将被添加回可用资源向量中,以供其他进程请求和分配。
-
循环:
- 步骤2到步骤5会循环执行,直到所有进程完成。
通过这些步骤,银行家算法可以确保资源的合理分配,从而预防死锁的发生。如果系统进入不安全状态,算法会等待,直到安全状态恢复。请注意,这是一个高层次的描述,实际实现可能涉及更多细节和复杂性。
以下是银行家算法的简单示例,用于模拟资源分配和安全性检查。这个示例假设有n个进程和m种资源。
java
import java.util.Scanner;
public class BankersAlgorithm {
private int[][] max; // 最大需求矩阵
private int[][] allocation; // 分配矩阵
private int[][] need; // 需求矩阵
private int[] available; // 可用资源向量
private int n; // 进程数量
private int m; // 资源种类数量
public BankersAlgorithm(int n, int m) {
this.n = n;
this.m = m;
max = new int[n][m];
allocation = new int[n][m];
need = new int[n][m];
available = new int[m];
}
// 初始化资源矩阵
public void initialize() {
Scanner scanner = new Scanner(System.in);
System.out.println("输入每种资源的总数:");
for (int i = 0; i < m; i++) {
available[i] = scanner.nextInt();
}
System.out.println("输入每个进程的最大需求矩阵:");
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
max[i][j] = scanner.nextInt();
}
}
System.out.println("输入每个进程的已分配矩阵:");
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
allocation[i][j] = scanner.nextInt();
need[i][j] = max[i][j] - allocation[i][j];
}
}
}
// 检查是否存在安全序列
public boolean isSafe() {
int[] work = new int[m];
boolean[] finish = new boolean[n];
// 初始化work向量
for (int i = 0; i < m; i++) {
work[i] = available[i];
}
int count = 0;
int[] safeSequence = new int[n];
while (count < n) {
boolean found = false;
for (int i = 0; i < n; i++) {
if (!finish[i]) {
boolean canAllocate = true;
for (int j = 0; j < m; j++) {
if (need[i][j] > work[j]) {
canAllocate = false;
break;
}
}
if (canAllocate) {
for (int j = 0; j < m; j++) {
work[j] += allocation[i][j];
}
safeSequence[count] = i;
finish[i] = true;
count++;
found = true;
}
}
}
if (!found) {
break; // 没有找到可分配的进程,不安全
}
}
if (count == n) {
System.out.println("存在安全序列:");
for (int i = 0; i < n; i++) {
System.out.print("P" + safeSequence[i]);
if (i < n - 1) {
System.out.print(" -> ");
}
}
System.out.println();
return true;
} else {
System.out.println("不存在安全序列,系统处于不安全状态。");
return false;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("输入进程数量(n):");
int n = scanner.nextInt();
System.out.println("输入资源种类数量(m):");
int m = scanner.nextInt();
BankersAlgorithm banker = new BankersAlgorithm(n, m);
banker.initialize();
banker.isSafe();
}
}
这段代码实现了银行家算法的主要功能。用户需要输入每种资源的总数、每个进程的最大需求矩阵以及每个进程的已分配矩阵。然后,程序会检查是否存在安全序列,并在存在安全序列时输出安全序列。如果不存在安全序列,它会提示系统处于不安全状态。
银行家算法的主要优点是它可以防止死锁并确保资源的合理分配。然而,它也有一些限制,如必须知道进程的最大资源需求和资源总量等信息,这在某些情况下可能不容易获得。此外,它假设进程的资源需求是静态的,这在某些实际情况下可能不成立。
死锁的检测
如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法:
① 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。
② 死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。
为了能对系统是否已经发生了死锁进行检测,必须用某种数据结构来保存资源的请求和分配信息。提供一种算法,利用所给的信息来检测系统是否已经进入死锁状态。
下面是一个简单的资源分配图:
如果系统中剩余的可用资源数足够满足进程的需求,那么这个进程暂时是不会阻塞的,可以顺利地执行下去。
如果这个进程执行结束了把资源归还系统,就可以使某些正在等待资源的进程被激活,并且顺利地执行下去。
对应的,这些被激活的进程执行完之后又会归还一些资源,这样可能又会激活另外一些阻塞的进程。
如果按照上述过程分析,最终能消除所有的边,则称这个图为可完全简化 的。此时一定没有发生死锁(即相当于能找到一个安全序列)。相反,如果最终不能消除所有边,则此时就发生了死锁(最终还连着边的哪些进程就是处于死锁状态的进程)。
死锁的解除
死锁的解除是指采取措施来终止已经发生的死锁,以使系统能够恢复正常运行。死锁解除的方法有多种,以下是一些常见的死锁解除策略:
-
进程终止:强制终止一个或多个导致死锁的进程,以释放它们持有的资源。通常,选择终止哪个进程需要谨慎考虑,可能会选择影响最小的进程或优先级较低的进程。
-
资源抢占:将资源从一个或多个进程中抢占,然后将它们分配给其他进程。这需要确保被抢占的进程能够在适当的时候重新获得资源,以避免数据不一致或其他问题。
-
进程回退:当一个进程请求无法满足时,它可以释放一些已经占用的资源,然后等待一段时间后再次请求。这可以减少资源争用,有助于解除死锁。
-
等待超时:为资源请求设置超时限制,如果一个进程在等待资源的时间超过限制,就放弃资源请求并释放已占用的资源。这有助于防止进程陷入无限等待状态。
-
资源回收:引入一种机制,允许系统回收长时间未被使用的资源,然后将这些资源重新分配给其他进程。这可以减少资源的浪费和持有时间。
-
全局回滚:如果死锁解除的方法较复杂,可以选择执行全局回滚,即将系统恢复到某个先前的快照状态,然后重新开始执行进程。这种方法会丢失一些进程的进度,但可以解除死锁。
如何决定该处理哪个进程?
- 首先根据进程的优先级,首先可以将优先级较低的任务处理掉。
- 根据进程执行的时间长短,将执行时间长的进程解决掉后,其恢复会消耗成本远远大于执行时间短的。
- 根据进程任务的执行进度,与执行时间相同,进度快要完成的进程恢复的速度往往比刚开始执行的进程慢、成本高。
- 根据进程已经使用多少资源,优先解决掉消耗资源多的进程,将其释放之后,所获得的资源较多,效率越高。
- 该进程是否是交互式的,如果是,则不能处理,否则用户体验会下降。优先解决掉批处理式的进程。
死锁解除的具体方法取决于系统的要求和资源管理策略,需要谨慎考虑,以确保解除死锁后系统可以继续正常运行。此外,死锁解除通常需要监控和检测死锁的存在,以及选择合适的解除策略。死锁解除通常是在死锁检测之后执行的,以确保系统能够尽早恢复正常运行。