目前正在备考 24 考研,现将 24 计算机王道的 408 学习整理的知识点进行汇总整理。
博主博客文章目录索引:博客目录索引 (持续更新)
一、进程
1.1、进程的概念、组成与特征

1.1.1、进程的概念
进程的概念:
- 程序:静态的,就是存放在磁盘里的可执行文件,是一系列的指令集合。
- 进程(Process):动态的,是程序的一次执行过程。同一个程序多次执行会对应多个进程。
举例:我们启动 QQ 程序的三个进程,如下所示:

1.1.2、进程的组成
进程的组成:PCB、数据段、程序段。

e
认识 PCB
问题:操作系统是这些进程的管理者,那么它是怎么区分的各个进程?
- 操作系统在创建一个进程的时候会给这个进程分配一个唯一的,不重复的 ID 叫做
PID(Process ID,进程 ID),也就是进程 ID,它就相当于我们人类世界的身份证号,我们每个人的身份证号都是唯一的,
对于操作系统不仅仅记录 PID、进程所属用户 ID(UID,通过基本的进程描述信息,可以让操作系统区分各个进程),还要记录给进程分配了哪些资源,如内存、使用的 I/O 设备以及使用的文件等;还要记录进程运行情况,如 CPU 时间时间、磁盘使用情况、网络流量使用情况等。
- 借助进程分配了资源信息:这些记录信息可用于实现操作系统对资源的管理。
- 借助进程运行情况:这部分信息可以用于操作系统对进程的控制、调度。
每一次新建一个进程都会给它分配一个不重复的唯一的 ID。在很多操作系统当中 PID 的分配都是每一次加一,都是采用这样的一个很简单的策略。

对于每一个进程,操作系统还会记录进程的其他信息,例如使用了多久的 CPU,由哪个用户所创建的,还有一些各个进程对内存的使用信息、磁盘、网络情况等等。

PCB(Process Control Block,进程控制块):对于上述所说对每个进程分配的 PID、资源以及使用情况,这些数据都被保存到一个数据结构PCB中。
- 操作系统需要对各个并发运行的进程运行管理,但凡管理时所需要的信息,都会存放在 PCB 中。

看一下实际 Linux 中 PCB 结构体的实现,其中包含了对应的进程状态、信息字段,线程信息等等:

认识程序段与数据段(包含进程实体概念)
除了 PCB 之外进程还有两个很重要的组成:程序段与数据段
程序段:除了 PCB,指定程序中的所有指令序列会读到内存中,这一系列的指令序列,将它称为程序段,之后程序执行的过程里,CPU 就会从内存当中读入一条一条指令,接着来执行指令。数据段:在执行指令的过程中,会有一些中间数据,例如定义了变量等,这些变量内容也会放入到主存中,此时还会有一个叫做数据段的区域用来存放这个程序运行过程中所产生的所需要使用的各种数据。
进程实体:一个进程实体(进程映像)由 PCB、程序段、数据段组成。
- 进程是动态的,进程实体(进程印象)是静态的。对于进程实体可以将其理解为进程在动态执行过程中某一个时刻的快照。
- 对于操作系统管理进程所需要的数据都在 PCB 当中,对于进程自己所需要的数据是放在程序段和数据段中的。

程序段、数据段、PCB 三个部分组成了进程实体(进程映像)。
引入进程实体的概念后,可把进程定义为:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
注意:PCB 是进程存在的唯一标志。
举例:若是同时挂三个 QQ 那么对应三个 QQ 进程,它们的 PCB、数据段各不相同,但是程序段的内容都是相同的(都是运行着 QQ 程序)。
1.1.3、进程的特征
程序是静态的,进程是动态的,相对比程序,进程具有以下的特征:
- 动态性(进程最基本的特征):进程是程序的一次执行过程,是动态地产生、变化和消亡的。
- 并发性:主存中有多个进程实体,各进程可并发执行。
- 独立性:进程是能够独立运行、独立获得资源、独立接受调度的基本单位。
- 异步性:各进程按各自独立的、不可预知的速度向前推进,操作系统要提供 “进程同步机制” 来解决异步问题。
- 异步性会导致并发程序执行结果的不确定性。
- 结构性:每个进程都会配备一个 PCB,结构上来看进程由程序段、数据段、PCB 组成。
知识回顾与重要考点

1.2、进程的状态、状态间的转换和组织方式

1.2.1、进程的状态
进程的五种状态
进程的五种状态如下:
- 三种基本状态:运行态、就绪态、阻塞态。
- 另外两种状态:创建态、结束态。
对于运行态的说明以及一些状态的别名如下图:

进程的状态是如何表示的?
- 进程 PCB 中,会有一个变量 state 来表示进程的当前状态,例如:1 表示创建态,2 表示就绪态,3 表示运行态…,对于同一个状态下的各个进程进行统一的管理,操作系统会将各个进程的 PCB 组织起来。
详细描述各个状态
创建态:进程正在被创建时,它的状态是 “创建态”,在这个阶段操作系统会为进程分配资源、初始化 PCB。
就绪态:当进程被创建完成之后,便进入了 “就绪态”,处于就绪态的进程已经具备了运行条件,但由于没有空闲 CPU,暂时不能够进行运行。

运行态:当 CPU 空闲时,操作系统就会选择一个就绪程序,让它上处理机运行。
- 若是一个进程此时在 CPU 上运行,那么这个进程就会处于 “运行态”。

对于阻塞态并不是所有的进程都会进入到该状态,何时会进入到该状态呢,例如当前 CPU 正在执行进程的指令,此时进程的一条指令是要请求打印机,传输数据让打印机打印,此时由于打印机已经被其他进程占用了,此时程序就无法继续往下运行,此时操作系统会剥夺这个进程对 CPU 的使用,让这个进程进入到新的状态下也就是阻塞态。

阻塞态:在进程运行的过程中,可能会请求等待某个事件的发生(例如等待某种系统资源的分配,或者等待其他进程的响应)。在这个事件发生之前,进程无法继续往下执行,此时操作系统会让这个进程下 CPU,并让它进入到 “阻塞态”。
- 此时当 CPU 空闲时则会选择另一个 “就绪态” 进程上 CPU 运行。

- 此时若是打印机已经完成了其他进程的任务并空闲了下来,此时就可以将这个打印机资源分配给刚才的进程二,当操作系统把这个打印机资源分配给进程二的时候,进程二等待的事件其实就已经发生了,此时操作系统会让进程二的状态从阻塞态 → 就绪态。
若是此时在 CPU 上运行的进程 1 结束了,此时在运行结束的时候,①会发出一个 exit 系统调用,请求操作系统终止这个进程。②此时这个进程的状态就会变成终止态。③接着操作系统会让这个进程下 CPU 作一系列善后的工作,回收这个进程所有的资源、内存空间、打印机设备等所有资源,最终还会回收 PCB 此时就从系统中彻底消失了。

终止态:一个进程可以执行 exit 系统调用,请求操作系统终止该进程,此时进程会进入到 “终止态”,操作系统接下来会让该进程下的 CPU 回收内存空间等等,最后回收该进程的 PCB,一旦终止进程的工作完成后,这个进程彻底消失。
1.2.2、进程状态的转换
进程状态的转换:见下图

- 五状态模型的转换,丁字裤模型。
对于上面五种状态之间转换的补充:- 进入到就绪态时,进程除了处理机没有其他资源都已就位。
- 进入到运行态的进程是在 CPU 上运行中,既拥有了其所需要的资源还有处理机。
- 在运行态的进程可能会请求一些需要等待的资源,此时就会被剥夺处理机资源此时进入到阻塞态。
- 若是在阻塞态的进程等待的资源发生了,此时进程可以从阻塞态回到就绪态。
注意:
- 对于运行态 → 阻塞态是进程自身做出的主动行为。对于阻塞态 → 就绪态不是自身所能控制的,是被动完成的(对于打印机这类资源什么时候完成并不是进程自己能够决定的)。
- 阻塞态 → 运行态、就绪态 → 阻塞态是不能够完成的,因为进入到阻塞态是进程主动请求的,必须需要进程在运行时才能发出这种请求。而对于进入运行态必须是当前为就绪态才能够进入。
还有一种特殊情况:有时候进程可以直接从运行态转换成就绪态,例如操作系统给进程分配的时间片用完的时候,此时进程就会从运行态转换成就绪态了。
- 时间片实际就是给与这个进程分配的 CPU 运行时间,一旦时间用完了,此时就会时钟部件就会发送一个时钟中断的信号,一旦这个进程运行了很少时间,就不应该让它继续往下执行了。

1.2.3、进程的两种组织方式
如何将进程组织起来?
进程的组织包含有两种方式:组织方式、索引方式

方式一:链接方式
简洁:操作系统会管理一系列的队列,每一个队列都有指向相应的状态的进程 PCB。
举例一个执行指针就会执行正在处于运行态的进程的 PCB:

就绪队列指针则会指向处于就绪态的进程 PCB,为了方便这些就绪进程的调度,操作系统会将优先级更高的进程 PCB 放在队列的队头。

阻塞队列指针也同样会指向处于阻塞状态的 PCB 如下:

在很多操作系统中实际会根据阻塞原因的不同,将阻塞队列分为多个,例如等待打印机、等待磁盘的:

操作系统采用这样子链式的方式将进程统一组织管理起来
方式二:索引方式
操作系统会给各种状态的进程建立相应的索引表,每个索引表的表项则是指向一个PCB,如下图所示:

实际应用中大多数还是使用的链式存储。
知识回顾与重要考点
对于绿框的则是比较重要的考点:

1.3、进程控制

1.3.1、进程控制的基本概念
1.3.1.1、什么是进程控制?
进程控制本质:实现进程的状态转换。
创建新进程就是创建态的过程,撤销已有进程则是终止态。

1.3.1.2、如何实现进程控制?
原语:是一种特殊的程序,其执行具有原子性,这段程序的运行必须是一气呵成的,不可中断。
为什么进程控制(状态转换的过程)需要一气呵成?
- 若是在转换的过程中不能够一气呵成,那么可能导致转换过程中进行了一半就结束,此时就可能会造成同一数据结构中状态信息不统一情况,影响操作系统完成别的工作。
这里对于数据结构中状态信息不统一情况来用一个阻塞唤醒原理举例子:

- 想要完成状态转换必须要完成这两步才可以。
首先有两个状态指针采用的是链式分别指向一个 PCB:

若是在阻塞唤醒转换过程中执行了步骤 1 后此时来了一个中断信号,若是没有使用原语,此时操作系统就会对这个中断进行处理,注意此时第②步骤并没有完成,当前两个指针指向的状态如下:

此时可以发现数据结构中的状态字段信息不统一。
1.3.1.3、如何实现原语的 “原子性”?
如何实现原语的原子性?:通过使用关中断与开中断指令两个特权指令实现原子性,被这两个指令包裹的指令中间不会被中断,而是会一气呵成的完成指令。
原理:CPU 执行关中断指令后,就不再例行检查中断信号,直到执行中断指令之后才会恢复检查。通过这样子的关中断、开中断指令序列就是不可被中断的,这就实现了 “原子性”。

如果这两个特权指令允许用户程序使用的话,会发生什么事情?
- 若是允许用户使用,那么意味着我可以在程序开头设置关中断,结尾设置开中断,此时一旦程序上 CPU 运行了,那么程序肯定会霸占 CPU,不会被中断,这种情况不应该让它发生。
- 关中断、开中断是特权指令,只能够让内核程序使用,而不能让普通的用户程序使用。
1.3.2、进程控制相关的原语
①创建进程原语
若是操作系统想要创建一个进程,那么就必须使用创建原语,下面是对应步骤:
- 根据进程来进行申请 PCB。
- 需要给进程分配资源,如内存空间等等。
- 需要对 PCB 的内容进行一些初始化的工作,例如分配 PID 设置 UID 等。
- 将 PCB 插入到就绪队列中。【创建态 → 就绪态】
对于一些事件实际会引起操作系统的进程创建,此时就会间接的调用创建原语,下面是一些事例举例:
①用户登录:在分时系统中,若是有用户登录成功,此时系统会建立一个新的进程。
②作业调度:多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程。
- 作业:指的是从外存当中挑选一个程序,将它放入到内存并让它开始运行的时候,创建和它相对应的进程。
③提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求。
④应用请求:由用户进程主动请求创建一个子进程。
②撤销原语
撤销原语:主要是终止进程的时候需要进行使用。
状态转换:就绪态/阻塞态/运行态—>终止态->无。
撤销原语实现的步骤:
- 从 PCB 集合中找到终止进程的 PCB。
- 若进程正在运行,此时立即剥夺 CPU,将 CPU 分配个其他进程。
- 终止其所有的子进程。
- 将该进程的所有资源还给父进程或者操作系统。
- 删除 PCB。
可能会引起进程终止的事件:
- 正常结束:如进程自己请求终止(exit 系统调用)。
- 异常结束:如整数除以 0,、非法使用特权指令,然后被操作系统强行杀掉。
- 外界干预:如 ctrl+alt+delete,用户选择杀掉进程。
父子进程的实际例子,任务管理器即可查看到:

父子进程之间分配关联:一开始,对于装入进程 launch 几乎整个内存空间如 8GB 都是它的,之后它在创建进程的时候会将对应的资源分配给它的子进程,如 50MB 分配给进程 A,50MB 分配给进程 B,一旦这些子进程终止了之后,此时会将自己的手里的这些资源还给它们的父进程。
- 对于父子进程之间的关系是树形结构。
③阻塞原语
状态转换:运行态—> 阻塞态
阻塞原语过程:
- 找到要阻塞的进程对应的 PCB。
- 保护进程运行的现场,系统将 PCB 状态信息设置为 “阻塞态”,暂时停止进程运行。
- 将 PCB 插入相应事件的等待队列。
引起进程阻塞的事件:
- 需要等待系统分配某种资源。
- 需要等待相互合作的其他进程完成工作。
④唤醒原语
唤醒原语的状态转换:阻塞态—> 就绪态
唤醒原语的步骤:
- 在事件等待队列中找到 PCB。
- 将 PCB 从等待队列中移除,设置进程为就绪态。
- 将 PCB 插入到就绪队列,等待被调度。
引起进程唤醒的事件:等待的事件发生(因何事阻塞,就应由何事唤醒)。
⑤切换原语(进程的切换)
切换原语的状态转换:运行态—> 就绪态、就绪态—> 运行态。
- 运行态—> 就绪态的过程就是将正在处于运行态的进程下处理机。
- 就绪态—> 运行态的过程就是选择一个处于就绪态的进程,让它上处理机运行。
切换原语的步骤:
- 将 ** 运行环境信息(进程的上下文)** 存入 PCB。
- PCB 移入相应队列。例如运行态—> 就绪态,就是将 PCB 移动到就绪态的队列中。
- 选择另一个进程执行,并更新其 PCB。
- 根据 PCB 恢复新进程所需的运行环境。
认识进程的运行环境:
- 当一个进程要下处理机的时候,可以将之前运行的环境信息保存到自己的 PCB 当中。并不是保存所有的寄存器信息,而是一些必要信息即可。
示例,下图对 x++ 这条 C 语言最终会转变为 4 条机器指令,当执行到指令 3 的时候,完成寄存器的 + 1 操作,此时通用寄存器的值为 2,注意此时执行完指令 3 时间片用完了,此时需要切换到给其他进程来进行使用,那么对于当前进程的状态信息我们是需要进行保存的,这个就是保存现场,用于之后切换到该进程的时候恢复现场使用。

根据上述内容,我们保护现场应当存储的部分数据为 PSW 状态寄存器、PC 指令、通用寄存器。保存完之后由于 CPU 执行其他的进程,此时原先的寄存器值会被覆盖掉如下:

当该进程执行完毕,此时切换到原先进程,恢复先前进程保存的值(PSW 状态寄存器、PC、IR 指令、当前运算的操作数等),最终成功的写入到了指定的主存地址:

知识回顾与总结
- 更新进程控制块中的信息
- 修改进程状态(state),保存/恢复运行环境
- 将PCB插入合适的队列
- 分配/回收资源

1.4、进程间通信(IPC)

1.4.1、什么是进程通信?
进程通信(Inter-Process Communication, IPC):是指的两个进程之间产生数据。
实际例子:微博中的图文分享到其他应用当中。

1.4.2、进程直接通信如何实现?
进程之间的通信如何实现?需要操作系统的支持。
为什么进程通信需要操作系统支持?
进程:是分配系统资源的单位(包括内存地址空间),各个进程拥有的内存地址空间相互独立。
首先对于进程访问的地址空间是有限制的,如下图进程 P 开辟的空间是 P 地址,进程 Q 开辟的空间是 Q 地址,那么对于进程 P 是无法访问进程 Q 地址空间的:

目的:一个进程无法随意修改其他进程之间的数据。
规定的限制:各个进程之间只能访问自己的一片内存地址空间,不能访问其他进程的内存地址空间,无论是读或者写数据都不行。此时若是想要两个进程之间进行通信,那么就需要操作系统的支持才可以完成数据通信。

1.4.3、三种进程通信
方式一:共享存储
认识共享存储
共享存储:各个进程只能访问自己的空间,如果操作系统支持共享存储这种功能,那么进程是可以申请一片共享存储器,在这片共享存储器中,也可以被其他进程所共享。

流程:若是进程 p 要给进程 q 传送数据,那么 p 就可以先将数据写到这一片共享存储区域中,p 进程本身是可以访问该区域的,接着进程 q 也从这片区域中读出数据即可。
实现共享存储的 Linux 代码:
同样按照流程中的所说,进程 p 会使用下面的 open 函数来进行系统调用申请一片共享内存区:

接着进程 p 与进程 q 都想要使用这一片共享内存区,此时就可以使用这个系统调用 mmap,使用这个系统调用后,将这一片存储区域映射到自己的虚拟地址空间当中。
- 虚拟地址空间描述:通过增加 “页表项 / 段表项” 即将同一片共享区域映射到各进程的地址空间中去。此时哥哥进程的虚拟地址空间当中够包含了该共享存储区,它们都可以自行的去访问这一片共享存储区。

如何应对多个进程都对同一块共享存储区域写操作?
- 对于多个进程向同一块空间写,可能会产生数据覆盖的问题,所以对于各个进程若是之间使用了共享存储的方式来进行通信,那么就需要保证各个进程对于共享存储区的访问是互斥的(如进程 p 若是在访问这片区域,其他进程都无法进行访问)。
- 操作系统对于实现同步互斥也提供了一些工具,如 P、V 操作。对于各个进程也可以使用这样子的 P、V 操作来实现资源互斥访问的功能。
两种共享存储方式(数据结构、存储区)
对于共享存储也分为多种方式:

①基于存储区的共享:操作系统在内存中划出了一块共享存储区,数据的形式、存放位置都由通信进程控制,而不是操作系统,对于这种共享方式速度很快,是一种高级通信方式。
- 例如操作系统给你划定了 4KB 大区域,此时几个进程对于对这块区域进行读还是写都是十分自由的,操作系统仅仅只是做划分的工作,数据具体如何存,都是由通信的这些进程之间来决定。
优点:速度快。
②基于数据结构的共享:例如共享空间里只能够放长度为 10 的数组,这种共享方式速度慢、限制多,是一种低级通信方式。
- 可以将这个数组理解为一个特殊的全局变量,这个全局变量可以被各个进程所访问,若是多个进程来共享这个数据结构长度为 10 的数组,那么就只 能够按照该数据结构所规定的格式来进行读 / 写,若是 int 类型,同样只能按照这个类型来决定。
缺点:进程之间通信自由度不高,速度比较慢。
方式二:消息传递
认识消息传递
第二种进程之间通信的方式:消息传递
消息传递:进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的 “发送消息 / 接收消息” 两个原语进行数据交换。
格式化的消息则是分为两个部分:消息头与消息体。
- 消息头包含概要的信息如发送人、发送给谁、长度等,消息体则是具体的一个进程要传给另一个进程的数据。

两种消息传递方式(直接、间接)
消息传递之间的通信包含两个:直接通信方式与间接通信方式。

方式一:直接通信方式:消息发送进程要指明接收进程的 ID。
- 这个 ID 指的是对应进程的 PID,表示谁来接收。简单来说就是我发送进程直接点名是谁,然后发给他。

- 操作系统的内核区域中管理者各个进程的 PCB,在各个进程的 PCB 中包含着消息队列(存储着对应 msg 消息),例如进程 p 给进程 q 发送消息,或者是其他进程给进程 q 发送消息,那么最终消息都会挂到进程 q 的这个消息队列中。
下面描述进程 P 给进程 Q 发送消息,以及 Q 接收消息的过程:
1、首先进程 P 在自己的内存空间构建一个消息(包含消息头、消息体),接着进程 P 给进程 Q 发送一个消息,此时就是用发送原语 send 来指明 message 消息发送给进程 Q
- 这里 send 的第一个参数是指定的接收方为进程 Q,第二个参数则是消息内容。

2、指明了接收者之后,经过【send(Q, msg) 发送原语】会让操作系统的内核接收到这个消息并将刚才的消息挂到进程 Q 的消息队列里边。实际这个过程则是消息从进程 p 的这个空间复制到了内核空间。

3、此时进程 Q 则可以使用【接收原语 receive(P, &msg)】来接收消息,此时操作系统就会检查进程 Q 的消息队列看一下在消息队列中有哪几个是进程 P 发送过来的,接着会将发送方 P 的消息体从操作系统的内核区复制到进程 Q 的用户去这里。
- receive(P, &msg):第一个参数指明发送发进程 P,第二个参数为消息。

方式二:间接通信方式:以 “信箱” 作为中间实体来进行信息传递。
下面描述进程 P 给进程 Q 发送消息,以及 Q 接收消息的过程:
1、进程 P 首先在自己的空间完善得到一个 msg 对象,等待消息体赋值完成之后,此时可以使用发送原语 send(A, msg),指明要发送到 A 邮箱。
- 进程可以通过使用系统调用来申请一个邮箱,一个进程可以申请多个。

2、执行完 send 后,操作系统会将当前进程 P 地址空间的信息复制到操作系统内核空间中。

3、接着进程 Q 则可以执行接收原语 receive(A, & msg),表示要从邮箱 A 中接收一个 msg,此时操作系统会将邮箱 A 中的消息复制到进程 Q 的内存空间。

- 操作系统允许多个进程向一个信箱发消息,也允许多个进程从同一个邮箱中接收消息。
方式三:管道通信
管道:是一个特殊的共享文件,又叫 pipe 文件,实际就是在内存空间中开辟一个大小固定的内存缓冲区。
数据的流向:只能是单向的,而不是双向的。

与共享内存的区别:对于共享内存对于读取没有限制;对于管道内是遵照先进先出的,可以理解为循环队列。在进程里边写数据和读数据要遵循先进先出的规则。
特点:
1、管道只能够采用半双工通信,某一时间段内只能够实现单向的传输,不能够实现双向传输。若是要实现双向的传输,则需要设置两个管道(即为全双工)。
下面是想要实现双向的示例图:

2、各个进程对管道的访问是互斥进行的,这个互斥是由操作系统实现的。
3、当管道写满时,写进程将会被阻塞,直到读进程将管道中的数据取走,即可唤醒写进程。

4、当管道读空时,读进程将阻塞,直到写进程往管道中写入数据,即可唤醒读进程。
5、管道中的数据一旦被读出,就彻底消失。因此当多个进程读同一个管道的时候,就可能会错乱。
- 主要原因就是,对于管道并没有像之前直接通信方式一样,每一个进程 PCB 都有一个消息队列,没有指定是谁的,所以可能就会出现错乱的原因,可能就出现第一块数据被 m 读走了,第二块数据被 q 读走了。
(特点的第 5 点解答)针对于不同的操作系统,对于这个可能会造成错乱的问题提出了解决方案:
方案 1:对于 system five 系统,一个管道允许多个写进程,读进程只允许有一个,不允许有多个读进程同时读这个管道【2014 年 408 真题高教社官方答案】。
- system five 系统:最初由 AT&T Bell Labs 在 20 世纪 80 年代开发。System Five 是 UNIX 操作系统的一个分支,主要用于商业领域的大型计算机和服务器。
方案 2:在 linux 系统中,允许有多个写进程,多个读进程,操作系统会自己来控制各个读进程轮流读数据的过程。
如何选择方案呢?对于实际应用角度来看是采用方案 2 的,但是对于应试考试,我们则依据官方的答案标准来记也就是允许多个写进程,1 个读进程。
知识回顾与重要考点
三种进程通信功能都需要操作系统的底层支持

二、线程
2.1、线程的概念与特点

2.1.1、什么是线程?引入线程的原因?
什么是线程,为什么要引入线程?

引入了进程之后,想要实现进程的并发,那么我们对于时间片都是直接分配给某个进程,此时就能够实现边聊 QQ 和边听音乐。

在深入一个进程看,对于 QQ 这一个应用实际上要完成很多事情,例如视频、文字聊天、发文件。
进程是程序的一次执行,但是这些功能显然并不可能是由一个程序顺序处理就能够实现的,若是采用进程的顺序执行,那么我们就不能够感知到一个 QQ 是同时发送这些事情的。
对于传统的进程来说,进程是执行的最小单位:

对于部分进程可能需要同时做很多事情,对于传统进程只能够串行的执行一系列的程序,此时就引入了线程的概念用于增加并发度。此时 CPU 的调度对象不再是进程,而是进程之中的线程,每一个进程中包含多个线程,之后 CPU 会轮流的使用一定的算法来为这些线程进行服务:

- 这些线程都有各自不同的代码,也可以运行同一份代码,这些代码都会并发的被 CPU 处理的,然后依次执行下去。
2.1.2、认识线程
线程:理解为是一种 “轻量级的进程”,是一个基本的 CPU 执行单元,也是程序执行流的最小单位,以前 CPU 调度的是进程,此时以线程为单位。
特点:引入线程之后,不仅仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以处理各个任务(如 QQ 视频、文字聊天、传文件)。
注意:引入线程之后,进程不再作为CPU的调度单位,而是作为除了CPU之外的系统资源的分配单元(如打印机、内存地址空间等都是分配给进程的)。
如何理解 “进程作为除了CPU之外的系统资源的分配单元”? 此时作为系统资源的基本分配单位,比如对于这些如打印机等资源分配给左边进程,内存条与另一个打印机分配给右边进程,此时分配资源给到的进程作为操作系统的基本分配单位。

2.1.3、引入线程后的变化?
引入了线程机制后,有什么变化?

着重点:对于同一进程内的线程切换,不需要切换进程环境,系统开销更小。
如何理解切换进程的运行环境?举例:在图书馆看书
- 切换进程运行环境:有一个不认识的人要用桌子,你需要把书收走,接着他把自己的书放到桌上。
- 切换同一进程中的线程:此时认识的室友要用桌子,你不用将书收走。
2.1.4、线程的属性
线程的属性:
1、线程是处理机调度的单位。
2、是否占用相同的 CPU?多 CPU 计算机中,各个线程可占用不同的 CPU。
- 对于各个线程可以被分配到不同的核心当中去。
3、线程组成:每个线程都有一个线程 ID、线程控制块(TCB)。
- 线程控制块类似于进程的 PCB,也是一种数据结构。
4、线程三种阻塞状态:同样有就绪、阻塞、运行三种基本状态。
5、线程是否拥有系统资源?线程集合不拥有系统资源。(线程只是处理机调度的基本单位,而系统资源分配是分配给进程的)
6、同一进程的不同线程共享进程的资源。(系统都是在进程,线程锁使用的系统资源则是同一个进程里的)
7、线程通信是否需要干预?由于共享内存地址空间,同一进程中的线程间通信甚至无需操作系统干预。
8、关于进程切换:若是同一进程中的线程切换,不会引起进程切换;若是不同进程中的线程切换,则会引起进程切换。
9、线程切换的开销:切换同进程内的线程,系统开销很小;切换进程的话开销十分大。
2.2、线程的实现方式和多线程模型

2.2.1、线程实现的两种方式
方式一:用户级线程
用户级线程(User-Level Thread, ULT)

历史背景:早期的操作系统(如:早期 UnIx)只支持进程,不支持线程,此时 “线程” 是由线程库来实现的。
此时有一个需求对于 QQ 中的功能包含有视频、文字聊天、传送文件工作,在这个阶段如何去实现并发工作?
- 由于这个阶段操作系统的调度单位为进程,我们此时可以编写多个进程,每一个进程来描述完成的一个功能,实现如下,通过如下编写三个进程来执行就能够完成并发的工作:

由于三个进程代码都是 while 循环,此时我们实际可以只用一个进程加上一个 while 循环,将三个功能写到一个进程中,此时根据 if 判断来完成相应的不同功能:

说明:通过这样子一段 while 循环,我们实际上完成了一个最简单的 “线程库”,实际在很多编程语言中提供了用户级线程比当前这个 while 复杂的多,程序员可以借助线程库来实现一系列的工作。
对于实现一个最简单的线程库,通过使用 while 循环来实现提出下面几个问题:
1、线程的管理工作由谁来完成?
- 用户级线程管理工作是由应用程序通过线程库来完成,并不是操作系统负责。
2、线程切换是否需要 CPU 变态?
- 由于线程切换是使用 while 来控制的,此时并不涉及到系统调用请求操作系统服务之类的事情,在用户态就可以完成线程的切换工作,不需要操作系统的干涉。
3、操作系统能否意识到用户级线程的存在?
- 意识不到,因为操作系统只能够看到进程存在,只知道这个进程是一大段代码,而这段代码中又被分为几个线程,所以操作系统意识不到这些线程的存在,所以这也是为什么这种线程的实现方式叫做用户级线程原因。
4、这种线程实现方式的优缺点?
优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。
缺点:当一个用户级线程被阻塞后,此时整个进程都会被阻塞,并发读不高,多个线程不可在多个处理机上并行运行。
- 举例:对于之前通过 while 来实现的多个 “线程”,若是其中某个申请摄像头资源并没有得到满足,此时就会进入到阻塞等待,此时其他的 “线程” 也无法执行,此时整个进程都会被阻塞住。
用户级线程的调度单位:对于这种早期操作系统实现的这种用户级线程方式,CPU 的调度单位依旧是进程,操作系统会给进程来分配 CPU 时间,并且即使电脑是多核,由于进程为调度基本单位,一个进程也只能够被分配一个核心,所以这些线程不能够并行运行!
最大的问题:这种用户级线程的实现方式在遇到进程中的一个线程阻塞时,那么会导致整个进程进入到阻塞,那么如何解决这样子的问题呢?
- 随着操作系统发展,越来越多操作系统开始支持线程,对于操作系统支持的线程则是内核级的线程。
方式二:内核级线程
内核级线程(Kernel-Level Thread,KLT,又称 “内核支持的线程”):其是由操作系统支持的线程。
应用:目前大多数现代操作系统实现了内核级线程,如 windows、Linux。

针对内核级线程,提出以下几个问题:
1、线程的管理工作由谁来完成?
- 由于这个内核级线程是在操作系统层面实现的线程,因此这个内核级的管理工作是由操作系统来完成。
2、线程切换是否需要 CPU 变态?
4、实现方式的优点与缺点。
优点:当一个线程被阻塞后,同一进程的其他线程依旧可以继续执行,并发能力强。多线程可以在多核处理机上并行执行。
- 在当前这种操作系统中,内核级线程是处理机的基本调度单位,进程则是分配资源的基本单位,此时多核 CPU 的情况,同一进程的不同线程可以分别派到不同的和信息啊并行执行,并且不同的内核级线程中可以跑不同的代码逻辑,所以即使其中的一个线程被阻塞,那么依然另外几个线程可以继续执行下去。
缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态。所以线程的管理成本高,开销大。
2.2.2、三种多线程模型(用户线程级与内核级结合在一起)
用户线程级与内核级结合在一起:在支持内核级线程的系统中,再引入线程库,此时就可以实现将若干个用户级线程映射到某一个内核级线程中。
①一对一模型
一对一模型:一个用户级线程映射到一个内核级线程,每个用户进程有与用户级线程同数量的内核级线程。

优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强,多线程可在多核处理机上并行执行。
缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
②多对一模型
多对一模型(等同用户级线程):多个用户级线程映射到一个内核级线程,且一个进程只被分配一个内核级线程。
此时就退化成了之前纯用户线程实现方式。

优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。
缺点:当一个用户线程被阻塞后,整个进程都会被阻塞,并发读不高,多个线程不可同时在多核处理机上并行运行,因为只有一个内核级线程。
重点:操作系统只 “看得见” 内核级线程,只有内核级线程才是处理机分配的单位。
③多对多模型(前两种的结合版,弥补缺陷)
多对多模型:n 用户级线程映射到 m 个内核级线程上。每个用户进程对应 m 个内核级线程。

- 在这个模型中,有三个用户级线程,两个内核级线程,该进程最多分配两个 CPU 的核心,只有一段代码逻辑获得了运行机会的时候,才可以被 CPU 执行。
- 举例 1:若是 QQ 视频聊天需要耗费比较多的 CPU 资源时,那么可以让左边的内核级线程单独执行,右边的内核级线程则是包含文字聊天和文件传输两个。
- 举例 2:若是文件传输又耗费很多 CPU 资源,那么可以将文字聊天映射到左边的内核级线程,那么此时左边的内核级线程需要处理视频聊天和文字聊天。
优点:克服了一对一模型中一个用户进程占用一个内核级线程,开销太大 (太多的内核会导致过多的变态操作) 的缺点;克服了多对一模型并发度不高的缺点(一个阻塞全体阻塞)。
深入理解:用户级线程是 “代码逻辑” 的载体,内核级线程是 “运行机会” 的载体。 - 内核级线程是处理机分配的单位,对于上图多核 CPU 环境下,左边进程最多只能够被分配两个核。
- 一段 “代码逻辑” 只有获得了 “运行机会” 才能够被 CPU 执行。内核级线程中可以运行任意一个有映射关系的用户级线程代码,只有两个内核级线程中运行的代码逻辑都阻塞时,这个进程才会阻塞。
知识回顾与重要考点

2.3、线程的状态与转换

与进程状态与转换类似。
2.3.1、线程的状态转换(核心三个)
最核心、主要的有三个状态:就绪、运行、阻塞。

①运行—> 阻塞态:若是在运行请求某个资源需要进行等待,此时就会进入到阻塞态。
②运行态—> 就绪:若是一个系统支持线程,若是运行态的线程分配的时间用完了,此时就会下处理机进入到就绪态。
③就绪—> 运行态:若是就绪态的线程被调度程序选中,此时就绪态回到运行态。
④阻塞—> 就绪态:若是等待的事件可以被分配了,那么此时就会从阻塞态转为就绪态。
2.3.2、线程的组织与控制
原本调度单位为进程的时候,操作系统会对进程的 PCB 进行一个管理,其中包含了进程的各种信息,那么对于线程来说,管理线程,就需要给这个线程一个对应的数据结构,即为 TCB(线程控制块)。
线程控制块 TCB 的组成:

- 线程标识符:与 PID 类似,线程控制块中是 TID。
- 程序计数器:由于每个线程中可以运行的代码块不同,此时就需要包含一个程序计数器 PC 用于指明线程执行到哪里。【若是一个线程切换下处理机时,那么就需要将这个 PC 值保存到 TCB 的 PC 中;若是一个线程切换上处理机的时候,那么就需要将这个 TCB 中的 PC 值放回到 PC 寄存器中】
- 通用寄存器:保存线程现场信息数据,保存线程的各种寄存器的值。
- 堆栈指针:用于记录函数调用的信息,如调用函数的返回地址,每一层函数的局部变量,由于线程的堆栈十分大,并不会直接将整个区域放入到 TCB 中,而是保存一个堆栈指针,之后使用这样的一个指针来找到该线程,它的堆栈在内存的哪个位置,哪一片区域即可。通常恢复到 SP 指针中去。
- 线程运行状态:运行、就绪、阻塞态。
- 优先级:保存线程的优先级,通常在线程调度或者系统在分配资源的时候可以根据线程的优先级来进行一个分配制定策略。
线程表:将多线程的 TCB 组织起来就可以构成一个线程表,可以将所有的线程组织成一张线程表,也可以按照线程状态的不同,组织成不同的线程表,不同的系统可以采取不同的策略,来根据需求组织分类管理。

三、处理机调度
3.1、处理机调度的概念、层次

3.1.1、调度的基本概念
调度:当有一堆任务要处理,但由于资源有限,这些事情没法同时处理,此时就需要确定某种规则来决定处理这些任务的顺序,这就是 “调度” 研究的问题。
举例 1:银行很多人排队取款,此时若是有一个 VIP 用户也来办理服务,此时就会被优先服务。

举例 2:例如宿舍的厕所只有一个位置,那么此时可以根据各自要使用的时间越短的越优先,若是时间一致的则根据先排队的先使用次序。

3.1.2、调度的三个层次
①高级调度
作业:一个具体的任务。
- 用户向系统提交一个作业 = 用户让操作系统启动一个程序(来处理一个具体的任务)
发生场景:若是此时内存空间有限,无法将用户提交的作业全部放入内存时该如何进行选择?
- 此时就可以采用高级调度,也就是对于好几个程序都需要启动,根据某种规则来先启动哪一个。
高级调度(作业调度):按一定的原则从外存的作业后备队列中挑选一个作业调入内存,并创建进程,每个作业在整个生命周期中只调入一次,调出一次。作业调入时会建立 PCB,掉出时才撤销 PCB。

②低级调度
低级调度:
发生场景:在内存中有很多进程,但是系统里 CPU 资源有限,操作系统需要制定某一种策略,从我们的进程就绪队列中挑选出一个进程,将处理机分配给它。
低级调度(进程调度 / 处理机调度):按照某种策略从就绪队列中选取一个进程,将处理机分配给它。进程调度是操作系统最基本的一种调度,在一般的操作系统中都必须配置进程调度。

进程调度的频率:几十毫秒一次。这样才能够让用户在宏观上来看感觉是同时进行的。
③中级调度
发生场景:当要使用某个进程时,此时计算机会出现内存资源不足的情况,此时会将一些不紧急、不太重要的进程先将它们从内存调出到外存,之后再由中间调度来将进程数据调回到内存。
- 挂起队列:对于这些内存调入到外存的进程此时都会进入到挂起状态。对于这些挂起状态的进程将它们的 PCB 组织成一个队列,即为挂起队列。
中级调度(内存调度):经过上面的调出操作之后,此时有了空闲的内存资源,操作系统会通过中级调度来决定是否要把外存某个进程的数据先把它调回内存。

举例:平时使用手机时会经常执行切换应用的操作,有时候切换进程十分流程,有时候会有一些卡顿,对于切换十分流畅的说明这个进程的数据本身就是内存中;对于有一些卡顿的那么就是在进行中级调度,选中的那个进程会从外存调入到内存来进行执行。
生命周期:进程运行生命周期中,可能会出现多次调出以及多次调入。
发生频率:比高级调度高。
3.1.3、三层调度的联系、对比

- 高级调度是选择合适的作业调入到内存中。【外存—> 内存,面向作业,无—> 创建态—> 就绪态】
- 中级调度则是从挂起队列中选择合适的进程(选择之前挂起的进程)将其数据调回内存。【外存—> 内存,面向进程,挂起态—> 就绪态 或者 阻塞挂起—> 阻塞态】
- 低级调度是在就绪队列中选择一个进程为其分配处理机。【内存—>CPU,就绪态—> 运行态 】
补充知识:进程的挂起态与七状态模型
对于 408 中要掌握的是五状态模型,一些自命题可能会要求七状态模型。
挂起状态(suspend):暂时调到外存等待的进程状态为挂起状态。
挂起状态分为两种状态:就绪挂起、阻塞挂起两种状态。
- 就绪挂起:若是内存空间不足,可能就会将就绪态的进程转变为就绪挂起。若是内存空间有空闲,那么就会激活此时就绪挂起—> 就绪态。
- 阻塞挂起:同样若是内存空间不足,可能会将阻塞态的进程转换为阻塞挂起。同样若是内存空间空间,就会激活此时阻塞挂起—> 阻塞态。

其他一些特殊状态转换:
①阻塞挂起—>就绪挂起:若是此时某个进程为阻塞挂起,此时它等待的资源已经释放出现,此时就会转为就绪挂起。

②运行态—>就绪挂起:有些进程当它处于运行态时,运行结束之后可能进程下处理机的时候就会被直接放到外存当中去,让它进入就绪挂起的状态。

③创建态—>就绪挂起:有的时候一个处于创建态的进程,当它常见结束之后,创建完 PCB 此时可能出现内存空间不够的情况此时就会直接进入到就绪挂起的状态。

注意:“挂起” 和 “阻塞” 的区别,两种状态都是暂时不能够获得 CPU 的服务了,挂起态是将进程映像调到外存去,而阻塞态下进程映像还在内存当中。
关于队列创建:有的操作系统会将就绪挂起、阻塞挂起分为两个挂起队列,甚至会根据阻塞原因的不同再把阻塞挂起进程进一步细分为多个队列。
知识回顾与重要考点

3.2、调度的目标(调度算法的评价指标)

①CPU 利用率
CPU利用率:表示 CPU 处于忙碌的时间,精确指的是 CPU”忙碌” 的时间占总时间的比例。
目的:由于早期的 CPU 造价及其昂贵,因此人们会希望让 CPU 尽可能多的工作。
公式:利用率 = 忙碌的时间 / 总时间
实际例题:

②系统吞吐量
系统吞吐量:单位时间内完成作业的数量
目的:对于计算机来说,希望能够使用尽可能少的时间处理完尽可能多的作业。
公式:系统吞吐量 = 总共完成了多少道作业 / 总共花了多少时间,其中实际计算的是每秒能够完成的道数
实际例题:

③周转时间
周转时间:是指从作业被提交给系统开始,到作业完成为止的这段时间间隔。
目的:对于计算机的用户来说,十分关心自己的作业从提交到完成花了多少时间。
包含四个部分:
- 作业在外存后备队列上等待作业调度(高级调度)的时间。
- 进程在就绪队列上等待进程调度(低级调度)的时间。
- 进程在 CPU 上执行的时间。
- 进程等待 I/O 操作完成的时间。
说明:对于后三项在一个作业的整个处理过程中,可能发生多次
公式一:(作业)周转时间 = 作业完成时间 - 作业提交时间
- 对于用户来说,更关心自己的单个作业的周转时间。
公式二:平均周转时间 = 各作业周转时间之和 / 作业数
- 对于操作系统来说,更关心系统的整体表现,因此更关心所有作业周转时间的平均值。
思考:对于有的作业运行时间短,有的作业运行时间长,因此在周转时间相同的情况下,运行时间不同的作业,给用户的感觉肯定是不一样的。
举例:例如排队等厕所,用户 A 排队等 10 分钟,使用厕所 1 分钟,那么整个周转过程花费 11 分钟,用户 B 排队等 1 分钟,使用厕所 10 分钟,整个周转过程同样花费 11 分钟。对于用户 B 来说其中只有 1 分钟等待,实际感受并没有很糟糕。【那么对于周转时间相同的情况下,作业的实际运行时间长短不同,会导致对于用户的感受也是有区别的,此时提出了另外一个指标:带权周转时间】
公式三:带权周转时间 = 作业周转时间 / 作业实际运行的时间 = (作业完成时间 - 作业提交时间) / 作业实际运行的时间。
- 结论 1:观察这个式子,对于周转时长相同的两个作业,实际运行时间长的作业在相同时间内被服务的时间更多(除数是实际运行时间),带权周转时间更小,用户满意度也就更高。
- 同样用举例来看:用户 A 的带权周转时间为:11 / 1 = 11,用户 B 的带权周转时间为:11 / 10 = 1.1。可以看出用户 B 的带权周转时间更小,用户的满意度也就越高。
- 结论 2:对于实际运行时间相同的两个作业,周转时间短的带权周转时间更小,用户满意度更高。
- 因为本身干活的时间都一样,那么我们就可以直接看谁整体干活的时间少,那么肯定满意度更高。
- 小总结:带权周转时间必然 >=1;带权周转时间与周转时间都是越小越好。
公式四:平均带权周转时间 = 各作业带权周转时间之和 / 作业数
④等待时间
等待时间:指进程 / 作业处于等待处理机状态时间之和,等待的时间越长,用户满意度越低。
目的:计算机的用户希望自己的作业尽可能少的等待处理机。
过程:作业在后备队列等待被服务(高级调度),接着作业调入内存中,建立对应的进程(PCB),这个进程会被 CPU 服务,也会被 I/O 设备服务,也会有等待被服务的时候。

计算通俗易懂解释:
①对于进程来说,等待时间就是指进程建立后等待被服务的时间之和,在等待 I/O 完成的期间其实进程也是在被服务的,所以不计入等待时间。
②对于作业来说,不仅要考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间。
注意:对于调度算法而言其实只会影响作业 / 进程的等待时间(因为一个作业总共需要被 CPU 服务多久,被 I/O 设备服务多久一般是确定不变的)。也可以使用 “平均等待时间” 来评价整体性能。
⑤响应时间
响应时间:从用户提交请求到首次产生响应所用的时间。
目的:对于计算机用户来说,会希望自己的提交的请求(如通过键盘输入了一个调试指令)尽早的开始被系统服务、回应。
知识回顾与重要考点

3.3、进程调度的时机、切换与过程调度方式

3.3.1、进程调度的时机
3.3.3.1、认识进程调度(主动放弃、被动放弃)
进程调度(低级调度):就是按照某种算法从就绪队列中选择一个进程为其分配处理机。
需要进行进程调度与切换的情况:
情况 1:当前运行的进程主动放弃处理机,下面是一些实际情况。
- 进程正常终止。
- 运行过程中发生异常而终止。
- 进程主动请求阻塞,例如等待 I/O。
情况 2:当前运行的进程被动放弃处理机,下面是一些实际情况。
- 分给进程的时间片用完。
- 有更紧急的事需要处理,如 I/O 中断。
- 有更高优先级的进程进入到就绪队列。
不能够进行进程调度与切换的情况:
1、在处理中断的过程中,中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换。
2、进程在操作系统内核程序临界区(临界区指的是同一时间只能供一个进程访问的代码块)。
3、在原子操作过程中(原语),原子操作不可中断,要一气呵成(如之前某一个原语为两步:①修改 PCB 中进程状态标志。②并把 PCB 放到相应队列。对于这个原语过程中我们不能够进行进程调度与切换,否则会造成一个队列中状态不一致的情况)
3.3.3.2、深入理解对于临界区的调度与切换
关于下面表述内容的正确与否:
表述 1:进程在操作系统内核程序临界区中不能进行调度与切换。【正确】
表述 2:(2012 年联考真题)进程处于临界区时不能够进行处理机调度。【错误】
分析:
首先了解几个名词:
- 临界资源:一个时间段只允许一个进程使用的资源, 各进程需要互斥的访问临界资源。
- 临界区:访问临界资源的那段代码。
- 内核程序临界区:一般是用来访问某种内核数据结构的,例如进程的就绪队列(由各就绪进程的PCB组成)
内核临界区举例:当一个进程若是处于一个内核程序临界区,此时这个临界区是要访问就绪队列的话,那么访问之前它会把这个就绪队列上锁,在上锁期间若是发生调度,此时进程调度相关程序会需要访问就绪队列从队列中挑选一个进程,但由于这个就绪队列在上锁状态那么是无法进行的,只有解锁了之后才可以。
- 核心:对于内核程序临界区访问的资源,这些是内核的数据结构,当上锁时应当尽快的被释放,不然可能会影响到其他系统内核其他的管理工作,如进程切换等,应当更加尽快的去完成此次内核程序,所以此时进程在操作系统内核临界区不能够被调度与切换。。

普通临界资源举例:若是此时进程访问的是一种普通临界资源,如打印机,那么同样访问它的时候会先上锁,打印机完成打印之前,进程是一直在临界区的,此时依旧保持着对打印机访问,还在上锁中,由于打印机是一种慢速的 I/O 设备,对于这种情况若是不允许进程调度与切换,那么会导致处理机一直等待 IO 结束,这个时间段会一直霸占着 CPU,CPU 一直是在空闲状态。
- 核心:对于普通临界区访问的临界资源对它的访问并不会影响到操作系统内核的工作,那么为了增加系统的并发度,增加 CPU 的利用率,那么在访问这些普通的临界区的时候,是可以进行调度和切换的。

通过这段描述,那么对于上面两个表述就能够直到正确与错误的原因了。
3.3.3.3、两种进程调度方式(非剥夺调度、调度方式)
为什么有的系统中,只允许进程主动放弃处理机?有的系统中,进程可以主动放弃处理机,当有更紧急的任务要处理时,也会强行剥夺处理机(被动放弃)?
- 这个问题涉及到进程调度的方式:非剥夺调度方式、剥夺调度方式。
非剥夺调度方式:又称为非抢占方式,即只允许进程主动放弃处理机,在运行过程中即便有更加紧迫的任务到达,进程会继续使用处理机,直到该进程或主动要求进入阻塞态。
- 优点:实现简单,系统开销小。
- 缺点:无法及时处理紧急任务,适合于早期的批处理系统。
剥夺调度方式:又称为抢占式方式。当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用到处理机,那么会立即暂停正在执行的进程,将处理机分配给更重要紧迫的进程。
- 优点:可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中断),适用于分时操作系统,实时操作系统。
3.3.2、进程调度的切换与过程
若是要分配处理机时,在进程与进程切换的过程中,又会发生什么事情?
主要分为 “狭义的进程调度” 与 “进程切换” 的区别:
狭义进程调度:指的是从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程,也可能是另一个进程,后一种情况需要进程切换)。广义的进程调度:包含了选择一个进程与进程切换两个步骤。【狭义进程调度 + 进程切换,①选择进程②一个进程让出处理机③另一个进程占用处理机】进程切换:指的是一个进程让出处理机,由另一个进程占用处理机的过程。
实际题目中指的是是广义的进程调度。
进程切换的过程主要完成了:
- 对原来运行进程各种数据的保存。(保存在 PCB 中)
- 对新的进程各种数据的恢复。(如程序计数器、程序状态字、各种数据寄存器等处理机现场信息,这些信息通常保存在进程控制块 PCB 中,从 PCB 读出数据)
注意:对于进程的切换并不是一瞬间可以完成的,需要付出一定的时间代价,不能简单理解进程切换越频繁,进程并发度越高,因为若是进程的切换调度过于频繁,其实会导致整个系统效率变低,因为系统将大部分的时间都花在这个进程切换开销上,真正用于执行进程时间反而比较少。
知识回顾与重要考点

3.4、调度器 / 调度程序(scheduler)
3.4.1、认识调度器与调度时机
调度器:操作系统的调度程序主要就是用来管调度的,当前运行的进程是否要让他下处理机,若是下处理机了,那么接下来就绪队列中的进程要谁上处理机运行?

- 对于②③则是由调度程序引起的、调度程序决定:由谁运行?(调度算法)、运行多长时间?(时间片大小)
调度时机:在何时回触发 “调度程序”?
①创建新进程:当创建新进程的时候,此时就绪队列会发生改变,就有可能让新进程抢占正在运行的这个进程 CPU。
②进程退出:若是进程终结了自己,此时 CPU 就会空闲,这个时候调度程序会来进行调度。
③运行进程阻塞。
④I/O 中断发生(可能唤醒某些阻塞进程)。
⑤非抢占式调度策略,只有运行进程阻塞或退出才触发调度程序工作。
- 只有当前运行进程主动放弃处理机时候,才有必要的唤醒调度程序。不会使用时钟中断来唤醒调度程序。
⑥抢占式调度策略,每个时钟中断或 k 个时钟中断会触发调度程序工作。
- 这个调度检查的工作是由时钟中断来触发,每过一个时钟周期或者每过 k 个时钟周期都会例行的唤醒一下这个调度程序让其来检查。只要就绪队列一改变,那么就必须检查一下新旧区的进程有没有可能抢占当前正在运行的这些进程。
共同点:当前 CPU 运行的进程主动退出或者被动退出(可能进入到不同的状态),由于为了不让 CPU 空闲,那么都会去调用调度程序。
3.4.2、调度单位
若是系统不支持线程,那么调度处理单位就是进程;若是系统支持线程,那么调度的处理单位则是内核级线程,此时进程作为资源分配的基本单位。

3.4.3、理解闲逛进程
闲逛进程(Idle process):在计算机系统中,处于空闲状态并且没有任何可执行任务的进程,通常用于使用空闲的处理器时间来执行一些系统维护任务,例如更新系统状态、清理内存、检查硬件设备等。
- 在 Windows 操作系统中,闲逛进程是一个特殊的进程,其进程标识符(PID)通常为 0,并且它在任务管理器中显示为占用 CPU 时间的百分比为 0。
时机:若是就绪队列中没有其他就绪进程,调度程序会选中闲逛进程,让其上处理机运行。
闲逛进程的特性:
- 优先级最低。
- 可以是 0 地址指令,占一个完整的指令周期(指令周期末尾例行检查中断)。【此时 CPU 要反复执行的是零地址的指令,零地址的指令意味着不需要进行访存,甚至不需要去访问 CPU 中的任何一个寄存器】
- 能耗低。
说明:在执行闲逛进程的每一条指令之后,这个指令周期的末尾也会例行的检查中断,这个中断就可以周期性的唤醒调度程序,让调度程序来检查,若是有,那么就让其他进程上处理机,闲逛进程下处理机。
3.5、调度算法
3.5.1、批处理阶段的三种调度算法
批处理系统的三种调度算法:先来先服务算法(FCFS)、短作业优先算法(SJF)、高响应比优先算法(HRRN)。

①先来先服务算法(FCFS)
理解先来先服务算法概念
先来先服务算法(FCFS,First Come First Service)
FCFS先来先服务调度算法,算法思想:主要从 “公平” 的角度考虑(类似于生活着排队买东西)。
算法规则:按照作业 / 进程到达的先后顺序进行服务。
用于作业 / 进程调度:
- 用于作业调度:考虑的是哪个作业先到达后备队列。
- 用于进程调度:考虑的是哪个进程先到达就绪队列。
是否可抢占:非抢占式的算法。
- 只有当占用 CPU 的进程主动的放弃 CPU 使用时,才会进行调度。
优点:公平、算法实现简单。
缺点:排在长作业(进程)后面的短作业需要等待很长时间,带权的周转时间很大,对短作业来说用户体验不好,即 FCFS 算法对长作业有利,对短作业不利。
- 排队买奶茶举例:若是去排队买奶茶一般都是按照先来先服务的策略,此时你的前面一个人突然说要买 20 杯奶茶,那么实际上就是一个长作业,对于这个短作业、短进程来说,体验十分糟糕,最终算到的带权周转时间就会特别大,那么对于先来先服务是对长作业有利,对短作业不利。
FCFS是否导致饥饿:不会。
饥饿:某进程 / 作业长期得不到服务。
实际计算题(含分析)
题目:

解题:
首先是调度顺序,每一个进程根据到达时间确认执行的顺序,之后每一个进程需要等待前一个进程执行完成之后才能够被调度和执行:

根据调度算法来分别计算周转时间、带权周转时间、等待时间、平均周转 / 带权周转 / 等待时间:
周转时间:每一个进程最终的完成时间来减去该进程一开始到达的时间

带权周转时间:借助周转时间来计算,除数为实际该进程的运行时间


- 注意在本题中进程都是纯计算型的进程,一个进程要么就是等待,要么就是运行。若是对于有 I/O 操作的进程,那么就需要包含其中的 I/O 时间,此时公式为:等待时间 = 周转时间 - 运行时间 - I/O操作一时间。
平均周转、带权、等待时间计算:其中的除数就是任务的数量

说明:通过上面计算,可以注意到其中 P3 进程的带权周转时间特别长,高达 8,也就表示是这个进程本来是只需要很少时间就能够执行完成的,但是等待了很长的时间才可以被处理完,对于该进程的用户来说体验十分糟糕。
②短作业优先算法(SFJ)
理解短作业优先算法(抢占与非抢占区别)
先来先服务对于带权周转时间、平均等待时间这些指标并不是很优先,此时就有了短作业优先算法的提出。
短作业优先(SFJ,Shortest Job First)
SFJ调度算法,算法思想:追求最少的平均等待时间,最少的平均周转时间、最少的平均带权周转时间。
算法规则:最短的作业 / 进程优先得到服务(所谓 “最短”,是指要求服务时间最短)。
用于作业 / 进程调度:即可用于作业调度,也可用于进程调度。用于进程调度的可称为 “短进程优先(SPF,Shortest Job First)算法”。
是否可抢占:SJF 与 SPF 是非抢占式的算法,但是也有抢占式的版本(最短剩余时间优先算法 SRTN,Shortest Remaining Time Next)。
优点:“最短的” 平均等待时间、平均周转时间。
缺点:不公平,对短作业有利,对长作业不利,可能产生饥饿现象。另外,作业 / 进程的运行时间由用户提供(这个用户可以把自己本来应该是长作业,但是自己提交的数据写的很短),并不一定真实,不一定能够做到真正的短作业优先。
是否会导致饥饿:会,如果源源不断的有短作业 / 进程到来,可能使长作业 / 进程长时间得不到服务,产生 “饥饿” 现象,如果一直得不到服务,则称为 “饿死”。
计算题 1:非抢占式的短作业优先算法
短作业优先算法(非抢占式的,对于 SJF 与 SPF 可以指的是作业或进程)
例题说明:用于进程调度,这里实际上由于是面向进程,应该是非抢占式的短进程优先调度算法,而不是短作业优先,但是很多题目并不会很在意这个细节,实际这类的规则一样。

分析:
短作业/进程优先调度算法:每次调度时选择当前已到达且运行时间最短的作业 / 进程。
调度顺序思路如下:
- 首先进程 P1 先到达,此时并没有进程执行,那么 P1 第一个执行。运行时间为【0→7】
- 接着在 0-7 这个时间段中,进程 P2、P3、P4 都已经到达了,此时其中 P3 的运行时间最短,那么 P3 第二个执行。
- 由于 P2 与 P4 的运行时间都是最短,此时就根据谁先到达来确认谁先执行,此时 P2 第三个执行。
- P4 第四个执行。
最终的顺序为:P1→P3→P2→P4

那么对于一系列的指标计算如下:

可以看到通过使用短作业优先算法,对于平均相关的指标时间都更低。
计算题 2:抢占式的短作业优先算法
抢占式的短作业优先算法又称为"最短时间优先算法(SRTN)"
题目:针对抢占式的

分析:
最短剩余时间优先算法:每当有进程加入就绪队列改变时就需要调度,如果有新到达的进程剩余时间比当前运行的进程剩余时间更短,则由新进程抢占处理机,当前运行进程重新回到就绪队列,另外,当一个进程完成时也需要调度。
调度顺序思路如下:
0时刻(P1到达,P1开始执行) 最新各进程剩余执行时间:P1(7)
2时刻(P2到达,P2开始执行) 最新各进程剩余执行时间:P1(5)、P2(4) | 执行时间段:P1:0->2
4时刻(P3到达,P3开始执行) 最新各进程剩余执行时间:P1(5)、P2(2)、P3(1) | 执行时间段:P2:3->4
5时刻(P3执行完,P4到达,P2执行) 最新各进程剩余执行时间:P1(5)、P2(2)、P4(4) | 执行时间段:P3:5
----接下来之后没有新的进程到达,依次就是找到剩余执行时间短的先执行,之后依次找
7时刻(P2执行完,P4执行) 最新各进程剩余执行时间:P1(5)、P4(4) | 执行时间段:P2:6->7
11时刻(P4执行完,P1执行) 最新各进程剩余执行时间:P1(5) | 执行时间段:P4:8->11
16时刻(P1执行完) 最新各进程剩余执行时间:无 | 执行时间段:P1:12-16那么根据上面思路推导出的各进程及执行顺序如下:
P1:0->2
P2:3->4
P3:5
P2:6->7
P4:8->11
P1:12-16
此时根据图来进行计算各个指标:

说明:相对比非抢占式的,对于抢占式的短作业优先算法最终得到的几个平均时间的指标更低。
四点注意点(对于短作业 / 进程优先算法)
注意细节:
1、若是题目中没有特别说明,那么所提到的 **“短作业 / 进程优先算法” 默认是非抢占式 ** 的。
2、对于书上写 “SJF 调度算法的平均等待时间、平均周转时间最少” 严格意义上来说是错误不严谨的。根据上面实际例子来看对于最短剩余时间优先算法得到的平均等待时间、平均周转时间还要更少。
- 实际还需要加一个条件:当所有进程同时可运行时,采用 SJF 调度算法的平均等待时间、平均周转时间最少。
- 或者说:当所有进程都几乎同时到达时,采用 SJF 调度算法的平均等待时间、平均周转时间最少。
上面对于非抢占式的,则都需要加上相应的条件才可。
若是对于抢占式的则无需增加条件,应该说:抢占式的短作业 / 进程优先调度算法(最短剩余时间有限,SRNT算法)的平均等待时间、平均周转时间最少。
3、虽然严格来说,SJF 的平均等待时间、平均周转时间并不一定最少,但对比其他(如先来先服务算法,FCFS),SJF 依旧可以获得比较少的平均等待时间、平均周转时间。
4、对于选择题中若是出现 “SJF 算法的平均等待时间、平均周转时间最少” 的选项【最佳的为:抢占式短作业优先算法 SRNT 的平均等待时间、平均周转时间最少,该含义仅仅指的是非抢占式的】,最好判断其他选项是不是有明显的错误,若是没有更合适的选项才去选择该选项。
③高响应比优先算法(HRRN,综合前两个算法)
认识高响应比优先算法(HRRN)
对 FCFS 和 SJF 两种算法的思考:
RCFS算法:每次调度的时候选择一个等待时间最长的作业 / 进程为其服务,但是没有考虑作业的运行时间,因此导致了对短作业不友好的问题。SJF算法:选择一个执行时间最短的作业为其服务,但是又完全不考虑各个作业的等待时间,因此导致了对长作业不友好的问题,甚至还会造成饥饿问题。
此时有没有算法能够既考虑各个作业的等待时间,也能够兼顾运行时间?
- 高响应比优先算法
高响应比优先算法(HRRN,Highest Response Ratio Next)
高响应比优先调度算法,算法思想:综合考虑作业 / 进程的等待时间和要求服务的时间。
算法规则:在每次调度时先计算各个作业 / 进程的响应比,选择响应比最高的作业 / 进程为其服务。
- 公式:
响应比 = (等待时间+要求服务时间) / 要求服务时间,响应比 >=1
用于作业 / 进程调度:即可用于作业调度,也可用于进程调度。
是否可抢占:非抢占式的算法,因此只有当前运行的作业 / 进程主动放弃处理机时,才需要调度,才需要计算响应比。
优点:
1、综合考虑了等待时间和运行时间(要求服务时间)。
2、等待时间相同时,要求服务时间短的优先(SJF 的优点)。
3、要求服务时间相同时,等待时间长的优先(FCFS 的优点)。
- 这个人感觉通过这两个特点来去构建出来的响应比公式,最终响应比越高的则更优先使用。
4、对于长作业来说,随着等待时间越来越久,其响应比也会越来越大,避免了长作业饥饿的问题。
是否会导致饥饿:不会
实际计算题(含分析)
题目:

分析:
高响应比优先算法:非抢占式的调度算法,只有当前运行的进程主动放弃 CPU 时(正常 / 异常完成,或主动阻塞),才需要进行调度,调度时计算所有就绪进程的响应比,选响应比最高的进程上处理机。
本题中并没有涉及到 I/O 相关阻塞情况,所以本题中无需考虑阻塞的时间。
进程调度的顺序分析:
0时刻:只有P1到达就绪队列,P1上处理机
7时刻(p3执行):0-7这个过程中P2、、P3、P4都已到达,此时依次计算响应比:p2(2.25) p3(4) p4(1.5)
p2:等待时间=7-2=5(当前是7时刻,到达时间为2时刻) 要求服务时间为4,即为 (5+4)/4=2.25
p3:等待时间=7-4=3(当前是7时刻,到达时间为4时刻) 要求服务时间为1 即为 (3+1)/1=4
p4:等待时间=7-5=2(当前是7时刻,到达时间为5时刻) 要求服务时间为4 即为 (2+4)/4=1.5
| 实际运行时间:p1:0-7
8时刻(p2执行):就绪队列有P2、P4,此时依次计算响应比:P2(2.5) P4(2.25)
P2:等待时间=8-2=6(当前是8时刻,到达时间为2时刻) 要求服务时间为4 即为 (6+4)/4=2.5
P4:等待时间=8-5=3(当前是8时刻,到达时间为5时刻) 要求服务时间为4 即为 (3+4)/4=2.25
| 实际运行时间:p2:8
12时刻(P4执行):就绪队列有P4,无需计算
| 实际运行时间: p2:8-12
16时刻(全部执行完毕)
| 实际运行时间: p4:12-16根据上述分析来梳理出每个进程执行的时间:
p1:0-7
p2:8
p2:8-12
p4:12-16小结论:P2 和 P4 要求服务时间一样,但 P2 等待时间长,所以 P2 的响应比更大。

知识回顾与重要考点(包含注意点,整体分析)

注意:
1、缺陷点:这几种算法主要用于早期的批处理系统,由于计算机十分昂贵,人们更追求系统的整体表现,而并没有过多的关注用户交互的体验与交互性。
2、关心点:这几种算法主要关心的是对用户的公平性、平均周转时间,平均等待时间等评价系统整体性能的指标,并不关心 “响应时间”,也不区分任务的紧急程度。
3、对于其中 FCFS 算法通常结合其他算法一起使用,目前也扮演着十分重要的角色,下一章节则是讲解比较适合用于交互式系统的调度算法。
3.5.2、交互式系统的三种调度算法

①时间片轮转调度算法
认识时间片轮转
时间片轮转(RR,Round-Robin)
时间片轮转调度算法,算法思想:公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。
- 时间片轮转调度算法是伴随着分时操作系统诞生而诞生的。
算法规则:按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片(如 100ms)。若进程未在一个时间内执行完,则剥夺处理机,将进程重新放到就绪队列队尾重新排队。
- 由于会剥夺处理机,那么就是抢占式的。
用于作业 / 进程调度:用户进程调度(只有作业放入内存建立了相应的进程后,才能够被分配处理机时间片)。
- 进程是执行的基本单位。
是否可抢占?若是进程未能够在时间片内运行完,将被强行剥夺处理机使用权,因此时间片轮转调度算法属于抢占式的算法,由时钟装置发出时钟中断来通知 CPU 时间片已到。
优点:公平,响应快,适用于分时操作系统。
缺点:由于高频率的进程切换,因此有一定开销,不区分任务的紧急程度。
是否会导致饥饿:不会。这种算法会轮流地为各个进程服务。
补充:对于时间片太大以及太小的情况
补充:对于时间片太大以及太小的情况如下:可见下面例题会给出当时间片为 5 时退化成先来先服务算法。
①若是时间片太大情况:会增大这些进程的响应,就会失去时间片轮转调度算法最大的优点。
- 如何理解增大各个进程的响应时间?例如系统中有 10 个进程并发执行,若是时间片为 1 秒,则一个进程被响应时间可能需要等待 9 秒(极端这个进程排到最后一个,那么之前的需要等待 9 个进程响应,所以就是 9 秒)。那么也就是说如果用户在自己进程的时间片外通过键盘发出调试命令,可能需要等待 9 秒才能够被系统响应。
②若是时间片太小情况:对于进程调度、切换是有时间代价的(保存、恢复运行环境),因此如果时间片太小,会导致进程切换过于频繁,系统会花大量的时间来处理进程切换,从而导致实际用于进程执行的时间比例减少,可见时间片也不能太小。
- 进程切换过于频繁,实际每一个进程切换都会有一定的时间开销用于去保护、恢复现场。
那么如何设计相对合适的时间片大小呢?
- 一般来说,设计时间片要让切换进程的开销占比不超过 1%。
实际案例题(列举时间片小和大两种情况)
题目:

分析:
时间片轮转调度算法:轮流让就绪队列中的进程一次执行一个时间片(每次选择的都是排在就绪队列队头的进程)。
- 主要更关心进程的响应时间,此时这里就不必计算等待时间、周转时间。
情况 1:时间片大小为 2 时的情况
调度顺序分析:
0时刻 就绪队列:P1(5) P1上处理机运行一个时间片
2时刻 就绪队列:P2(4)->P1(3) P2上处理机运行一个时间片 | 执行完时间段:P1:0-2
4时刻 就绪队列:P1(3)->P3(1)->P2(2) P1上处理机运行一个时间片 | 执行完时间段:P2:3-4
5时刻(时间片没用完,来了一个进程) 就绪队列:P3(1)->P2(2)->P4(6)
6时刻 就绪队列:P3(1)->P2(2)->P4(6)->P1(1) P3上处理机运行一个时间片 | 执行完时间段:P1:5-6
7时刻(P3进程结束,时间片提前结束)P2(2)->P4(6)->P1(1) P2上处理机运行一个时间片 | 执行完时间段:P3:6-7
9时刻(P2进程结束) 就绪队列:P4(6)->P1(1) P4上处理机运行一个时间片 | 执行完时间段:P2:7-9
11时刻 就绪队列:P1(1)->P4(4) P1上处理机运行一个时间片 | 执行完时间段:P4:10-11
12时刻(P1进程结束,时间片提前结束) 就绪队列:P4(4) P4上处理机运行一个时间片 | 执行完时间段:P2:11-12
14时刻 就绪队列:P4(2) P4上处理机运行一个时间片 | 执行完时间段:P4:12-14
16时刻(P4进程结束) 就绪队列:无 | 执行完时间段:P4:14-16解答 1:第二行中若是某个进程分配到时间片为,此时时间片用完的时刻正好又来了一个新的进程,此时如何确定顺序?
- 默认新来的进程先进入到队列,之后再是分配完时间片的进程。
解答 2:第六行中若是时间片没有用完就结束了,此时会由于进程的主动放弃来重新发生一次调度选择。
接着根据上面调度顺序写出时刻表:
P1:0-2
P2:3-4
P1:5-6
P3:6-7
P2:7-9
P4:10-11
P2:11-12
P4:12-14
P4:14-16
情况 2:时间片大小为 5 时的情况
调度顺序分析:
0时刻 就绪队列:P1(5) P1上处理机运行一个时间片
2时刻(时间片未结束,来P2) 就绪队列:P2(2)
4时刻(时间片未结束,来P3) 就绪队列:P2(4)->P3(1)
5时刻(P1结束,来P4) 就绪队列:P2(4)->P3(1)->P4(6) P2上处理机运行一个时间片 | 执行完时间段:P1:0-5
9时刻(P2结束,时间片提前结束) 就绪队列:P3(1)->P4(6) P3上处理机运行一个时间片 | 执行完时间段:P2:5-9
10时刻(P3结束,时间片提前结束) 就绪队列:P4(6) P4上处理机运行一个时间片 | 执行完时间段:P3:9-10
15时刻(时间片结束) 就绪队列:P4(1) P4上处理机运行一个时间片 | 执行完时间段:P4:10-15
16时刻(P4结束,时间片提前结束) 就绪队列:无 此时全部结束 | 执行完时间段:P4:15-16接着根据上面调度顺序写出时刻表:
P1:0-5
P2:5-9
P3:9-10
P4:10-15
P4:15-16
我们再看下先来先服务调度算法的时序图:

可以发现与时间片设置为 5 的时候是一模一样,为什么会造成这种情况呢?
- 通过时间片为 5 的情况可以看到,若是我们的时间片太大,导致每一个进程都可以在一个时间片内完成,那么时间片轮转调度算法就会退化为先来先服务调度算法。
②优先级调度算法
优先级调度算法,算法思想:随着计算机的发展,特别是实时操作系统的出现,越来越多的应用场景需要根据任务的紧急程度来决定处理顺序。
算法规则:每个作业 / 进程都有各自的优先级,调度时选择优先级最高的作业 / 进程。
用于作业 / 进程调度:即可用于作业调度,也可用于进程调度以及 I/O 调度。
- 作业调度就是从外存中调度作业到就绪队列;进程调度则是从内存中调度进程到 CPU 执行。
是否可抢占:抢占式、非抢占式都有。
- 做题区别:非抢占式只需在进程主动放弃处理机时进行调度即可,而抢占式则需要在就绪队列变化时,检查是否发生抢占,是被动的。
优缺点:
- 优点:可以使用优先级来区分紧急程度、重要程度,适用于实时操作系统,可灵活地调整对各种作业 / 进程的偏好程度。
- 缺点:若是源源不断有高优先级的进程到来,则可能导致饥饿。
是否会导致饥饿:会。
补充:优先级静态、动态改变
补充:就绪队列未必只有一个,可以按照不同优先级来组织,也可以把优先级高的进程排在更靠近队头的位置。
根据优先级是否可以动态改变,可以将优先级分为静态优先级、动态优先级两种。
- 静态优先级:创建进程时确定,之后一直不变。
- 动态优先级:创建进程时有一个初始值,之后会根据情况动态的调整优先级。
思考 1:如何合理的设置各类进程的优先级?
- 系统进程的优先级 高于 用户进程。
- 前台进程的优先级 高于 后台进程。(在手机上就是这样的原则,前台进程用户可以看到,涉及到使用流畅度等)
- 操作系统更偏好 I/O 型进程(或称为 I/O 繁忙型进程)。
为什么更偏好繁忙型进程?
- I/O 设备和 CPU 可以并行工作,如果优先让 I/O 繁忙型进程优先工作的话,则越有可能让 I/O 设备尽早地投入工作,则资源利用率、系统吞吐量都会得到提升。
思考 2:如果采用的是动态优先级,那么什么时候应该调整?
- 可以从追求公平、提升资源利用率等角度考虑。
- ①如果某进程在就绪队列中等待了很长时间,则可以适当提升其优先级。(与高响应比优先算法类似)
- ②如果其进程占用处理机运行了很长时间,则可适当降低其优先级。
- ③如果发现一个进程频繁的进行 I/O 操作,则可适当提升其优先级。
实际案例题
对于优先数与优先级概念并不一样,有些题目优先数越大,优先级越高;但是有些题目优先数越小,优先级越高,根据题目而定。
题目:

分析:
实现方式一:非抢占式的优先级调度算法:每次调度时选择当前已到达且优先级最高的进程。当前进程主动放弃处理机时会进行调度。
调度顺序分析:
0时刻 已来临进程:P1 P1上处理机
7时刻(P1结束) 已来临进程:P2(4)、P3(1)、P4(4) P3先上处理机 | 执行完时间:P1:0-7
8时刻(P2结束) 已来临进程:P2(4)、P4(4) P2、P4优先级相同,P2先来 P2先上处理机 | 执行完时间:P2:7-8
12时刻(P2结束) 已来临进程:P4(4) P4先上处理机 | 执行完时间:P2:8-12
16时刻(P4结束) 已来临进程:无 | 执行完时间:P4:12-16接着根据上面调度顺序写出时刻表:
P1:0-7
P3:7-8
P2:8-12
P4:12-16
实现方式二:抢占式的优先级调度算法:每次调度时选择当前已到达且优先级最高的进程。当前进程主动放弃处理机时发生调度。另外,当就绪队列发生改变时也需要检查是否发生抢占。
- 也就是说当一个进程到达就绪队列的时候,此时要检查当前运行的进程优先级是否与新到达的优先级高。
调度顺序分析:
0时刻 已来临进程:P1(7) P1上处理机运行
2时刻(来进程) 已来临进程:P1(5)、P2(4) 由于P2优先级高,P2上处理机运行 | 执行完P1:0->2
4时刻(来进程) 已来临进程:P1(5)、P2(2)、P3(1) 由于P3优先级高,P3上处理机运行 | 执行完P2:2->4
5时刻(P3执行结束,来进程) 已来临进程:P1(5)、P2(2)、P4(4) 由于P2、P4优先级一样P2先来,先执行P2 | 执行完 P3:4->5
7时刻(P2执行完毕) 已来临进程:P1(5)、P4(4) P4进程上处理机运行 | 执行完P4:5->7
11时刻(P4执行完毕) 已来临进程:P1(5) P1进程上处理机运行 | 执行完P4:7-11
16时刻(P1执行完毕) 已来临进程:无 | 执行完P1:11-16
③多级反馈队列调度算法(引出与介绍)
认识多级反馈队列调度算法
引出多级反馈队列
首先对上面的四个算法优点来进行汇总:
- FCFS 算法优点是公平。
- SJF 算法的优点是能尽快处理完短作业,平均等待 / 周转时间等参数很优秀。
- 时间片轮转调度算法可以让各个进程得到及时的响应。
- 优先级调度算法可以灵活地调整各种进程被服务的机会。
是否能够对这些算法都能够折中权衡,得到一个综合表现优先平衡的算法呢?
介绍多级反馈队列调度算法
此时,提出了多级反馈队列调度算法。
多级反馈队列调度算法,算法思想:对其他调度算法的折中权衡。
算法规则:
- 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大。
- 新进程到达时先进入第 1 级队列,按照 FCFS 原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时已经是在最下级的队列,则重新放回该队列队尾。
- 只有第 k 级队列为空时,才会为 k+1 级队头的进程分配时间片。
用于作业 / 进程调度:用于进程调度。
是否可抢占:抢占式的算法。在 k 级队列的进程运行过程中,若更上级的队列(1~k-1 级)中进入了一个新进程,则由于新进程处于优先级更高的队列,因此新进程会抢占处理机,原先运行的进程放回 k 级队列队尾(也就是原先最近一次的 k 级)。
- 在实际的操作系统中也有可能是实现为非抢占版本。考试的话以抢占式为准。
优点:①对各类型进程相对公平(FCFS 的优点);②每个新到达的进程都可以很快得到响应(RR 优点);③短进程只用较少的时间就可完成(SPF 的优点)。④不必实现估计进程的运行时间,避免用户作假。⑤可灵活地调整对各类进程的偏好程度,如 CPU 密集型、I/O 密集型进程。
- 扩展:可以将因 I/O 阻塞的进程重新放回到原队列,这样 I/O 型进程就可以保持比较高的优先级。
是否会导致饥饿:会。
- 若是源源不断有短进程到来的话,那么已经被降优先级的进程会长期得不到服务,从而也会导致饥饿的现象。
实际案例题

设置多级就绪队列,各级队列优先级从高到低,时间片从小到大,如下图所示:

流程梳理:
- 新进程到达时先进入第 1 级队列,按照 FCFS 原则排队等待被分配时间片。若用完时间片进程还未结束,则进程进入到下一级队列队尾。如果此时已经在最下级的队列,则重新放回最下级队列队尾。
- 只有第 K 级队列为空时,才会为 k+1 级队头的进程分配时间片被抢占处理机的进程重新放回原队列队尾。
详细调度过程:
时刻为 0 时,由于是第一级队列,那么就只有 1 个时间片,当时间片用完时,他还没有结束此时就会进入到下级队尾(也就是第二级队列)。
第1级队列:P1(8)
第2级队列:无
第3级队列:无
P1上处理机,分配时间片为1
时刻为 1,P2 进程也已经到达,此时会进入到第一级的队尾。此时开始进行调度,从优先级高的第 1 级队列开始,由于其中不为空,此时选择队尾的 P2 进程并分配给其时间片上 CPU。
第1级队列:P2(4)
第2级队列:P1(7)
第3级队列:无
P2上处理机,分配时间片为1 | 执行任务:P1:0-1
时刻为 2:当运行了 1 个时间单位后,P2 进程会下处理机放置到下一级队列也就是第二级队列的队尾中。由于第 1 级队列为空,此时则会取第 2 级队列中的第一个进程,此时由于是第二级其时间片为 2
第1级队列:无
第2级队列:P2(3)->P1(7)
第3级队列:无
P1上处理机,分配时间片为2 | 执行任务:P2:1-2
时刻为 4 时,P1 进程下处理机,此时将 P1 放入到第三级队列末尾,此时 P2 上处理机
第1级队列:无
第2级队列:P2(3)
第3级队列:P1(5)
P2上处理机,分配时间片为2 | 执行任务:P1:2-4
时刻为 5 时(进程到来,尝试抢占 CPU),P3 进程到达第一级的队尾,此时会与处在 CPU 的进程 2 优先级进行比较,由于 P3 优先级为 3,P2 为 2,此时会发生抢占情况,此时会将 P2 放回到原来队列 2 的末尾,之后让 P3 上处理机抢占 CPU:
第1级队列:P3(1)
第2级队列:P2(2)
第3级队列:P1(5)
P3上处理机,分配时间片为1 | 执行任务:P2:4-5
时刻为 6 时,P3 执行完任务,此时开始调度,由于第 1 级队列中没有任务,此时会调度第 2 级队列中的任务:
第1级队列:无
第2级队列:P2(2)
第3级队列:P1(5)
P2上处理机,分配时间片为2 | 执行任务:P3:5-6
时刻为 8 时,P2 完成任务,此时开始调度,由于第 1、2 级队列没有任务,此时会调度第三级队列任务:
第1级队列:无
第2级队列:无
第3级队列:P1(5)
P1上处理机,分配时间片为4 | 执行任务:P2:6-8
时刻为 12 时,P1 时间片使用完,由于此时 P1 本身已经在最下面一级队列,此时会将其依旧是放入到队列 3 中的末尾继续被调度:
第1级队列:无
第2级队列:无
第3级队列:P1(1)
P1上处理机,分配时间片为4 | 执行任务:P1:8-12
时刻为 13 时,P1 任务完成,此时所有进程结束
第1级队列:无
第2级队列:无
第3级队列:无
所有任务完成 | 执行任务:P1:12-13
所有进程各自的执行顺序:
P1:0-1
P2:1-2
P1:2-4
P2:4-5
P3:5-6
P2:6-8
P1:8-12
P1:12-13知识回顾与重要考点

3.5.3、多级队列调度算法
多级队列调度算法:按照进程类型设置多个队列,并且给不同的队列设置不同的优先级(优先级从高向下依次降低)。并且每一次调度发生的时候,首先要选中一个队列,接着根据这个队列内的调度算法来选取队列内的某一个进程,让它上处理机运行。

- ①系统进程的优先级最高。②交互式进程应该赋予相对更高的优先级。③批处理进程(一般用户更关注当前与其交互的应用,也就是前台应用)
重点 1:不同的队列之间可以采取不同方式:
①固定优先级:高优先级为空时,低优先级进程才能够被调度。
- 问题存在:若是一直存在有高优先级的系统进程,那么此时用户交互式的进程无法被及时调度。
②时间片划分:如三个队列分配时间各自为 50%、40%、10%。
- 这样子划分可以在每秒的时间内,那类进程都能够被最少执行一次。
重点 2:对于一个队列中有很多同类的进程,如何抉择?此时可以采用不同的调度策略:
① 系统进程队列可以采用优先级调度算法。(可以保证优先级更高的先执行)
②交互队列采用 RR 时间片轮转算法。(很短的时间周期内都能够被响应一次)
③批处理队列可以采用 FCFS 先来先服务调度策略。
四、同步与互斥
4.1、同步与互斥的基本概念
4.1.1、什么是进程同步
回顾:进程具有异步性的特征,对于异步性指的是各并发执行的进程以各自独立的、不可预知的速度向前推进。
- 由于异步性导致了这两个并发的进程,他们的推进顺序是不可预知的。
例 1:一个人约会两个人,对于心只有一个。

那么对于心先给 1 号还是先给 2 号,我们无法进行预测,此时就会由如下两种情况:

这就是程序异步性的一个体现。
此时我们设置一个条件:心首先得给 1 号,之后再给 2 号,此时根据这个条件,那么渣男在并发执行这两个约会进程的时候,就必须保证 “1 号的指令 2” 一定要在 2 号的 “指令 1” 之前执行,此时如下:

可以通过设置条件,从而保证各个进程之间的推进次序是按照我们预想的方式进行。
例 2:进程通信中的管道通信

对于管道通信中读进程与写进程是并发运行的,由于并发必然导致异步性,对于 “写数据” 与 “读数据” 两个操作的执行顺序是不确定的,实际应用中,又必须按照 “写数据—> 读数据” 的顺序执行。
如何解决这种异步问题? 进程同步。
对于以上有特定顺序的要求,此时操作系统提供 ” 进程同步机制 ” 来实现上述的需求。
- 同步也称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源自它们之间的相互合作。
4.1.2、什么是进程互斥?
两种资源共享方式
进程的 “并发” 需要 “共享” 的支持,各个并发执行的进程不可避免的需要共享一些系统资源,如:内存、打印机、摄像头等 I/O 设备。
主要包含两种资源共享方式:互斥共享、同时共享

- 互斥共享:系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源。
- 同时共享:系统中的某些资源,允许一个时间段内由多个进程 “同时” 对它们进行访问。
临界资源:将一个时间段内只允许一个进程使用的资源。许多物理设备(如摄像头、打印机)都属于临界资源,对于还有一些变量、数据、内存缓冲区等都属于临界资源。
对临界资源的访问,必须互斥地进行。进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待,当前访问临界资源的进程访问结束,释放资源后,另一个进程才能够去访问临界资源。
互斥:间接制约关系。
互斥资源访问的四个部分
对于临界资源的互斥访问,可以来逻辑上分为下面四个部分:

- 进入区:负责检查是否可进入临界区,若可进入,则应该设置正在访问临界资源的标志(或称上锁),以组织其他进程公式进入临界区。
- 临界区:访问临界资源的那段代码。
- 退出区:负责解除正在访问临界资源的标志(可理解为解锁)。
- 剩余区:做其他处理。
注意:临界区是进程中访问临界资源的代码段。进入区和退出区是负责实现互斥的代码段。临界区也称为 “临界段”。
若是一个进程暂时不能进入临界区,是否该进程能够一直占着处理机?该进程有没有可能一直进不了临界区。
遵循临界资源互斥访问的四个原则
为了实现对临界资源的互斥访问,同时保证系统的整体性能,需要遵循以下原则:
- 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
- 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
- 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)。
- 有权等待。当进程不能进入临界区时,应该立即是释放处理机,防止进程忙等待。
- 忙等待指的是:当进程暂时无法继续向下推进,但是这个进程还一直占用着处理机,使得处理机一直处于一个忙碌的状态,没有办法给别的进程服务。
知识回顾与重要考点

4.2、进程互斥的软件实现方式

4.2.1、没有进程互斥可能出现的情况?
若是进程中没有进程互斥?

过程:若是进程 A、进程 B 并发运行,由于两个进程中都包含有使用打印机资源,此时先调度 A 上处理机运行,当 A 在使用打印机的过程中,分配给它的时间片使用完了,接下来操作系统又调度 B 让它上处理机运行,进程 B 也在使用打印机。
最终结果:此时会导致 A、B 的打印内容会混在一起。
若是我们想要实现进程 A 在访问打印机的时候,B 不会再访问,此时就需要使用到进程互斥。
接下来使用软件代码的方式来尝试实现互斥的访问资源,介绍四种,对于这四种都还是有一定的缺陷。
实现方式一:单标志法(并发违反 “空闲让进”)
算法思想:两个进程在访问完临界区后把使用临界区的权限交给了另一个进程,也就是说每个进程进入临界区的权限只能被另一个进程赋予。
通过使用一个变量来表示的对应自己的进程号,首先 turn 的初值为 0,表示刚开始只允许 0 号进程进入临界区。

流程:
- 可以看到初始 P1 先上处理机,此时由于 turn=0,turn!=1 会不断的阻塞在代码⑤位置,知道 p1 的时间片用完,此时会发生调度,切换 P0 上处理机运行。
- P0 进程中 turn=0,此时会直接进入到临界区,此时若是 P0 访问临界区时又时间片停止,切换回到 P1 进程。此时 P1 进程依旧会卡在⑤位置,直到 P1 进程时间片用完切换到 P0 进程中执行到退出区代码,将 turn 值设置为 1 时,之后切换回 P1 进程才能够进入到临界区。
更加通俗易懂的理解:

特征:通过该算法可以实现 “同一时刻最多只允许一个进程访问临界区”,进程是互相之间轮流使用的。
缺点:各个进程只能够轮流的使用临界区,每个进程在退出区这部分代码表示一个谦让的动作让对方使用,但是对方一直不使用这个资源的时候可以发现会一直轮不到自己。最大的问题就是违反了 “空闲让进” 的原则。
实现方式二:双标志先检查法(并发违反忙则等待,两个都会进入临界区)
算法思想:设置一个布尔型数组 flag[2],数组中各个元素用来标记各进程想要临界区的意愿,比如 “flag[0]=true” 意味着此时 0 号进程 P0 想要进入到临界区。每个进程再进入临界区之前先检查当前有没有别的进程想要进入到临界区,若是没有,则把自身对应的标志 flag[i] 设为 true,之后开始访问临界区。
- 首先是需要确认是否需要进入到临界区,若是对方不想进入临界区,此时当前进程则会进入到临界区。
对于进入区中实际做了两个事情:①检查是否对方想进入临界区。②表示自己进入临界区。

通俗易懂理解:

与单标志的区别在于:
- 单标志是根据一个资源表示可供哪个进程使用,那么若是进程 A 临界区执行完后,释放资源此时资源可供进程 B 使用,但要是进程 B 一直不使用那么就会导致进程 A 若是想要再使用是不能够再次通过进入区的;
- 双标志对于是否能够进入到进入区则是看对方是否占用了资源又两个变量表示,那么若是进程 A 首先进入到了临界区,之后不使用临界区时,此时就设置自己的变量为不使用,此时若是进程 B 迟迟不进入临界区也不会影响进程 A 自己再通过进入区,来访问到临界区资源。
问题:若是两个进程并发运行,按照①⑤②⑥③⑦… 的顺序执行,则会导致出现问题两个进程都会进入到临界区。
- 问题发生流程:当前是再进行并发阶段,进程 P0 通过①后还没有设置②flag[0]=true,此时时间片用完,CPU 使用权到了 P1 进程中,此时同样⑤同样也能够通过进入区,此时两个进程都能够同时访问进入到临界区。
缺点:违反了 “忙则等待” 原则。原因在于,进入区的 “检查” 和 “上锁” 两个处理不是一气呵成的,“检查” 后,“上锁” 前可能发生进程切换。
实现方式三:双标志后检查法(并发会导致都进入不到临界区)
算法思想:双标志先检查法的改变,前一个算法的问题是先 “检查” 后 “上锁”,对于这两个操作无法一气呵成,此时导致了两个进程同时进入临界区的问题。因此,人们想到先 “上锁” 后 “检查” 的方法,来避免上述问题。

若是按照①⑤②⑥… 的顺序,此时会出现进程 P0、P1 都进入不到临界区的问题。
问题:双标志后检查法虽然解决了 “忙则等待” 的问题,但是又违背了 “空闲让进” 和 “有限等待” 原则,会因各进程都长期无法访问临界资源而产生 “饥饿” 的现象。
- 违背空闲让进指的是:两者都进入不到临界区情况。
- 违背有限等待指的是:由于两者都进入不到临界区,那么就会永远不能够向下推进,此时算法违背了有限等待的原则。
实现方式四:Peterson 算法(无并发问题,仅仅只是违反让权等待)
算法思想:结合双标志位、单标志法的思想。如果双方都争着想进入临界区,那么可以让进程尝试谦让,做一个有礼貌的进程。

通俗易懂描述:每个进程首先表示我要使用,接着再进行谦让一下,最后执行谦让的进程,那么另一个进程则会先进入到临界区。

举两个实际收红包的例子,最终是否收到红包是看最后谁谦让的:
例 1:最终是阿姨谦让,然后自己收下

例 2:最终是自己谦让,然后阿姨收回红包

我们尝试使用不同的顺序穿插执行如下,不会出现之前的都能够进入临界区以及两个都阻塞在临界区外的情况:

对于代码再次进行深入解读下:

每个进程的【进入区】都分为三个步骤:
- 主动争取。// 设置自己的 flag 位置为 true
- 主动谦让。// 设置 turn = 对方进程,谦让给对方使用
- 检查对方是否也想使用,且最后一次是不是自己说了 “客气话”,若都是那么此时当前进程会进入到阻塞中。
写一遍条件:
//进程P0
flag[0] = true;//表示P0进程自己想要使用资源
turn = 1;//表示谦让给P1进程使用
while (flag[1] && turn == 1);//若是进程P1想要使用 同时 最后一步是自己(P0)谦让的,那么进入到循环
//进程P1
flag[1] = true;//表示P1进程自己想要使用资源
turn = 0;//表示谦让给P0进程使用
while (flag[0] && turn == 1);//若是进程P0想要使用 同时 最后一步是自己(P1)谦让的,那么进入到循环优点:使用 Peterson 算法解决了进程互斥问题、遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则。
- 让权等待指的是:若是进程 A 进不了临界区,那么应该立即释放处理机资源,而不是继续在 CPU 上跑。但是在这个进程上处理方式是若是进不了临界区,此时会一直卡在 while 循环这里,实际自己还一直在 CPU 上执行不断的来检查这个外循环的条件,虽然该进程进不了临界区,但是依然会占用 CPU 资源,就没有满足让权等待原则。
评价:Peterson 算法是相较于之前三种软件解决方案来说是最好的,但依然不够好。
知识点回顾与重要考点

4.3、进程互斥的硬件实现方式

方式一:中断屏蔽方法
实现思路:利用 “开 / 关中断指令” 实现。
- 实际与原语实现思路相同,也就是在某段进程开始访问临界区到结束访问为止都不允许被中断,在这个过程里也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况。

优点:简单、搞笑。
缺点:不适用于多处理机。由于关中断、开中断是内核指令,所以只适用于操作系统内核进程,不适用于用户进程(因为开 / 关中断指令)只能够运行在内核区,主要原因是若是不运行在内核让用户使用会很危险。
- 不适用于多处理机理解:关中断指令只对执行关中断指令的那个处理机有用。若是此时处理机 A 执行了关中断指令,那么意味着这个处理机上面的进程不会被切换,那么这个进程就可以顺利的访问临界区。但是若是对于另一个处理机 B 来说,起始还是可以切换进程,如果另一个处理机上运行的也是这一个进程,那么这个处理机上的进程同样会访问临界区。【简洁:可能会发生两个处理机上两个进程同时对临界区进行访问的情况。】
方式二:TestAndSet 指令
简介:TSL 指令,也有地方称为 TestAndSetLock 指令,或 TSL 指令。
实现原理:TSL 指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。
下面使用 C 语言描述其硬件实现的逻辑,并非真实就是这么实现的!!!

- 实际硬件执行的过程当中其实就是将 lock 这个值放入到某一个物理寄存器里,然后将 lock 这个值覆盖为 true。
实现流程:若刚开始 lock 是 false,则 TSL 返回的 old 值为 false,while 循环条件不满足,直接跳过循环,进入临界区。若刚开始 lock 是 true,则执行 TLS 后 old 返回的值为 true,此时 while 循环满足,会一直循环,直到当前访问临界区的进程在退出区进行 “解锁”。
与软件实现相比:实际 TSL 指令把 “上锁” 和 “检查” 操作用硬件的方式变成了一气呵成的原子操作。
优点:实现简单,无需像软件实现方法那样严格检查是否有逻辑漏洞,适用于多处理机环境。
- TestAndSet(测试并设置)指令是一种能够在一个原子操作中读取和修改变量的指令。在多处理机环境中,通过使用 TestAndSet 指令可以实现一种互斥访问共享资源的机制,即同一时间只允许一个处理器访问共享数据。。这样就可以避免了多个处理器同时修改共享数据而导致的冲突和不一致性问题。
缺点:不满足 “让权等待” 原则,暂时无法进入临界区的进程会占用 CPU 并循环执行 TSL 指令,从而导致 “忙等”。【那这个算法实际上不就是我们之前的 Peterson 算法嘛,无非是硬件与软件不同的实现区别,并且它们都会出现忙等情况】
对于 TestAndSet 指令与总线的关系:
- 原子性:TestAndSet 指令的原子性是通过在处理器和内存之间使用总线进行通信实现的。在执行 TestAndSet 指令期间,处理器会向内存发送读取和写入总线事务,确保其他处理器无法同时读取或修改被操作的变量。这样可以保证在多处理器环境中,TestAndSet 操作是原子的,不会被中断或干扰。
- 内存一致性:多处理器系统中的处理器通常共享同一片物理内存。通过总线,处理器可以对共享变量进行读取和写入操作。使用 TestAndSet 指令对共享变量进行读取和修改时,这些操作会通过总线传输到内存,并更新内存中的值。其他处理器在读取相同的共享变量时,也会通过总线从内存加载最新的值,确保了数据的一致性。
- 总线竞争:在多处理器环境中,多个处理器可以同时发起总线事务,并试图访问共享资源。当多个处理器同时进行 TestAndSet 指令或其他涉及共享数据的操作时,可能会引发总线竞争。总线竞争可能导致性能下降,因为同时使用总线的处理器需要等待总线可用,并可能导致连锁反应,影响系统的整体性能。
方式三:Swap 指令
简介:有些地方叫做 Exchange 指令,或简称为 XCHG 指令。
实现原理:Swap 指令用硬件实现,执行的过程不允许被中断,只能够一气呵成。
下面使用 C ���言描述逻辑,并非真实实现:

对 TSL 指令实现流程:都是先记录下此时临界区是否已经被上锁(记录在 old 变量上),再将上锁标记 lock 设置为 true,最后检查 old,如果 old 为 false 则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。
- 对于 Swap 指令与 TSL 指令在硬件层次上可能实现的方式不一样,但是逻辑上它们做的事情没有太大区别。
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞,适用于多处理机环境。
缺点:不满足 “让权等待” 原则,暂时无法进入临界区的进程会占用 CPU 并循环执行 TSL 指令,从而导致 “忙等”。
知识点回顾与重要考点

4.4、互斥锁
实现目的:用于实现进程互斥。
互斥锁介绍:解决临界区最简单的工具就是互斥锁(mutex lock)。一个进程在进入临界区时应获得锁;在退出临界区时释放锁。
自旋锁:需要连续循环忙等的互斥锁,都可称为自旋锁,如 TSL 指令、swap 指令、单标志法。
实现原理:acquire() 获得锁,函数 release() 释放锁。每个互斥锁有一个布尔变量 available,表示锁是否可用,如果锁是可用的,调用 acquire() 会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会一直自旋,直到锁被释放。

- acquire() 或 release() 的执行必须是原子操作,所以互斥锁通常采用硬件机制来实现。
优点:等待期间可以 (并不是一定) 不用切换进程上下文,多处理器系统中,若上锁的时间短,则等待代价很低。
缺点:忙等待,当一个进程在临界区中,任何其他进程在进入临界区必须连续循环调用 acquire()。当多个进程共享同一 CPU 时,就浪费了 CPU 周期,因此,互斥锁通常用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行。
特性:
1、需要忙等,进程时间片用完才下处理机,违反 “让权等待”。
- 对于上锁的时间短,等待代价很低理解(针对于多处理器系统):在多处理器的系统中,若是某一个进程 P1 此时正在自旋等待,那么只会吃掉这一个核的计算能力,如果此时,上锁的进程 P2 也在某一个核心里运行,那么 P2 也有可能在接下来快速的使用好临界区,接着快速的解锁,此时 P1 在自旋的过程中,是不是很快发现解锁了,是不是可以顺利的进入临界区,那么这个自旋盲等的过程没有发生进程切换,此时代价是十分低的。
- 不太适用于单处理机系统原因:因为单处理机系统 P1 进程,它会吃掉唯一的一个核,在这个时间片里是不可能等到解锁的,只有等待它这个时间片用完,切换上锁的那个进程上处理机解锁之后才有可能获得这个锁。
2、常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区。
3、不太适用于单处理机系统,忙等的过程中不可能解锁。
4.5、信号量(实现进程互斥、同步)
复习回顾+思考:之前学习的这些进程互斥的解决方案分别存在哪些问题?
进程互斥的四种软件实现方式(单标志法、双标志先检查、双标志后检查、Peterson算法)
进程互斥的三种硬件实现方式(中断屏蔽方法、TS/TSL指令、Swap/XCHG指令)
1.在双标志先检查法中,进入区的“检查”、“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题:
2.所有的解决方案都无法实现“让权等待”

问题 1:是否能够解决之前在软件实现的进程互斥前三种方法由于检查、上锁操作无法一气呵成的问题?
问题 2:是否能够使用某种方式令并发执行进程能够遵守让权等待的原则?
解答:1965 年,荷兰学者 Dijkstra 提出了一种卓有成效的实现进程互斥、同步的方法:信号量机制。
4.5.1、认识信号量机制
信号量:实际就是一个变量(可以是一个整数,也可以是一个复杂的记录性变量),可以使用信号量来表示系统中某种资源的数量。例如,系统中只有一台打印机,就可以设置一个初始值为 1 的信号量。
- 信号量机制采用原语来解决问题,之前的问题主要是由于检查、上锁操作无法一气呵成,那么我们是否可以在这两个步骤外面使用原语来实现解决这个问题。
原语:是一种特殊的程序段,其执行能够一气呵成,不可被中断。
一对原语:wait(S)、signal(S),可以将原语理解为自己写的函数,其中的信号量 S 就是函数调用时传入的一个参数(若是考试出现 P(S)、V(S) 的操作,没有其他指定 S 是整型的话,默认 S 为记录型信号量!!!)。
- wait(S)、signal(S) 原语简称为 P、V 操作(来自荷兰语 proberen 和 verhogen)。因此通常将 wait(S)、signal(S) 写成 P(S)、V(S)。
本节主要讲的内容为:
- ①信号量是一种变量,可以使用信号量来表示系统中某些资源的数量。
- ②可以使用系统提供的一对原语来对信号量进行操作。
4.5.2、两种类型信号量
类型一:整型信号量(让权等待问题)
介绍:用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。例如计算机系统中有一台打印机,那么可以使用一个整型 1 来表示。
- 与普通整数变量的去呗:对信号量的操作只有三种:初始化、P 操作、V 操作。
具体实现(代码模拟):

对于 wait() 函数中的检查、上锁实际上与双标志检查法做的是同样的事情,只不过不一样的是在 wait() 中实现的检查和上锁步骤是一气呵成的,那么就避免了并发、异步导致的问题。
举例:若是此时进程 P0 通过了进入去,访问临界区时,其实其他的进程会阻塞等待在 wait() 中,只有等到进程 P0 使用 V 操作释放资源后其他进程才可进入。

存在的问题:不满足 “让权等待” 原则,会发生 “忙等”。
- 若是一个进程暂时进不了临界区,那么意味着会卡在 wait() 原语这个循环中,由于 wait 原语是不可中断的,那么也就意味着这个 while 循环的进程是不是一直不会被切换?由于许多经典的教材都是这样写的,这里姑且认为没有问题,不会导致一个进程一直占用处理机的情况。
- chatgpt 回答:现代操作系统通常会提供更高级的同步原语(如互斥锁、条件变量等),这些原语可以更好地解决临界区访问的同步问题,并且在等待期间可以被中断。这样,在使用这些高级原语时,进程就可以被切换出去执行其他任务,而不是一直等待在循环中。
类型二:记录型信号量
认识记录型信号量
对于之前的整型信号量有很大的缺陷:若是一个进程暂时进不了临界区的资源,此时会一直占用处理机循环检查,进而导致盲等的情况不满足 “让权等待” 的原则。
之后提出了记录型信号量,主要就是解决刚刚的问题,这个信号量是使用一个记录型的数据结构来表示。
记录型的信号量定义如下:
/* 记录型信号量的定义 */
typedef struct {
int value; // 剩余资源数
struct process *L; // 等待队列
} semaphore;接下来是 wait 与 signal 实现,wait 原语中包含了阻塞原语 block,signal 原语中包含了唤醒原语 wakeup(代码来实现逻辑):

wait 实现:
- 将这个资源的也就是其结构体中的值 - 1,当 - 1 之后,会对 value 进行检查,若是我们在执行了 - 1 操作后,导致 value 的值已经 < 0,那么表示在 - 1 之前就没有了这种系统资源,此时是没有系统资源可以分配给这种系统资源的,此时就会执行一个 block 原语。
block原语:将当前的这个进程,阻塞起来,主动的阻塞放弃处理机。如果剩余资源数不够,使用 block 原语使进程从运行态进入阻塞态,并把挂到信号量 S 的等待队列(即阻塞队列)中。
signal 实现:
- 将这个资源结构体中的值 + 1,若是仍然 ⇐0,那么说明还有一些进程是处于等待队列的,此时就需要去调用一个 wakeup 原语从信号量的等待队列里唤醒其中的某一个进程,接着让该进程从阻塞态回到就绪态,并将它所申请的等待的这个资源分配给它。
wakeup原语:释放资源后,若是还有别的进程在等待该资源,则使用 wakeup 去唤醒等待队列中的一个进程,该进程会从阻塞态—> 就绪态。
记录型信号量的实际案例
默认的初始状态如下所示:由两台打印机以及四个进程并发执行

1、首先 P0 进程被分配,资源数 - 1 为 1,使用打印机;接着 P1 进程也被分配,资源数 - 1 为 0,使用打印机 2,此时如下:
- 由于为分配的过程都是 >=0 的,所以不会将这两个进程的任意一个放入等待队列中。

2、此时进程 P2 又被分配给了处理器,此时资源数 - 1 为 - 1,由于 - 1<0,那么会执行 wakeup 原语,并将进程 P2 放入到等待队列中(进程 P2 进入到阻塞态):

3、此时进程 P3 也被调度,资源数 - 1 为 - 2,又 - 2<0,那么又会执行 wakeup 原语,此时进程 P3 会放入到等待队列中(进程 P3 也会进入到阻塞态):

4、接着时间片又切回给了 P0 进程,在使用完打印机后,此时就会执行 signal 原语,此时首先会进行资源 + 1 为 - 1,由于 ⇐0,释放资源后就会从等待队列中取出队头的进程并将资源分配给它:

5、此时会执行 P2 进程,当使用完打印机之后,此时也会执行 signal 原语,资源数 + 1 为 0,由于 0⇐0 此时在释放资源的同时又会从等待队列中取出 P3 进程并将资源分配给它:

6、此时经过了一段时间 CPU 又分给了 P1 进程使用,使用打印机完之后此时资源 + 1 为 1, 由于 1>0 表示没有资源在阻塞等待,此时正常结束:

7、最终 P3 进程执行完毕之后,在执行 signal 原语时资源 + 1 为 2,最终回到了初始状态:

记录型信号量流程梳理
/* 记录型信号量的定义 */
typedef struct {
int value; // 剩余资源数
struct process *L; // 等待队列
} semaphore;
/* 某进程需要使用资源时,通过 wait 原语申请 */
void wait(semaphore S) {
S.value--;
if (S.value < 0) {
block(S.L);
}
}
/* 进程使用完资源后,通过 signal 原语释放 */
void signal(semaphore S) {
S.value++;
if (S.value <= 0) {
wakeup(S.L);
}
}在考研题目中,wait(S)、signal(S)也可以记为P(S)、V(S),这对原语可用于实现系统资源的“申请”和“释放”。S.value的初值表示系统中某种资源的数目。
对信号量S的一次P操作意味着进程请求一个单位的该类资源,因此需要执行S.value--,表示资源数减1,当S.value<0时表示该类资源已分配完毕,因此进程应调用block原语进行自我阻塞(当前运行的进程从运行态→阻塞态),主动放弃处理机,并插入该类资源的等待队列SL中。可见,该机制遵循了“让权等待”原则,不会出现“忙等”现象。
对信号量S的一次V操作意味着进程释放一个单位的该类资源,因此需要执行S.value++,表示资源数加1,若加1后仍是S.value<=0,表示依然有进程在等待该类资源,因此应调用wakeup原语唤醒等待队列中的第一个进程(被唤醒进程从阻塞态→就绪态)。

对于记录型信号量中 wait 原语若是没有资源可用时,是进行阻塞等待的!
知识回顾与重要考点

注意:若是考试出现 P(S)、V(S) 的操作,没有其他指定 S 是整型的话,默认 S 为记录型信号量!!!
4.5.3、信号量实现进程互斥、同步、前驱关系

- 对于互斥的软件、硬件实现方式都有共同的缺点就是无法实现 “让权等待”(简单来说就是获取不到资源时,CPU 会占用不断地判断是否有可用资源)。
- 信号量机制当中设置了进程的阻塞与唤醒,此时可以解决上面软硬件未实现的 “让权等待”。
注意点:应当去理解信号量背后的含义,一个信号量对应一种资源。
信号量的值 = 这种资源的剩余数量(信号量的值如果 < 0,说明此时有进程在等待这种资源)
对于 P、V 操作的含义:
- P(S):申请一个资源 S,如果资源不够就阻塞等待。
- V(S):释放一个资源 S,如果有进程在等待该资源,则唤醒一个进程。
4.5.3.1、信号量机制实现进程互斥
信号量机制实现进程互斥的步骤:
1、分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)。
2、设置互斥信号量 mutex(表示进入临界区的名额),初值为 1。
3、在进入区 P(mutex):申请资源。
4、在退出区 V(mutex):释放资源。
三个注意点:
注意点 1:实际对于信号量的定义,可以直接使用 semaphore 来表示记录型信号量。
原本是专门定义了一个数据结构:

实际的话可以将信号量声明简写为如下:

注意点 2:对于访问不同的资源,我们设置的信号量也要不同,例如打印机设置为 mutex1,摄像头设置为 mutex2

注意点 3:PV 操作应该成对出现,若是缺少 P(mutex) 就不能保证临界资源的互斥访问;缺少 V(mutex) 会导致资源永不被释放,等待进程永不被唤醒。
4.5.4、信号量机制实现进程同步
由于两个进程是并发运行,那么对于他们代码的顺序无法预知。
目标:让代码执行顺序想要的那种方式进行。
使用信号量实现进程同步步骤:
1、分析在什么地方实现 “同步关系”,即代表保证 “一前一后” 执行的两个操作(或两句代码)。
2、设置同步信号量 S,初始为 0。
3、在 “前操作” 之后执行 V(S)。
4、在 “后操作” 之前执行 P(S)。
巧记:前 V 后 P。
案例目标:想要让 P1 进程的代码 2 在 P2 进程的代码 4 之前运行。
思路:①初始话信号量为 0。②在代码 2 之后设置 V 操作,在代码 4 之前设置 P 操作。

- 这里对于初始值 S=0,实际表示刚开始没有这种资源,若是 P2 进程需要使用这种资源,那么就必须由 P1 进程执行到 V 原语时产生这种资源后,P2 进程才可往下执行!
核心流程梳理,下面列举两种情况看是否能够实现进程同步:
- 若是先执行进程 2 的 P(S) 操作,s–为 - 1,表示此时没有可用资源,因此会通过这个 P 操作间接执行 block 原语,主动请求阻塞。当 P1 进程执行完代码 2 时,接着就会执行 V(S) 原语,此时 s++ 为 0,并且由于 0⇐0 表示当前有进程在该信号量对应的阻塞队列中,此时会在其中执行 wakeup 原语,唤醒 P2 进程,此时 P2 就可以继续执行代码 4 了。
- 若是先执行进程 1 的 V(S) 操作,那么就会执行 S++ 为 1,此时并不会唤醒,之后时间片给到 P2 进程的 P(S) 后,由于 S=1 表示是有资源的,执行 S–为 0,P2 进程不会执行 block 原语那么就会往下执行代码 4。
综合:信号量机制 实现前驱关系
题目:如何使用信号量来实现下面前驱图能够顺序执行?

实际在每一个前驱关系中设置一个信号量,然后都是前 V 后 P 顺序即可实现!
实际设计步骤:
1、每一对前驱关系各设置一个同步信号量。
2、在 “前操作” 之后对相对应的同步信号量执行 V 操作。
3、在 “后操作” 之前对相对应的同步信号量执行 P 操作。
示例代码如下:

S1与S2、S3有前驱关系,那么当S1执行完临界区之后,此时使用V指令释放相对应的S2、S3所需要资源。
S2、S3一开始都是使用P来进行阻塞,之后各自完成之后再去释放相对应后续执行进程所需的指令。
对于S6那么在临界区之前则需要执行三重P操作,分别是P(e)、P(f)、P(g),只有这三个资源都获取到了才能够进入到S6的临界区中。
知识回顾与重要考点

若是实现互斥,则信号量初始值设置为 1,先 P 后 V。
- 额外:若是考察多个资源问题,那么有多少资源就把信号量最初值设置为多少,申请资源采用 P 操作,释放资源采用 V 操作。
若是实现同步,则信号量初始值设置为 0,先 V 后 P
若是对于前驱关系,那么每一对之间都使用一个信号量来进行表示。
4.6、管程(封装同步与互斥)

4.6.1、为什么要引入管程?
对于信号量机制容易出现的问题:编写程序困难,易出错。
例如下方我们写将 (mutex) 与 P(empty)写反了,实际前者在后,后者在前,这样子会十分容易得出现死锁问题!!!

是否恶意设计一种机制,能够让程序员写程序时不需要再关注复杂的 PV 操作,让写代码更加轻松呢?
1973 年,Brinch Hansen 首次在程序设计语言 (Pascal) 中引入了 “管程” 成分:一种高级同步机制。
4.6.2、管程的定义与基本特征
管程:是一种高级同步机制。本质上也是来实现进程的同步与互斥。管理是一种特殊的软件模块,有这些部分组成:
1、局部于管程的共享数据结构说明。
2、对数据结构进行操作的一组过程。
- 过程指的是函数。
3、对局部于管程的共享数据设置初始值的语句。
- 这里指的是对数据结构的初始化。
4、管程有一个名字。
管程的基本特征:
1、局部于管程的数据只能被局部于管程的过程所访问。
2、一个进程只有通过调用管程内的过程才能够进入管程访问共享数据。
- 管程当中定义的这些共享的数据结构只能被管程内定义的一些函数所修改。
3、每次仅允许一个进程在管程内执行某个内部过程。
- 每次仅允许一个进程在管程内执行,在同一时间只能有一个进程在使用管程当中的某一个函数,若是其他进程也想使用就需要等待当前的进程使用完才能使用。
扩展 1:用管程解决生产者、消费者问题(包含示例体现互斥、同步)
介绍:使用特殊的方法来定义管程:monitor、end monitor,管程的内容就是这个关键字中间的部分::
monitor ProducerConsumer {
// 生产进程
condition full, empty; // 条件变量用来实现同步排队
producer() {
int count = 0; // 缓冲区中的产品数
while (1) {
void insert(Item item) { // 把产品item放入缓冲区
//由编译器负责实现,各进程互斥地进入,管程中的进程
item = 生产一个产品;
if (count == N)
wait(full);
ProducerConsumer.insert(item);
count++;
insert_item(item);
if (count == 1)
signal(empty);
}
}
}
consumer() {
while (1) {
Item remove() { // 从缓冲区中取出一个产品
item = ProducerConsumer.remove();
if (count == 0)
消费产品item;
wait(empty);
count--;
if (count == N - 1)
signal(full);
return removeitem();
}
}
}
}
end monitor;// 生产者进程
producer() {
while (1) {
item = 生产一个产品;
ProducerConsumer.insert(item);
}
}// 消费者进程
consumer() {
while (1) {
item = ProducerConsumer.remove();
消费产品 item;
}
}
- 其中定义了一些条件变量来实现同步,也可以定义一些普通的变量来表示我们想要的信息,例如缓冲区当中产品个数。
管程提供的互斥、同步功能相关体现:
- 可以在管程中定义一些条件变量 full、empty,与之对应的实现等待、唤醒操作来实现同步问题。
- 可以看到上面的管程中定义了两个函数,一个是 insert()、另一个是 remove(),那么之后我们实现生产者 - 消费者就十分简单了,直接在响应的地方直接调用 insert、remove 即可,无需像之前一样需要自己写 PV 操作,采用了管程,代码就变得十分简洁,作为生产者进程、消费者进程无需去关心互斥相关直接交由管程完成,这些函数在编译器级中就实现了互斥调用函数!(这个是代码看不到的)
实际在编译的时候,会由编译器负责实现各个进程互斥的进入管程当中的过程!
引入管程的目的无非就是要更方便地实现进程互斥和同步。
- 需要在管程中定义共享数据(如生产者消费者问题的缓冲区)
- 需要在管程中定义用于访问这些共享数据的“入口”——其实就是一些函数(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)
- 只有通过这些特定的“入口”才能访问共享数据
- 管程中有很多“入口”,但是每次只能开放其中一个“入口”,并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。注意:这种互斥特性是由编译器负责实现的,程序员不用关心)
- 可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,进程应先释放管程的使用权,也就是让出“入口”);可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。
程序员可以用某种特殊的语法定义一个管程(比如:monitor ProducerConsumer { ... }),之后其他程序员就可以使用这个管程提供的特定“入口”很方便地使用实现进程同步/互斥了。

例 1:两个生产者进程并发执行,依次调用了 insert 过程。【互斥例子】
过程:当第一个进程先进入到 insert 函数时,若是没有执行完,此时第二个进程就尝试着调用 insert 函数,由于编译器实现的功能,此时会暂时阻止第二个进程进入 insert 函数,此时第二个进程会阻塞 insert 函数后,只有第一个 insert 执行完才能够停止阻塞,继续执行。

例 2:两个消费者进程先执行,生产者进程后执行。【同步例子】
过程:①默认 count=0,由于两个消费者先执行,此时都会因为 count==0 都被阻塞住,首先 consumer1 进入等待队列,接着 consumer2 再次进入等待队列;②此时生产进程执行,会先执行 count++ 为 1,当 = 1 是就会唤醒消费者进程,此时首先出队的测试 consumer2,也就是消费者 2 进程,此时会执行 count–,并且会调用 remove_item() 返回一个产品。
扩展 2:Java 中类似于管程的机制
可以使用synchronized关键字来表示管程,这里的话实际上是实现了一个函数互斥使用的过程。

知识回顾与重要考点
特定的 “入口” 指的是函数。

4.7、经典同步问题
通用 PV 问题的分析模板
使用 PV 操作来进行解决,对于一般 PV 操作题目分析步骤如下:
1、关系分析。找到题目中描述的各个进程,分析它们之间的同步、互斥关系。
2、整理思路。根据各进程的操作流程确定 P、V 操作的大致顺序。
3、设置信号量,并根据题目条件确定信号量初值(互斥信号量初值一般为 1,同步信号量的初值要看对应资源的初始值是多少)。
4.7.1、生产者 - 消费者问题(同步问题)
4.7.1.1、认识生产者 - 消费者问题
生产者 - 消费者问题描述:系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(这里的产品可以理解为某种数据)。
- 初始条件:生产者、消费者共享一个初始为空、大小为 n 的缓冲区。

4.7.1.2、分析问题及解决
开始问题分析,按照上面给出的分析步骤来进行:
步骤一:关系分析
首先确定出两个一前一后的关系:
①缓冲区没满的时候,生产者才能生产。【缓冲区没满—> 生产者生产】
②缓冲区不空的时候,消费者才能消费。【缓冲区不空—> 消费者消费】

明确互斥规则:缓冲区是临界资源,各进程必须互斥的访问缓冲区。【互斥关系】
-
互斥访问的原因?若是生产者进程有多个此时进行并发访问:可能会出现数据覆盖的问题。
-
举例:此时两个生产者进程同时并发访问缓冲区,可能会出现向同一个位置去写数据的情况,此时就会出现数据覆盖问题。

2、整理思路,确定各个进程的操作流程并确认 P、V 操作的大致顺序
- 互斥:在临界区前后分别 PV。
- 同步:前 V 后 P。
3、设置信号量
根据下图中的前后关系,我们来确认出同步信号量:

semaphore full = 0;//full用来表示 缓冲区不空—>消费者消费 之间的信号量,那么对于消费者消费的是产品数量,那么所对应的资源应该是产品的数量(【非空缓冲区的数量】),根据题目初始条件,一开始产品的数量为0,所以这里full设置为0
semaphore empty = n;//empty用来表示 缓冲区没满—>生产者生产 之间的信号量 对于生产者能够生产的条件则是是否还有空间能够去给生产,那么这里对应的应该是【空闲缓冲区数量】,根据题目初始条件初始产品数量为0,所以对应的空闲缓冲区数量为n,此时empty设置为n实际在并发过程中,生产者在生产时可能会对同一个位置进行数据覆盖,对于消费者也有可能取出的产品也是同一个,主要原因就是各自临界区中的步骤不是一气呵成的,此时我们来使用一个互斥信号量来表示之间的互斥关系:
semaphore mutex = 1;//互斥信号量 表示对缓冲区的互斥访问最终结果:
根据两个前后关系,我们来添加 PV 指令:

针对于生产产品、取出产品的过程中我们都使用信号量互斥锁住,最终同步、互斥的结果如下图所示:

思考 1:是否能够改变相邻的 P 或 相邻的 V 操作的顺序?
想法 1:改变相邻的 P(mutex) 与 P(empty) 这两个 P 操作是否有无问题?
结论:造成死锁。两个进程都无法推进下去。

实际例子1:我们将P(mutex)与P(empty)两个P操作进行互换,接着来尝试执行①②③④顺序。
当前条件:缓冲区内放满了产品,empty=0,full=n。
流程:生产者进程执行①使mutex变为0,再执行②,由于empty为0此时表示当前产品已满,此时生产者会进入到阻塞状态;接着切换到消费者进程执行③由于mutex为0,生产者目前并没有释放锁,此时消费者也被阻塞。
结果:生产者等待消费者释放空闲缓冲区,而消费者又等待生产者释放临界区的情况,生产者和消费者循环等待被对方唤醒,出现”死锁”问题。
实际例子2:若是缓冲区中没有产品,full=0,empty=n,按照③④①的顺序执行下去依旧会发生死锁。
强调必做:实现互斥的 P 操作一定要在实现同步的 P 操作之后,若是颠倒,那么就会出现死锁问题。
想法 2:改变相邻的 V(mutex) 与 V(full) 是否有无问题?

结论:没有问题。因为两个 V 操作时不会导致进程阻塞的问题,那么两个 V 操作顺序是可以调换的。
思考 2:对于生产者生产一个产品以及消费者使用一个产品是否可以访问 PV 操作之间呢?
对于生产者生产一个产品以及消费者使用一个产品是否可以访问 PV 操作之间呢?

结论:可以的,逻辑上来看放到 PV 操作之间是没有问题的,但是放入进去之后会影响系统的效率。
- 影响系统效率的原因:若是将这两部分的处理代码都放在 PV 操作之中就会导致临界区代码很长,此时就会导致一个进程对临界区上锁的时间会增长,这样是不利于各个进程交替的来使用这个临界区的资源,会对系统的效能产生影响。
知识回顾与重要考点

4.7.2、多生产者 - 多消费者问题
4.7.2.1、认识多生产者 - 多消费者问题

一种情况描述:若是当前盘子里只有一个苹果时,那么老妈无法再向盘子放橘子,儿子也无法从盘子中取水果,实际若是苹果在盘子上时,生产者进程 2、消费者进程 2 若是执行了则都会进入到阻塞过程:

梳理条件:
盘子只能放一个水果
生产者:老爸、老妈
老爸生产苹果、老妈生产橘子
消费者:儿子、女儿
儿子消费苹果、女儿消费橘子4.7.2.2、分析问题及解决
步骤一:关系分析。找到题目中描述的各个进程,分析它们之间的同步、互斥关系。
与生产者 - 消费者问题不同的是:在这里的多生产者 - 多消费者问题,每个生产者生产的东西以及对应消费者消费的东西是不一样的。
- 多:指的是多类。

- 互斥关系:都是对盘子这个资源进行访问。
- 同步关系:消费者消费都是需要有对应的水果在才可触发,此时为 ①父亲生产—> 女儿取出、②母亲生产—> 男孩取出,而对于女孩、男孩取出之后实际空盘子里可以放苹果或者橘子都可以,此时再设置一个信号量表示盘子为空,此时第三个同步关系出来了③男孩、女孩取出 —> 盘子为空。这里的盘子为空实际指的是可以通知父亲、母亲两个生产者了。
步骤二:整理思路。根据各进程的操作流程确定 P、V 操作的大致顺序。
- 互斥:在临界区前后分别 PV。
- 同步:前 V 后 P。
步骤三:设置信号量。
①父亲生产—> 女儿取出:条件应该为盘子中的数量(苹果数)-1 才能够取出,表示苹果数量,此时根据初始条件应该表示的是 apple=0.
②母亲生产—> 男孩取出:条件应该为盘子中的数量(橘子数)-1 才能够取出,表示橘子数量,此时根据初始条件应该表示的是 orange=0.
③男孩、女孩取出 —> 盘子为空:条件应该是盘子目前可存放的数量 (空数量),表示空位置数量,此时根据初始条件没有水果(此时有一个空位置) 是 plate=1。

最终我们来实际代码添加同步与互斥代码:
首先是同步代码:对应的箭头指的是通过这个原语操作则会唤醒对应的进程

接着是互斥代码:

实际我们在本题中由于缓冲区只为 1,无需来为每一个操作添加互斥 PV 的,具体原因见下面思考解释!!!
思考 1:在本题中可不可以不使用互斥信号量?
分析如下:

结论:可以,即使不设置专门的互斥变量 mutex,也不会出现多个进程同时访问盘子的现象。
- 可以的原因:本题中的缓冲区大小为 1,在任何时刻,apple、orange、plate 三个同步信号量中最多只有一个是 1,因此在任何时刻,最多只有一个进程的 P 操作不会被阻塞,并顺序地进入临界区。
思考 2:若是将缓冲区的容量设置为 2,是否可以不使用互斥信号量?
结论:不可以,此时可能导致两个进程写入缓冲区的数据相互覆盖的情况!:
分析如下:

结论:若是缓冲区太小 > 1,那么就必须设置一个互斥信号量 mutex 来保证互斥访问缓冲区。若是缓冲区大小为 1,那么有可能不需要设置互斥信号量就可以实现互斥访问缓冲区的功能,这并不是绝对的需要具体功能具体分析。
回顾知识与重要考点

解决多生产者 - 多消费者问题的关键:

4.7.3、吸烟者问题(一生产者 - 多生产者)
4.7.3.1、认识吸烟者问题
关于进程同步互斥问题,下面是问题描述:

- 有一个生产者它对应三个消费者,这个生产者每次可以提供两种材料,对于每个吸烟者它各自有一个材料,那么每次生产者提供的一组材料都会被吸烟者抽掉,之后生产者循环放置一组材料到桌上。
解决的问题:可以生产多个产品的单生产者在并发中实现同步、互斥。
梳理条件:
生产者 生产组合1 -> 吸烟者1号
生产者 生产组合2 -> 吸烟者2号
生产者 生产组合3 -> 吸烟者3号
吸烟者1号、吸烟者2号、吸烟者3号任意一者结束 —> 生产者生产下一组合4.7.3.2、分析问题及解决
步骤一:关系分析。找到题目中描述的各个进程,分析它们之间的同步、互斥关系。
实际我们可以将桌子抽象为容量为 1 的缓冲区,这个缓冲区需要互斥访问。
生产者每次生产的两种材料实际是有三种情况,一种情况我们看做一个组合,此时就有三个组合了。对于吸烟者三位各自若是完成了则会发出通知信号让生产者准备下一组烟:

步骤二:整理思路。根据各进程的操作流程确定 P、V 操作的大致顺序。
- 互斥:在临界区前后分别 PV。
- 同步:前 V 后 P。
步骤三:设置信号量。
由于本题缓冲区只能够放一个组合,此时我们就暂时无需考虑互斥的问题了!
主要的初始信号值如下:

对于 offer1、2、3 实际就是吸烟者进行消费,初始默认没有放材料此时设置为 0。
对于 finsh 则是表示是否抽烟已经完成,由于一开始没有放材料,那么默认也没有抽烟完成,此时我们设置 finish 为 0。
- 实际这里设置为 1 也是没问题的,不过若是设置为 1 就需要将 P(finish) 操作放到生产者的顶部,由于更能贴合本题的含义初始值,我们就放在生产最后。
最终我们来实际代码添加同步与互斥代码:
由于我们需要循环去制作不同组合的材料,所以我们这里使用一个局部临时变量 1 来进行不断循环:

接着我们来实现相对应的同步关系:注意这里 P(finish) 放在最底下的原因是初始值为 0

思考:是否需要设置一个专门的互斥信号量?
由于缓冲区为 1,同一时刻,四个同步信号量中之多有一个为 1。
知识回顾与重要考点

4.7.4、读者 - 写者问题(经典复杂互斥问题)
4.7.4.1、认识读者 - 写者问题
问题描述:读者、写者进程都可能有多个。 这两个进程在系统中并发运行,读就是会访问同一个共享文件,写进程就是不断的写这个共享文件,由于读、写这两个操作,对文件的影响是不一样的。
- 可能出现的问题:读、写进程并发同时访问可能会出现数据不一致的问题。

对于导致数据不一致问题,要求:
①允许多个读者可以同时对文件读操作。
②只允许一个写者在文件中写信息。
③任一写者在完成写操作之前不允许其他读者或写者工作。
④写者执行写操作前,应让已有的读者、写者全部退出。
对于这些要求为什么要这么设置这些要求呢?
①原因:两个读进程在读文件的时候并不会更改文件数据信息,并且这里的读与之前的消费者不一样,这里的读并不会取走数据。

②③④原因:当一个写进程正在对文件进行写操作的时候,其他进程是不能访问这个文件的。同时若是一个写进程想要写共享文件的时候,就必须等到其他进程对这个文件的操作结束之后,才能往里面写数据。
疑问 1:为什么写进程执行的时候不能读?
主要原因是若是读进程在读的这块数据刚好是写进程在写的,那么此时读到的数据可能就会是已经被覆盖掉了的,此时读到的就是原先旧的数据。由于写者进程会改变共享文件内容,所以写者进程是不可以与其他进程同时访问的!

疑问 2:若是两个写者进程并发运行会发生的情况?
并行运行的时候可能会出现两个写者进程都向同一块数据写数据的操作,此时就会造成后面发生的写者会将之前写者写的数据覆盖掉。

4.7.4.2、分析问题与解决(四种方案逐步迭代)
尝试使用 PV 操作来解决这个问题。
分析问题步骤:
步骤一:关系分析。找到题目中描述的各个进程,分析它们之间的同步、互斥关系。
两类进程:写进程、读进程。
互斥关系:写进程—写进程、写进程—读进程这两个存在互斥关系。读进程—读进程之间不存在互斥关系。
步骤二:整理思路。根据各进程的操作流程确定 P、V 操作的大致顺序。
- 互斥:在临界区前后分别 PV。
- 同步:前 V 后 P。
步骤三:设置信号量。
接下来我们通过多种方案来慢慢去完善实现读 - 写者所需要实现的特性:读 - 读并发执行、读写互斥、写写互斥。
方案 1:使用一个 rw 信号量,在读者进程、写者进程中分别上锁解锁。但是存在问题,也就是无法进行读者与读者并发访问。

方案 2:在方案 1 的基础上增加一个 count 变量用于表示当前有几个读进程在访问文件
思路:可以看到我们增加了一个 count 判断,第一个读进程进来时 count==0,此时就会加锁接着 count++,此时若是又有一个读进程进来则会跳过 P 操作,执行向下的读操作了。此时若是写进程来则会由于已经上锁无法向下执行进入阻塞状态。
实现效果:可以实现读者—读者进程同时共享文件(但还是有问题)。
- 问题:若是读者 1 进入了 if 判断后并且在执行 count++ 之前切换到了读者 2,此时读者 2 由于 count=0,此时依旧还是能够进入到 if 判断条件中,此时又出现了读 - 读出现阻塞的问题。
- 主要原因:读者进程中检查与 count++ 并不是一气呵成。

方案 3:在方案 2 的基础之上增加了一个 mutex 用于保证对 count 变量的互斥访问
思路:我们将原先 if 判断以及 count++ 中添加互斥的 PW 操作。此时若是第一个读进程进来时进入到 if 判断,此时第二个读进程再次进来时会被阻塞直到第一个读进程 count++ 后第二个读进程才会 mutex 阻塞结束来进行 if 判断,此时就解决了方案 2 中出现的读 - 读进程不能够进行并发访问的问题。
潜在问题:只有有读进程还在读,那么写进程就会一直阻塞等待,可能导致会出现 “饿死” 的问题。在这种算法中,读进程是优先的。
- 试想:由于读进程与写进程是使用 rw 作为互斥资源的,此时若是读进程不断进入,那么 count 会一直 ++,由于我们之前是设定好的只有当最后一个读进程读完结束之后,才能够去解锁,此时如果说一直有源源不断的读进程进入,那么我们写进程会一直处在阻塞的状态,就可能会出现 “饿死” 的情况。

方案 4(最终方案):此时可以再设置一个互斥信号量 w,来用于实现写优先。

实现效果:解决了之前提到的写者可能出现饥饿的问题。
- 若是没有 P(w)、V(w) 方案也就是方案 4 这里新加的,此时读者 2 会非常顺序的再次执行到下方读文件,此时若是源源不断来读,就会导致写进程饥饿的问题。
- 本质:借助了新的 P(w) 可以将之后来的读进程阻塞住,只要写者在读之前阻塞,那么之后会先执行阻塞队列中的第一个也就是这个写进程了完成指定的同步操作。
对于方案 4 中的五种情况示例走一遍:
①读者 1—> 读者 2:可以实现两个读者同时访问。
②写者 1—> 写者 2:可以实现两个写者互斥访问。
③写者 1—> 读者 1:可以实现写者写,读者阻塞,可以实现互斥访问。
④读者 1—> 写者 1—> 读者 2:
- 过程步骤:①当读者 1 首先进入到读文件的时候,此时写者 1 就会进入到 P(rw) 的阻塞当中,此时若是读者 2 也执行,那么会在 P(w) 中阻塞。②只有当读者 1 读完之后,由于读者 1 是第一个读的此时会执行 V(rw) 此时写者 1 阻塞就会停止开始进行写操作,此时读者 2 依旧是在阻塞当中。③只有到写者 1 写完之后执行 V(w) 此时读者 2 才能够停止阻塞,继续往下执行。
⑤写者 1—> 读者 1—> 写者 2:
- 过程步骤:①写者 1 首先进入到写文件的时候,此时切换到读者 1 时,会阻塞在 P(w) 中(还会将当前的读者 1 入等待队列);此时又切换到了写者 2,此时同样会阻塞在 P(w) 中(同样也会进入等待队列,注意此时是在队头)。②当写者 1 写完并且执行了 V(rw)、V(w) 之后,此时就会唤醒阻塞队列中的队头进程,也就是读者 1(因为它是首先入队的),此时写者 2 依旧是在阻塞中。③读者 1 读完之后,执行 V(w) 后,写者 1 还是会阻塞在 P(rw) 中,此时只有当读者 1 写完之后,写者 1 才会去执行写文件操作。
- 效果:可以看到写者 2 依旧是排在读者 1 后面进行执行的,写者并不会饥饿但是在这里的实现也并不是真正的 “写” 优先,而是相对公平的先来先服务原则。【部分书上将该算法称为 “读写公平法”】
总结:经过多种情况测试,对于我们想要实现的读 - 读并发执行、读写互斥、写写互斥都能够通过方案 4 实现,并且其是 “读写公平法”,对于在写 - 读 - 写时,由于读先进入阻塞,所以在读 - 写时会先进行读操作。
知识回顾与重要考点
读者写者问题为我们解决复杂的互斥问题提供了一个参考思路。其核心思想在于设置了一个计数器cout用来记录当前正在访问共享文件的读进程数。我们可以用cout的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。另外,对cout变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。
最后,还要认真体会我们是如何解决“写进程饥饿”问题的。
绝大多数的考研V操作大题都可以用之前介绍的几种生产者消费者问题的思想来解决,如果遇到更复杂的问题,可以想想能否用读者写者问题的这几个思想来解决。

4.7.5、哲学家进餐问题(每次需要持有两个临界资源)
4.7.5.1、认识哲学家进餐问题
问题描述:

4.7.5.2、分析问题与解决(解决死锁问题)
问题分析:
1、关系分析:系统中有 5 为哲学家进程,5 位哲学家与左右邻居对其中间筷子的访问是互斥关系。
2、整理思路:这个问题中只有互斥关系,与之前互斥问题不太一样的是,每一位哲学家需要拿起两根筷子,也就是两个临界资源才能够正常吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学将问题的精髓。
- 对于之前例子每一个进程一般来说只需要一个临界资源即可,这也是哲学家问题与之前的问题不同的地方。
3、信号量设置:定义互斥信号量数组 chopstick[5]={1,1,1,1,1} 用于实现对 5 个筷子的互斥访问,并对哲学家按 0-4 编号,哲学家 i 左边的筷子的编号为 i,右边的筷子编号为 (i+1)%5。
方案:在吃饭之前与之后各自使用对应的筷子资源
出现问题:每位哲学家都会循环等待右边的人放下筷子(阻塞),就会发生死锁问题。

如何防止死锁的发生呢?
策略 1:可以对哲学家进程施加一些限制条件,例如最多允许四个哲学家同时进餐,这样可以保证至少有一个哲学家是可以拿到左右两只筷子的。
- 实现思路:设置一个初始值为 4 的同步信号量。

策略 2:要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其他一个可以拿起第一只筷子,另一个会直接阻塞,这就避免了占有一支后再等待另一只的情况。
- 实现思路:每一个哲学家在拿筷子之前判断一下是奇数还是偶数,接着根据自己序号来做相应的处理。
- 说明:并确保只有一个相邻的奇偶号哲学家可以拿起第一只筷子,另一个会被阻塞。这种阻塞和等待的方式避免了同时占用两只筷子而导致的死锁情况。

策略 3:仅仅当一个哲学家左右两只筷子都可用时,才允许它抓起筷子。
这里信号量设置为 1,当一个哲学家 A 拿起筷子的时候,其他哲学家不能够拿任何筷子,哲学家 A 全部拿完之后此时其他哲学家才可以开始。

注意:使用这种方法并不能保证只有两边的筷子都可用时,才允许哲学家拿起筷子。
- 更准确说法:各哲学家拿筷子这件事必须互斥的执行,这就保证了即使一个哲学家在拿筷子拿到一般时被阻塞,也不会有别的哲学家会继续尝试拿筷子,这样当正在吃饭的哲学家放下筷子后,被阻塞的哲学家也可以获得等待的筷子了。
知识回顾与重要考点

五、死锁
5.1、死锁的概念

5.1.1、什么是死锁?
例子 1:哲学家并发时每个人只拿一根筷子就会出现一个人都拿不到两根筷子的情况
哲学家进餐例子中并发会出现死锁问题,若是每个哲学家都拿一根筷子,那么就会导致进程无法继续向下推进,都会在不断的循环等待另一根筷子资源而阻塞,此时就会出现 “死锁”。

例 2:每个人都占有着另一个人的心,而真正喜欢那个人的心不在自己这里

在并发情况下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象,就是 “死锁”。发生死锁后若无外力干涉,这些进程都无法向前推进。
5.1.2、死锁、饥饿、死循环的区别
死锁:各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。
饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象,比如:在短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程一直得不到处理机,此时会发生长进程 “饥饿”。
死循环:某进程执行过程中一直跳不出某个循环的现象,有时是因为程序逻辑 bug 导致的,有时候是程序员故意设计的。

公共点:都是系统发生了异常,无法向下推进的这种现象。
不同点:
- 死锁:至少两个或两个以上,都处于阻塞态。
- 饥饿:可以是阻塞态,也有可能是就绪态。
- 死循环:是代码逻辑导致的,死循环是管理者的问题。
5.1.3、死锁产生的必要条件
必须满足下面四个条件,若是一个不成立,死锁就不会发生:
1、互斥条件:必须需要互斥使用的资源争抢才会导致死锁,若是可以允许多个进程使用的资源如内存、扬声器就不会导致死锁。
2、不可剥夺条件:进程所使用资源必须是该进程主动释放,而不是其他进程强行夺走,若是能够被抢走,那么就不会发生死锁。
-
请求和保持条件:以哲学家例子每一个哲学家在保持一个资源不放的同时,还在请求另一个新的资源,而新的资源则又是其他进程所持有的,当满足了请求和保持的条件会发生死锁。每个哲学家都在等待右手边的哲学家放下资源,此时形成了一种资源的循环等待链。
-
注意:发生死锁的时候,一定会有循环等待资源的想想,若是发生了循环等待资源现象,未必一定发生死锁。
3、请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,而又对自己有的资源保持不放。
4、循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
小结论:若是同类资源数 > 1,即使有循环等待,那么也未必发生死锁,但如果系统中有没类资源都只有一个,那循环等待就是死锁的充分必要条件了。发生循环等待时未必死锁(循环等待是死锁的必要不充分条件)
示例:即使发生了循环等待资源的现象,但若是还有别的进程,持有一个同类型可以替代的资源,那么死锁是不一定发生的,如下若是原本五个哲学家发生了循环等待资源的现象,此时有第六个哲学家拥有一个筷子资源,这个筷子资源是可以让三号哲学家右手拿起来使用的,此时三号在等待四号哲学将放下筷子同时也可以等待第六个哲学家放下这只筷子。只要第六个哲学家放下筷子,此时三号就可以拿起来吃饭了。:

5.1.4、什么时候发生死锁?
1、对系统资源的竞争:各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源 (CPU) 的竞争是不会发生死锁的。
2、进程推进顺序非法:请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程 P1、P2 分别申请占有资源 R1、R2,之后进程 P1 又紧接着申请资源 R2,而进程 P2 又申请资源 R1,两者会因为申请的资源被对方占有而阻塞,从而发生死锁。
- 若是改变推进顺序,能够让其中的进程一气呵成不会发生死锁。
3、信号量的使用不当也会造成死锁,如生产者 - 消费者问题,若是实现互斥的 P 操作在实现同步的 P 操作之前,就可能导致死锁。(可以将互斥信号量、同步信号量看做是一种抽象的系统资源)
总之:对于不可剥夺资源的不合理分配,就会导致死锁。
5.1.5、死锁的处理策略
1、预防死锁:破坏死锁产生的四个条件中的一个或几个。
2、避免死锁:用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)。
3、死锁的检测与解除:允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种策略解除死锁。
知识回顾与重要考点

5.2、死锁的三种处理策略

5.2.1、静态策略:预防死锁(不允许死锁发生)

对于静态策略:预防死锁我们只需要破坏其中的一个条件即可。
①破坏互斥条件
互斥条件:只有对必须互斥使用的资源的正确才会导致死锁。
那么我们是否可以去破坏互斥条件从而不会导致死锁呢?
- 破坏互斥条件效果:将互斥使用的资源改造为可以同时使用的资源,此时就可以不会导致死锁。
举例:若是进程直接向同一个打印机发出打印命令,那么一定会出现进程 1 首先访问临界区使用打印机,接着进程 2 也想要使用此时会被阻塞住,只有等待进程 1 释放资源后,才可以使用。

此时我们实际可以去破坏它们之间的互斥性,此时引入 SPOOLing 技术,操作系统可以采用 SPOOLing 技术将独占设备在罗 i 就上改成共享设备,这里将打印机改造为共享设备,通过实现了一个输出进程,每一个进程来访问打印机时直接将任务放置到输出进程即可。

效果:使用了 SPOOLing 技术后,在各进程看来,自己对打印机资源的使用请 i 去立即就会被接收处理了,不需要再阻塞等待。
缺点:并不是所有的资源都可以改造成共享使用的资源,并且为了系统安全,很多地方必须保护这样的互斥性。因此,很多时候都无法破坏互斥条件。
②破坏不剥夺条件
不剥夺条件:进程所获得的资源再未使用完之前,不能够由其他进程强行夺走,只能主动释放。
破坏不剥夺条件方案:
- 方案 1:当某个进程请求的资源得不到满足时,必须立即释放保持的所有资源,待以后需要时再重新申请,也就是说,即使某些资源尚未使用完,也需要主动释放,此时就破坏了不可剥夺条件。
- 方案 2:当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺,这种方式则需要考虑各进程的优先级。(例如:剥夺调度方式,直接将处理机资源强行剥夺给优先级更高的进程使用)
策略缺点:
1、实现起来比较复杂。
2、释放已经获得的资源可能会造成前一阶段工作的失效,这种方法只适用于易保存和恢复状态的资源,如 CPU。
- 例如 CPU:CPU 资源被剥夺的时候,寄存器之类的一些中间数据就需要被保存
3、反复地申请和释放资源只会增加系统开销,降低系统吞吐量。
4、若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源都需要放弃,以后再重新申请,如果一直发生这样的情况,就会导致进程饥饿。
③破坏请求和保持条件
请求和保持条件:进程已保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。
可以采用静态分配法策略:进程再运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入使用,一旦运行后,这些资源就一直归它所有,该进程不会来请求别的任何资源。
该策略实现起来简单,有明显的缺点:
有些资源可能只需要很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,都会造成严重的资源浪费,资源利用率极低,另外,该策略也有可能导致某些进程饥饿。
举例如下:A 类进程需要资源 1,B 类进程需要资源 2 ,C 类进程则需要两个资源。此时若是资源 1 被释放了,接着 A 类进程又来请求资源,此时可能又会分配给 A 进程,之后可能操作 C 类进程阻塞,该策略也有可能导致某些进程饥饿

④破坏循环等待条件
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程都已获得的资源同时被下一个进程所请求。
可采用顺序资源分配法:首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。
原理分析:一个进程只有占有小编号的资源时才能够又资格申请更大编号的资源,按此规则,已持有大编号资源的进程不可能逆向地回来申请小编号地资源,从而就不会产生循环等待地现象。
- 通过这种规则,此时就不可能发生循环等待。
举例子:我们来为所有的输出设备进行编号,下面 P1 进程所占用的是 1 号、3 号设备;P2 进程是 2 号、4 号设备;P3 进程则是 5 号、7 号设备。

对于 P3 进程,实际可以畅通无阻的不断执行,因为之后所使用到的资源肯定都是 > 7 号的,那么是可以顺利的执行完的。
该策略的缺点:
1、不方便增加新的设备:因为可能需要重新分配所有的编号。若是有新的设备添加,此时可能需要修改所有的设备编号。
2、进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费。
- 例如上图中 P3 进程,若是它需要先使用 7 号设备扫描怡再使用 5 号设备打印机,那么依照这种策略则需要先将 5 号设备占用了,接着再占用 7 号设备使用,此时 5 号设备会被空闲占住,此时就造成 5 号打印机资源浪费。
3、必须按照规定次序申请资源,用户编程麻烦。
- 举例:例如在一个系统中,打印机为 5 号设备,扫描仪为 7 号设备,若是用户编程想要先使用扫描仪再使用打印机,此时就需要先去申请小的设备也就是 5 号设备,接着再申请 7 号设备;若是再另一个系统总,打印机的编号是 4,扫描仪是 8,那么就需要根据设备号重新编写一套去访问。
知识回顾与重要考点

5.2.2、动态策略:避免死锁(银行家算法,不允许死锁发生)

5.2.2.1、什么是安全序列?
实际银行案例如下:

首先会给你每个公司最大想要借的数,接着给出每个公司已借的钱,还有就是银行家手上的总金额钱。
- 最多还会借的钱数,我们时根据每个公司最大想借的钱 - 已借走的钱。
根据给出的初始条件,目前银行家手里还有 40 亿,能够分配一个合适的借钱序列最终借给所有的公司呢?
情况 1:B 公司此时先问你借 30 亿,是否敢借?

结论:不敢借,因为一旦借出去后 B 公司还最多会借 20 亿,其他分别是 30、20,那么剩余的 10 亿无法满足任何一家公司要借的钱款,此时就会进入不安全状态。
情况 2:A 公司问题借 20 亿,是否敢借。
结论:可以借。借钱可以有两种方案:
- 方案 1:按照 T→B→A 顺序借钱就可以。可以将手上的 20 亿先借给 T 公司此时手上还有 20 亿,此时 T 公司全部还回来为 50 亿,接着可以借给 B 公司 50 亿,此时 B 公司再把钱全部还回来就有 70 亿,最后我们可以直接再借给 B 公司。
- 方案 2:按照 A→T→B 顺序也可以。先借给 A 公司 20 亿,接着再借给 A 公司剩余的 10 亿,此时 A 公司全部还回来,此时手里有 50 亿,接着再借给 T 公司 20 亿,接着 T 公司还款此时为 80 亿,接着最后再借 B 公司 50 亿。
5.2.2.2、安全序列、不安全状态、死锁的联系
安全序列:如果系统按照这种序列分配资源,则每个进程都能够顺序完成,只要能找出一个安全序列,系统就是安全状态,安全序列可能有多个。
- 若是能够找到一个安全序列的,那么就称整个系统是处于安全状态的,那么就一定不会发生死锁。
- 若是分配了资源之后,系统找不出任何一个安全序列,系统就会进入不安全状态(可能会发生死锁)。处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态。
- 这就意味着之后可能所有进程都无法顺利的执行下去,当然,若是有进程提前归还了一些资源,那么系统也有可能重新回到安全状态,不过我们在分配资源之前就要考虑到最坏的情况。
我们是否能够提前判断系统是否是安全呢?
- 可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求,这也是 **“银行家算法” 的核心思想 **。
5.2.2.3、认识银行家算法
银行家算法:是荷兰学者 Dijkstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁。
核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态,如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
案例 1 - 系统包含安全序列
此时来举出一个实际在计算机系统中的例子:

题目自带给出每个进程的最大需求分配、已分配数量以及每个进程对每个资源的初始数量。
有了这三个条件之后我们可以通过银行家算法来实现预测系统是否是安全状态。
- 对于其中的剩余分配是通过初始数量 减去 已分配数量。
此时思考一个问题:BAT 例子中,只有一种类型的资源 “钱”,在计算机系统中会有多种多样的资源,应该怎么把算法扩展为多种资源的情况呢?
思路:可以将单维的数字扩展为多维的向量。比如:系统中有 5 个进程 P0~P4,3 种资源 R0-R2(使用一个数字来表示每个进程对于所有任意一个资源的需求数量),初始数量为 (10, 5, 7),此时某一时刻的情况可表示如下:

通过安全性算法来进行检验是否安全,循环一步步算,下面的例子是能够找到一组安全序列的:
首先依次来检查每个进程最多还需要的资源是否是 ⇐ 我们的剩余资源,此时首先我们找到了 P1 进程【(1,2,2) < (3,3,2)】,此时我们可以优先将资源分配给 P1,P1 一定是可以顺序执行结束的。

此时 P1 结束后,原本的资源数会增加 (3,3,2) + (2,0,0) = (5,3,2),此时继续向下匹配是否有能够找到 < 剩余资源的也就是 P3,此时借给他之后再次返回, (5,3,2)+(2,1,1) = (7,4,3)

实际我们再拿 (7,4,3) 对比一下剩余的所有进程,可以发现全部都能够满足,那么将所有的进程添加到安全序列中。
此时安全序列可以为(P1、P3、P0、P2、P4)。
案例 2 - 系统不包含安全序列
接着来举例一个找不到安全序列例子:

通过使用 (3,3,2) 来去匹配每个进程最多需要的,可以找到有 P1,此时借给 P1 之后,P1 归还金额,银行家剩余(3,3,2)+(2,0,0) = (5,3,2)。
接着又可以匹配到 P3,此时借给 P3 后,P3 再次归还此时银行家手里则是 (5,3,2)+(2,1,1) = (7,4,3)

进入到这个阶段的时候,我们拿 (7,4,3) 找不到任何一个进程最多还需要的资源是<的,此时我们找不到一组安全的序列,此时说明系统处于不安全状态,可能会发生死锁问题。
注意:若是系统进入了不安全状态,那么就会将数据回退到之前那一步,不答应这个进程的请求,让进程暂时阻塞等待,这就是银行家算法的流程。
5.2.2.4、银行家算法代码实现思路
前提条件:若是系统中有 n 个进程,m 种资源,每个进程再运行前都需要提供三个条件:
- 最大需求矩阵 Max:
的矩阵(可用二维数组实现),表示所有进程对各种资源的最大需求数。Max[i, j] = K 表示进程 Pi 最多需要 K 个资源。 - 分配矩阵 Allocation:
的矩阵表示对所有进程的资源分配情况。 - 需要矩阵 Need:Max-Allocation 来计算得到的还需要多少各类资源。
- 可用矩阵 Available:长度为 m 的一维数组表示当前系统中还有多少可用资源。

目标:某进程 Pi 向系统申请资源,可用一个长度 m 的一维数组 Requesti 来表示本次申请的各种资源量。
实现流程:
可以使用银行家算法预判本次分配是否会导致系统进入不安全状态:
- 如果 Requesti[j] ⇐ Need[i, j] (0⇐j⇐m)便转向 2,否则认为出错(所需要的资源所有都超过所宣布的目前进程所需要的资源)【check 进入条件】
- 如果 Requesti[j] ⇐ Available[i, j] (0⇐j⇐m)便转向 3,否则表示尚无足够资源,Pi 必须等待。
- 系统尝试把资源分配给进程 Pi,并修改相应的数据(并非真的分配,修改数值只是为了预判):Available = Available - Request;Allocation[i, j] = Allocation[i, j] +Requesti[j] ;Need[i, j] = Need[i, j] - Requesti[j] 。
- 操作系统执行安全性算法,检查此次资源分配后,系统是否处于安全状态。若安全,才正式分配;否则恢复相应数据,让进程阻塞等待。
知识回顾与重要考点

若是系统进入了不安全状态,那么就会将数据回退到之前那一步,不答应这个进程的请求,让进程暂时阻塞等待,这就是银行家算法的流程。
系统处于不安全状态未必死锁,但死锁时一定处于不安全状态,系统处于安全状态一定不会死锁。
5.2.3、死锁的检测与解除(允许死锁发生)

若是系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法。
①死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。
②死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。
5.2.3.1、死锁的检测
认识死锁的检测思路以及名词(节点、边)
实现死锁的检测思路:实现一种数据结构配合算法来检测系统是否已经进入死锁状态
①用某种数据结构来保存资源的请求和分配信息。
②提供一种算法,利用上述信息来检测系统是否已进入死锁状态。

节点介绍:
- 第一种节点对应一个进程;
- 第二种节点对应资源,一般使用矩阵来表示一类资源,矩形当中小圆表示资源有几个。在图中 R1 资源有三个,R2 资源有两个。
边介绍:
- 第一种使进程节点指向资源节点的边,这种边又称为请求边。表示的含义就是某个进程需要向资源节点获取资源。
- 第二种使资源节点指向进程节点的边,这种边称为分配边。表示的含义是这种资源已经给该进程分配了几个资源。
下面是一个节点、资源图的示例:

- R1 分配了两个资源给 P1,分配了一个资源给 P2。
- R2 分配一个资源给 P2。
- P1 进程向 R2 请求一个资源。
- P2 进程向 R1 进程请求一个资源。
检测存在、不存在死锁的案例
案例 1:不存在死锁情况
首先我们来对这个数据结构图开始检测是否存在死锁问题:
首先 P1 进程向 R2 资源请求一个资源,R2 资源给 P2 进程提供了一个资源,此时 R2 还有一个资源暂时没有分配,那么对应 P1 进程的请求是可以满足的,那么此时 P1 进程发起请求以及 R1 资源分配的两条边此时可以消除:

此时 P2 进程向 R1 请求一个资源,而 R1 种目前还有两个资源是空闲中的(其中一个是给了 P2 进程),那么对于 P2 的请求 R1 是能够满足它的,并且 R2 资源将一个资源分配给 P2 同样也是满足的:

按照上面的过程分析,我们可以看到最终可以消除所有的边,那么表示这个图是可以完全简化的,此时一定不会发生死锁(可以找到一个安全序列)。
案例 2:检测出系统出现死锁

首先我们来看 P2 进程,P2 进程向 R1 资源请求了一个资源,此时 R1 已经分配出去了三个资源(所有),那么此时 P2 进程就会进入到阻塞状态。
接着看 P1 进程,P1 进程向 R2 资源请求了两个资源,此时 R2 已经将两个资源分配给 P3 和 P2 了,那么此时 P1 也进入了阻塞状态,那么我们就可以对 P3 进程进行下手了。
由于 P3 只占用了 R2 资源,并没有向其他资源发起请求,此时它的边即可消除:

此时我们看看是否还能够消除边,看 P1 进程,此时 R2 资源被解除了一个,那么目前有 1 个资源是空闲的,而又 P1 进程是向 R2 资源发送两个请求的,此时 P1 依旧是在阻塞中,对于 P2 进程也是在阻塞中。
此时无法再消除其他的边,那么就发生了死锁,最终还连着边的就是处于死锁状态的进程。
死锁定理和死锁检测小结
死锁定理:

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

5.2.3.2、死锁的解除
能够检测出来死锁,那么就可以去想办法解除死锁。
补充:并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程。
解除死锁的主要方法有:
1、资源剥夺法。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程,但是应防止被挂起的进程长时间得不到资源而饥饿。
示例:我们将下面处于死锁状态的系统,剥夺 P1 进程(或者其他进程)的资源,将释放的资源给其他进程使用。

2、撤销进程法(或称终止进程法):强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,还需要从头再来。
- 简洁点:完成撤销某个进程的运行来释放资源,对于资源剥夺法仅仅只是进行挂起暂时放到外存中。
3、进程回退法:让一个或多个死锁进程回退到足以避免死锁的地步,要求系统要记录进程的历史信息,设置还原点。
举例:暂时将 P2 的一个占用资源的代码步骤回退掉。此时 P1 进程就能够执行完毕,等待 P1 进程释放资源后再回来执行 P2 进程。

如何决定 “对谁动手” 的指标:
1、进程优先级。
2、已执行多长时间。
3、还要多久能完成。
4、进程已经使用了多少资源。
5、进程是交互式的还是批处理式的。
知识回顾与重要考点
