第2章 进程与线程
1、进程
1)概念
- 进程是程序的一次执行过程,是动态的;程序是静态的,是存储在磁盘上的可执行文件;
- 同一个程序多次执行会对应多个进程。
2)组成
- PCB(Process Control Block 进程控制块):是进程存在的唯一标志,包含进程描述信息、控制管理信息、资源分配清单和处理机信息;
- 程序段:程序的代码指令序列;
- 数据段:程序运行过程中产生的数据。

3)特征
- 动态性:最基本的特征,是程序的一次执行过程;
- 并发性:多个进程实体可并发执行;
- 独立性:是独立运行、获得资源、接受调度的基本单位;
- 异步性:各进程以独立的、不可预知的速度向前推进;
- 结构性:每个进程都配置一个PCB。
2、进程的状态和转换
1)五种基本状态
- 运行态(Running):正在CPU上运行;
- 就绪态(Ready):已具备运行条件,等待CPU分配;
- 阻塞态(Waiting/Blocked/等待态):等待某一事件发生(如I/O完成);
- 创建态(New/新建态):正在被创建,分配资源、初始化PCB;
- 终止态(Terminated/结束态):正在被撤销,回收资源、撤销PCB。
进程的整个生命周期中,大部分时间都处于三种基本状态:运行态、就绪态、阻塞态。
进程PCB中,会有一个变量 state 来表示进程的当前状态。如:1表示创建态、2表示就绪态、3表示运行态…
为了对同一个状态下的各个进程进行统一的管理,操作系统会将各个进程的PCB组织起来。
2)状态转换
- 就绪态 → 运行态:进程被调度;
- 运行态 → 就绪态:时间片用完或更高优先级进程到达;
- 运行态 → 阻塞态:等待事件发生(主动行为);
- 阻塞态 → 就绪态:等待的事件发生(被动行为);
- 创建态 → 就绪态:系统完成创建;
- 运行态 → 终止态:进程运行结束或发生错误。

图中的处理机包括CPU等其他部件,CPU是处理机的运算核心;其他则是其他所需资源。
3)进程的组织方式
-
链接方式:按状态组织成队列(就绪队列、阻塞队列等);
-
索引方式:按状态建立索引表。
3、进程控制
1)程序的运行过程

CPU中会设置很多寄存器用来存放程序运行过程中所需的某些数据。
2)进程控制原语
- 创建原语:申请空白PCB、分配资源、初始化PCB、将PCB插入就绪队列(创建态→就绪态);
- 撤销原语:从PCB集合找到终止进程的PCB、回收资源、终止进程包括子进程、删除PCB(就绪态/阻塞态/运行态→终止态→无);
- 阻塞原语:找到相应PCB、保存运行环境、修改PCB状态、插入阻塞队列(运行态→阻塞态);
- 唤醒原语:找到相应PCB、从阻塞队列移出、修改状态、插入就绪队列(阻塞态→就绪态);
- 切换原语:保存当前进程环境存入PCB(进程上下文Context)、PCB移入相应队列、选择另外一个进程执行,并更新其PCB、根据PCB恢复新进程所需的运行环境(运行态→阻塞态/就绪态;就绪态→运行态)。
阻塞原语、唤醒原语必须成对使用。
无论哪个进程控制原语,要做的无非三类事情:
1.更新PCB中的信息(修改进程状态(state)保存/恢复运行环境);2.将PCB插入合适的队列;3.分配/回收资源。
①引发进程创建的事件
- 用户登录:分时系统中,用户登录成功,系统会建立为其建立一个新的进程;
- 作业调度:多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程;
- 提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求;
- 应用请求:由用户进程主动请求创建一个子进程。
②引起进程终止的事件
- 正常结束:进程自己请求终止(exit系统调用);
- 异常结束:整数除以0、非法使用特权指令,然后被操作系统强行杀掉;
- 外界干预:Ctrl+Alt+delete,用户选择杀掉进程。
③引起进程阻塞的事件
- 需要等待系统分配某种资源;
- 需要等待相互合作的其他进程完成工作。
④引起进程唤醒的事件
- 等待的事件发生。
⑤引起进程切换的事件
- 当前进程时间片到(时钟)
- 有更高优先级的进程到过
- 当前进程主动阻塞
- 当前进程终止
3)原语的原子性
- 通过关中断/开中断指令(特权指令)实现,保证操作一气呵成,期间不允许被中断;
- 原语是一种特殊的程序。

4、进程通信(IPC)
进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立;
为了保证安全,一个进程不能直接访问另一个进程的地址空间;
所以进程之间的通信有以下三种方式:共享存储、消息传递、管道通信。

1)共享存储
-
为避免出错,各个进程对共享空间的访问应该是互斥的;
-
各个进程可使用操作系统内核提供的同步互斥工具(如P、V操作);
-
基于数据结构的共享:限制多,低级通信;
-
基于存储区的共享:灵活,高级通信,需互斥访问。
注:通过“增加页表项/段表项”即可将同一片共享内存区映射到各个进程的地址空间中
2)消息传递
-
进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换;
-
直接通信方式:点名道姓发送消息(PID);
-
间接通信方式:通过“信箱”传递消息。
3)管道通信
“管道”是一个特殊的共享文件,又名pipe文件。其实就是在内存中开辟一个大小固定的内存缓冲区。
-
半双工通信:需互斥访问,缓冲区有限,读空写满会阻塞(本质上是队列)。
-
全双工通信:双向同时通信
-
当管道写满时,写进程将阻塞,直到读进程将管道中的数据取走,即可唤醒写进程;
-
当管道读空时,读进程将阻塞,直到写进程往管道中写入数据,即可唤醒读进程。
-
管道中的数据一旦被读出,就彻底消失。因此,当多个进程读同一个管道时,可能会错乱。对此,通常有两种解决方案:
- 一个管道允许多个写进程,一个读进程(高教社官方);
- 允许有多个写进程,多个读进程,但系统会让各个读进程轮流从管道中读数据(Linux的方案)。
写进程往管道写数据,即便管道没被写满,只要管道没空,读进程就可以从管道读数据;
读进程从管道读数据,即便管道没被读空,只要管道没满,写进程就可以往管道写数据。
5、信号
注意区别:
信号量(Semaphore):实现进程间的同步、互斥;
信号(Signal):实现进程间通信(IPC,Inter Process Communication)。
1)信号的作用
-
信号:用于**通知进程某个特定事件已经发生**。进程收到一个信号后,对该信号进行处理。
-
Linux操作系统定义的30种信号类型
- 不同的操作系统对信号类型的定义不一样;
- 通常用宏定义常量表示信号名,如:#define SlGINT 2。
2)信号的发送和保存
- 进程P1中会有两种类型的信号,待处理信号和被阻塞信号,通常用两个N bit 位向量表示;
- 当信号为1时,则表示阻塞/屏蔽,例如被阻塞信号中的7、8两个位置;
- 假设其他进程要给进程P1发送信号,以kill函数为例,第一个参数时发送给谁,第二个参数是发送什么信号?
- 进程P2、P3、OS内核进程都可以给进程P1发送该信号,即多个进程可以给同一进程发送信号;
- 假如有两个以上进程发送同样的信号则会丢弃“后来的”信号,例如OS内核进程发送的信号(假设该信号晚于进程P2信号);
- 其他信号相似以上过程。

注:进程之间允许发送的信号类型是有限制的。
3)信号的处理
①什么时候处理
当进程从内核态转为用户态时(如:系统调用返回、或中断处理返回时),例行检查是否有待处理信号,如果有,就处理信号。
- 如何检查是否有待处理信号?
- 通过待处理信号和被阻塞信号的按位取反的按位与运算进行检测,如下图所示;
- 假设需要处理1号位的信号,则进行以下操作:
- 进程执行指令过程中,进程收到信号,进程转为内核态;
- 在转回内核态的过程中,进行检测;
- 检测到待处理信号,则运行信号处理程序;
- 返回到下一条指令。

②怎么处理信号
- 执行操作系统为此类信号设置的缺省(默认)信号处理程序(某些信号默认忽略,不作处理)
- 执行进程为此类信号设置用户自定义信号处理程序(自定义信号处理程序将覆盖1.)
- 信号处理程序运行结束后,通常会返回进程的下一条指令继续执行(除非信号处理程序将进程阻塞或终止);
- 一旦处理了某个信号,就将pending位重置为0;
- 重复收到的同类信号,将被简单地丢弃(因为仅有 1 bit 记录一类待处理信号);
- 当同时收到多个不同类信号时,通常先处理序号更小的信号。
有些信号既不能被用户自定义处理函数,也不能被阻塞;如:Linux的SIGKILL、SIGSTOP 信号。
4)信号与异常的关系
- “信号”可以作为“异常”的配套机制,让进程对操作系统的异常处理进行补充
- 在进程运行过程中,某些特殊事件可能引发“异常”,操作系统内核负责捕获并处理异常
- 有些异常可以由内核完成全部处理(如:缺页异常),此时就不必再使用信号机制;
- 有些异常无法由内核完成全部处理,可能还需要用户进程配合,此时就可以用“信号机制”与“异常机制”相互配合(如:在Linux中,发生除以0异常时,内核的异常处理程序会向用户进程发送SIGFPE信号。SIGFPE信号的默认处理程序会将进程终止并转储内存;当然进程可以自定义SIGFPE信号处理程序)
6、线程
-
CPU调度进程和线程的区别
-
进程
-
线程
-
1)线程的定义
- 线程是CPU执行的基本单位,是轻量级进程。
- 引入线程后,进程是资源分配的单位,线程是调度的单位。
2)引入线程机制的变化
- 资源分配、调度
- 传统进程机制中,进程是资源分配、调度的基本单位;
- 引入线程后,进程是资源分配的基本单位,线程是调度的基本单位。
- 并发性
- 传统进程机制中,只能进程间并发;
- 引入线程后,各线程间也能并发,提升了并发度。
- 系统开销
- 传统的进程间并发,!需要切换进程的运行环境,系统开销很大;
- 线程间并发,如果是同一进程内的线程切换,则不需要切换进程环境,系统开销小;
- 引入线程后,并发所带来的系统开销减小。
3)线程的属性
- 线程几乎不拥有系统资源,共享进程资源;
- 多CPU计算机中,各个线程可占用不同的CPU;
- 每个线程都有一个线程ID、线程控制块(TCB);
- 线程也有就绪、阻塞、运行三种状态;
- 由于共享内存地址空间,同一进程中的线程间通信甚至无需系统干预;
- 线程切换开销小,并发性高。
4)线程的实现方式
-
用户级线程(ULT):由线程库实现,操作系统无感知;
- 优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高;
- 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。
-
内核级线程(KLT):由操作系统支持,是调度的基本单位;
- 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行;
- 缺点:一个用户进程会占用多个内核级线程线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
-
组合方式:上述两种方法的结合。
5)多线程模型
-
一对一模型:一个用户线程映射一个内核线程。(内核级线程)
- 优缺点同内核级线程。
-
多对一模型:多个用户线程映射一个内核线程
- 优缺点同用户级线程。
- 操作系统只“看得见”内核级线程,因此只有内核级线程才是处理机分配的单位。
-
多对多模型:n个用户线程映射m个内核线程(n ≥ m)
- 既避免了多对一模型的并发度低问题,又避免了一对一模型的开销大问题;
- 用户级线程是“代码逻辑”的载体;
- 内核级线程是“运行机会”的载体。
6)线程的状态与转换
①状态与转换
同进程的状态与转换

②组织与控制

7、处理机调度
基本概念:当有一堆任务要处理,但由于资源有限,这些事情没法同时处理。这就需要确定某种规则来决定处理这些任务的顺序,这就是“调度”研究的问题。
简单理解:好几个程序需要启动,到底先启动哪个。
1)三个层次
①高级调度(作业调度)
按一定的原则从外存的作业后备队列中挑选一个作业调入内存,并创建进程。每个作业只调入一次,调出一次。作业调入时会建立PCB,调出时才撤销PCB,发生频率低,(外存 → 内存:面向作业)。

作业:一个具体的任务;
用户向系统提交一个作业 ≈ 用户让操作系统 启动一个程序(来处理一个具体的任务)。
②中级调度(内存调度)
中级调度(内存调度)——按照某种策略决定将哪个处于挂起状态的进程重新调入内存;(外存 → 内存:面向进程)
一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高。

内存不够时,可将某些进程的数据调出外存。等内存空闲或者进程需要运行时再重新调入内存;
暂时调到外存等待的进程状态为挂起状态。被挂起的进程PCB会被组织成挂起队列。
③低级调度(进程调度)
低级调度(进程调度/处理机调度)——按照某种策略从就绪队列中选取一个进程,将处理机分配给它,(内存 → CPU)。

进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置;
进程调度进程调度的频率最高,一般几十毫秒一次。
2)三层调度的联系、对比

3)七状态模型
- 暂时调到外存等待的进程状态为挂起状态(挂起态,suspend);
- 挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态;
- 五状态模型 → 七状态模型。

注意“挂起”和“阻塞”的区别,两种状态都是暂时不能获得CPU的服务,但挂起态是将进程映像调到外存去了,而阻塞态下进程映像还在内存中;
有的操作系统会把就绪挂起、阻塞挂起分为两个挂起队列,甚至会根据阻塞原因不同再把阻塞挂起进程进一步细分为多个队列。
4)进程调度的时机、切换与过程与调度方式
①时机
进程调度(低级调度):就是按照某种算法从就绪队列中选择一个进程为其分配处理机。
-
需要进行进程调度与切换的情况
-
当前运行的进程主动放弃处理机
-
进程正常终止;
-
运行过程中发生异常而终止;
-
进程主动请求阻塞(如等待I/O)。
-
-
当前运行的进程被动放弃处理机
-
分给进程的时间片用完;
-
有更紧急的事需要处理(如I/O中断);
-
有更高优先级的进程进入就绪队列。
-
-
-
不能进行进程调度与切换的情况
- 在处理中断的过程中。中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换;
- 进程在操作系统内核程序临界区中;
- 在原子操作过程中(原语)。原子操作不可中断,要一气呵成(如之前讲过的修改PCB中进程状态标志,并把PCB放到相应队列)
-
进程在操作系统内核程序临界区中不能进行调度与切换
-
临界资源:一个时间段内只允许一个进程使用的资源。各进程需要互斥地访问临界资源;
-
临界区:访问临界资源的那段代码;
-
内核程序临界区一般是用来访问某种内核数据结构的,比如进程的就绪队列(由各就绪进程的PCB组成)。

②进程调度的方式
- 有的系统中,只允许进程主动放弃处理机;
- 有的系统中,进程可以主动放弃处理机,当有更紧急的任务需要处理时,也会强行剥夺处理机(被动放弃);
- 因此,有两种不同的进程调度的方式:分别是非剥夺调度方式和剥夺调度方式。
- 非剥夺调度方式(非抢占方式)
- 只允许进程主动放弃处理机。在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态;
- 实现简单,系统开销小但是无法及时处理紧急任务,适合于早期的批处理系统。
- 剥夺调度方式(抢占方式)
- 当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给更重要紧迫的那个进程;
- 可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中断)。适合于分时操作系统、实时操作系统。
③进程切换与过程
-
“狭义的进程调度”与“进程切换”的区别
-
狭义的进程调度指的是从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程,也可能是另一个进程,后一种情况就需要进程切换)
-
进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程。
-
-
广义的进程调度
- **广义的进程调度**包含了选择一个进程和进程切换两个步骤:
- 对原来运行进程各种数据的保存;
- 对新的进程各种数据的恢复(如:程序计数器、程序状态字、各种数据寄存器等处理机现场信息,这些信息一般保存在进程控制块)。
进程切换是有代价的,因此如果过于频繁地进行进程调度、切换,必然会使整个系统的效率降低,使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间减少。
5)调度器/调度程序(scheduler)

-
②、③由调度程序引起,调度程序决定:
- 让谁运行?—— 调度算法
- 运行多长时间?—— 时间片大小
-
调度时机—— 什么事件会触发 “调度程序”?
- 创建新进程
- 进程退出
- 运行进程阻塞
- I/O 中断发生(可能唤醒某些阻塞进程)
-
非抢占式调度策略:只有运行进程阻塞或退出才触发调度程序工作
-
抢占式调度策略:每个时钟中断或 k 个时钟中断会触发调度程序工作
-
调度对象:取决于操作系统支不支持内核级线程
- 闲逛进程:调度程序永远的备胎,没有其他就绪进程时,运行闲逛进程(idle)
- 优先级最低;
- 可以是0地址指令,占一个完整的指令周期(指令周期末尾例行检查中断);
- 能耗低。
6)调度算法的评价指标
①CPU利用率
CPU利用率=CPU忙碌时间/总时间 CPU利用率 = CPU忙碌时间 / 总时间 CPU利用率=CPU忙碌时间/总时间
②系统吞吐量
系统吞吐量指单位时间内完成作业的数量。
系统吞吐量=总共完成的作业数/总共花费的时间
系统吞吐量 = 总共完成的作业数 / 总共花费的时间
系统吞吐量=总共完成的作业数/总共花费的时间
③周转时间
周转时间是指从作业被提交给系统开始,到作业完成为止的这段时间间隔。
(作业)周转时间=作业完成时间−作业提交时间
(作业)周转时间 = 作业完成时间 - 作业提交时间
(作业)周转时间=作业完成时间−作业提交时间
平均周转时间=各作业周转时间之和/作业数 平均周转时间 = 各作业周转时间之和 / 作业数 平均周转时间=各作业周转时间之和/作业数
有的作业运行时间短,有的作业运行时间长,因此在周转时间相同的情况下,运行时间不同的作业,给用户的感觉是不一样的。
④带权周转时间
带权周转时间 ≥ 1,带权周转时间和周转时间越小越好,更小的周转时间,用户满意度也更高。
带权周转时间=作业周转时间/作业实际运行时间=作业完成时间−作业提交时间/作业实际运行时间
带权周转时间 = 作业周转时间 / 作业实际运行时间 = 作业完成时间 - 作业提交时间/作业实际运行时间
带权周转时间=作业周转时间/作业实际运行时间=作业完成时间−作业提交时间/作业实际运行时间
平均带权周转时间=各作业带权周转时间之和/作业数 平均带权周转时间 = 各作业带权周转时间之和 / 作业数 平均带权周转时间=各作业带权周转时间之和/作业数
⑤等待时间
计算机的用户希望自己的作业尽可能少的等待处理机;
等待时间指进程/作业处于等待处理机状态时间之和,等待时间越长,用户满意度越低。
等待时间=进程/作业等待被服务的时间之和
等待时间 = 进程/作业等待被服务的时间之和
等待时间=进程/作业等待被服务的时间之和
- 对于进程来说,等待时间就是指进程建立后等待被服务的时间之和,在等待I/0完成的期间其实进程也是在被服务的,所以不计入等待时间;
- 对于作业来说,不仅要考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间;
- 一个作业总共需要被CPU服务多久,被I/O设备服务多久一般是确定不变的,因此调度算法其实只会影响作业/进程的等待时间。当然,与前面指标类似,也有“平均等待时间”来评价整体性能。
⑥响应时间
- 响应时间指从用户提交请求到首次产生响应所用的时间。
8、调度算法
- 各种调度算法的学习思路
- 算法规则
- 这种调度算法是用于作业调度还是进程调度?
- 抢占式?非抢占式?
- 优点和缺点
- 是否会导致饥饿(某进程/作业长期得不到服务)
1)批处理系统

以下三种算法——不关心“响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。一般适用于早期的批处理系统。
①先来先服务(FCFS)
- 算法思想:从“公平”的角度考虑
- 算法规则:按照作业/进程到达的先后顺序服务
- 非抢占式算法
- 优点:公平,实现简单
- 缺点:对长作业有利,对短作业不利(例如:排队买奶茶,有一个人买20杯,自己只需要1杯,需要等待前面20杯做完)
- 不会导致饥饿

平均带权周转时间=(1+2.25+8+2.75)/4=3.5 平均带权周转时间=(1+2.25+8+2.75)/4=3.5 平均带权周转时间=(1+2.25+8+2.75)/4=3.5
平均等待时间=(0+5+7+7)/4=4.75 平均等待时间=(0+5+7+7)/4=4.75 平均等待时间=(0+5+7+7)/4=4.75
本例中的进程都是纯计算型的进程,一个进程到达后要么在等待,要么在运行。如果是又有计算、又有I/O操作的进程,其等待时间就是周转时间 - 运行时间 - I/O操作的时间。
②短作业优先(SJF)
- 算法思想:追求最少的平均等待时间,最少的平均周转时间、最少的平均平均带权周转时间
- 算法规则:选择估计运行时间最短(要求服务时间最短)的作业/进程优先服务
- 既可用于作业调度,也可用于进程调度。用于进程调度时成为“短进程优先(SPF, Shortest Process First)算法”
- 默认非抢占式,也有抢占式版本(最短剩余时间优先,SRTN)
- 优点:平均等待时间、平均周转时间最短
- 缺点:对长作业不利,可能导致饥饿,如果一直得不到服务,则称为“饿死”
非抢占式算法计算过程如下:

抢占式算法计算过程如下:


注意:第二点开始为正确说法(选择题)
- 如果题目中未特别说明,所提到的“短作业/进程优先算法”,默认是非抢占式的;
- 在所有进程同时可运行时,采用SJF调度算法的平均等待时间、平均周转时间最少;
- 在所有进程都几乎同时到达时,采用SJF调度算法的平均等待时间、平均周转时间最少;
- 抢占式的短作业/进程优先调度算法(最短剩余时间优先,SRNT算法)的平均等待时间、平均周转时间最少。
③高响应比优先(HRRN)
- 算法思想:要综合考虑作业/进程的等待时间和要求服务的时间
- 算法规则:在每次调度时先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务
- 既可以用于作业调度,也可以用于进程调度
- 响应比 = (等待时间 + 要求服务时间)/ 要求服务时间 (响应比 ≥ 1)
- 非抢占式
- 优点:综合考虑等待时间和运行时间,不会产生饥饿
- 缺点:需要计算响应比,增加开销

2)交互式系统

以下三种算法——关心“响应时间”,一般适用于分时操作系统。
①时间片轮转(RR)
- 算法思想:公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应
- 算法规则:按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片,若进程未在一个时间片内完成,则会被剥夺处理机,将进程重新放到就绪队列队尾重新排队
- 用于进程调度(只有作业放入内存建立了相应的进程才能被分配处理机时间片)
- 抢占式,由时钟装置发出时钟中断来通知CPU时间片已到
- 优点:公平,响应快,适用于分时操作系统,不会导致饥饿
- 缺点:高频率进程切换带来开销
时间片大小为2的情况:



时间片轮转算法:常用于分时操作系统,更注重“响应时间”,因而此处不计算周转时间
时间片大小为5的情况:

如果时间片太大,使得每个进程都可以在一个时间片内就完成,则时间片轮转调度算法退化为先来先服务调度算法,并且会增大进程响应时间,因此时间片不能太大。
如果时间片太小,会导致进程切换过于频繁,系统会花大量的时间来处理进程切换,从而导致实际用于进程执行的时间比例减少,可见时间片也不能太小。
②优先级调度
- 算法思想:应用场景需要根据任务的紧急程度来决定处理顺序
- 算法规则:每个作业/进程有各自的优先级,选择优先级最高的服务
- 可抢占或非抢占;非抢占式只需在进程主动放弃处理机时进行调度即可,而抢占式还需在就绪队列变化时,检查是否会发生抢占
- 优点:区分紧急程度、重要程度,适用于分时操作系统
- 缺点:可能导致饥饿
非抢占式情况:

抢占式情况:

补充:
- 就绪队列未必只有一个,可以按照不同优先级来组织。另外,也可以把优先级高的进程排在更靠近队头的位置
- 根据优先级是否可以动态改变,可将优先级分为静态优先级和动态优先级两种
- 静态优先级:创建进程时确定,之后一直不变;
- 动态优先级:创建进程时有一个初始值,之后会根据情况动态地调整优先级。
- 如何合理地设置各类进程的优先级?
- 系统进程优先级高于用户进程
- 前台进程优先级高于后台进程
- 操作系统更偏好 I/O型进程(或称 IO繁忙型进程)
- 相对的:计算型进程(或称CPU繁忙型进程)
- 如果采用的是动态优先,什么时级候应该调整?
- 可以从追求公平、提升资源利用率等角度考虑
- 如果某进程在就绪队列中等待了很长时间,则可以适当提升其优先级
- 如果某进程占用处理机运行了很长时间,则可适当降低其优先级
- 如果发现一个进程频繁地进行I/O操作,则可适当提升其优先级
③多级反馈队列
- 算法思想:对以上两种算法进行折中权衡
- 算法规则:
- 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大
- 新进程先进入第一级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾;如果此时已经是在最下级的队列,则重新放回该队列队尾
- 只有k级队列为空时,才会为k+1级队列的进程分配时间片
- 抢占式
- 优点:综合多种算法优点
- 缺点:可能饥饿

④多级队列调度

9、多处理机调度
1)目标
多处理机调度需要解决两个问题:①用调度算法决定让哪个就绪进程优先上处理机;②决定被调度的进程上哪个处理机。
单处理机调度 VS 多处理机调度
因此需要解决两个方面的内容:负载均衡和处理机亲和性。
- 负载均衡指尽可能让每个CPU都同等忙碌。
- 处理机亲和性指尽量让一个进程调度到同一个CPU上运行,以发挥CPU中缓存的作用(Cache)。
2)方案
①公共就绪队列
- 所有CPU共享同一个就绪进程队列(位于内核区)
- 每个CPU时运行调度程序,从公共就绪队列中选择一个进程运行
- 每个CPU访问公共就绪队列时需要**上锁**(确保互斥)
- 优点:可以天然地实现负载均衡
- 缺点:各个进程频繁地换CPU运行,“亲和性”不好
- 提高处理机亲和性(可混用)
- 软亲和:由进程调度程序尽量保证“亲和性”;
- 硬亲和:由用户进程通过系统调用,主动要求操作系统分配固定的CPU,确保“亲和性”。

②私有就绪队列
-
每个CPU都有一个私有就绪队列
-
CPU空闲时运行调度程序,从私有就绪队列中选择一个进程运行
-
实现负载均衡
-
推迁移(push)策略:一个特定的系统程序周期性检查每个处理器的负载,如果负载不平衡,就从忙碌CPU的就绪队列中“推”一些就绪进程到空闲CPU的就绪队列。
-
拉迁移(pull)策略:每个CPU运行调度程序时,周期性检查自身负载与其他CPU负载。如果一个CPU负载很低,就从其他高负载CPU的就绪队列中“拉”一些就绪进程到自己的就绪队列。
-
-
私有就绪队列天然地实现了“处理机亲和性”
10、进程同步、进程互斥
前情提要:进程具有异步性的特征。异步性是指,各并发执行的进程以各自独立的、不可预知的速度向前推进;
1)进程同步
- 进程同步要解决的问题是异步问题,需要按照规定方式进行数据或作业等内容的同步;
- 同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
2)进程互斥
进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,又比如打印机、摄像头这样的I/O设备)。
- 两种资源共享方式
- 互斥共享方式:系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源
- 同时共享方式:系统中的某些资源,允许一个时间段内由多个进程“同时”对它们进行访间。
- 临界资源:一个时间段内只允许一个进程使用的资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系;
- 进程互斥:进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。

-
临界区是进程中访问临界资源的代码段;
-
进入区和退出区是负责实现互斥的代码段;
-
临界区也可称为“临界段”。
-
为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:
- 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区;
- 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待;
- 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿);
- 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。

3)进程互斥的软件实现方式
①单标志法
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予。
主要问题:违背“空闲让进”原则。
- turn 的初值为0,即刚开始只允许0号进程进入临界区;
- 若 P1先上处理机运行,则会一直卡在 ⑤;
- 直到 P1的时间片用完,发生调度,切换 P0上处理机运行;
- 代码 ① 不会卡住 P0,PO可以正常访问临界区,在 P0访问临界区期间即时切换回 P1,P1依然会卡在 ⑤;
- 只有 P0 在退出区将 turn 改为1后,P1才能进入临界区。

该算法可以实现“同一时刻最多只允许一个进程访问临界区”。
②双标志先检查
算法思想:设置一个布尔型数组flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如“flag[0]=ture”意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志flag[i]设为true,之后开始访问临界区。
主要问题:违反“忙则等待”原则。
原因在于,进入区的“检查”和“上锁”两个处理不是一气呵成的。“检查”后,“上锁”前可能发生进程切换。

③双标志后检查
算法思想:双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”,但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到**先“上锁”后“检查”**的方法,来避免上述问题。
主要问题:违背了“空闲让进”和“有限等待”原则。会因各进程都长期无法访问临界资源而产生“饥饿”现象。

④Peterson算法
算法思想:结合双标志法、单标志法的思想。如果双方都争着想进入临界区,那可以让进程尝试“孔融让梨”(谦让)。做一个有礼貌的进程。
主要问题:违反了让权等待的原则。

4)进程互斥的硬件实现方式
①中断屏蔽方法
- 实现方法:利用“开/关中断指令”实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况)
- 优点:简单、高效
- 缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)
②TestAndSet(TS指令/TSL指令)
简称TS 指令,也有地方称为TestAndSetLock指令,或TSL指令。TSL指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞:适用于多处理机环境;
缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。
以下是用C语言描述的逻辑:

- 若刚开始lock是false,则TSL返回的 old值为false,while循环条件不满足,直接跳过循环,进入临界区;
- 若刚开始lock是true,则执行TLS后old返回的值为true,while 循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行“解锁”。
相比软件实现方法,TSL指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。
③Swap指令(XCHG指令)
- 也称 Exchange 指令,或简称 XCHG 指令;
- swap 指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成;
- 优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境;
- 缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。
以下是用C语言描述的逻辑

- 逻辑上来看 swap和 TSL并无太大区别,都是先记录下此时临界区是否已经被上锁(记录在 old 变量上);
- 再将上锁标记lock设置为true,最后检查old;
- 如果old为false 则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。
5)互斥锁
- 解决临界区最简单的工具就是互斥锁(mutex lock)。一个进程在进入临界区时应获得锁
- 在退出临界区时释放锁
- 函数acquire()获得锁,而函数release()释放锁
- 每个互斥锁有一个布尔变量available,表示锁是否可用
- 如果锁是可用的,调用 acgiure()会成功,且锁不再可用;
- 当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。
- acquire()或release()的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现
- 互斥锁的主要缺点是忙等待,当有一个进程在临界区中,任何其他进程在进入临界区时必须连续循环调用acquire()
- 当多个进程共享同一CPU时,就浪费了CPU周期。因此,互斥锁通常用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行

需要连续循环忙等的互斥锁,都可称为自旋锁(spin lock),如TSL指令、swap指令、单标志法。
自旋锁的特性
- 需忙等,进程时间片用完才下处理机,违反“让权等待”;
- 优点:等待期间不用切换进程上下文,多处理器系统中,若上锁的时间短,则等待代价很低;
- 常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区;
- 不太适用于单处理机系统,忙等的过程中不可能解锁。

11、信号量
1)信号量机制
- 用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步;
- 信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量;
- 一对原语:wait(S) 原语和 signal(S) 原语,可以把原语理解为我们自己写的函数,函数名分别为 wait和 signal,括号里的信号量S其实就是函数调用时传入的一个参数;
- wait、signal 原语常简称为P、V操作(来自荷兰语 proberen和verhogen)。因此,常把wait(S)、signal(s)两个操作分别写为 P(S)、V(S)。
除非特别说明,默认S为记录型信号量。
①整型信号量
用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。与普通整数变量的区别:对信号量的操作只有三种,即 初始化、P操作、V操作。
- “检查”和“上锁”一气呵成,避免了并发、异步导致的问题;
- 存在的问题:不满足“让权等待”原则,会发生“忙等”。

②记录型信号量
整型信号量的缺陷是存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构表示的信号量。
- block:自主进入阻塞态,并挂到等待队列;
- wakeup:唤醒等待队列中的进程,由阻塞态变为就绪态。

例如,打印机的例子,当打印机不够进程使用时,则会按进程访问顺序阻塞并挂到等待队列,当有空闲打印机时,则唤醒进程进入就绪态。

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

2)用信号量机制实现进程互斥、同步、前驱关系
①进程互斥
互斥问题:信号量初始值为1

- 分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)
- 设置互斥信号量 mutex,初值为 1
- 在进入区 P(mutex)——申请资源
- 在退出区 V(mutex)——释放资源
注意:对不同的临界资源需要设置不同的互斥信号量。P、V操作必须成对出现。缺少P(mutex)就不能保证临界资源的互斥访问。缺少 V(mutex)会导致资源永不被释放,等待进程永不被唤醒。

②进程同步
同步问题:信号量初始值为0
进程同步:要让各并发进程按要求有序地推进。
- 分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码);
- 设置同步信号量S, 初始为 0
- 在“前操作”之后执行 V(S)
- 在“后操作”之前执行 P(S)
- 记忆方法:前V后P

③实现进程的前驱关系
前驱关系问题:本质上是多级同步问题(类似拓扑图)
- 要为每一对前驱关系各设置一个同步信号量;
- 在“前操作”之后对相应的同步信号量执行V操作;
- 在“后操作”之前对相应的同步信号量执行P操作。

④生产者-消费者问题
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(注:这里的“产品”理解为某种数据)。
-
生产者、消费者共享一个初始为空、大小为n的缓冲区;
-
只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待;(缓冲区没满→生产者生产)
-
只有缓冲区不空时,消费者才能从中取出产品,否则必须等待;(缓冲区没空→消费者消费)
-
缓冲区是临界资源,各进程必须互斥地访问。
PV操作题目分析步骤:
- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系;
- 整理思路。根据各进程的操作流程确定P、V操作的大致顺序;
- 设置信号量。并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)

semaphore mutex = 1; //互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n //同步信号量,表示空闲缓冲区的数量
semaphore full = 0; //同步信号量,表示产品的数量,也即非空缓冲区的数量

P(empty) / P(full) 和 P(mutex) 不能交换顺序,否则会发生死锁;
V(full) / V(empty) 和 V(mutex) 可以交换顺序,V操作不会阻塞。
⑤多生产者-多消费者问题
桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。

对于该问题的一前一后关系实际上要看作两种“事件”的前后关系(见图右)

- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系;
- 整理思路。根据各进程的操作流程确定P、V操作的大致顺序;(互斥:在临界区前后分别;PV同步:前V后P)
- 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)

- 互斥关系:(mutex = 1)对缓冲区(盘子)的访问要互斥地进行;
- 同步关系(一前一后):
- 父亲将苹果放入盘子后,女儿才能取苹果;
- 母亲将橘子放入盘子后,儿子才能取橘子;
- 只有盘子为空时,父亲或母亲才能放入水果。(“盘子为空”这个事件可以由儿子或女儿触发,事件发生后才允许父亲或母亲放水果)
具体实现
semaphore mutex=1; //实现互斥访问盘子(缓冲区)
semaphore apple=0; //盘子中有几个苹果
semaphore orange=; //盘子中有几个橘子
semaphore plate=1; //盘子中还可以放多少个水果

假设不适用互斥信号量,即PV(mutex),不会影响该问题“错误”;
假设盘子/缓冲区大小不为1,则必须要互斥信号量,否则会出现数据覆盖问题。
④读者-写者问题
有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。
- 要求:
- 允许多个读者可以同时对文件执行读操作;
- 只允许一个写者往文件中写信息;
- 任一写者在完成写操作之前不允许其他读者或写者工作;
- 写者执行写操作前,应让已有的读者和写者全部退出。

- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系;
- 整理思路。根据各进程的操作流程确定P、V操作的大致顺序;
- 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)。
- 两类进程:写进程、读进程
- 互斥关系:写进程一写进程、写进程一读进程。读进程与读进程不存在互斥问题。

结论:在这种算法中,连续进入的多个读者可以同时读文件;写者和其他进程不能同时访问文件;写者不会饥饿,但也并不是真正的“写优先”,而是相对公平的先来先服务原则。也可以称为**“读写公平法”**。
核心:其核心思想在于设置了一个计数器 count 用来记录当前正在访问共享文件的读进程数。
可以用count的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。
另外,对count变量的检查和赋值不能一呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。
⑤哲学家进餐问题
一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。

- 关系分析。系统中有5个哲学家进程,5位哲学家与左右邻居对其中间筷子的访问是互斥关系;
- 整理思路。这个问题中只有互斥关系,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何**避免临界资源分配不当造成的死锁现象**,是哲学家问题的精髓;
- 信号量设置。定义互斥信号量数组chopstick[5]={1,1,1,1,1}用于实现对5个筷子的互斥访问。并对哲学家按0~4编号,哲学家 i 左边的筷子编号为 i,右边的筷子编号为(i+1)%5。
-
方法:
-
可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的;
-
要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况;
-
仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。
-
方法3:各哲学家拿筷子这件事必须互斥的执行。这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。因此,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了。
12、管程(封装PV)
①背景
- 信号量机制存在的问题:编写程序困难、易出错;
- 1973年,Brinch Hansen 首次在程序设计语言(Pascal)中引入了“管程”成分——一种高级同步机制。
②定义和基本特征
- 管程是一种特殊的软件模块,有这些部分组成:
- 局部于管程的共享数据结构说明;
- 对该数据结构进行操作的一组过程;
- 对局部于管程的共享数据设置初始值的语句;
- 管程有一个名字。
- 管程的基本特征:
- 局部于管程的数据只能被局部于管程的过程所访问;
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据;
- 每次仅允许一个进程在管程内执行某个内部过程。
③管程解决生产者消费者问题
将PV操作封装成一个函数,使用时只需要调用ProdecerConsumer的insert和remove方法。


④Java中类似管程的机制

13、死锁
1)概念
①进程死锁、饥饿、死循环的区别
死锁:各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象;
饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象。比如:在短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而发生长进程“饥饿”;
死循环:某进程执行过程中一直跳不出某个循环的现象。有时是因为程序逻辑bug导致的,有时是程序员故意设计的。

②死锁产生的必要条件
产生死锁必须同时满足一下四个条件,只要其中任一条件不成立,死锁就不会发生。
- 互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备);像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不用阻塞等待这种资源)。
- 不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放;
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己己有的资源保持不放;
- 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求;
- ⚠**发生死锁时一定有循环等待,但是发生循环等待时未必死锁**(循环等待是死锁的必要不充分条件)。
如果同类资源数大于1,则即使有循环等待,也未必发生死锁。但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件了。
③发生死锁的时间
对不可剥夺资源的不合理分配,可能导致死锁,例子如下:
- 对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源(CPU)的竞争是不会引起死锁的;
- 进程推进顺序非法。请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程P1、P2 分别申请并占有了资源 R1、R2,之后进程P1又紧接着申请资源R2,而进程P2又申请资源R1,两者会因为申请的资源被对方占有而阻塞,从而发生死锁;
- 信号量的使用不当也会造成死锁。如生产者-消费者问题中,如果实现互斥的P操作在实现同步的P操作之前,就有可能导致死锁(可以把互斥信号量、同步信号量也看做是一种抽象的系统资源)。
2)处理策略
①预防死锁
破坏死锁产生的四个必要条件中的一个或几个。
破坏互斥条件:在多个进程与输出设备中增加中间件(如输出进程),将进程n放入输出进程进行等待,进程n可以继续执行其他操作。

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

破坏循环等待条件:
可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。
原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按此规则,已持有大编号资源的进程不可能逆向地回来申请小编号的资源,从而就不会产生循环等待的现象。

该策略的缺点:
- 不方便增加新的设备,因为可能需要重新分配所有的编号;
- 进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费;
- 必须按规定次序申请资源,用户编程麻烦。
②避免死锁
用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)。

所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个。
如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)。
因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法”的核心思想。
本节详细解释可观看:
https://www.bilibili.com/video/BV1YE411D7nH?spm_id_from=333.788.player.switch&vd_source=cc2432322b31e1c60d007467270cb1ee&p=41
银行家算法是荷兰学者 Dikstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁。
核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞待。
对下图进行说明:
- 数据结构
- 长度为 m 的一维数组 Available 表示还有多少可用资源
- n * m 矩阵 Max 表示各进程对资源的最大需求数
- n * m矩阵 Allocation 表示已经给各进程分配了多少资源
- Max - Allocation = Need 矩阵表示各进程最多还需要多少资源
- 用长度为 m 的一位数组 Request 表示进程此次申请的各种资源数
- 银行家算法步骤
- 检查此次申请是否超过了之前声明的最大需求数
- 检查此时系统剩余的可用资源是否还能满足这次请求
- 试探着分配,更改各数据结构
- 用安全性算法检查此次分配是否会导致系统进入不安全状态
- 安全性算法步骤
- 检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,,并把该进程持有的资源全部回收。
- 不断重复上述过程,看最终是否能让所有进程都加入安全序列。

系统处于不安全状态未必死锁,但死锁时一定处于不安全状态。系统处于安全状态一定不会死锁。
③检测和解除
允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁。
- 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁;
- 死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。
死锁的检测
- 用某种数据结构来保存资源的请求和分配信息
- 提供一种算法,利用上述信息来检测系统是否已进入死锁状态
- 死锁的算法:依次消除与不阻塞进程相连的边,直到无边可消。
- 数据结构资源分配图
- 两种结点
- 进程结点(P1、P2):对应一个进程
- 资源结点(R1、R2):对应一类资源,一类资源可能有多个
- 两种边
- 进程结点→资源结点:表示进程想申请几个资源(每条边代表一个)
- 资源节点→进程结点:表示已经为进程分配了几个资源(每条边代表一个)
- 两种结点

如果系统中剩余的可用资源数足够满足进程的需求,那么这个进程暂时是不会阻塞的,可以顺利地执行下去。
如果这个进程执行结束了把资源归还系统,就可能使某些正在等待资源的进程被激活,并顺利地执行下去。相应的,这些被激活的进程执行完了之后又会归还一些资源,这样可能又会激活另外一些阻塞的进程。
如果按上述过程分析,最终能消除所有边,就称这个图是**可完全简化的。此时一定没有发生死锁**(相当于能找到一个安全序列)。
如果最终不能消除所有边,那么此时就是发生了死锁。最终还连着边的那些进程就是处于死锁状态的进程。

死锁的解除
