0
点赞
收藏
分享

微信扫一扫

计算机操作系统学习笔记 第二章、进程与线程


文章目录

  • ​​1 进程和线程​​
  • ​​1.1 进程的概念和特征​​
  • ​​1.1.1 进程的概念​​
  • ​​1.1.2 进程的特征​​
  • ​​1.2 进程的状态与转换​​
  • ​​1.3 进程的组织​​
  • ​​1.4 进程控制​​
  • ​​1.5 进程通信​​
  • ​​1.5.1 共享存储​​
  • ​​1.5.2 消息传递​​
  • ​​1.5.3 管道通信​​
  • ​​1.6 线程和多线程模型​​
  • ​​1.6.1 线程的基本概念​​
  • ​​1.6.2 线程与进程的比较​​
  • ​​1.6.3 线程的属性​​
  • ​​1.6.4 线程的实现方式​​
  • ​​1.6.5 多线程模型​​

  • ​​2 处理机调度​​
  • ​​2.1 调度的概念​​
  • ​​2.2 调度算法的评价指标​​
  • ​​2.3 调度的实现​​
  • ​​2.3.1 进程调度的时机​​
  • ​​2.3.2 调度的切换和过程​​
  • ​​2.3.3 进程调度的方式​​
  • ​​2.4 典型的调度算法​​
  • ​​2.4.1 先来先服务(FCFS)调度算法​​
  • ​​2.4.2 短作业优先(SJF)调度算法​​
  • ​​2.4.3 高响应比优先调度(HRRN)算法​​
  • ​​2.4.4 时间片轮转(RR)算法​​
  • ​​2.4.5 优先级调度算法​​
  • ​​2.4.6 多级反馈队列调度算法​​
  • ​​2.4.7 各调度算法对比​​

  • ​​3 同步与互斥​​
  • ​​3.1 同步与互斥的基本概念​​
  • ​​3.2 实现临界区互斥的基本方法​​
  • ​​3.2.1 软件实现方法​​
  • ​​3.2.2 硬件实现方法​​
  • ​​3.3 信号量机制​​
  • ​​3.3.1 整型信号量​​
  • ​​3.3.2 记录型信号量​​
  • ​​3.3.3 利用信号量实现进程互斥​​
  • ​​3.3.4 利用信号量实现进程同步​​
  • ​​3.3.5 使用信号量机制实现前驱关系​​
  • ​​3.4 管程​​
  • ​​4 死锁​​
  • ​​4.1 死锁的概念​​
  • ​​4.2 死锁处理​​
  • ​​4.2.1 预防死锁​​
  • ​​4.2.2 避免死锁​​
  • ​​4.2.3 死锁的检测和解除​​


1 进程和线程

1.1 进程的概念和特征

1.1.1 进程的概念

在多道程序环境下,允许多个程序并发执行,此时它们将失去封闭性,并具有间断性及不可再现性的特征。为此引入进程(Process)的概念,以便更好地描述和控制程序的并发执行,实现操作系统的并发性和共享性(最基本的两个特征)。

计算机操作系统学习笔记 第二章、进程与线程_死锁

为了让参与并发执行的每个程序(含数据)都能独立运行,必须为之配置一个专门的数据结构,称为进程控制块(Porcess Control Block,PCB)。系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程。

相应地,由程序段、相关数据段和PCB三部分构成了进程实体(又称进程映像)。

计算机操作系统学习笔记 第二章、进程与线程_笔记_02

所谓创建进程,实际上是创建进程实体中的PCB;而撤销进程,实质上是撤销进程的PCB。值得注意的是,进程映像是静态的,进程则是动态的。PCB是进程存在的唯一标志!

引入进程实体的概念后,我们可以把传统操作系统中的进程定义为:”进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。“

1.1.2 进程的特征

进程是由多道程序的并发执行而引出的,它和程序是两个截然不同的概念。进程的基本特征是对比单个程序的顺序执行提出的,也是对进程管理的基本要求。

1) 动态性。进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有确定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征。

2) 并发性。指多个进程实体同时存在内存中,能在一段时间内同时运行。引入进程的目的就是使进程能和其他进程并发执行。并发性是进程的重要特征,也是操作系统的重要特征。

3) 独立性。指进程实体是一个能独立运行、独立获得资源和独立接收调度的基本单元,凡未建立PCB的程序,都不能作为一个独立的单位参与运行。

4) 异步性。由于进程的相互制约,使得进程按各自独立的、不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此在操作系统中必须配置相应的进程同步机制。

5) 结构性。每个进程都会配置一个PCB。结构上看,进程由程序段、数据段、PCB组成。

1.2 进程的状态与转换

进程在其生命周期内,由于系统中各进程之间的相互制约及系统的运行环境的变化,使得进程的状态也在不断发生改变。通常进程有以下5种状态,前3种是进程的基本状态。

1) 运行态。进程正在处理机上运行。在单处理机中,每个时刻只有一个进程处于运行态。

2) 就绪态。进程获得了除处理机外的一切所需资源,一旦得到处理机,便可立即运行。系统中处于就绪状态的进程可能有多个,通常将它们拍成一个队列,称为就绪队列。

3) 阻塞态,又称等待态。进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。系统通常将处于阻塞态的进程也排成一个队列,甚至根据阻塞原因的不同,设置多个阻塞队列。

4) 创建态。进程正在被创建,尚未转到就绪态。创建进程需要多个步骤:首先申请一个空白PCB,并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后把该进程转入就绪态并插入就绪队列。

5) 结束态。进程正从系统中消失,可能是进程正常结束或其他原因退出运行。进程需要结束运行时,系统首先将该进程置为结束态,然后进一步处理资源释放和回收等工作。

下图说明了5种进程状态的转换,而3种基本状态之间的转换如下:

  • 就绪态->运行态:处于就绪态的进程被调度后,获得处理机资源(分派处理机时间片)。
  • 运行态->就绪态:处于运行态的进程在时间片用完后,不得不让出处理机。
  • 运行态->阻塞态:进程请求某一资源的使用和分配或等待某一事件的发生。
  • 阻塞态->就绪态:进程等待的事件到来时,中断处理程序必须将相应进程的状态由阻塞态转换为就绪态。
    计算机操作系统学习笔记 第二章、进程与线程_笔记_03

1.3 进程的组织

进程是一个独立的运行单位,也是操作系统进行资源分配和调度的基本单位。它由以下三部分组成,其中最核心的就是进程控制块(PCB)。PCB是给操作系统用的,而程序段、数据段是给进程自己用的。

  1. 进程控制块
    进程创建时,操作系统为它新建一个PCB,该结构之后常驻内存,任何时刻都可以存取,并在进程结束后删除。PCB是进程实体的一部分,是进程存在的唯一标志。
    进程执行时,系统通过其PCB了解进程的现行状态信息,以便操作系统对其进行控制和管理;进程结束时,系统收回其PCB,该进程随之消亡。
    计算机操作系统学习笔记 第二章、进程与线程_笔记_04
  2. 程序段
    程序段就是能被进程调度程序调度到CPU执行的程序代码段。注意,程序可被多个进程共享,即多个进程可以运行同一程序。
  3. 数据段
    一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。

1.4 进程控制

进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。简化理解:反正进程控制就是要实现进程状态转换。在操作系统中,一般把进程控制用的程序称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位。

  1. 进程的创建
    允许一个进程创建另一个进程,此时创建者称为父进程,被创建的进程称为子进程。子进程可以继承父进程所拥有的资源。当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程。此外,在撤销父进程时,通常也会同时撤销其所有的子进程。操作系统创建一个新进程的过程如下(创建原语):
  • 申请空白PCB(PCB是有限的)。若PCB申请失败,则创建失败。
  • 为进程分配其运行所需的资源。如果资源不足(如内存),则并不是创建失败,而是处于创建态,等待内存资源。
  • 初始化PCB。
  • 将PCB插入就绪队列。
  1. 进程的终止
    引起进程终止的事件有:正常结束;异常结束;外界干预。操作系统终止进程的过程如下(终止原语):
  • 从PCB集合中找到终止进程的PCB。
  • 若进程正在运行,立即剥夺CPU,将CPU分配给其他进程。
  • 终止其所有子进程。
  • 将该进程拥有的所有资源归还给父进程或者操作系统。
  • 删除PCB。
  1. 进程的阻塞和唤醒
    正在执行的进程,由于期待的某些事情未发生,进程便通过调用阻塞原语(Block),使自己由运行态变为阻塞态。可见,阻塞是进程自身的主动行为。阻塞原语的执行过程如下:
  • 找到要阻塞的进程对应的PCB。
  • 保护进程运行现场,将PCB状态信息设置为“阻塞态”,暂时停止进程运行。
  • 将PCB插入相应事件的等待队列。

当被阻塞进程所期待的事情出现时,由有关进程(比如提供数据的进程)调用唤醒原语(Wakeup),将等待该事件的进程唤醒。唤醒原语的执行过程如下:

  • 在该事件的等待队列中找到相应进程的PCB。
  • 将其从等待队列中一处,并置其状态为就绪态。
  • 将该PCB插入就绪队列,等待调度程序调度。

注意,阻塞原语和唤醒原语必须成对使用。否则会导致阻塞进程因不能唤醒永久地处于阻塞状态。


1.5 进程通信

进程间通信(Inter-Process Communication,IPC),是指两个进程之间产生数据交互。如我们将知乎的文章分享给QQ好友,这实际上就是进程通信。

进程是分配系统资源的单位(包括内存地址空间),所以各进程拥有的内存地址空间相互独立。为了保证安全,一个进程不能直接访问另一个进程的地址空间。所以,要实现进程通信需要操作系统支持。高级通信主要有三种:共享存储,消息传递,管道通信。

1.5.1 共享存储

在通信的进程之间存在一块可以直接访问的共享空间,通过对这片共享空间进行写/读操作实现进程之间的信息交换。为了避免出错,各个进程可使用操作系统内核提供的同步互斥工具(如P、V操作),对共享空间的写/读进行控制。共享存储又分为两种:低级方式的共享是基于数据结构的共享(比如共享空间只能放一个长度为10的数组);高级方式的共享则是基于存储区的共享(操作系统在内存中划出一块共享存储区,数据的形式、存储位置都由通信进程控制,而不是操作系统)。操作系统只负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换则由用户自己安排读/写指令完成。

计算机操作系统学习笔记 第二章、进程与线程_笔记_05

注意,进程空间一般是独立的,进程运行期间一般不能访问其他进程的空间, 想让两个进程共享空间,必须通过特殊的系统调用实现,而进程内的线程是自然共享进程空间的。

1.5.2 消息传递

在消息传递系统中,进程间的数据交换以格式化的消息(Message)为的那位。若通信的进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的“发送消息/接收消息”两个原语进行数据交换。

  1. 直接通信方式。发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列冲取得消息。
    计算机操作系统学习笔记 第二章、进程与线程_笔记_06
  2. 间接通信方式。发送进程把消息发送到某个中间实体,接收进程从中间实体取得消息。这种中间实体一般称为信箱。该通信方式广泛应用于计算机网络。
    计算机操作系统学习笔记 第二章、进程与线程_死锁_07

简单理解就是,甲要告诉乙某些事情,就要写信,然后通过邮差送给乙。直接通信就是邮差把信直接送到乙的手上;间接通信就是乙家门口有一个邮箱,邮差把信放到邮箱里。

1.5.3 管道通信

管道通信时消息传递的一种特殊方式。所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间的通信的一个共享文件,又名pipe文件。其实就是在内存中开辟了大小固定的内存缓冲区。进程P(写进程)以字符流形式将大量数据送入(写)管道;而进程Q(读进程)则从管道中接收(读)数据。
计算机操作系统学习笔记 第二章、进程与线程_操作系统_08

管道机制必须提供以下三个方面的协调能力:互斥、同步和确定对方的存在。

注意:

  1. 从管道读数据是一次性操作,数据一旦被读取,就释放空间以便读写更多数据。因此,当多个进程读写管道时,可能会出现错乱。对此有两种解决方案:一个管道允许多个写进程,一个读进程;允许有多个写进程,多个读进程,但系统会让各个读进程轮流从管道中读数据。
  2. 管道只能采用半双工通信,即某一时刻只能单向传输。如果要实现双向同时通信,则需要设置两个管道。
  3. 各个进程要互斥访问管道(由操作系统负责实现互斥)。管道写满时,写进程阻塞,管道读空时,读进程阻塞。

1.6 线程和多线程模型

1.6.1 线程的基本概念

可以把线程理解为“轻量级进程”。线程是一个基本的CPU执行单元,也是程序执行流的最小单位,由线程ID、程序计数器、寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程中的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现间断性。线程也有就绪、阻塞和运行三种基本状态。

引入线程之后,进程的内涵发生了改变,进程只作为除CPU之外的系统资源的分配单元(如打印机、内存地址空间等都是分配给进程的)。线程则作为处理机的分配单元。

1.6.2 线程与进程的比较

  1. 调度。在传统的操作系统中,拥有资源和独立调度的基本单位都是线程,每次调度都需要上下文切换,开销较大。而在引入线程的操作系统中,线程是独立调度的基本单位,线程切换代价远低于进程。在同一进程中,线程的切换不会引起进程切换。但从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  2. 并发性。不仅是进程之间可以并发,而且一个进程内的各线程之间也可以并发,甚至不同进程中的线程也能并发执行,从而进一步提升了系统的并发度,提高了系统的资源利用率和系统的吞吐量,使得一个进程内也可以并发处理各种任务(如QQ视频、文字聊天、传文件)。
  3. 拥有资源。进程是系统中拥有资源的基本单位,而线程不拥有系统资源,但线程可以访问其隶属进程的系统资源,这主要表现在属于同一进程的所有线程都拥有相同的地址空间。
  4. 独立性。每个进程都拥有独立的地址空间和资源,除了共享全局变量,不允许其他进程访问。同一进程中的不同线程是为了提高并发性及进行相互之间的合作而创建的,它们共享进程的地址空间和资源。
  5. 系统开销。传统的进程间并发,需要切换进程环境,系统开销大。线程间并发,如果是同一进程内线程进行切换,则不需要切换进程环境。
  6. 支持多处理器系统。对于传统的单线程进程,不管有多少处理机,进程只能运行在一个处理机上。对于多线程进程,可以将进程中的多个线程分配到多个处理机上执行。

1.6.3 线程的属性

  1. 线程是一个轻型实体,它不拥有系统资源,但每个线程都应有一个唯一的标识符和一个线程控制块(TCB)。
  2. 不同的线程可以执行相同的程序,即同一个服务程序被不同的用户调用时,操作系统把它们创建成不同的线程。
  3. 同一进程中的各个线程共享该进程拥有的资源。
  4. 线程是处理机的独立调度单位,多个线程是可以并发执行的。
  5. 一个线程被创建后,便开始了它的生命周期,直至终止。

1.6.4 线程的实现方式

线程的实现方式可以分为两类:用户级线程(User-Level Thread,ULT)和内核级线程(Kernel-Level Thread, KLT)。内核级线程又称内核支持的线程。

  • 用户级线程(ULT)
    早期的操作系统只支持进程,不支持线程。所以当时的“线程”都是由线程库实现的。我们可以通过一个很形象的例子说明线程库是如何实现的:
// 文字聊天进程
void process1() {
while (true) {
处理文字聊天
}
}
// 视频聊天进程
void process2() {
while (true) {
处理视频聊天
}
}
// 文件传输进程
void process3() {
while (true) {
处理文件传输
}
}

以上即为3个进程代码段,分别实现了文字聊天,视频聊天以及文件传输。那么我们的QQ进程代码段可以如下:

void qq() {
int i = 0;
while (true) {
if (i == 0) {
// 处理文字聊天
} else if (i == 1) {
// 处理视频聊天
} else {
// 处理文件传输
}
i = (i + 1) % 3;
}
}

从代码的角度来看,线程实际上就是一段代码逻辑。上述三段代码逻辑上可以看作3个“线程”。while循环就是一个弱智的“线程库”,线程库完成了对线程的管理工作(如调度)。

很多编程语言提供了强大的线程库,可以实现线程的创建、销毁、调度等功能。如C++中的​​thread​​库。

  • 用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程切换)
  • 用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预。
  • 在用户看来,是有多个线程。但是在操作系统内核看来,并意识不到线程的存在。“用户级线程”就是“从用户视角看能看到的线程”。

对于设置了用户级进程的系统,其调度仍是以进程为单位进行的。各个进程轮流执行一个时间片。假设进程A包含1个用户级线程,进程B包括100个用户级线程,这样,进程A中线程的运行时间将是进程B中各线程运行时间的100倍,因此对线程来说实质是不公平的。这种实现方式的优点如下:

  • 优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。调度算法可以是进程专用的。用户级线程的实现与操作系统平台无关。
  • 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。不能发挥多处理机的优势,内核每次分配给进程的仅有一个CPU,因此进程中仅有一个线程能执行。
  • 内核级线程(KLT)
    内核级线程的管理工作由操作系统内核完成。线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成。操作系统会为每个内核级线程建立相应的TCB(Thread Control Block,线程控制块),通过TCB对线程进行管理。“内核级线程”就是“从操作系统内核视角看能看到的线程” 。下图说明了内核级线程的实现方式。

    这种实现方式的优点如下:
  • 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。内核支持线程具有很小的数据结构和堆栈,线程切换比较快、开销小。
  • 缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
  • 组合方式
    有些系统使用组合方式的多线程实现。在组合实现方式中,内核支持多个内核级线程的建立、调度和管理,同时允许用户程序建立、调度和管理用户级线程。一些内核级线程对应多个用户级线程,这是用户级线程通过时分多路复用内核级线程实现的。同一进程中的多个线程可以同时在多处理机上并行执行,且在阻塞一个线程时不需要将整个进程阻塞。所以这种方式结合了ULT和KLT的优点,并且克服各自的不足。

1.6.5 多线程模型

在支持内核级线程的系统中,可以根据用户级线程和内核级线程的映射关系划分为几种多线程模型。

  • 多对一模型:多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程。如下图所示。
    计算机操作系统学习笔记 第二章、进程与线程_笔记_09
    优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。
    缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。在任何时刻只有一个线程能够访问内核,多个线程不可在多核处理机上并行运行。
  • 一对一模型:一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级进程。
    计算机操作系统学习笔记 第二章、进程与线程_操作系统_10
    优点:当一个线程被阻塞后,允许调度另一个线程运行,所以并发能力较强。
    缺点:每创建一个用户线程,相应地就需要创建一个内核线程,开销较大。
  • 多对多模型:n 用户级线程映射到 m 个内核级线程(n >= m)。每个用户进程对应 m 个内核级线程。
    计算机操作系统学习笔记 第二章、进程与线程_学习_11
    特点:既克服了多对一模型并发度不高的缺点,又克服了一对一模型的一个用户进程占用太多内核级线程而开销太大的缺点。此外,还拥有上述模型各自的优点。
    可以这么理解,用户级线程是“代码逻辑”的载体,而内核级线程是“运行机会”的载体。内核级线程才是处理机分配的单位。例如:多核CPU环境下,上图这个进程最多能被分配两个核。一段“代码逻辑”只有获得了“运行机会”才能被CPU执行。内核级线程中可以运行任意一个有映射关系的用户级线程代码,只有两个内核级线程中正在运行的代码逻辑都阻塞时,这个进程才会阻塞。

2 处理机调度

2.1 调度的概念

在多道程序系统中,进程的数量往往多于处理机的个数,因此进程争用处理机的情况在所难免。处理机调度是对处理机进行分配,即从就绪队列中按照一定的算法(公平、高效的原则)选择一个进程并将处理机分配给它运行,以实现进程并发地执行。

处理器调度可按照层级分为三级:高级调度、中级调度和低级调度。

用户作业从进入系统成为后备作业开始,知道运行结束退出系统为止,最多经历三层级别的调度。低级调度是各类操作系统的必备功能。在纯粹分时操作系统或实时操作系统中通常不需要高级调度。一般操作系统都配置了高级调度和低级调度,而功能完善的操作系统为了提高内存利用率和作业吞吐率引进了中级调度。所以从处理器调度的层次来说,可分为三级调度模型和两级调度模型。

下图为处理机的三级调度模型。

计算机操作系统学习笔记 第二章、进程与线程_笔记_12

  • 高级调度(作业调度)
    按照一定的原则从外存的后备作业队列中挑选一个(或多个),给它们分配内存、输入/输出设备等必要的资源,并建立相应的进程,以使它们获得竞争处理机的权利。简而言之,作业调度就是内存与辅存之间的调度。对于每个作业只调入一次、调出一次。作业调入时会建立PCB,调出时才撤销PCB。
    多道批处理系统中大多配有作业调度,而其他系统中通常不需要配置作业调度。
  • 中级调度(内存调度)
    引入中级调度的目的是提高内存利用率和系统吞吐量。为此,将那些暂时不能运行的进程调至外存等待,此时进程的状态称为挂起态。当它们已具备运行条件且内存又稍有空闲时,由中级调度来决定把外存上的那些已具备运行条件的就绪进程再重新调入内存,并修改其状态为就绪态,挂在就绪队列上等待。
  • 低级调度(进程调度)
    按照某种算法从就绪队列中选取一个进程,将处理机分配给它。进程调度是最基本的一种调度,在各种操作系统中都必须配置这级调度。进程调度的频率很高,一般几十毫秒一次。

三层调度之间的联系如下表:

调度层次

要做什么

调度发生在…

发生频 率

对进程状态的影响

高级调度 (作业调度)

按照某种规则,从后备队列 中选择合适的作业将其调入 内存,并为其创建进程

外存—>内存 (面向作业)

最低

无—>创建态—>就绪态

中级调度 (内存调度)

按照某种规则,从挂起队列 中选择合适的进程将其数据 调回内存

外存—>内存 (面向进程)

中等

挂起态—>就绪态 (阻塞挂起—>阻塞态)

低级调度 (进程调度)

按照某种规则,从就绪队列 中选择一个进程为其分配处 理机

内存—>CPU

最高

就绪态—>运行态

2.2 调度算法的评价指标

  • CPU利用率:指CPU“忙碌”的时间占总时间的比例。计算方式如下:
    计算机操作系统学习笔记 第二章、进程与线程_死锁_13
    尽可能使CPU保持“忙”状态,使这一资源利用率最高。
  • 系统吞吐量:单位时间内CPU完成作业的数量。计算方式如下:
    计算机操作系统学习笔记 第二章、进程与线程_死锁_14
  • 周转时间:从作业提交到作业完成所经历的时间,是作业等待(高级调度)、在就绪队列中排队(低级调度)、在处理机上运行及输入/输出操作所花费时间的总和。周转时间的计算方法如下:
    计算机操作系统学习笔记 第二章、进程与线程_操作系统_15
    而平均周转时间则是指多个作业周转时间的平均值。
    计算机操作系统学习笔记 第二章、进程与线程_学习_16
    对于用户来说,更关心自己单个作业的周转时间,而对于操作系统来说,更关心系统的整体表现,因此更关心所有作业的平均周转时间。
    带权周转时间是指作业周转时间与作业实际运行时间的比值:
    计算机操作系统学习笔记 第二章、进程与线程_笔记_17
    平均带权周转时间是指多个作业带权周转时间的平均值:
    计算机操作系统学习笔记 第二章、进程与线程_学习_18
    其中带权周转时间必然计算机操作系统学习笔记 第二章、进程与线程_死锁_19,带权周转时间和周转时间都是越小越好。
  • 等待时间:进程处于等待处理机的时间之和,等待时间越长,用户满意度越低。处理机调度算法实际上不影响作业执行或输入/输出操作的时间,只影响作业在就绪队列中等待所花的时间。所以衡量一个调度算法的优劣,常常只需简单地考察等待时间。
  • 响应时间:从用户提交请求到系统首次产生响应所用的时间。在交互式系统中,一般采用响应时间作为衡量调度算法的重要准则。

涉及调度程序时,一方面要满足特定系统用户的需求,另一方面要考虑系统整体效率(如减少整个系统的进程平均周转时间),同时还要考虑调度算法开销。

2.3 调度的实现

2.3.1 进程调度的时机

现代操作系统中,应进行进程的调度与切换的情况有以下几种:

  • 主动放弃处理机
  • 进程正常终止。
  • 运行过程中发生异常而终止。
  • 进程主动请求阻塞(如等待I/O)。
  • 被动放弃处理机
  • 分给进程的时间片用完。
  • 有更紧急的事需要处理(如I/O中断)。
  • 有更高优先级的进程进入就绪队列。

但如果发生以下情况,则不能马上进行调度和切换,应置系统的请求调度标志,直至发生的情况结束后再进行响应的调度和切换。

  • 在处理中断的过程。
  • 进程在操作系统内核程序临界区中。(和临界资源不同,临界区指的是访问临界资源的那段代码;而临界资源则是指一个时间段只能允许一个进程使用的资源。而这里内核临界区则是一般用来访问某种内核数据结构的代码,在普通临界区可以调度和切换)。
  • 在原子操作过程中(原语)。

2.3.2 调度的切换和过程

狭义的进程调度指的是从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程,也可能是另一个进程,后一种情况就需要进程切换)进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程。广义的进程调度包含了选择一个进程和进程切换两个步骤。进程切换的过程主要完成了:

  • 对原来运行进程各种数据的保存
  • 对新的进程各种数据的恢复
    (如:程序计数器、程序状态字、各种数据寄存器等处理机现场信息,这些信息一般保存在进程控制块)

注意:进程调度、切换是有代价的,因此如果过于频繁的进行进程调度、切换,必然会使整个系统的效率降低,使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间减少。

2.3.3 进程调度的方式

所谓进程调度方式,是指当某个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要处理,即有优先权更高的进程进入就绪队列,此时应该如何分配处理机。通常有以下两种进程调度方式:

  • 非剥夺调度方式,又称非抢占方式。即只允许进程主动放弃处理机,在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态。
  • 剥夺调度方式,又称抢占方式。当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给更重要紧迫的那个进程。

2.4 典型的调度算法

学习调度算法的思路如下:

  1. 算法思想
  2. 算法规则
  3. 这种调度算法是用于作业调度还是进程调度?
  4. 抢占式?非抢占式?
  5. 优点和缺点
  6. 是否会导致饥饿?

2.4.1 先来先服务(FCFS)调度算法

该算法思想是从“公平”的角度考虑(类似我们生活中排队买东西的例子)。该算法按照作业/进程到达的先后顺序进行服务。用于作业调度时,考虑的是哪个作业先到达后备队列;用于进程调度时,考虑的是哪个进程先到达就绪队列。是非抢占式的算法,不会导致饥饿。其特点如下。

  • 优点:公平,算法实现简单。
  • 缺点:对长作业有利,对短作业不利。

2.4.2 短作业优先(SJF)调度算法

该算法思想追求最少的平均等待时间、最少的平均周转时间、最少的平均带权周转时间。该算法按照最短的作业/进程优先得到服务(所谓“最短”,是指要求的服务时间最短)。即可用于作业调度,也可以用于进程调度。用于进程调度时称为“短进程优先(SPF)算法”。SJF和SPF都是非抢占式的算法,但是也有抢占式的版本—最短剩余时间优先(SRTN,Shortest Remaining Time Next)算法。其特点如下:

  • 优点:“最短的“平均等待时间、平均周转时间。
  • 缺点:不公平,对短作业有利,长作业不利。另外,作业/进程的运行时间是用户提供的,并不一定真实,不一定能做到真正的短作业优先。

该算法会导致饥饿。如果源源不断的短作业/进程到来,会使长作业/进程长期得不到服务。

2.4.3 高响应比优先调度(HRRN)算法

该算法思想综合考虑作业/进程的等待时间和要求服务的时间,是对FCFS调度算法和SJF调度算法的一种综合平衡。在每次调度时,先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务。响应比计算公式如下。

计算机操作系统学习笔记 第二章、进程与线程_死锁_20

该算法既可用于作业调度,也可以用于进程调度,是一种非抢占式的算法,因此只有当前运行的作业/进程主动放弃处理机时,才需要调度,才需要计算响应比。其特点如下:

  • 综合考虑了等待时间和要求服务时间
  • 等待时间相同时,要求服务时间短的优先;要求服务时间相同的同时,要求等待时间长的优先。
  • 避免了长作业饥饿的问题。

2.4.4 时间片轮转(RR)算法

该算法思想为公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。按各个进程到达就绪队列的顺序,轮流让各个进程执行一个时间片。若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列队尾重新排队。用于进程调度(只有作业放入内存建立了响应的进程后,才可以被分配处理机时间片),不会产生饥饿。若进程未能在时间片内运行完,将被强行剥夺处理机使用权,因此时间片轮转调度算法属于抢占式的算法。由时钟装置发出时钟中断来通知CPU时间片已到。 其特点如下。

  • 优点:公平;响应快,适用于分时操作系统;
  • 缺点:由于高频率的进程切换,因此有一定开销;不区分任务的紧急程度。

在时间片轮转算法中,时间片的大小对系统性能的影响很大。若时间片足够大,以至于每个进程都能在一个时间片内执行完,则时间片轮转算法就退化为FCFS算法。若时间片很小,则处理机在进程间过于频繁切换,使处理机开销增大,则真正用于运行用户进程的时间将减小。时间片的长短通常由系统的响应时间、就绪队列中的进程数目和系统的处理能力决定。

2.4.5 优先级调度算法

该算法思想为根据任务的紧急程度来处理处理顺序。在该算法中每个作业/进程有各自的优先级,调度时选择优先级最高的作业/进程。可用于作业调度和进程调度,甚至还可以用于I/O调度。抢占式、非抢占式都有,只需要在意抢占和非抢占式的区别:非抢占式只需在进程主动放弃处理机时进行调度即可,而抢占式还需在就绪队列变化时,检查是否会发生抢占。 其特点如下。

  • 优点:用优先级区分紧急程度、重要程度,适用于实时操作系统。可灵活地调整对各种作业/进程的偏好程度。
  • 缺点:若源源不断地有高优先级进程到来,则可能导致饥饿。

2.4.6 多级反馈队列调度算法

该算法思想是基于其他调度算法的折中权衡。其算法规则如下:

  • 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大。
  • 新进程到达时先进入第1级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时已经是在最下级的队列,则重新放回该队列队尾。
  • 只有第 k 级队列为空时,才会为 k+1 级队头的进程分配时间片。

该算法用于进程调度,是抢占式的算法,会导致饥饿。在 k 级队列的进程运行过程中,若更上级的队列(1~k-1级)中进入了一个新进程,则由于新进程处于优先级更高的队列中,因此新进程会抢占处理机,原来运行的进程放回 k 级队列队尾。算法的优点有:对各类型进程相对公平(FCFS的优点);每个新到达的进程都可以很快就得到响应(RR的优点);短进程只用较少的时间就可完成(SPF的优点);不必实现估计进程的运行时间(避免用户作假);可灵活地调整对各类进程的偏好程度,比如CPU密集型进程、I/O密集型进程(拓展:可以将因I/O而阻塞的进程重新放回原队列,这样I/O型进程就可以保持较高优先级)。

2.4.7 各调度算法对比

算法

可抢占?

优点

缺点

考虑到等待时 间&运行时间?

会导致 饥饿?

FCFS

非抢占式

公平;实现简单

对短作业不利

等待时间√ 运行时间×

不会

SJF/S PF

默认为非抢占式, 也有SJF的抢占式 版本最短剩余时间 优先算法(SRTN)

“最短的”平均等待/周转时间;

对长作业不利,可能导致饥饿;难以做到真正的短作业 优先

等待时间× 运行时间√


HRRN

非抢占式

上述两种算法的权衡折中,综合考虑的等 待时间和运行时间

等待时间√运行时间√

不会

时间 片轮 转

抢占式

公平,适用于分时系统

频繁切换有开销,不区分优先级

不考虑

不会

优先 级调 度

有抢占式的,也有非抢占式的。

区分优先级 适用于实时系统

可能导致饥饿

不考虑


多级 反馈 队列

抢占式

平衡优秀 666

一般不说它有缺点,不过可能导 致饥饿


3 同步与互斥

3.1 同步与互斥的基本概念

  • 进程同步
    我们首先要知道,进程具有异步性的特征(异步性是指,各并发执行的进程以各自独立的、不可预知的速度向前推进)。那么操作系统就需要提供“进程同步机制”来解决异步问题。
    同步也称为直接制约关系,是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系来源于它们合作。
    例如,输入进程A通过单缓冲区向进程B提供数据。当缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B就被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。
  • 进程互斥
    进程的“并发”需要共享的支持。各个并发执行的过程不可避免的需要共享一些系统资源。我们把一个时间段内只允许一个进程使用的资源叫作临界资源,对临界资源的访问,必须互斥地进行。
    互斥,亦称间接制约关系。进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。对临界资源的互斥访问,可以在逻辑上分为如下四个部分:
do {
entry section; // 进入区。负责检查是否可以进入临界区,若可以进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区
critical section; // 临界区。访问临界区的那段代码,又称为临界段。
exit section; // 退出区。负责解除正在访问临界区的标志
remainder section; // 剩余区。做其他处理
} while (true);

其中进入区和退出区是实现互斥的代码段。

为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:

  1. 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区;
  2. 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待;
  3. 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿);
  4. 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。

3.2 实现临界区互斥的基本方法

3.2.1 软件实现方法

  • Dekker算法
  • Peterson算法

3.2.2 硬件实现方法

  • 中断屏蔽(禁止中断)
    当一个进程正在执行它的临界区代码时,防止其他进程进入其临界区的最简方法就是禁止中断。因为CPU只在发生中断时引起进程切换,因此屏蔽中断能够保证当前运行的进程让临界区代码顺利地执行完,进而保证互斥的正确执行,然后执行开中断。其典型模式如下:
do {
<disable interrupts>; // 关中断
<critical section>; // 临界区
<enable interrupts>; // 开中断
} while (true);

这种方法代价较高,而且执行效率也会显著地降低,因为处理机受到不能插入的限制。其次这种方法不能用于多处理机系统,对于含有不止一个处理机的计算机系统,在同一时间通常有一个以上的进程在执行。在这种情况下,禁止中断不能保证互斥。

  • 使用机器指令
    在多处理机系统中,多个处理机共享一个主存,这里并没有主/从关系也没有实现互斥的中断机制。在硬件水平上,对同一存储区的访问是互斥执行的。以此为基础,设计者设计了几个机器指令以执行两个原子操作,例如读/写或读测试,因为这些操作都是在一个指令周期内完成的,所以不会被其他指令打断。这里给出两个最一般的指令:test and set指令和exchange指令。定义如下:
bool test_and_set(bool *lock) {
bool old;
old = *lock;
*lock = true;
return old;
}
void exchange(bool *a, bool *b) {
bool temp = *a;
*a = *b;
*b = temp;
}

这两条指令实现互斥的方式如下:

// test_and_set的实现互斥方式
// lock为检查标志,若无进程在临界区,则其值为false,否则为true
while (test_and_set(&lock));
进程的临界区代码;
lock = false;
进程的其他代码
// exchange的实现互斥方式
// key用于和lock交换信息
key = true;
while (key != false) {
exchange(&lock, &key);
进程的临界区代码;
lock = false;
进程的其他代码

使用特殊的机器指令实现互斥有许多优点:

  • 可用于含有任意数量进程的单处理机或共享主存的多处理机;
  • 比较简单,所以易于验证;
  • 可支持多个临界段,每个临界段用各自的变量加以定义。

当然,这类方法也有缺点:

  • 由于采用忙—等,所以在进程等待进入临界段时,将耗费处理机时间。
  • 有可能产生饥饿。
  • 有可能产生死锁。

3.3 信号量机制

3.2节学习的解决方案都无法实现“让权等待”。1965年,荷兰学者Dijkstra提出了一种卓有成效的实现进程互斥、同步的方法—信号量机制。用户进程通过使用操作系统提供的一对原语对信号量进行操作,从而很方便的实现了进程互斥、进程同步。

其中信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量。除了对信号量设置初值之外,对信号量只能施加特殊的操作:P操作和V操作,P操作和V操作都是不可分割的原语操作,因此也成为原语。P操作和V操作也可记为wait()和signal()。

3.3.1 整型信号量

用一个整数型的变量作为信号量,用来表示系统中某种资源的数量,例如某计算机系统中有一台打印机。其中定义操作如下:

int s = 1; // 初始化整型信号量s,用来表示系统中可用的打印机资源数
void wait(int &s) { // wait原语,相当于“进入区”
while (s <= 0); // 如果资源数不够,就一直循环等待
s = s - 1; // 如果资源数够,就占用一个资源
}
void signal(int &s) { // signal原语,相当于“退出区”
s = s + 1; // 使用完资源后,在退出区释放资源
}

在整型信号量机制中的wait操作,只要信号量计算机操作系统学习笔记 第二章、进程与线程_笔记_21,就会不断地测试。因此,该机制并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。

3.3.2 记录型信号量

记录型信号量机制是一种不存在“忙等”现象的进程同步机制。除了需要一个用于代表资源数目的整形变量value外,再增加一个进程链表L,用于链接所有等待该资源的进程。

/* 记录型信号量的定义 */
typedef struct {
int value; // 剩余资源数
struct process *queue; // 等待队列
} semaphore;
semaphore s;
/* 某进程需要使用资源时,通过wait原语申请 */
void wait(semaphore &s) {
-- s.value; // 使用资源,如果资源数不够了,就阻塞
if (s.value < 0) {
将该进程置入s.queue中;
阻塞该进程
}
}
void signal(semaphore &s) {
++ s.value; // 释放资源
if (s.value <= 0) {
将进程p从s.queue中移除;
将进程p置入就绪队列;
}
}

其中,在任意时刻,我们都可以将​​s.value​​的值解释如下:

  • ​s.value>=0​​​:​​s.value​​是在执行wait(s)操作而不会阻塞的进程数(这期间没有执行任何signal(s)操作)。
  • ​s.value<0​​​:​​s.value​​​是阻塞在​​s.queue​​队列中的进程数。

3.3.3 利用信号量实现进程互斥

信号量机制也能很方便地解决进程互斥问题。设计算机操作系统学习笔记 第二章、进程与线程_死锁_22为实现进程计算机操作系统学习笔记 第二章、进程与线程_计算机操作系统_23互斥的信号量,由于每次只允许一个进程进入临界区,所以计算机操作系统学习笔记 第二章、进程与线程_死锁_22的初值应为计算机操作系统学习笔记 第二章、进程与线程_笔记_25(即可用资源数为计算机操作系统学习笔记 第二章、进程与线程_笔记_25)。所以只需把临界区置于计算机操作系统学习笔记 第二章、进程与线程_死锁_27计算机操作系统学习笔记 第二章、进程与线程_学习_28之间,即可实现对临界资源的互斥访问。算法如下:

semaphore s = 1; // 互斥信号量
void procedure(int i) { // 第i个进程
while (true) {
wait(s); // 申请资源,加锁
<critical section>; // 临界区
signal(s); // 释放资源, 解锁
<remainder>;
}
}
int main() {
procedure(1);
procedure(2);
...
procedure(n);
return 0;
}

注意:对于不同的临界资源需要设置不同的互斥信号量。P、V操作必须成功出现。缺少P操作就不能保证临界资源的互斥访问,缺少V操作就会导致资源永不被释放,等待进程永不被唤醒。

3.3.4 利用信号量实现进程同步

进程同步即是让各并发进程按要求有序地推进。设。

void P1() {
代码1;
代码2;
代码3;
}
void P2() {
代码4;
代码5;
代码6;
}

比如,P1、P2要并发执行,由于存在异步性,因此二者的交替推进次序是不确定的,若P2的“代码4”需要基于P1的“代码1”和“代码2”的运行结果才能执行,那么我们必须保证“代码4”一定是在“代码2”之后才会执行。

用信号量实现进程同步,需要注意以下几点:

  • 分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作。
  • 设置同步信号量s,初始为0。
  • 在“前操作”之后执行V(S)。
  • 在“后操作”之前执行P(S)。
semaphore s = 0; // 初始化信号量
void P1() {
代码1;
代码2;
signal(s); // 释放资源
代码3;
}
void P2() {
wait(s); // 请求资源
代码4;
代码5;
代码6;
}

3.3.5 使用信号量机制实现前驱关系

进程P1中有句代码S1,P2中有句代码S2,…P6中有句代码S6。这些代码要求按如下前驱图所示的顺序执行。
计算机操作系统学习笔记 第二章、进程与线程_死锁_29

其实每一段前驱关系都是一个进程同步问题(需要保证一前一后的操作),因此:

  • 要为每一对前驱关系各设置一个同步信号量。
  • 在“前操作”之后对相应的同步信号量执行V操作。
  • 在“后操作”之前对相应的同步信号量执行P操作。

算法如下:

semaphore a = 0, b = 0, c = 0, d = 0, e = 0;
void P1() {
S1;
signal(a);
}
void P2() {
S2;
signal(b);
}
void P3() {
S3;
signal(c);
}
void P4() {
wait(a);
wait(b);
S4;
signal(d);
}
void P5() {
wait(c);
S5;
signal(e);
}
void P6() {
wait(d);
wait(e);
S6;
}

3.4 管程

在信号量机制中,每个要访问临界资源的进程都必须自备同步的PV操作,这存在编写程序困难,易出错等问题。于是便产生了一种新的进程同步工具—管程。管程的特性保证了进程互斥,无需程序员自己实现互斥,从而降低了死锁发生的可能。同时,管程提供了条件变量,可以让程序员灵活地实现进程同步。管程是一种特殊的软件模块,由这些部分组成:

  • 管程的名称;
  • 局部于管程内部的共享数据结构说明;
  • 对该数据结构进行操作的一组过程;
  • 对局部于管程内部的共享数据设置初始值的语句。

其基本特征如下:

  • 局部于管程的数据只能被局部于管程的过程所访问;
  • 一个进程只有通过调用管程内的过程才能进入管程访问共享数据;
  • 每次仅允许一个进程在管程内执行某个内部过程。

当有一个进程进入管程后被阻塞,直到阻塞的原因解除时,在此期间,如果该管程不释放管程,那么其他进程无法进入管程。为此,将阻塞原因定义为条件变量condition。通常,一个进程被阻塞的原因可以有多个,因此在管程中设置了多个条件变量。每个条件变量保存了一个等待队列,只能对条件变量进行两种操作,即wait和signal。其描述如下:

monitor Demo {
共享数据结构S;
condition x; // 定义一个条件变量x
init_code {
...
}
take_way() {
if (S <= 0) {
x.wait(); // 资源不够,在条件变量x上阻塞等待
}
资源足够,分配资源,做一系列相应处理;
}
give_back() {
归还资源,做一系列相应处理;
if (有进程在等待) {
x.signal(); // 唤醒一个阻塞进程
}
}
}

4 死锁

4.1 死锁的概念

死锁是指各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。所有的死锁问题都涉及到两个或更多的进程由于竞争资源引起的冲突。

死锁产生有如下几个原因:

  • 系统资源的竞争
    各个进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源(CPU)是不会引起死锁的。
  • 进程推进顺序非法。请求和释放的顺序不当,也同样会导致死锁。
  • 信号量的使用不当也会造成死锁。

产生死锁必须同时满足以下4个条件,只要其中任意一个条件不成立,死锁就不会发生。

  1. 互斥条件:只有对必须互斥使用的资源争抢才会导致死锁。
  2. 非抢占条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
  3. 占用并等待条件:进程已经至少保持了一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

注意:发生死锁的时一定有循环等待,但是发生循环等待时未必死锁。因为如果同类资源数大于1,即使有循环等待,也未必发生死锁。但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件了。

4.2 死锁处理

那么怎么处理死锁呢?有如下方案:

  1. 预防死锁。破坏死锁产生的四个必要条件中的一个或几个;
  2. 避免死锁。用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法);
  3. 死锁的检测和解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种策略解除死锁。

4.2.1 预防死锁

  1. 破坏互斥条件
    若允许系统资源都共享使用,则系统不会进入死锁状态。但有些资源根本不能同时访问,所以这种方法不太可行,而且在某些场合应该保持这种互斥性。
  2. 破坏非抢占条件
    一种方法是,拥有某种资源的进程在申请其他资源遭到拒绝时,它必须释放其占有的资源,如果以后有必要,它可再次申请上述资源。另一种方法是当一个进程申请的资源被其他进程占用时,可以通过操作系统抢占这一资源,但当两个进程的优先级相同时,不能防止死锁。
    这种策略的缺点如下:
  • 实现起来比较复杂。
  • 释放已获得的资源很可能造成前一阶段的工作失效,因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。
  • 反复地申请和释放资源会增加系统开销,降低系统吞吐量。
  • 若采用方案一,如果一直发生这样的情况,会导致进程饥饿。
  1. 破坏占用并等待条件
    可以采用静态分配的办法,即进程在运行前请求分配其所需的资源,并且阻塞该进程直至所有请求同时满足。该策略实现比较简单,但也有明显的缺点:有些资源可能只需要很短的时间,如果一直保持着所有资源,会导致严重的资源浪费,资源利用率极低,甚至会导致其他进程饥饿。
  2. 破坏循环等待条件
    可采用顺序资源分配法,首先给系统的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源一次申请完。

4.2.2 避免死锁

死锁避免有以下几种方法:

  • 如果进程对资源的申请可能导致死锁,就不启动这个进程。
  • 如果进程对资源的申请可能导致死锁,就不给进程分配该资源。

4.2.3 死锁的检测和解除

如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁,在这种情况下,系统应当提供两个算法:

  • 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。
  • 死锁解除算法:当认定系统中已经发生了死锁,利用算法可以将系统从死锁中解脱出来。

为了能对系统是否已经发生了死锁进行检测,必须:

  • 用某种数据结构来保存资源的请求和分配信息;
  • 提供一种算法,利用上述信息来检测系统是否已进入死锁状态。

检测死锁的算法:

  • 在资源分配图中,找出既不阻塞又不是孤点的进程 Pi​(即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量。如下图中,R1没有空闲资源,R2有一个空闲资源。若所有的连接该进程的边均满足上述条件,则这个进程能继续运行直至完成,然后释放它所占有的所有资源)。消去它所有的请求边和分配变,使之称为孤立的结点。在下图中,P1 是满足这一条件的进程结点,于是将P1的所有边消去。
  • 进程 Pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。在下图中,P2 就满足这样的条件。根据 1)中的方法进行一系列简化后,若能消去途中所有的边,则称该图是可完全简化的。

计算机操作系统学习笔记 第二章、进程与线程_死锁_30

死锁定理:如果某时刻系统的资源分配图是不可完全简化的,那么系统死锁。

一旦检测出死锁的发生,就应该立即解除死锁。主要方法如下:

  1. 杀死所有的死锁进程。这是操作系统中最常用的方法。
  2. 所有的死锁进程都退回到原来已定义的检测点,然后重新执行它们。则会中方法的危险性在于原来的死锁仍有可能出现。
  3. 逐个杀死死锁进程直至死锁消除。
  4. 组个抢占死锁的其他资源直到死锁不再存在。


举报

相关推荐

0 条评论