文章目录
- 进程管理
- 1. 进程
- 2. 线程
- 3. CPU调度
- 4. 同步与互斥
- 5. 死锁
更多 资料与 笔记
进程管理
1. 进程
1.1. 顺序与并发
进程是OS分配资源,独立运行的基本单位
1.1.1. 前驱图
1️⃣结点:一个语句/一段程序/一个进程
2️⃣ P i → P j P_i\to{}P_j Pi→Pj意思是 P i P_i Pi必须在 P j P_j Pj开始执行前执行完成
3️⃣前驱后继,直接前驱直接后继的概念同离散舒徐。没有前驱的叫初始节点,没有后继的叫终止节点
1.1.2. 程序的顺序执行
1️⃣含义:一个程序分为若干程序,前一个执行完后一个才能执行
2️⃣特点:顺序性,封闭性(独占资源,结果不受外界影响),可再现(相同程序执行结果总相同)
1.1.3. 程序的并发执行
1️⃣含义:无论上个程序是否执行完,下一程序强行执行。导致一定时间内多个程序同时运行且次序不事先确定
2️⃣与顺序执行不同的特点:
- 间断性(异步性):程序走走停停,失去输入时的时序
- 失去封闭性:程序得共享资源,运行互相影响,数据可能被乱改
- 不可再现性:失去封闭性的结果,例如当两个程序共享一个变量时,运行结果是怎么样不可预测
1.1.4. 程序并发执行的条件
1️⃣换而言之:让程序在并发执行时保持封闭/可再现性
2️⃣Bernstein条件(理想化条件)
- 读集 R ( p i ) = { a 1 , a 2 , . . . , a m } R(p_i)=\lbrace{}a_1,a_2,...,a_m\rbrace{} R(pi)={a1,a2,...,am}/写集 W ( p i ) = { b 1 , b 2 , . . . , b m } W(p_i)=\lbrace{}b_1,b_2,...,b_m\rbrace{} W(pi)={b1,b2,...,bm}是 p i p_i pi执行所引用/改变变量集合
- Bernstein条件:对于 p 1 , p 2 p_1,p_2 p1,p2两个程序,满足以下条件就可再现
- 条件1:两次读操作间存储器不变, R ( p 1 ) ∩ W ( p 2 ) = R ( p 2 ) ∩ W ( p 1 ) = ∅ R(p_1)\cap{}W(p_2)=R(p_2)\cap{}W(p_1)=\varnothing R(p1)∩W(p2)=R(p2)∩W(p1)=∅
- 条件2:写操作结果不丢失, W ( p 1 ) ∩ W ( p 2 ) = ∅ W(p_1)\cap{}W(p_2)=\varnothing W(p1)∩W(p2)=∅
PS:串行与并行
1️⃣串行:一个任务执行单元,从物理上就只能执行一个任务,顺序执行在逻辑上也是执行一个任务
2️⃣并行:多个任务执行单元,从物理上多个任务一起执行,并行在逻辑上(一段时间内分时)是多任务但是物理上是单任务(同一时刻不行)
1.2. 进程定义和描述
1.2.1. 进程定义
1️⃣结点定义:是程序在CPU上一次执行过程;是可和别的进程并行执行的计算;程序序在一个数据集合上的运行过程
2️⃣不同OS下的含义:批处理OS中作业=进程(基本认为二者含义相同)。分时OS中进程=用户程序/任务
1.2.2. 进程的特性
1️⃣动态性:创建后产生,调度而执行,得不到资源会暂停,撤销后消亡
2️⃣并发性:可以多个进程都在内存,多个内存同时执行(进程就是因为并发执行才提出)
3️⃣独立性:程是一个能独立运行/分配调度资源的单位
4️⃣异步性:进程以各自独立,不可预知的速度推进
5️⃣结构特征:进程控制块(见后)+程序段(进程中能被调度到CPU上执行的程序代码段)+数据段(初始/中间/执行产生的数据)
1.2.3. 进程&其它概念的辨析
1️⃣进程和程序:进程是程序的执行(动态/静态),进程是暂时的程序是永久的,一个程序可每次执行出不同进程/一个进程可调用多个程序
2️⃣进程和进程映像:进程映像(实体)是进程某一时刻的静态视图(把进程定住,就是进程映像),和进程一样都由程序段+数据段+PCB构成
3️⃣进程和作业:
- 作业是用户向计算机提交任务的任务实体,作业的完成过程为提交+收容+执行+完成。
- 作业会在外存中排队进入内存,作业进入内存后就是进程(作业是提交后的作业的执行过程)
- 一个作业有一个及以上进程,一个进程不能有多个作业
1.3. 进程的状态与转换
1.3.1. 五状态模型
1️⃣就绪:已获得除CPU以外的所有资源,CPU时间片没转到这个进程时
2️⃣执行/运行:获得CPU后在CPU中运行
3️⃣阻塞/等待:正在执行的进程被打断(如IO完成,缺少数据),即使CPU给了进程也执行不了
4️⃣创建:申请空白PCB,填写PCB,系统分配资源,最后转入就绪
5️⃣结束状态:正常执行完/中断退出
1️⃣就绪→执行:进程被进程调度程序选中
2️⃣执行→等待(阻塞):请求,等待某个事件(IO/填补数据)发生完毕
3️⃣执行→就绪:时间片用完or优先级更高的进程插队
4️⃣阻塞→就绪:等待到了某此除CPU以外的资源,被唤醒
5️⃣Tips:进程转化不可都逆(上图),进程间转化并非都是程序主动的(只有执行→阻塞为主动)
1.3.2. 七状态模型:引入了挂起
1️⃣就绪/阻塞挂起:把就绪/阻塞进程从内存丢到外存,保存就绪/阻塞信息,等待重新调入内存
2️⃣引入挂起的意义:优化不活跃的进程,用户可以挂起进程来调查问题,父进程挂起子进程来同步,优化内存等资源利用
1.4. 进程调度
1.4.1. 调度队列
作业队列(所有进程集合)+就绪队列(主存中就绪执行的进程)+设备队列(等待某IO设备的进程)
1.4.2. 三种调度:详见CPU的三级调度
1.4.3. 上下文切换:详见进程切换
1.5. 进程控制数据结构:PCB
1️⃣概述:每个进程都有一个,在进程创建时生成并跟随全程,系统通过PCB识别/感知到/控制/描述进程,常驻内存
2️⃣PCB(进程控制块)的内容
- 进程标识符(PID):每个进程唯一持有,进程创建时创建
- 进程状态:作为进程调度程序分配CPU的依据
- 进程队列指针:记录PCB队列(如就绪队列/等待队列)中下一个PCB的地址,PCB队列有就绪队列/拥塞队列等
- 进程的程序与数据的地址
- 进程优先级(优先级高的可以先被处理器处理)
- CPU现场保护区:进程脱离CPU时,CPU现场信息(PC,寄存器)被保留到PCB中
- 通信信息:与其它进程的通信记录
- 家族联系:比如记录父进程,本进程,子进程,子子进程的关系树
- 占有资源清单:进程所需/当前已分配资源
3️⃣PCB的组织方式:链接(PCB链表)+索引(按索引表找PCB)
1.6. 进程控制基础操作
也就是进程管理,由OS内核实现
1.6.1. 创建进程:从程序到进程
1️⃣进程前驱图(进程家族树):进程创建n个子进程,子进程又创建m个子子进程…
2️⃣进程创建的诱因:
- 用户登录(分时OS):用户在终端输入登录信息,OS为其建立进程然后就绪
- 作业调度(批处理OS):作业调度程序让某个作业装入内存,分配资源,变成就绪进程
- 请求服务:一个进程创建一个子进程,以此类推形成进程树
➕关于父进程子进程
- 资源共享方式:子进程共享父进程所有/部分/资源+无资源共享
- 子进程的执行:父子并发执行,父进程等待子进程终止
- 父子进程的地址空间:两者程序/数据相同,子进程另外加载一个程序
3️⃣进程创建过程:原语,过程为
向OS申请一个PCB(带有PID)→分配资源→初始化PCB(名称/优先级等)→PCB插入就绪队列
4️⃣UNIX中创建进程的实例
int main(int argc, char *argv[]) { /*pid存储进程ID,调用fork()系统函数创建子进程*/ int pid; pid = fork(); /*发生错误,输出错误信息,异常退出*/ if (pid < 0) {fprintf(stderr, "Fork Failed\n");exit(-1);} /*处于新创建的子进程中,子进程的内存空间替换成了"/bin/ls"程序*/ else if (pid == 0) {execlp("/bin/ls", "ls", NULL);} /*处于父进程中,等待父进程的结束,打印子进程结束的信息,正常退出*/ else {wait(NULL);printf("Child Complete\n");exit(0);} }
1.6.2. 进程撤销
1️⃣含义:进程因为完成任务/异常/外界干预,释放各种资源,通过调用exit()返回状态值到OS/父进程
2️⃣撤销原语(OS撤销进程的低级操作)
- 策略:撤销有指定PID的进程,或者顺带其后代进程一起撤销
- 过程:撤销PCB→停止执行(设置重新调度标志)→回收进程占用资源(给OS或者父进程)
3️⃣有关概念
- 僵尸进程:执行完成且释放资源但仍占据进程表的进程,源于其父进程还未调用wait()读取子进程退出状态
- 孤儿进程:父进程已经结束(如崩溃)但还在运行(未完成)的子进程,会被init进程,后者定期执行wait()清除这些子进程
1.6.3. 进程阻塞与唤醒
1️⃣阻塞/唤醒原语:功能分别为使得进程执行→阻塞,阻塞→就绪(不是执行)
2️⃣阻塞/唤醒的原因:
- 阻塞:当一个进程期待的某一事件未出现时,进程主动调用阻塞原语阻塞自己
- 唤醒:当一个进程期待的某一事件出现时,发现者进程主动调用唤醒原语,使得阻塞进程被动唤醒
- PS:发现者进程和唤醒进程并发
3️⃣阻塞原语的操作流程
中断CPU停止进程→保存CPU现场→进程阻塞并加入等待队列→转到进程调度程序去选一个新进程执行
4️⃣唤醒原语的操作流程:将被唤醒进程从等待队列中移出→就绪→插入就绪队列
1.7. 进程控制其他操作
1.7.1. 进程切换/上下文切换(开销较大)
1️⃣含义:处理器从一个进程的运行转到另一个进程的运行
2️⃣进程切换:CPU将旧进程上下文保存在PCB中→加载新进程上下文
3️⃣这一过程中会产生中断,CPU会从用户模式→内核模式→用户模式
1.7.2. 协同进程
1️⃣独立/协同进程:是(协同)否(独立)需要与其他进程共享信息或在执行上相互作用
PS: 进程是否可以进行进程通信和他是独立/协同没有必然联系
2️⃣协同进程的好处:信息共享,模块化,加速运算
1.7.3. 进程通信⬇️
1.8. 进程通信(IPC)
1.8.1. 低级进程通信
其实就是进程的同步P和互斥V,对应的P/V原语为低级进程通信原语
1.8.2. 三种高级进程通信
1️⃣共享存储系统
- 含义:主存中的共享区域,多个进程通过对这个区域读写来实现通信
- 特点:由通信进程确定交换的数据和位置,不受OS控制
2️⃣消息传递系统
- 含义:进程通过建立通信连接(物理的总线/逻辑的程序),通过send/receive交换信息
- 直接通信方式:发送进程发信给接收进程→消息进入接收进程的缓冲队列→接收进程从队列中取得消息
- 间接通信方式:创建邮箱→发送进程把消息给信箱(端口Port)→接收进程从邮箱取得消息→销毁邮箱(Optional),邮箱分为私有(进程创建的)/公有的(OS创建的)
3️⃣管道通信系统
- 管道:连接读写进程,实现二者间通信的共享文件,并不是一个传输通道
- 过程:向管道提供写进程,信息以字符流形式送入管道,而接收管道通过读进程输出
1.8.3. 消息传递的异步/同步
1️⃣零容忍/同步/阻塞:发送者必须等待接收者
2️⃣有限容忍:接收缓冲队列里达到n长后,发送者就必须等了
3️⃣无限容忍/异步/非阻塞:发送者一直发送消息不等待,接收者同样不等待
2. 线程
2.1. 线程的引入
1️⃣早期OS进程的基本属性:拥有资源的独立单位+可调度的基本单位
2️⃣弊端:进程拥有资源又要频繁调度,开销大,限制了并发
3️⃣解决方案:让进程成为拥有资源的单位,不频繁切换;让线程成为调度单位,不拥有资源
2.2. 线程的概念
2️⃣线程定义:作为CPU调度单位(进程只作为资源分配单位),aka轻型进程
3️⃣线程的资源:只拥有必不可少资源(线程状态+程序计数器+寄存器上下文+栈),但是和同属一个进程的线程们共享资源(代码段+数据段+OS资源)
4️⃣多线程:一个进程有多个线程并发执行,一线程改了数据其他线程也使用修改数据, 一线程读文件时其他线程也可同时读
5️⃣线程切换:速度极快,只需切换寄存器+栈
2.3. 进程VS线程
1️⃣调度:耗时
同一进程内切换上下文<进程切换上下文=不同进程的线程切换上下文(需要进程切换)
2️⃣拥有资源:进程拥有资源,线程只拥有必要资源(线程状态+程序计数器+寄存器上下文+栈)不拥有系统资源
3️⃣并发性:引入线程的OS中,进程可并发,同一进程的线程也可并发
4️⃣系统开销:线程切换是涉及少量寄存器内容,开销很小;进程切换需要分配/回收资源
5️⃣通信:线程通信不用OS干预,通过读写进程数据段通信
2.4. 线程的优点
1️⃣响应度高:多线程中即使某线程阻塞,其他线程还可以顶上,不用等待
2️⃣经济性:并发执行的时空开销小(线程建立/终止/切换耗时短),由于共享进程资源故可以减少通信频率,有助于提高并发度
3️⃣多线程更利于多处理器(MP)架构
2.5. 内核线程与用户线程
2.5.1. 核级线程
1️⃣含义:由OS内核创建/撤销的线程,存在于在支持内核级线程的OS中,此时CPU调度的是线程
2️⃣特点:
- 内核维护进程和线程的上下文信息并完成线程切换
- 内核级线程IO操作阻塞时,不影响其他线程
- 处理器时间片分配对象是线程,多个线程的进程将获得更多处理器时间
2.5.2. 用户级线程
1️⃣含义:由用户级线程库管理的线程,线程库创建/管理线程(无需内核),此时CPU调度的是进程
2️⃣特点:
- 用户级线程切换不需要内核特权
- 其调度在应用进程内部进行,可针对应用优化调度,调度过程简单快速(无须用户态/核心态切换)
- 缺点在于OS不知道线程的存在,一个线程阻塞时整个进程都等待
- 处理器时间片分配给进程,进程多线程时,每个线程执行时间减少
2.6. 多线程模型
首先明确一点:用户级/内核级线程通常在同一进程进行映射/管理
1️⃣多对一(不可并发/开销小):多用户级线程映射到一内核级线程
- 优点:线程在用户空间管理,效率高
- 缺点:OS只能识别那个内核级线程,一个内核线程只能执行其中一个用户进程,一个用户级线程堵住进程就会堵
2️⃣一对一(可并发/开销大):一用户级线程映射到一内核级线程
- 优点:一线程阻塞不影响其他线程,可以多线程并行
- 缺点:创建用户线程必须创建内核级线程,开销大
3️⃣多对多(可并发/开销相对小):多用户级线程映射到多内核级线程(内核级线程数不多于用户级线程数),多对多模型允许真正并行,打破用户级线程限制,优化阻塞与调度
2.7. 线程锁
锁的功能越强大,性能就越拉跨
1️⃣互斥锁:信号量,确保同时只有一个线程能够访问特定的资源,一个进程有锁其它的都等待
2️⃣条件锁:条件变量,允许线程在特定条件(互斥锁大哥同不同意)暂停/继续
3️⃣自旋锁:与互斥锁类似,区别在于无法获得资源时,互斥锁会让线程滚/自旋锁会让线程等(不断检测锁是否可用)
4️⃣读写锁:允许多线程同时读共享资源,但只允许一个线程写
2.8. 线程的生命周期(状态转化)
1️⃣初始:创建线程
2️⃣就绪:创建后,通过其它运行线程调用start()方法,加入可运行线程池(就绪)
3️⃣运行:就绪+CPU开始运行,2️⃣+3️⃣也称可运行状态
4️⃣阻塞:线程放弃CPU使用权,暂停,可自动唤醒
5️⃣等待:线程调用wait()方法,进入等待队列释放占用资源,不可自动唤醒(依赖其他线程调用notify()方法)
6️⃣超时等待:与等待的区别仅在于超时等待是等一段时间,等待是一直等
7️⃣终止:线程执行完了
PS:概念补充
PS.1. 线程池
1️⃣含义:一组预先初始化的线程
2️⃣工作方式:要执行某任务时免去建立线程的开销,直接调用线程池中的空闲线程去执行,执行完后也不销毁又丢回线程池
3️⃣好处:资源消耗小,响应快
PS.2. 线程库
1️⃣含义:是程序员创建和管理线程的API
2️⃣分类:用户级线程库(POSIX Pthreads)+内核级(Win32 threads)+其他(Java thread)
3. CPU调度
3.1. CPU的三级调度
3.1.1. 作业调度/高级调度/宏观调度/长程调度
1️⃣内容:选取外存中后备状态的作业→装入内存/IO后建立进程→进程就绪
2️⃣特点:效率低几分钟一次;仅在通用/批处理OS上才有作业调度,决定了有多少程序在多道运行
3️⃣核心问题:多少作业进内存?(由规模速度决定);哪些作业进内存?(先进外存的先进内存/短作业优先等等)
3.1.2. 中级调度/交换调度
1️⃣目的:提高内存利用率/系统吞吐
2️⃣内容:把内存中阻塞进程交换到外存对换区(挂起),必要时再调入内存
3.1.3. 低级调度/进程调度/微观调度/短程调度
从就绪队列→CPU执行,特点是高频
3.2. 调度性能评价指标(调度准则)
1️⃣CPU利用率大:CPU被工作占用时间/总时间
2️⃣系统吞吐大:单位时间CPU完成作业数(但长作业会降低吞吐)
3️⃣响应时间短:用户提交请求到系统首次做出响应的时间
4️⃣周转时间:作业从提交到完成耗时
- 平均周转时间:是指多个作业(如n个作业)周转时间的平均值
- 带权周转时间:作业周转时间与作业实际运行时间(服务时间)的比
5️⃣等待时间短:进程在就绪队列中等待被调度的时间
3.3. 进程调度概述
3.3.1. 进程调度的功能
1️⃣记录所有进程的状态,进程管理模块会把每个进程的状态记录在其PCB中,组织PCB队列
2️⃣选取就绪进程获得CPU资源,开始执行(先来先服务,时间片)
3️⃣处理器分配:在将程序从执行转为就绪/阻塞前保护CPU现场,然后从转为执行时恢复CPU现场
3.3.2. 进程调度的诱因(时机)
1️⃣运行→等待:运行进程因为IO/阻塞原语等阻塞
2️⃣运行→就绪:(抢占调度)有更高优先级程序要用CPU,(分时系统)时间片用完
3️⃣等待→就绪:(抢占调度)执行完系统调用后返回用户进程
4️⃣终止运行:进程运行结束(正常/出错异常)
3.3.3. 不能调度的情况
1️⃣在处理中断:逻辑上中断处理属于OS,不属于任一进程,时刻不能被剥夺CPU资源
2️⃣程序进入OS kernel程序临界区:此时程序需要独占共享数据,所以要加锁防止其他程序进入,也不可切换
3️⃣需要完全屏蔽中断的原子操作过程:加锁/解锁/中断现场保护(恢复)
3.3.4. 进程调度的方式
突然某个更紧迫的进程需要处理,CPU应该如何分配
1️⃣非抢占方式/不可剥夺方式:优先级高的进程进入就绪队列,也要排队(等目前进程结束/阻塞后再执行),实现简单开销小但是实时性差
2️⃣抢占方式/剥夺方式:进程进入就绪队列,可以插队(立即暂现在进程去执行优先级高的进程)
3️⃣抢占原则:什么进程可以插队?
- 时间片原则(分时系统):用完一个时间片后,停止目前程序运行并重新调度
- 优先权原则:优先级高的进入队列,停止目前的进程,去执行优先级更高的进程
- 短作业优先:新到达作业比执行作业明显短时,停止目前的进程,去执行优先级更高的进程
3.3.5. 分派程序
1️⃣定义:OS的一部分,负责按照某种策略(优先级/轮转法)选一个就绪进程给CPU,是就绪到执行的最后一步
2️⃣工作原理:调度器选好进程→分派程序上下文切换(保存前一进程状态/加载下个进程上下文到CPU)
3️⃣分派延迟:分派程序终结上个进程—[分派延迟]—→启动另一个进程
4️⃣如何降低延迟:系统调用可抢占(确保高优先级进程快速响应)+
3.4. 常见调度算法(如何把CPU分配给进程)
调度算法影响的是等待时间,而不能影响进程真正使用CPU的时间和I/O时间
3.4.1. 先来先服务(作业/进程调度)FCFS
1️⃣概述:按进程进入就绪队列的先后来分配CPU,非抢占方式(一旦一个进程占据CPU就会一直执行)
2️⃣特点:有利于长作业不利于短作业;有利于CPU繁忙型不利于I/O繁忙型
3️⃣适用范围:结合其它调度策略使用,例如优先级调度策略中,用一优先级的进程就采用FCFS
3.4.2. 短作业优先调度算法(作业/进程调度)SJF
1️⃣概述:从后备作业/进程队列选估计运行时间最短的几个调入内存,非抢占
2️⃣特点:全部作业同时到达时SJF算法最佳(平均周转时间最短),对长作业不利(长作业容易等到饿死),同时也难以实现
3️⃣最短剩余时间优先调度:SJF的抢占调度版本。当某一进程到达,其时间片比当前执行进程剩余时间片更少时,抢占调度版会强行执行新进程/非抢占调度版本会保持原有进程执行
3.4.3. 优先级调度算法(作业/进程调度)
用整数小/大区分优先级高/低,优先级高的优先分配CPU,所以优先级如何确定?
3.4.3.1. 静态优先级:进程创建时确定后不变
1️⃣按进程类型:系统进程>用户进程,前台作业>后台作业,I/O繁忙的进程>CPU繁忙进程
2️⃣按作业需要的资源:进程占据资源(CPU时间/内存大小/IO类型)越多优先级越低
3️⃣按用户类型和要求:用户收费越高优先级越高(如服务器租用)
3.4.3.2. 动态优先级:优先级随进程推进而改变
1️⃣进程使用CPU情况:使用时间越长优先级越低
2️⃣进程就绪等待情况:等的越久优先级越高
3️⃣进程占用资源情况:占用资源越多优先级高还是低不好说
PS: 一些概念
1️⃣优先级倒置:低优先级进程占据内核数据,高优先级进程必须等
2️⃣优先级继承:低优先进程用高优先资源时提升其优先级,但资源回收后其优先级又会回归原样
3.4.4. 时间片轮转调度(进程调度)RR
1️⃣概述:进程调度程序选择就绪队列中的一个进程,执行一个时间片后将其送入队尾,去执行下一个时间片,以此类推
2️⃣时间片多长(核心问题):太长(所有进程一个时间片内完成)则算法就退化为FCFS,如果时间片太短则切换频繁CPU利用率不高,通常为10-100ms
3️⃣时间片大小确定因素
- 系统响应时间T=N×q=就绪队列中进程数×时间片大小,分时系统对时间片有要求
- 就绪队列进程数
- 系统处理能力:计算机速度越快,单位时间处理命令就越多,时间片越小
4️⃣特点:
- 平均周转时长长于SJF,但是当大多进程在一个时间片内完成,周转时间就会减少
- 响应时间短于SJF
3.4.5. 高响应比(作业调度)
FCFS+短作业优先
0️⃣ 响应比 = 响应时间 预估运行时间 = 作业等待时间 + 作业运行时间 预估运行时间 ≈ 作业等待时间 作业运行时间 + 1 响应比=\cfrac{响应时间}{预估运行时间}=\cfrac{作业等待时间+作业运行时间}{预估运行时间}\approx{}\cfrac{作业等待时间}{作业运行时间}+1 响应比=预估运行时间响应时间=预估运行时间作业等待时间+作业运行时间≈作业运行时间作业等待时间+1
1️⃣概述:每次调度时先计算就绪队列中每个作业响应比,响应比高的优先级高
2️⃣特点:对短作业有利(预估运行时间短)+兼顾长作业(等足够长就优先级高了),但计算响应比增加了开销
3️⃣与其他调度类型相比
- 若等待时间相同,则作业越短,运行时间越短,响应比越大,优先级越高,等于SJF
- 若运行时间相同,则先来的进程等的时间长,优先级高,等于FCFS
3.4.6. 多级队列(进程调度)
1️⃣含义:将进程按照类型/优先级/占用资源分类,每类进程弄一个就绪队列(每个进程固定属于一个),每个队列调度算法不一样
2️⃣实例:就绪队列分为,前台/交互式/时间片调度,后台/批处理/FCFS
3️⃣队列层级的调度
- 队列优先级:比如,前台运行完后再运行后台
- 给队列时间片:给每个队列一个时间片,80%时间执行前台,20%时间执行后台
3.4.7. 多级反馈队列(进程调度)
时间片轮转调度+优先级调度
1️⃣概述:有多个就绪队列,每个队列有优先级,各自按时间片轮转,调度允许进程跨队列移动
2️⃣关于时间片的长度:优先级越高的队列时间片越小,通常第i+1队列的时间片是第i队列时间片的两倍
3️⃣一个进程要放在什么优先级的队列中?(优先级从上到下减小)
- 新进程先进入第一队列末尾(FCFS调度),随之被执行一个时间片,若执行完就退出
- 若一时间内未执行完,就把他丢到第二个队列尾(FCFS调度),以此类推
- 若进程到了优先级最低队列都没执行完,就只有重新塞回本队列尾了
PS: 阻塞进程的优先级低于以上一切队列
4️⃣按什么顺序执行优先级不同的队列?
- 优先执行优先级高的队列:只当第一队列空才执行第二队列进程
- 抢占:执行第二队列进程时,有一进程插入第一队列,则转而执行插入进程(第二队列原来的进程丢到队尾)
3.5. 多处理器调度
1️⃣对称多CPU(SMP):每个CPU都有自己调度方案,他们互斥访问公共就绪队列,领取进程执行
2️⃣非对称多CPU(AMP):只有一个CPU能管理OS资源,其余执行用户级任务,数据共享更容易
PS:进程从一个CPU到另一个CPU需要更新Cache所以开销大,SMP不允许进程迁移到另一个CPU
3.6. 实时调度:基于优先级+抢占
1️⃣目的:完成实时任务()而分配CPU的调度方法
2️⃣实时任务:硬实时(规定时间内必须执行完)+软实时(允许偶尔的延迟)
3️⃣实现:基于优先级调度,任何时候实时进程优先级最高,调度延时小
4. 同步与互斥
4.1. 进程同步的基本概念
1️⃣背景:多个进程对数据并发访问会导致数据不一致(如共享变量修改),所以要保证并发进程按顺序执行
2️⃣进程类型:协作进程、独立进程
3️⃣进程间的制约关系
- 间接互相制约关系(互斥):进程-资源-进程。同种进程互斥共享某种系统资源,如打印机
- 直接互相制约关系(同步):进程-进程。一进程收到另一进程的必要信息,才能继续运行
PS: 一般同类进程互斥,不同进程同步
4️⃣进程间的交互关系
- 互斥:多个进程不能同时使用同一个资源
- 同步:异步执行过程中,相合作的进程在关键点互相等待/交换信息
- 死锁:多个进程互不相让,都得不到足够的资源
- 饥饿:资源被其他进程轮奸,该进程一直得不到它
4.2. 临界资源与临界区
1️⃣临界资源:只能同时给一个进程使用的资源,比如打印机
2️⃣临界区:进程中访问临界资源的一段代码,每进程都有一段临界区代码(可不同),在该区中进程可修改共享变量等,一个进程在其临界区时,同类进程都不可以进入临界区
3️⃣访问临界资源的过程:
- 进入区:检查可否进入临界区的一段代码,若可以则设置相应“正在访问临界区”标志
- 临界区:
- 退出区:属于临界区,清除“正在访问临界区”标志
- 剩余区:代码其余部分
4️⃣进程对临界区互斥访问的实现
- 空闲则入:临界区无进程时,进程请求加入临界区就进吧
- 忙则等待:临界区有进程了,禁止其他请求进入临界区的请求
- 有限等待:进程请求访问临界资源后,就应该在有限时间内加入临界区,不死等
- 让权等待:一个进程不能进入自己的临界区时,释放处理器阻塞自己
4.3. 互斥的实现方式
4.3.1. 软件方法(困难复杂)
1️⃣算法1:两个进程P0,P1使用公共变量turn来实现交替进入临界区
int turn = 0; void processP0() //进程P0 { while(true) //无限循环,表示进程的持续执行 { while(turn != 0); //不为0就卡在这,直到turn为0,P0进入临界区 /*进程P0的代码区*/ turn = 1; //退出区 /*进程P0其它代码*/ } } void processP1() //进程P1 { while(true) //无限循环,表示进程的持续执行 { while(turn != 1); //不为1就卡在这,直到turn为1,P1进入临界区 /*进程P1的代码区*/ turn = 0; //退出区 /*进程P0其它代码*/ } }
- 强制轮流进入临界区,没有考虑进程的实际需要
- 不保证空闲则入:一个进程处于非临界区(即便临界区空闲),另一个进程也进不去临界区
- 例如:P0执行完后,置turn=1他自己就进不去了,而P1此时也没请求进入,临界区就空了
2️⃣算法2:设置标志数组flag[]表示进程是否在临界区中执行
/*每个进程访问临界资源前,检查另一个进程是否在临界区中 *若不在,则修改本进程的临界区标志为真并进入临界区 *退出时,在退出区修改本进程临界区标志为假*/ bool flag[2] = {0,0}; //初始均为假 void processP0() //进程P0 { while(true) //无限循环,表示进程的持续执行 { while(flag[1]); //不为0就卡这,直到falg[1]=0(P1退出临界区了),P0进入临界区 flag[0] = 1; //声明我P0进程在临界区 /*进程P0的代码区*/ flag[0] = 0; //退出区 /*进程P0其它代码*/ } } void processP0() //进程P1 { while(true) //无限循环,表示进程的持续执行 { while(flag[0]); //不为0就卡这,直到falg[0]=0(临界区没东西了),P1进入临界区 flag[1] = 1; /*进程P1的代码区*/ flag[1] = 0; //退出区 /*进程P1其它代码*/ } }
- 此算法保证空闲让进,不保证忙则等待
- 会出现死锁:想象如下场景
P0置flag[0]=0退出→P0执行剩余代码(此时: P1进入临界区→快速执行完后置flag[1]=0)→此时flag数组中两项都为0→两个进程都要进入临界区→都进不了,死锁
3️⃣算法3:设标志组flag(进程是否希望进入临界区)
bool flag[2] = {0, 0}; // 初始化为假,表示两个进程初始时都不希望进入临界区 void processP0() // 进程P0 { while(true) // 无限循环,表示进程的持续执行 { flag[0] = 1; // 声明进程P0希望进入临界区 while(flag[1]); // 如果进程P1也希望进入,则等待 /* 进程P0的代码区*/ flag[0] = 0; // 进程P0不再希望进入临界区 /* 进程P0其它代码*/ } } void processP1() // 进程P1 { while(true) // 无限循环,表示进程的持续执行 { flag[1] = 1; // 声明进程P1希望进入临界区 while(flag[0]); // 如果进程P0也希望进入,则等待 /* 进程P1的代码区*/ flag[1] = 0; // 进程P1不再希望进入临界区 /* 进程P1其它代码*/ } }
- 不满足有限等待:一个进程一直执行,另一个就一直无法进入
- 防止了两进程同时进入临界区,但可能两个进程都进不了临界区(都表示不希望进入)
4️⃣在算法3️⃣基础上加上一个turn变量,turn=0/1表示允许P0/P1进程访问临界区
bool flag[2] = {0, 0}; // 初始化为假,表示两个进程初始时都不希望进入临界区 int turn = 0; // 初始时,让进程P0先进入 void processP0() // 进程P0 { while(true) // 无限循环,表示进程的持续执行 { flag[0] = 1; // 声明进程P0希望进入临界区 turn = 1; // 此时P0还没进去,让进程P1还有机会进入 while(flag[1] && turn == 1); // 如果进程P1也希望进入且turn为P1,则等待 /* 进程P0的代码区 */ flag[0] = 0; // 进程P0退出临界区 /* 进程P0其它代码 */ } } void processP1() // 进程P1 { while(true) // 无限循环,表示进程的持续执行 { flag[1] = 1; // 声明进程P1希望进入临界区 turn = 0; // 此时P1还没进去,让进程P0还有机会进入 while(flag[0] && turn == 0); // 如果进程P0也希望进入且turn为P0,则等待 /* 进程P1的代码区 */ flag[1] = 0; // 进程P1不再希望进入临界区 /* 进程P1其它代码 */ } }
4.3.2. 硬件方法(当前主流)
1️⃣主要思想:通过硬件指令/中断屏蔽,确保关键代码段在不被打断的情况下连续执行,从而保障进程间的互斥访问
2️⃣优势:适用广(进程数随意/处理器数随意),简单(容易验证正确性),支持多临界区
3️⃣缺点:不能让权等待(只能忙等耗费CPU时间),看你饥饿(有的进程可能一直选不上到临界区)
PS—让全等待:进程抛弃CPU资源等待,区别于不放弃CPU的忙则等待
4.4. 信号量semaphore(Dijkstra提出的同步机构)
之前的互斥算法都是平等进程间的协商,信号量使得有一个更高地位的进程管理者来分配资源
4.4.1. 信号量及同步原语
1️⃣信号量是一个二元组 [s,q] ——且初值非负,q为初始为空的队列
- s是信号量的值:初值非负表示可用资源数,其值只能被P(wait)操作/V(signal)操作改变
- q是初始为空的队列:就是该信号量的进程等待队列
2️⃣P/V操作:申请/释放资源,二者成对出现,被视为不可分割原子操作
- 原始版本:会忙则等待,s>0表示可用资源数,s不可为负数
P(S); { if(S<=0); //不做仍和操作 if(S>0) S--; //一个资源被申请走了,所以信号量的值减少 } V(S){S++;} //一个资源被释放了,信号量增加
- 改进版:不会忙则等待,s>0表示可用资源数,s<0表示请求该资源而阻塞的进程数(绝对值)
/*详见记录型信号量*/
4.4.2. 信号量的分类
1️⃣整型信号量:就是上面所提到的(int)s,只有初始化/p/v操作可以改变s。存在忙等,因为P操作后若无资源进程会持续测试s直到其有资源了
2️⃣记录型信号量:int s + 链表q(链接了等待该资源的进程)。P操作后无资源则进程自我阻塞放弃CPU,插入等待链表(让权等待),V操作时唤醒链表中第一个程序
typedef struct { int value; struct process *L; }semaphore; semaphore s; P(s) { s.value--; //可用资源数-1,或者等待资源进程数+1 if(s.value<0) //如果已经没有可用资源了 {/*将该进程加入到s.L中去,然后阻塞*/} } V(s) { s.value++; //可用资源数+1,或者等待资源进程数-1 if(s.value<=0) //如果此刻正在有进程等待这个资源 {/*将该进程从s.L中移除,然后唤醒*/} }
4.4.3. 信号量的应用
PV操作成对出现,同步时PV不在一个进程,互斥时PV在同一进程
1️⃣同步进程:P1(含S1),P2(含S2)两进程并发,S1要在S2之前执行
int N=0; //信号量,初值为0 P1(){...;S1;V(N);...} //执行S1,后通过V操作增加信号量N的值,这表示S1已执行完毕 P2(){...;P(N);S2;...} //P操作试图减少信号量N的值但被阻塞,S1执行完N增加后才执行S2
举例:S1生成S2/S3,S2继续生成S4/S5,最后S3/S4/S5一起生成S6。如下总结就是入为P出为V
2️⃣进程互斥:P1,P2只有一个进程可以进入自己的临界区
int N=1; //互斥信号量,初值为1,只有一个进程可以获得资源 P1(){...;P(N);P1的临界区代码;V(N);...} //临界区代码置于P/V原语之间 P2(){...;P(N);P2的临界区代码;V(N);...}
反过来想,如果P12同时进入临界区N就会变成负数,不可能的,所以只能进入一个
🌘相连两个P操作,同步P应该在互斥P之前(先检查是否满足同步再进入临界区),但是二者的V操作顺序无关紧要
4.4.4. 信号量集:多个信号量的集合
1️⃣概述:用于处理复杂进程同步/互斥,允许进程在执行操作前同时检查多个信号量
2️⃣AND信号量集:
- 功能:保证代码执行前获得多有临界资源(避免锁死)
- 原子操作Swait:要么一次分配所有资源,要么一个都不分配,防止中间态而死锁
- 操作Ssignal:释放所有资源,检查等待队列中是否有其他简称能因此获得全部资源
3️⃣一般信号量集:AND信号量集的扩展
- 允许进程请求/释放不同数目的多种资源
Swait(S1, t1, d1; ...; Sn, tn, dn)
:对每个信号量Si,都设置测试值ti+占用值diSwait(S,m,n); //S每次申请m个资源,不够则阻塞,够则S减n Swait(S1,m1,n1; S2,m2,n2) //S1,S2每次申请m1,m2个资源,不够则阻塞,够则分别减n1,n2 Swait(S,1,1) //表示互斥信号量; Swait(S,1,0) //作为一个可控开关
Ssignal(S1, d1; ...; Sn, dn)
:对每个信号量,只设置占用值diSsignal(S,n) //释放n个资源S=S+n Ssignal(S1,n1; S2,n2) //S1=S1+n1,S2=S2+n2
4.5. 经典同步问题
4.5.1. 生产者-消费者问题
1️⃣问题描述:进程通过共享缓冲区交换数据,生产者写入/消费者读出,共享缓冲区共N个但一个时刻只有一个进程可操作缓冲区
2️⃣三种信号量激起初值
Semaphore full=0; //表示当前已满的缓冲区数量 Semaphore empty=n; //表示当前空的缓冲区数量 Semaphore mutex=1; //互斥信号量,用于确保同一时刻只有一个进程对缓冲区进行操作
4️⃣P/V操作
P(empty) //检查是否有空余位来填充,若没有则阻塞直到有空位 V(empty) //填充完了一个空闲区 P(full) //检查缓冲区是否有数据可以来取,若没有就阻塞直到有数据可读 V(full) // P(mutex) //取得互斥锁,告诉其他进程临界区我占了你们都别来 V(mutex) //释放互斥锁,高速
4️⃣实现:P操作顺序不可倒(先检查资源数目,再检查是否互斥)否则可能死锁
死锁情况:生产者先执行P(mutex)进入缓冲区→缓冲区满但执行P(empty)→没有空位然后阻塞
4.5.2. 读者-写者问题
多进程共享数据区,进程分为读者写者(读者只能读/写者只能写),同一时刻可多读但最多一写
4.5.2.1. 读者优先:写者排队,读者插队,多读一写
1️⃣示例:(最左边表示最新到达的进程)
[W][W][W][W][W]-[数据区:R]--读者优先+R读完后-->[ ][W][W][W][W]-[数据区:W] [R][W][R][R][W]-[数据区:W]--读者优先+W写完后-->[ ][ ][ ][W][W]-[数据区:RRR] [R][W][R][R][W]-[数据区:R]--不可能出现这种情况 [R][W][W][W][W]-[数据区:R]--读者优先+R没读完-->[ ][W][W][W][W]-[数据区:RR]
2️⃣信号量
readcount=0//记录读者的数量 rmute=1 //保证读者进程对readcount的互斥访问(只有当前的唯一读者可修改readcount) mutex=1 //标识允许写
3️⃣实现
reader() { while(1) { P(remutex); //申请readcount的使用权 if(readcount==0);P(mutex);//第一个读者,阻止写入 readcount++; //读者数量+1 V(rmutex); //释放readcount的使用权,允许其他读者读 /*读操作,完成读操作后:*/ P(remutex); //申请readcount的使用权 readcount--; //读者数量-1 if(readcount==0);V(mutex);//读者全部读完了,就允许写入 V(rmutex); //释放readcount的使用权,允许其他读者读 } } write() { while(ture) { P(mutex); //允许写 /*写操作,完成写操作后:*/ V(mutex); //释放写的许可 } }
4.5.2.2. 平等策略:读者写者都要排队,不可插队,仍然多读一写
1️⃣示例:(最左边表示最新到达的进程)
[W][W][R][R][R]-[数据区:W]--平等策略+W写完后-->[ ][ ][ ][W][W]-[数据区:RRR] [W][W][R][R][R]-[数据区:R]--这种情况不可能出现 [R][W][R][W][W]-[数据区:R]--平等策略+R读完后-->[ ][R][W][R][W]-[数据区:W]
2️⃣信号量
readcount=0//记录读者的数量 rmute=1 //保证读者进程对readcount的互斥访问(只有当前的唯一读者可修改readcount) mutex=1 //标识允许写 wmutex=1: //是否存有在写/等着写的写者,存在的话就禁止新读者进入
3️⃣实现
reader() { while(1) { P(wmutex); //是否有写者存在(多读一写→不可能全是读者等),无则进 P(remutex); //申请readcount的使用权 if(readcount==0);P(mutex);//如果这是第一个读者,那么占据数据区阻止其他写着进入 readcount++; //读者数量+1 V(rmutex); //释放readcount的使用权,允许其他读者用 V(wmutex); //恢复wmutex /*读操作,完成读操作后:*/ P(remutex); //申请readcount的使用权 readcount--; //读者数量-1 if(readcount==0);V(mutex);//如果读者都没有了,就允许写者进入 V(rmutex); //释放readcount的使用权,允许其他读者使用 } } write() { while(ture) { P(wmutex); //检测是否有其他写者存在,无写者时进入 P(mutex); //申请对数据区进行访问 /*写操作,完成写操作后:*/ V(mutex); //释放数据区,允许其他进程读写 V(wmutex); //恢复wmutex } }
4.5.2.3. 写者优先:读者排队,写者插队,多读一写
1️⃣示例:(最左边表示最新到达的进程)
[W][W][R][R][R]-[数据区:W]--写者优先+W写完后-->[ ][W][R][R][R]-[数据区:W] [ ][W][R][R][R]-[数据区:W]--写者优先+W写完后-->[ ][ ][R][R][R]-[数据区:W] [ ][ ][R][R][R]-[数据区:W]--写者优先+W写完后-->[ ][ ][ ][ ][ ]-[数据区:RRR]
队列中有读者写者时,先按顺序执行完所有写者,然后才开始执行读者
2️⃣信号量
readcount=0 //记录读者的数量 writecount=0//记录写者的数量 rmutex=1 //保证读者进程对readcount的互斥访问(只有当前的唯一读者可修改readcount) wmutex=1 //保证写者进程对writecount的互斥访问 mutex=1 //互斥访问数据区 readable=1 //表示当前是否有写者
3️⃣实现
reader() { while(1) { P(readable) //检查是否存在写者,若没有则占用 P(remutex); //申请readcount的使用权 if(readcount==0);P(mutex);//如果这是第一个读者,那么占据数据区阻止其他写着进入 readcount++; //读者数量+1 V(rmutex); //释放readcount的使用权,允许其他读者使用 V(readable); //释放readable,允许其他读者或写者占用 /*读操作,完成读操作后:*/ P(remutex); //申请readcount的使用权 readcount--; //读者数量-1 if(readcount==0);V(mutex);//如果读者都没有了,就允许写者进入 V(rmutex); //释放readcount的使用权,允许其他读者使用 } } write() { while(ture) { P(wmutex); //检测是否有其他写者存在,无写者时进入 if(writecount==0) P(readable); //若为第一个写者,则阻止后续读者进入 writecount++; //写者数量加1 V(wmutex); //释放wmutex,允许其他写者修改writecount P(mutex); //等当前正在操作的读者或写者完成后,占用数据区 /*写操作,完成写操作后:*/ V(mutex); //写完,释放数据区,允许其他进程读写 P(wmutex); //占用wmutex,准备修改writecount writecount--; //写者数量减1 if(writecount==0) V(readable); //若为最后一个写者,则允许读者进入 V(wmutex); //释放wmutex,允许其他写者修改writecount } }
4.5.3. 哲学家进餐问题
1️⃣问题描述:5人围桌而坐,两人间各一根筷子(临界资源),每人有两个动作,进餐(先左后右拿起筷子)和思考(先左后右放回筷子)
2️⃣死锁:所有人都同时拿起左筷子,同时等待右筷子(但等不到)
3️⃣避免死锁:赶走一个人/同时拿起左右筷子/给人编号然后奇数先拿左边偶数先拿右边再奇偶交替
4.5.4. 理发师问题
1️⃣描述:理发师+理发椅+等待椅,无顾客时理发师就会在理发椅上睡觉,顾客到达会唤醒理发师,理发时新顾客会在等待椅空闲/满时选择等待/离开
2️⃣问题核心:保证顾客对于理发师的互斥访问,确保等待队列满后顾客会走,服务完一个顾客后会服务下一个顾客
3️⃣解决方案:使用信号量来控制对临界资源的访问
- 5个信号量:记录等待顾客数,代表理发椅,代表等待凳子,两个记录理发师和顾客的同步
- 临界资源:凳子和理发椅
4.6. 管程:优于信号量的进程同步机构(了解即可)
1️⃣定义:关于共享资源的数据结构及一组针对该资源的操作过程所构成的软件模块
2️⃣基本思想:把信号量+操作原语(共享变量+对共享变量的操作)封装在一个对象内部
3️⃣功能:集中管理进程中互斥访问的临界区,保证进程对于共享资源的互斥访问
4️⃣特点:局部于管程的数据只能被管程内部访问,进程只有通过调用进入管程才能访问共享数据,每次只允许一个进程调用管程
5️⃣管程的同步设施
- 条件变量:仅能从管程内进行访问,用于表示进程不同的等待原因
- wait和signal函数:进程调用wait后会被阻塞然后释放管程,调用signal唤醒在该条件变量上阻塞的进程
5. 死锁
5.1. 死锁概念于特征
1️⃣概念:两个及以上进程互相等待对方资源,无外力作用就无法推进执行
2️⃣实例:进程P1占用打印机且申请IO设备,进程2占用IO但是申请打印机
3️⃣死锁的特点:
- 互斥:对于资源,一次性只能有一个进程访问
- 占有并等待:进程至少占有一个进程,等待另一个被其他进程占有的资源
- 不可抢占:进程要在执行完后才释放资源,不会中途被抢走
- 循环等待:等待资源的进程间存在环
总结:至少两个占有资源但又等待资源的进程才会产生死锁
5.2. 死锁的必要条件
5.2.1. OS的资源分类
1️⃣根据资源性质(事实上进程可抢占与否完全取决于资源类型)
- 可剥夺/抢占资源:别的进程可以从本进程处把这个资源抢走(打印机)
- 不可剥夺/抢占资源:除非本进程释放,别的进程根本抢不走(主存/CPU)
2️⃣根据使用方式:共享/独占资源
PS—共享资源的获取与释放:请求然后得到资源(不被立即允许时一直等待)→使用→释放
3️⃣根据使用期限:永久资源(无法被消耗,如打印机)+临时资源(可以被消耗殆尽,如内存)
5.2.2. 死锁产生的原因
并发执行的资源竞争+系统资源不足(根本原因)+进程推进顺序不当(直接原因)
如下图只有4(落入虚线方框内)会锁死
5.2.3. 进程锁死的必要条件
1️⃣互斥条件:一段时间内某种资源仅为一个进程占有
2️⃣不剥离条件:资源在未使用完之前,不能被其他进程夺走
3️⃣请求与保持条件:进程申请新资源的同时,继续占有以获得的资源
4️⃣环路等待条件:如下
- 进程P1已经拥有资源A,但它还需要资源B来继续运行
- 进程P2已经拥有资源B,但它还需要资源C来继续运行
- 进程P3已经拥有资源C,但它还需要资源A来继续运行
5.3. 死锁的处理
1️⃣ 鸵鸟算法:不对死锁进行任何处理,如UNIX,降低了系统复杂性/但死锁时会导致资源浪费和响应延长
2️⃣ 死锁预防:设置严格限制来破坏死锁的必要条件,如可剥夺式的进程调度(优先级),系统是永远不会死锁了/但会对影响并发性的性能
3️⃣ 死锁避免:分配资源时OS会预测接下来会不会死锁(再决定要不要这么分配资源),如银行家算法,比死锁预防更有利于并发执/但预测操作会增加计算成本
4️⃣ 死锁检测及解除:(被动策略)OS定期检索是否死锁然后在死锁后采取措施,如剥终止or剥夺某进程资源来打破死锁,OS可以在大多数时间内高效地运行,但死锁后代价大
5.4. 死锁的预防:四个必要条件各个击破
1️⃣互斥条件:资源只能给一个进程→能给多个进程,但是这会打破进程固有属性(两个进程公用打印机?)所以不实际
2️⃣不剥夺(非抢占)条件:进程持有资源后就霸占→若该进程新的资源请求不被满足就放弃之前已经有的资源,但是这太复杂还可能造成以前工作作废(打印到一半丢掉)
3️⃣请求与保持条件:进程持有资源后请求其他资源→强制进程一次性申请得到所有资源后再运行,但是这样资源利用率低且进程容易饥饿
4️⃣环路等待条件:将所有的资源类型放入资源列表中,并且要求进程按照资源表申请资源;编号递增申请,但限制了新设备的增加(重写编号),吞吐量低
5.5. 死锁的避免:相比死锁的预防限制更弱
5.5.1. 安全/不安全状态
1️⃣基本概念
- 安全状态:系统按某顺序(安全序列,不唯一)为每进程分配其所需资源,保证每个进程都可顺利完成
- 不安全状态:不存在上述的安全顺序,但是不一定所有不安全状态都有死锁,只是可能(有些资源执行到一半就放弃了)
2️⃣安全状态实例:P1-3共享一个资源,资源总数为10
可用资源按照P2→P1→P3顺序配是安全的(安全序列),如果可用资源先分给P1就会直接锁死
3️⃣死锁避免的核心:使OS一直处于安全状态之一的状态
分配资源前先计算分配的安全性→确认能安全分配再分配
5.5.2. 资源分配图算法:每种资源只有一个实例
1️⃣请求边与需求边
- 请求边:实线 < P i , r i > <P_i,r_i> <Pi,ri>表示 P i P_i Pi请求一个 r i r_i ri资源且尚未分配
- 需求边:虚线 < P i , r i > <P_i,r_i> <Pi,ri>表示 P i P_i Pi可能会请求一个 r i r_i ri资源
- 转化:申请资源后虚线转实线,释放资源后实现转虚线
2️⃣死锁预防
把申请边实线 < P i , r i > <P_i,r_i> <Pi,ri>转化为分配边 < r i , P i > <r_i,P_i> <ri,Pi>,如果出现环路则不安全,如果不出现环路那么安全可分配
5.5.3. 银行家算法:每种资源可有多个实例
5.5.3.1. 算法中用到的数据结构
假设有进程 ( P 1 , P 2 … P n ) (P_1,P_2\dots{}P_n) (P1,P2…Pn)and资源 ( R 1 , R 2 … R m ) (R_1,R_2\dots{}R_m) (R1,R2…Rm),则用到的数据结构
Available[i] //可用资源向量,表示第Ri类资源的现有空闲数量 Request[i][j] //请求矩阵,表示进程Pi请求的Rj类资源的数量 max[i][j] //最大需求矩阵,表示进程Pi对Rj类资源最大需求数 Allocation[i][j] //分配矩阵,进程Pi对Rj类资源的持有数 Need[i][j] //需求矩阵,进程Pi对Rj类资源仍然需要的数目 Need[i] //Pi的资源需求向量,即所需全部资源类型及数目
Need[i] [j] = Max[i] [j] - Allocation[i] [j]
5.5.3.2. 银行家算法的描述
1️⃣银行家算法
2️⃣安全性检测算法:
- 先要建立两个向量Work(可用资源)和Finish(进程结束)
Work=Available; Finish[i]=false; //表示Pi进程还未执行完
- 找到符合条件的进程:未结束+所需资源小于系统可用
- 如果有这样的进程则执行完进程后释放其所有资源Allocation
- 找不到这样的进程的话有两种可能,那就是全部执行完了(安全),否则不安全
5.5.3.3. 银行家算法实例
1️⃣题目
2️⃣解答
5.6. 死锁的检测
5.6.1. 资源分配图:SRAG=(V,E)的有向图
1️⃣点集:分为两类,每个进程 P i P_i Pi一个点(圆圈),一类资源 r i r_i ri一个点(方框),而一类资源中还可能有多个资源则用方框中的点来表示(每个资源表示一个实例)
2️⃣有向边集: < P i , r i > <P_i,r_i> <Pi,ri>表示 P i P_i Pi请求一个 r i r_i ri资源且尚未分配(申请边), < r i , P i > <r_i,P_i> <ri,Pi>表示 r i r_i ri资源中一个资源已经分配给了 P i P_i Pi(分配边)
3️⃣图例:
5.6.2. 死锁定理:用SRAG检验系统是否死锁
1️⃣图中的非阻塞进程:首先要有边与进程结点连接(允许仍未结束),其次该节点申请的资源数要小于等于该类资源的空弦数。如下图的P1就是非阻塞的
1️⃣可完全简化
找到非阻塞结点,该节点会执行到底然后释放资源,然后孤立。如下图
其他进程因为得到了被释放的资源,也开始执行。在本例中是P2
按照这个规则周而复始,如果最后图中无边则图是可完全简化的,否则得到唯一的不可简化图
2️⃣死锁定理:状态S是死锁 ⟺ \iff ⟺S状态的资源分配图不可完全简化
5.6.3. 死锁检测方法
1️⃣死锁的必要性检测:
- 如果SRAG无环,那么比不可能死锁
- 如果SEGA有环,每类资源都只有一个实例,则一定死锁。否则见下:
2️⃣死锁的检测(他妈就是死锁定理吧!):确定是否存在一种方式使所有进程都可以获得所需资源并运行完
- Work表示当前可用的资源,如果Allocation不为零(分配有资源)则Finish为假(进程不该结束),反之
- 然后寻找这样的进程:进程仍未结束,请求的资源小于可用资源
- 如果有这样的进程就让他执行完然后释放所占资源
- 没有的话,要么所有进程都执行完了,要么就死锁了
5.7. 死锁的解除
1️⃣剥夺资源:从其他进程处抢来足够资源解除死锁
2️⃣撤销进程:灭掉一些进程为其他进程提供更多资源
3️⃣进程回退:根据记录信息,进程回到死锁前(自愿放弃资源)
PS. 活锁/饥饿/饿死
1️⃣饥饿:进程长时间等待
2️⃣饿死:进程等待时间过长,即使得到资源执行了也无意义了
3️⃣活锁:特殊的一种饥饿,进程在执行但是无法被调度前进,像被死锁了一样