一、进程与线程
1. 进程的概念、组成与特征
1.1 进程的概念
- 程序
是静态的,就是个存放在磁盘里的可执行文件,就是一系列的指令集合。 - 进程
是动态的,是程序的一次执行过程
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
1.2 进程实体的组成
- 进程控制块(PCB)
操作系统需要对各个并发运行的进程进行管理,但凡管理时所需要的的信息,都会被放在PCB中。- 进程描述信息
进程标识符PID
用户标识符UID - 进程控制和管理信息
CPU、磁盘、网络流量使用情况统计……
进程当前状态:就绪态/阻塞态/运行态…… - 资源分配清单
正在使用哪些文件
正在使用哪些内存区域
正在使用哪些I/O设备 - 处理机相关信息
如PSW、PC等等各种寄存器的值(用于实现进程切换)
PCB是进程存在的唯一标志,当进程被创建时,操作系统为其创建PCB,当进程结束时,会回收其PCB。
- 进程描述信息
- 程序段
程序的代码(指令序列) - 数据段
运行过程中产生的各种数据(如:程序中定义的变量)
1.3 进程与进程实体
一个进程实体(进程映像)是由PCB、程序段、数据段组成。进程是动态的,进程实体(进程映像)是静态的。进程实体反映了进程在某一时刻的状态。
PCB是操作系统用的;程序段、数据段是给进程自己用的。
进程的特征
- 动态性
进程是程序的一次执行过程,是动态地产生、变化和消亡的 - 并发性
内存中有多个进程实体,各进程可并发执行 - 独立性
进程是能独立运行、独立获得资源、独立接受调度的基本单位。 - 异步性
各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题 - 结构性
每个进程都会配置一个PCB。结构上看,进程由程序段、数据段、PCB组成。
2. 进程的状态与转换
2.1 状态
- 创建态
进程正在被创建时,它的状态是“创建态”。在这个阶段,操作系统会为进程分配资源、初始化PCB - 就绪态
当进程创建完成后,便进入“就绪态”,处于就绪态的进程已经具备运行条件,但由于没有空闲的CPU,就暂时不能运行。 - 运行态
如果一个进程上处理机运行,就处于“运行态 - 阻塞态
在进程运行过程中,可能会请求等待某个时间的发生(如等待某种系统资源的分配,或者等待其他进程的响应)。在这个事件发生之前,进程无法继续往下执行,此时操作系统会让这个进程下CPU,并让它进入“阻塞态"。当CPU空闲时,又会选择另一个“就绪态”进程上CPU运行 - 终止态
一个进程可以执行exit系统调用,请求操作系统终止该进程。此时该进程会进入“终止态”,操作系统会让该进程下CPU,并回收内存空间等资源,最后还要回收该进程的PCB。当终止进程的工作完成之后,这个进程就彻底消失了。
2.2 状态间的转换
- 创建态->就绪态
系统完成创建进程的一系列工作 - 就绪态->运行态
进程被调度 - 运行态->就绪态
时间片到,或处理机被抢占 - 运行态->阻塞态
进程用“系统调用”的方式申请某种系统资源,或者请求等待某个事件发生。是一种进程自身作出的主动行为 - 阻塞态->就绪态
申请的资源被分配,或等待的事件发生。不是进程自身能控制的,是一种被动行为 - 运行态->终止态
进程运行结束,或运行过程中遇到不可修复的错误
注意:
不能由阻塞态直接转换为运行态,也不能由就绪态直接转换为阻塞态。
因为进入阻塞态是进程主动请求的,必然需要进程在运行时才能发出这种请求。
2.3 进程的组织方式(各个进程PCB的组织方式)
- 链接方式
按照进程状态将PCB分为多个队列。操作系统持有指向各个队列的指针- 执行指针
- 就绪队列指针
- 等待打印机的阻塞队列
- 等待磁盘的阻塞队列
- 索引方式
根据进程状态的不同,建立几张索引表。操作系统持有指向各个索引表的指针
3. 进程控制
3.1 基本概念
什么是进程控制?
进程控制就是要实现进程状态转换
如何实现进程控制?
- 用“原语”实现,原语的执行具有“原子性”,一气呵成,期间不允许被中断
- 可以用“关中断指令”和“开中断指令”这两个特权指令实现原子性。
- CPU执行了关中断指令之后,就不再例行检查中断信号,直到执行开中断指令之后才会恢复检查。
这样,关中断、开中断之间的这些指令序列就是不可被中断的,这就实现了“原子性”。
3.2 进程控制相关的原语
进程的创建
1. 创建原语
- 申请空白PCB
- 为新进程分配所需资源
- 初始化PCB
- 将PCB插入就绪队列
2. 引起进程创建的事件
- 用户登录
分时系统中,用户登录成功,系统会建立一个新的进程 - 作业调度
多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程 - 提供服务
用户向操作系统提出某些请求时,会新建一个进程处理该请求。 - 应用请求
由用户进程主动请求创建一个子进程
进程的终止
1. 撤消原语
- 从PCB集合中找到终止进程的PCB
- 若进程正在运行,立即剥夺CPU,将CPU分配给其他进程
- 终止其所有子进程
- 将该进程拥有的所有资源归还给父进程或操作系统
- 删除PCB
2. 引起进程终止的事件 - 正常结束
进程自己请求终止(exit系统调用) - 异常结束
外界干预
进程的阻塞和唤醒
进程的阻塞
1. 阻塞原语
- 找到要阻塞的进程对应的PCB
- 保护进程运行现场,将PCB状态信息设置为“阻塞态”,暂时停止进程运行
- 将PCB插入相应事件的等待队列
2. 引起进程阻塞的事件
- 需要等待系统分配某种资源
- 需要等待相互合作的其他进程完成工作
进程的唤醒
1. 唤醒原语
- 在事件等待队列中找到PCB
- 将PCB从等待队列移除,设置进程为就绪态
- 将PCB插入就绪队列,等待被调度
2. 引起进程唤醒的事件
- 等待的事件发生
因何事阻塞,就应由何事唤醒
进程的切换
1. 切换原语
- 将运行环境信息存入PCB
- PCB移入相应队列
- 选择另一个进程执行,并更新其PCB
- 根据PCB恢复新进程所需的运行环境
2. 引起进程切换的事件
- 当前进程时间片到
- 有更高优先级的进程到达
- 当前进程主动阻塞
- 当前进程终止
4. 进程通信
4.1 基本概念
什么是进程通信?
顾名思义,进程通信就是指进程之间的信息交换。
进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。
为了保证安全,一个进程不能直接访问另一个进程的地址空间。但是进程之间的信息交换又是必须实现的。为了保证进程间的安全通信,操作系统提供了一些方法。
4.2 方式
共享存储
两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统提供的工具(比如PV操作)来实现
分类
- 基于数据结构的共享
比如共享空间里只能放一个长度为10的数组。
这种共享方式速度慢、限制多,是一种低级通信方式。 - 基于存储区的共享
在内存中划出一块共享存储区,数据的形式、存放位置都由进程控制,而不是操作系统。
相比之下,这种共享方式速度更快,是一种高级通信方式。
消息传递
进程间的数据交换以格式化的消息为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。
- 直接通信方式
消息直接挂到接收进程的消息缓冲队列上。 - 间接通信方式(信箱通信方式)
消息要先发送到中间实体(信箱)中,因此也称“信箱通信方式”。Eg:计网中的电子邮件系统。
管道通信
管道是指用于连续读写进程的一个共享文件,又名pipe文件。其实就是在内存中开辟一个大小固定的缓冲区(其大小一般和内存页面大小一样)。
- 管道只能采用半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道。
- 各进程要互斥地访问管道。
- 数据以字符流的形式写入管道,当管道写满时,写进程write()系统调用将被阻塞,等待读进程将数据取走。当读进程将数据全部取走后,管道变空,此时读进程的read()系统调用将被阻塞。
- 如果没写满,就不允许读;如果没读空,就不允许写。
- 数据一旦被读出,就从管道中被抛弃,这就意味着读进程最多只能有一个,否则可能会有读错数据的情况。
5. 线程
5.1 线程的概念和特点
什么是线程?为什么要引入线程?
资源分配、处理机调度
- 各个进程的内存地址空间相互独立,只能通过请求操作系统内核的帮助来完成进程间的通信
- 同一进程下的各个线程间共享内存地址空间,可以直接通过读/写内存空间进行通信
- 从属于同一进程的各个线程共享进程拥有的资源。
进程间通信必须请求操作系统服务(CPU要切换到核心态),开销大。
同进程下的线程间通信,无需操作系统干预,开销更小。
并发性
引入线程机制后,并发带来的系统开销降低,系统并发性提升。
注意:从属于不同进程的线程间切换,也会导致进程的切换!开销也大!
(实现并发的)系统开销
当切换进程时,需要保存/恢复进程运行环境,还需要切换内存地址空间(更新快表、更新缓存)开销很大
同一个进程内的各个线程间并发,不需要切换进程运行环境和内存地址空间,省时省力。
与进程相比,线程有哪些特点?
- 线程是处理机调度的单位,进程是资源分配的单位
- 同一进程的各个线程共享进程拥有的资源
- 同一进程内的线程切换不会导致进程切换
- 引入线程后,进程是资源分配的基本单位。而线程几乎不拥有资源,只拥有极少量的资源(线程控制块TCB、寄存器信息、堆栈等)
5.2 线程的实现方式
用户级线程(User-Level Thread, ULT)
历史背景: 早期的操作系统(如:早期Unix)只支持进程,不支持线程。当时的“线程”是由线程库实现的。
- 用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程切换)
- 用户级线程中,线程切换在用户态下即可完成,无需操作系统干预。
- 在用户看来,是有多个线程。但是在操作系统内核看来,并意识不到线程的存在。“用户级线程”就是“从用户视角看能看到的线程”
- 优缺点
- 优点: 用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。
- 缺点: 当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。
内核级线程(Kernel-Level Thread, KLT)
- 内核级线程的管理工作由操作系统内核完成
- 线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成
- 操作系统会为每个内核级线程建立相应的TCB(Thread Control Block, 线程控制块),通过TCB对线程进行管理。“内核级线程”就是“从操作系统内核视角看能看到的线程”
- 优缺点
优点: 当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
缺点: 一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
5.3 多线程模型
一对一模型
- 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
- 缺点: 一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
多对一模型
退化成了用户级线程
- 优点: 用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。
- 缺点: 当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。
重点:操作系统只看得见内核级线程,因此只有内核级线程才是处理机分配的单位。
多对多模型
n用户级线程映射到m个内核级线程(n>=m)。每个用户进程对应m个内核级线程。
克服了多对一模型并发度不高的缺点(一个阻塞全体阻塞),又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。
可以这么理解:
- 用户级线程是“代码逻辑”的载体
- 内核级线程是“运行机会”的载体
- 内核级线程才是处理机调度的基本单位。
一段“代码逻辑”只有获得了“运行机会”才能被CPU执行
内核级线程中可以运行任意一个有映射关系的用户级线程代码,只有两个内核级线程中正在运行的代码逻辑都阻塞时,这个进程才会阻塞。
二、 处理机调度
1. 基本概念
调度的基本概念
在多道程序系统中,进程的数量往往是多于处理机的个数的,这样不可能同时并行地处理各个进程。
处理机调度就是从就绪队列中按照一定的算法选择一个进程并将处理机分配给它运行,以实现进程的并发执行。
2. 三个层次
2.1 高级调度(作业调度)
由于内存空间有限,有时无法将用户提交的作业全部放入内存,因此就需要确定某种规则来决定将作业调入内存的顺序
高级调度(作业调度)。按照一定的原则从外存上处于后备队列的作业中挑选一个(或多个)作业,给他们分配内存等必要资源,并建立相应的进程(建立PCB),以使它(们)获得竞争处理机的权利。
高级调度是辅存(外存)与内存之间的调度。每个作业只调入一次,调出一次。作业调入时会建立相应的PCB,作业调出时才撤销PCB。高级调度主要是指调入问题,因为只有调入的时机需要操作系统来决定,但调出的时机必然是作业运行结束才调出。
2.2 中级调度(内存调度)
引入了虚拟存储技术之后,可将暂时不能运行的进程调至外存等待。等它重新具备了运行条件且内存又稍有空闲时,再重新调入内存。
这么做的目的是为了提高内存利用率和系统吞吐量。
暂时调到外存等待的进程状态为挂起状态。值得注意的是,PCB并不会一起调到外存,而是会常驻内存。PCB中会记录进程数据在外存中的存放位置,进程状态等信息,操作系统通过内存中的PCB来保持对各个进程的监控、管理。被挂起的进程PCB会被放到挂起队列中。
中级调度就是要决定将哪个处于挂起状态的进程重新调入内存。一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高。
2.3 低级调度(进程调度)
其主要任务是按照某种算法和策略从就绪队列中选取一个进程,将处理机分配给它。
进程调度的频率很高,一般几十毫秒一次
三层调度的联系、对比
3. 进程调度
3.1 时机
什么时候需要进程调度?
- 当前运行的进程主动放弃处理机时
- 进程正常终止
- 运行过程中发生异常而终止
- 进程主动请求阻塞(如 等待I/O)
- 当前运行的进程被动放弃处理机时
- 分给进程的时间片用完
- 有更紧急的事情需要处理(如I/O中断)
- 有更高优先级的进程进入就绪队列
什么时候不能进行进程调度?
- 在处理中断的过程中。 中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换。
- 进程在操作系统内核程序临界区中
- 内核程序临界区访问的临界资源如果不尽快释放的话,极有可能影响到操作系统内核的其他管理工作。因此在访问内核程序临界区期间不能进行调度与切换。
- 普通临界区访问的临界资源不会直接影响操作系统内核的管理工作。因此在访问普通临界区时可以进行调度与切换。
- 在原子操作过程中(原语)。原子操作不可中断,要一气呵成(如之前讲过的修改PCB中进程状态标志,并把PCB放到相应队列)。
3.2 切换与过程
"狭义的调度“与”切换“的区别
狭义的进程调度指的是从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程,也可能是另一个进程,后一种情况就需要进程切换)
进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程
广义的进程调度包含了选择一个进程和进程切换两个步骤。
进程切换的过程需要做什么?
- 对原来运行进程各种数据的保存
- 对新的进程各种数据的恢复(如:程序计数器,程序状态字,各种数据寄存器等处理机现场信息,这些信息一般保存在进程控制块)
注意:
进程切换是有代价的,因此如果过于频繁地进行进程调度、切换,必然会使整个系统的效率降低,使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间减少。
3.3 方式
- 非剥夺调度方式(非抢占式)
实现简单,系统开销小,但是无法及时处理紧急任务,适合于早期的批处理系统。 - 剥夺调度方式(抢占式)
可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中断)。适合于分时操作系统、实时操作系统。
4. 调度算法
4.1 评价指标
- CPU利用率
由于早期的CPU造价极其昂贵,因此人们会希望让CPU尽可能多地工作。因此出现CPU利用率这个指标,指CPU忙碌的时间占总时间的比例。
C P U 利 用 率 = 忙 碌 的 时 间 / 总 时 间 CPU利用率 = 忙碌的时间/总时间 CPU利用率=忙碌的时间/总时间 - 系统吞吐量
对于计算机来说,希望能用尽可能少的时间处理完尽可能多的作业。
单位时间内完成作业的数量
系 统 吞 吐 量 = 总 共 完 成 了 多 少 道 作 业 / 总 共 花 了 多 少 时 间 系统吞吐量 = 总共完成了多少道作业/总共花了多少时间 系统吞吐量=总共完成了多少道作业/总共花了多少时间 - 周转时间
对计算机的用户来说,他很关心自己的作业从提交到完成花了多少时间。
周转时间是指从作业被提交给系统开始,到作业完成为止的这段时间间隔。
它包括四个部分:作业在外存后备队列上等待作业调度(高级调度)的时间、进程在就绪队列上等待进程调度(低级调度)的时间、进程在CPU上执行的时间、进程等待I/O操作完成的时间。后三项在一个作业的整个处理过程中,可能发生多次。
作业周转时间 = 作业完成时间 - 作业提交时间
- 平均周转时间
平 均 周 转 时 间 = 各 作 业 周 转 时 间 之 和 / 作 业 数 平均周转时间 = 各作业周转时间之和 / 作业数 平均周转时间=各作业周转时间之和/作业数 - 带权周转时间
带 权 周 转 时 间 = 作 业 周 转 时 间 / 作 业 实 际 运 行 的 时 间 = ( 作 业 完 成 时 间 − 作 业 提 交 时 间 ) / 作 业 实 际 运 行 的 时 间 带权周转时间 = 作业周转时间 / 作业实际运行的时间 = (作业完成时间 - 作业提交时间) / 作业实际运行的时间 带权周转时间=作业周转时间/作业实际运行的时间=(作业完成时间−作业提交时间)/作业实际运行的时间 - 平均带权周转时间
平 均 带 权 周 转 时 间 = 各 作 业 带 权 周 转 时 间 之 和 / 作 业 数 平均带权周转时间 = 各作业带权周转时间之和 / 作业数 平均带权周转时间=各作业带权周转时间之和/作业数 - 等待时间
计算机的用户希望自己的作业尽可能少的等待处理机 - 响应时间
指用户提交请求到首次产生响应所用的时间
4.2 几种算法
Tips: 各种调度算法的学习思路
- 算法思想
- 算法规则
- 这种调度算法是用于作业调度还是进程调度?
- 抢占式?非抢占式?
- 优点和缺点
- 是否会导致饥饿
先来先服务(FCFS)
算法思想
主要从“公平”的角度考虑
算法规则
- 按照作业/进程到达的先后顺序进行服务
用于作业/进程调度
* 用于作业调度时,考虑的是哪个作业先到达后备队列;
* 用于进程调度时,考虑的是哪个进程先到达就绪队列。
是否可抢占?
非抢占式的算法
优缺点
优点:公平、算法实现简单
缺点: 排在长作业(进程)后面的短作业需要等待很长时间,带权周转时间很大,对短作业来说用户体验不好,即,FCFS算法对长作业有利,对短作业不利。
是否会导致饥饿
不会
短作业优先(SJF)
算法思想
追求最少的平均等待时间,最少的平均周转时间、最少的平均平均带权周转时间
算法规则
最短的作业/进程优先得到服务(所谓“最短”,是指要求服务时间最短)
用于作业/进程调度
既可用于作业调度,也可用于进程调度。用于进程调度时称为“短进程优先(SPF,Shortest Process First)算法“
是否可抢占?
SJF和SPF是非抢占式的算法。但是也有抢占式的版本——最短剩余时间优先算法(SRTN,Shortest Remaining Time Next)
许多书上都说,SJF调度算法的平均等待时间、平均周转时间最少。 严格来说,这个表述是错误的。最短剩余时间优先算法得到的平均等待时间、平均周转时间会更少。
应该说抢占式的短作业优先调度算法(SRNT)的平均等待时间、平均周转时间最少。
优缺点
- 优点: “最短的”平均等待时间、平均周转时间
- 缺点: 不公平。 对短作业有利、对长作业不利。可能产生饥饿现象。另外,作业/进程的运行时间是由用户提供的,并不一定真实,不一定能做到真正的短作业优先。
是否会导致饥饿
会。可能使长作业/进程长时间得不到服务,产生“饥饿”现象。如果一直得不到服务,则称为”饿死“。
高响应比优先(HRRN)
算法思想
要综合考虑作业/进程的等待时间和要求服务的时间。
算法规则
在每次调度时先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务。
响
应
比
=
(
等
待
时
间
+
要
求
服
务
时
间
)
/
要
求
服
务
时
间
。
响应比 = (等待时间 + 要求服务时间) / 要求服务时间。
响应比=(等待时间+要求服务时间)/要求服务时间。
用于作业/进程调度
都可
是否可抢占?
非抢占式的算法。因此只有当前运行的作业/进程主动放弃处理机时,才需要调度,才需要计算响应比。
优缺点
- 综合考虑了等待时间和要求服务时间。等待时间相同时,要求服务时间短的优先(SJF优点);
- 要求服务时间相同时,等待时间长的优先(FCFS的优点)。
- 对于长作业来说,随着等待时间越来越久,其响应比也会越来越大,从而避免了长作业饥饿的问题。
是否会导致饥饿
不会
这几种算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心“响应时间“,也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。因此这三种算法一般适合用于早期的批处理系统,当然,FCFS算法也常结合其他的算法使用,在现在也扮演着很重要的角色。而适合用于交互式系统的调度算法将在下面介绍。
时间片轮转
算法思想
公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。
算法规则
按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片(如100ms)。若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放倒就绪队列队尾重新排队。
用于作业/进程调度
用于进程调度(只有作业放入内存建立了相应的进程后,才能被分配处理机时间片)
是否可抢占?
抢占式。由时钟装置发出时钟中断来通知CPU时间片已到。
优缺点
- 优点: 公平;响应快,适用于分时操作系统
- 缺点:由于高频率的进程切换,因此有一定开销;不区分任务的紧急程度。
是否会导致饥饿
不会
补充
- 时间片太大会退化成FCFS,并且会增大进程响应时间。因此时间片不能太大。
- 时间片太小,会导致进程切换过于频繁,切换时也是有代价的,从而导致实际用于进程执行的时间比例减少。可见时间片也不能太小。
- 一般来说,设计时间片时要让切换进程的开销占比不超过1%。
优先级调度
算法思想
随着计算机的发展,特别是实时操作系统的出现,越来越多的应用场景需要根据任务的紧急程度来决定处理顺序。
算法规则
每个作业/进程有各自的优先级,调度时选择优先级最高的作业/进程
用于作业/进程调度
都可
是否可抢占?
都有。区别在于:非抢占式只需在进程主动该放弃处理机时进行调度即可;而抢占式还需要再就绪队列发生变化时,检查是否会发生抢占。
优缺点
- 优点:用优先级区分紧急程度、重要程度,适用于实时操作系统。可灵活地调整对各种作业/进程的偏好程度。
- 缺点: 若源源不断地有高优先级进程到来,则可能导致饥饿。
是否会导致饥饿
会
补充
就绪队列未必只有一个,可以按照不同优先级来组织。另外,也可以把优先级高的进程排在更靠近对头的位置。
根据优先级是否可以动态改变,可将优先级分为静态优先级和动态优先级两种。
- 静态优先级:创建进程确定,之后一直不变。
- 动态优先级:创建进程时有一个初始值,之后会根据情况动态地调整优先级。
如何合理设置各类进程的优先级?
- 系统进程优先级高于用户进程
- 前台进程优先级高于后台进程
- 操作系统更偏好I/O型进程(或称I/O繁忙型进程)
I/O设备和CPU可以并行工作。如果优先让I/O繁忙型进程运行的话,则越有可能让I/O设备尽早地投入工作,则资源利用率、系统吞吐量都会得到提升。
注:与I/O型进程相对的是计算型进程(或称CPU繁忙型进程)
多级反馈队列
算法思想
对其他调度算法的折中权衡
算法规则
- 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大
- 新进程到达时先进入第1级度列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时已经是在最下级的队列,则重新放回该队列队尾。
- 只有第k级队列为空时,才会为k+1级队头的进程分配时间片。
用于作业/进程调度
用于进程调度
是否可抢占?
抢占式的算法。在k级队列的进程运行过程中,若是更上级的队列(1~k-1级)中进入了一个新进程,则由于新进程处于优先级更高的队列中,因此新进程会抢占处理机,原来运行的进程放回k级队列的队尾。
优缺点
优点:
4. 对各类型进程相对公平(FCFS的优点);
5. 每个新到达的进程都可以很快就得到响应(RR的优点);
6. 短进程只用较少的时间就可以完成(SPF)的优点;
7. 不必实现估计进行的运行时间(避免用户作假);
8. 可灵活地调整对各类进程的偏好程度,比如CPU密集型进程、I/O密集型进程(拓展:可以将因I/O而阻塞的进程重新放回原队列,这样I/O型进程就可以保持较高优先级)
是否会导致饥饿
会
比起早期的批处理操作系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括分时操作系统、实时操作系统等)更注重系统的响应时间、公平性、平衡性等指标。而这几种算法恰好也能较好地满足交互式系统的需求。因此这三种算法适合用于交互式系统。(比如UNIX使用的就是多级反馈队列调度算法)
三、 进程同步
1. 进程同步
进程的异步性
各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题
同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
2. 进程互斥
进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免地需要共享一些系统资源(比如内存,又比如打印机、摄像头这样的I/O设备
2.1 两种资源共享方式
- 同时共享方式
系统中的某些资源,允许一个时间段内由多个进程“同时”对它们进行访问 - 互斥共享方式
系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源。
我们把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。
对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系。进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。
2.2 对临界资源的互斥访问,可以在逻辑上分为如下四个部分
- 进入区
负责检查是否可进入临界区,若可进入,则应设置正在访问临界资源的标志(可理解为“上锁”),以阻止其他进程同时进入临界区。 - 临界区
访问临界资源的那段代码 - 退出区
负责解除正在访问临界资源的标志(可理解为“解锁”) - 剩余区
做其他处理
2.3 为了实现对临界资源的互斥访问,同时保证系统的整体性能,需要遵循以下原则:
-
空闲让进。
临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。 -
忙则等待。
当已有进程进入临界区时,其他试图进入临界区的进程必须等待。 -
有限等待。
对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)。 -
让权等待。
当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。
2.4 进程互斥的软件实现方法
学习提示:
- 理解各个算法的思想、原理;
- 结合上节的“实现互斥的四个逻辑部分”,重点理解各个算法在进入区、退出区都做了什么;
- 分析各算法存在的缺陷(结合“实现互斥要遵循的四个原则”进行分析)
单标志法
算法思想:
两个进程在访问完临界区后会把临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予。
- 必须轮流使用,如果某个进程不使用了,另一个进程也将无法再使用该临界资源。
- 违反“空闲让进”原则
表达谦让
双标志先检查法
算法思想
设置一个布尔型数组flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如“flag[0]=true”意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志flag[i]设为true,之后开始访问临界区。
违反“忙则等待”原则。原因在于,进入区的“检查”和“上锁”两个处理不是一气呵成的。“检查”后,“上锁”前可能发生进程切换。
表达意愿
双标志后检查法
算法思想
双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”,但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到先“上锁”后“检查”的方法,来避免上述问题。
虽然解决了“忙则等待”问题,但是又违背了“空闲让进”和”有限等待“原则,会因各进程都撑起无法访问临界资源而产生”饥饿“现象。
Peterson算法
算法思想
结合双标志法、单标志法的思想。如果双方都争着想进入临界区,那可以让进程尝试“孔融让梨”(谦让)。做一个有礼貌的进程~
flag[]数组表达意愿
true变量表达谦让
进入区做了以下几个事情
- 主动争取
- 主动谦让
- 检查对方是否也想使用,且最后一次是不是自己说了“客气话”
用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则。 相较于之前的三种算法是最好的,但依然不够好。
2.5 进程互斥的硬件实现方法
学习提示:
- 理解各方法的原理;
- 了解各方法的优缺点;
中断屏蔽方法
利用“开/关中断指令”实现(与原语实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况)
优点:简单、高效
缺点:
- 不适用于多处理机,关/开中断指令只对当前处理机有效;
- 只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在核心态,这组指令如果能让用户随意使用会很危险)。
TestAndSet指令(TS指令/TSL指令)
简称TS指令,也称为TestAndSetLock指令,或TSL指令。
使用硬件实现的,执行的过程不允许被中断,只能一气呵成。
相比软件实现方法,TSL指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。
优点:
- 实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;
- 适用于多处理机环境。
缺点:
3. 不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。
Swap指令(XCHG指令)
有的地方也叫Exchange指令,或简称XCHG指令。
Swap指令使用硬件实现的,执行的过程不允许被中断,只能一气呵成。
可能硬件实现上不太一样,但逻辑上看,和TS指令差不多,所有优缺点都差不多。
优点:
- 实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;
- 适用于多处理机环境。
缺点: - 不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。
3. 信号量机制
- 在双标志先检查法中,进入区的“检查”、“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题;
- 以上提到的所有的解决方案都无法实现“让权等待”
1965年,荷兰学者Dijkstra提出了一种卓有成效的实现进程互斥、同步的方法——信号量机制
3.1 基本描述
用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。
信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。
原语是一种特殊的程序段,其执行职能一气呵成,不能被中断。原语是由“关中断/开中断指令”实现的。软件解决方案的主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区、退出区的操作都用“原语”实现,使这些操作能“一气呵成”就能避免问题。
一对原语:wait(S)原语和signal(S)原语,可以把原语理解为我们自己写的函数,函数名分别为wait和signal,括号里的信号量S其实就是函数调用时传入的一个参数。
wait、signal原语常简称为P、V操作(来自荷兰语proberen和verhogen)。因此,做题的时候常 把wait(S)、signal(S)两个操作分别写为P(S)、V(S)
3.2 整型信号量
用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。
与普通整数变量的区别:对信号量的操作只有三种,即初始化、P操作、V操作。
存在问题:不满足“让权等待”原则,会发生“忙等”
3.3 记录型信号量
解决了整型信号量不满足“让权等待”的问题
wait原语中,如果剩余资源数不够,使用block原语使进程从运行态进入阻塞态,并将其挂到信号量S的等待队列(即阻塞队列)中
signal原语中,释放资源后,若还有别的进程在等待这种资源,则使用wakeup原语唤醒等待队列中的一个进程,该进程是从阻塞态变为就绪态。
3.4 用信号量机制实现进程互斥、同步、前驱关系
实现进程互斥
- 分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)
- 设置互斥信号量mutex,初值为1
- 在进入区P(mutex)——申请资源
- 在退出区V(mutex)——释放资源
实现进程同步
- 分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码)
- 设置同步信号量S,初始为0
- 在“前操作”之后执行V(S)
- 在“后操作”之前执行P(S)
实现进程的前驱关系
前V后P
每一对前驱关系都是一个进程同步问题
- 为每一对前驱关系各设置一个同步信号量
- 在“前操作”之后对相应的同步信号量执行V操作
- 在“后操作”之前对相应的同步信号量执行P操作
4. 经典的进程同步问题
4.1 PV操作分析步骤:
- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系;
- 整理思路。根据各进程的操作流程确定P、V操作的大致顺序;
- 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)
生产者-消费者问题
缓冲区没满,生产者才能生产;
缓冲区没空,消费者才能消费。
缓冲区是临界资源,各进程必须互斥地访问。
semaphore mutex = 1; // 互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; // 同步信号量,表示空闲缓冲区的数量
semaphore full = 0; // 同步信号量,表示产品的数量,也即非空缓冲区的数量
实现互斥的P操作一定要在实现同步的P操作之后!否则就可能发生死锁。
V操作不会导致进程阻塞,因此两个V操作顺序可以交换。
多生产者-多消费这问题
母亲往盘子装橘子、父亲往盘子装苹果;
女儿从盘子拿苹果、儿子从盘子拿橘子。
互斥关系:
对缓冲区(盘子)的访问要互斥地进行
同步关系(一前一后):
- 父亲将苹果放入盘子后,女儿才能取苹果;
- 母亲将橘子放入盘子后,儿子才能取橘子;
- 只有盘子为空(可以由女儿或儿子触发)时,父亲或母亲才能放入水果
mutex = 1; // 实现互斥访问缓冲区(盘子)
apple = 0; // 盘子中有几个苹果
orange = 0; // 盘子中有几个橘子
plate = 1; // 盘子中还可以放多少个水果
即使不设置专门的互斥变量mutex,也不会出现多个进程同时访问盘子的现象。
原因在于:本题中的缓冲区大小为1,在任何时可,apple、orange、plate三个同步信号量中最多只有一个是1.因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区…
因此,如果缓冲区大小大于1,就必须专门设置一个互斥信号量mutex来保证互斥访问缓冲区。
吸烟者问题
假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸、第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉完成了,供应者就会放另外两种材料在桌上,这个过程一直重复(让三个抽烟者轮流地抽烟)
本质上这题也属于“生产者-消费者”问题,更详细地说应该是“可生产多种产品的单生产者-多消费者”
offer1 = 0; // 桌上组合一的数量
offer2 = 0; // 桌上组合一的数量
offer3 = 0; // 桌上组合三的数量
finish = 0; // 抽烟是否完成
int i = 0; // 用于实现“三个抽烟者轮流抽烟”
读者-写者问题
- 允许多个读者同时对文件执行读操作;
- 只允许一个写者往文件中写信息;
- 任一写者在完成写操作之前不允许其他读者或写者工作;
- 写者执行写操作前,应让已有的读者和写者全部退出。
rw = 1; // 用于实现对共享文件的互斥访问
int count = 0; // 记录当前有几个读进程在访问文件
由第一个读进程负责加锁; 由最后一个读进程负责解锁。
也存在问题:
对count的检查和赋值无法一气呵成,
mutex = 1; // 用于保证对count变量的互斥访问
因此可以设置另一个互斥信号量来保证各读进程对count的访问是互斥的
潜在的问题:
只要有读进程还在读,写进程就要一直阻塞等待,可能“饿死”。因此,这种算法中,读进程是优先的。
semaphore w = 1; // 用于实现“写优先”
分析:
读者1-读者2;
写者1-写者2;
写者1-读者1;
读者1-写者1-读者2
写者1-读者1-写者2
结论:在这种算法中,连续进入的多个读者可以同时读文件;写者和其他进程不能同时访问文件;写者不会饥饿,但也并不是真正地“写优先”,而是相对公平的先来先服务原则。
有的书上把这种算法称为“读写公平法”
读者-写者问题为我们解决复杂的互斥问题提供了一个参考思路。
- 其核心思想在于设置了一个计数器(count)用来记录当前正在访问共享文件的读进程数。我们可以用count的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。
- 另外,对count变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。
- 最后,还要认真体会我们是如何解决“写进程饥饿”问题的。
哲学家进餐问题
- 关系分析。 系统中有5个哲学家进程,5位哲学家与左右邻居对其中间的筷子的访问是互斥关系。
- 整理思路。这个问题中只有互斥关系,但与之前遇到的问题不同的是,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的精髓。
- 信号量设置。定义互斥信号量数组chopstick[5] = {1, 1, 1, 1, 1}用于实现5个筷子的互斥访问。并对哲学家按0~4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5.
如果按常规思路,对左边和右边的筷子分别建立一个互斥信号量,因此执行P操作的话,很可能造成死锁。如何防止死锁的发生呢?
-
可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的。 (数量为4的同步信号量)
-
要求奇数号哲学家先拿左边筷子,然后在哪右边筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。(拿筷子之前判断是奇数号还是偶数号,确定拿筷子顺序)
-
仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。(看起来像是破坏了请求并保持条件,但并不严谨。只是保证了同一时刻只有一个哲学家同时抓起左边和右边的筷子)
关键在于解决进程死锁
5. 管程
为什么要引入管程
信号量机制存在的问题:编写程序困难、易出错
能不能设计一种机制,让程序员写程序时不需要关注复杂的PV操作,让写代码更轻松呢?
1973年,Brinch Hansen首次在程序设计语言(Pascal)中引入了“管程”成分——一种高级同步机制
管程的定义和基本特征
管程的定义
管程是一种特殊的软件模块,有这些部分组成:
- 局部于管程的共享数据结构说明;
- 对该数据结构进行操作的一组过程;
- 对局部于管程的共享数据设置初始值的语句;
- 管程有一个名字。
管程的基本特征
- 局部于管程的数据只能被局部于管程的过程所访问;
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据;
- 每次仅允许一个进程在管程内执行某个内部过程
拓展1:用管程解决生产者消费者问题
由编译器负责实现各进程互斥地进入管程中的过程
管程中设置条件变量和等待/唤醒操作,以解决同步问题
拓展2:Java中类似于管程的机制
Java中,如果用关键字synchronized来描述一个函数,那么这个函数同一时间段内只能被一个线程调用
四、死锁
1. 死锁的概念
1.1 什么是死锁
在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象,就是“死锁”。发生死锁后若无外力干涉,这些进程都将无法向前推进。
1.2 进程死锁、饥饿、死循环的区别
**死锁:**各个进程互相等待对方手里的资源,导致各个进程都阻塞,无法向前推进的现象。
发生死锁的进程一定处于阻塞态
**饥饿:**由于长期得不到想要的资源,某进程无法向前推进的现象。比如:在短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而发生长进程“饥饿”。
也可能阻塞态、也可能就绪态
**死循环:**某进程执行过程中一直跳不出某个循环的现象。有时是因为程序逻辑bug导致的,有时是程序员故意设计的。
可以是运行态。
死锁和饥饿问题是由于操作系统分配资源的策略不合理导致的,而死循环是由代码逻辑的错误导致的。
死锁和饥饿是管理者(操作系统)的问题,死循环是被管理者的问题。
1.3 死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
互斥条件
只有对必须互斥使用的资源的争抢才会导致死锁。
不可剥夺条件
进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
请求和保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。
循环等待条件
存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
注意!
- 发生死锁时一定有循环等待,但是发生循环等待时未必死锁(循环等待时死锁的必要不充分条件
- 但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件了。
1.4 什么时候会发生死锁
- 对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源(CPU)的竞争是不会引起死锁的。
- 进程推进顺序非法。请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程P1、P2分别申请并占有了资源R1、R2,之后进程P1又紧接着申请资源R2,而进程P2又申请资源R1,两者会因为申请的资源被对方占有而阻塞,从而发生死锁。
- 信号量的使用不当也会造成死锁。如生产者-消费者问题中,如果实现互斥的P操作在实现同步的P操作之前,就有可能导致死锁。(可以把互斥信号量、同步信号量也看做是一种抽象的系统资源)
总之,对不可剥夺的资源的不合理分配,可能导致死锁。
1.5 死锁的处理策略
- 预防死锁。破坏死锁产生的四个必要条件中的一个或几个。
- 避免死锁。用某种方法防止系统进入不安全状态,从而避免死锁。(银行家算法)
- 死锁的检测与解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁。
2. 死锁的处理策略——预防死锁
2.1 破坏互斥条件
互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁
如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。比如SPOOLing技术。
操作系统可以采用SPOOLing技术把独占设备在逻辑上改造成共享设备。比如,用SPOOLing技术将打印机改造为共享设备。
使用了SPOOLing技术后,在各进程看来,自己对打印机资源的使用请求立即就被接收处理了,不需要再阻塞等待。
该策略的缺点:并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。
2.2 破坏不可剥夺条件
不剥夺条件: 进程所获得的资源在未使用之前,不能由其他进程强行夺走,只能主动释放。
- 方案一: 当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件。
- 方案二: 当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)
缺点:
- 实现起来比较复杂;
- 释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU;
- 反复地申请和释放资源会增加系统开销,降低系统吞吐量。
- 若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿。
2.3 破坏请求和保持条件
请求和保持条件: 进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。
可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。
该策略实现起来简单,但也有明显的缺点:
有些资源可能只需要利用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。
另外,该策略也有可能导致某些进程饥饿。
2.4 破坏循环等待条件
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。
原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按此规则,已持有大编号资源的进程不可能逆向地回来申请小编号的资源,从而就不会产生循环等待的现象。
该策略的缺点:
- 不方便增加新的设备,因为可能需要重新分配所有的编号;
- 进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费。
- 必须按规定次序申请资源,用户编程麻烦。
3. 死锁的处理策略——避免死锁
3.1 什么是安全序列
所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个。
3.2 什么是系统的不安全状态,与死锁有何联系
**如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。**这就意味着之后可能所有进程都无法顺利地执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
因此可以在分配资源之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法“的核心思想。
3.3 如何避免系统进入不安全状态——银行家算法
银行家算法是荷兰学者Dijkstra为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁。
核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
寻找安全序列的算法叫做安全性算法。安全性算法步骤:
检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收。
不断重复上述过程,看最终能否让所有进程都加入安全序列。
- 最大需求矩阵 M a x Max Max矩阵, M a x [ i , j ] = K Max[i,j]=K Max[i,j]=K表示进程 P i Pi Pi做多需要 K K K个资源 R j Rj Rj。
- 分配矩阵 A l l o c a t i o n Allocation Allocation矩阵,表示对所有进程的资源分配情况。
- M a x − A l l o c a t i o n = N e e d Max - Allocation = Need Max−Allocation=Need矩阵,表示各进程最多还需多少各类资源。
- 另外,还要用一个长度为 m m m的一位数组 A v a i l a b l e Available Available表示当前系统中还有多少可用资源。
某进程 P i Pi Pi向系统申请资源,可用一个长度为 m m m的一位数组 R e q u e s t i Request_i Requesti表示本次申请的各种资源量。
可用银行家算法预判本次分配是否会导致系统进入不安全状态:
- 如果 R e q u e s t i [ j ] < = N e e d [ i , j ] ( 0 < = j < = m ) Request_i[j] <= Need[i,j] (0<=j<=m) Requesti[j]<=Need[i,j](0<=j<=m)便转向2; 否则认为出错,因为它所需要的资源数已超过它所宣布的最大值。
- 如果 R e q u e s t i [ j ] < = A v a i l a b l e [ j ] ( 0 < = j < = m ) Request_i[j]<=Available[j] (0<=j<=m) Requesti[j]<=Available[j](0<=j<=m),便转向3;否则表示尚无足够资源,Pi必须等待。
- 系统试探着把资源分配给进程Pi,并修改相应数据(并非真的分配,修改数值知识为了做预判):
Available = Available - Request;
Allocation[i,j] = Allocation[i,j] + Request_i[j];
Need[i,j] = Need[i,j] - Request_i[j];
- 操作系统执行安全性算法,检查此次资源分配后,系统是否处于安全状态。若安全才正式分配;否则,恢复相应数据,让进程阻塞等待。
4. 死锁的处理策略——检测和解除
4.1 死锁的检测
为了能对系统是否已发生了死锁进行检测,必须:
- 用某种数据结构来保存资源的请求和分配信息
资源分配图
两种结点
- 进程结点:对应一个进程
- 资源结点:对应一类资源,一类资源可能有多个
一般用矩形表示资源结点,矩形中的小圆代表该类资源的数量
两种边
- 进程结点指向资源结点:表示进程想申请几个资源(每条边代表一个)
- 资源结点指向进程结点:表示已经为进程分配了几个资源(每条边代表一个)
- 提供一种算法,利用上述信息来检测系统是否已进入死锁状态。
- 在资源分配图中,找出既不阻塞又不是孤点的进程Pi(即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量。)。消去它所有的请求边和分配边,使之成为孤立的结点。
- 进程Pi所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。根据1中的方法进行一系列简化后,若能消去图中所有的边,则称该图是可完全简化的。
死锁定理
如果某时刻系统的资源分配图是不可完全简化的,那么此时系统死锁。
4.2 死锁的解除
一旦检测出死锁的发生,就应该立即解除死锁
解除死锁的主要方法
- 资源剥夺法。
挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。 - 撤销进程法(或称终止进程法)
强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。 - 进程回退法
让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。
如何决定“对谁动手”
- 进程优先级
- 已执行多长时间
- 还要多久能完成
- 进程已经使用了多少资源
- 进程是交互式的还是批处理式的