考纲内容
- 进程与线程
- 进程的概念
- 进程的状态与转换
- 进程控制
- 进程组织
- 进程通信
- 线程概念与多线程模型
- 处理机调度
- 调度的基本概念
- 调度时机、切换与过程
- 调度的基本准则
- 调度方式
- 典型调度算法
- 进程同步
- 进程同步的基本概念
- 实现临界区互斥的基本方法
- 信号量、管程、经典同步问题
- 死锁
- 死锁的概念
- 死锁处理策略
- 死锁预防、死锁避免、死锁的检测和解除
1.进程与线程
1. 进程的概念和特征
1. 进程的概念
- 进程映像:由程序段、相关数据段和PCB构成,静态
- 创建进程:实质是创建进程映像中的PCB(进程存在的唯一标志)
- 定义
- 进程是程序的一次执行过程,具有生命周期
- 进程是一次程序及其数据在处理机上顺序执行时所发生的活动
- 进程是具有独立功能的程序在一个数据集合上运行的过程,是资源分配和调度的独立单位(没有引入线程)
- 引入目的:为了更好地描述和控制程序的并发执行,实现操作系统的并发性和共享性
- 进程与程序的区别
- 进程由程序、数据和PCB组成;程序是一组代码的集合
- 进程是动态的,是动态地创建和消亡的;程序是静态的,永久存在的
- 一个进程可以执行一个或几个程序;一个程序也可构成多个进程
2. 进程的特征
- 动态性:最基本的特征,进程有着创建、活动、暂停、终止等过程,具有生命周期
- 并发性:多个进程实体同时存在内存中
- 独立性:进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位
- 异步性:进程相互制约,以不可预知的速度向前推进(导致结果的不可再现性)
- 结构性:每个进程都配置一个PCB对其进行描述
2. 进程的状态和转换
1. 状态
- 运行态:进程正在处理机上运行
- 就绪态:进程已处于准备运行状态(已获得除CPU外的所有资源)
- 阻塞态:进程正在等待某个事件而暂停运行
- 创建态:进程正在被创建,还没进入就绪态
- 创建进程的步骤
- 申请一个空白的PCB,并向PCB中填写一些控制和管理进程的信息
- 系统为该进程分配运行时所需的资源
- 把该进程转入就绪态
- 创建进程的步骤
- 结束态:进程正在从系统中消失(包括正常结束或异常终止)
- 注意
- 前三种是进程的基本状态
- 进程的生命周期中,大部分时间处于三种基本状态
2. 相互转换
- 就绪态 → \rightarrow →运行态:处于就绪态的进程获得处理机进入运行态
- 运行态 → \rightarrow →就绪态:处于运行态的进程时间片用完后,让出处理机进入就绪态
- 运行态 → \rightarrow →阻塞态:进程请求除处理机外的其他资源,此时运行态进入阻塞态(系统调用请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式)——进程的主动行为
- 阻塞态
→
\rightarrow
→就绪态:进程等待其他资源的获得,如IO资源、或者中断结束——进程的被动行为(需要其他相关进程的协助)
3. 进程控制
- 主要功能:对系统中的所有进程实时有效的管理
- 原语
- 定义:控制进程用的程序段(用开中断、关中断配合实现)
- 操作
- 更新PCB中的信息:修改进程状态,保存/恢复运行环境
- 将PCB插入合适的队列
- 分配/回收资源
- 特点:允许一个进程创建另一个进程
- 子进程:被创建的进程
- 可以继承父进程所拥有的资源
- 子进程被撤销时,继承的资源要归还给父进程
- 父进程:创建者
- 父进程被撤销时,其子进程不一定被撤销
- 子进程:被创建的进程
1. 进程的创建——创建原语
- 分配唯一进程标识号,申请PCB(PCB是有限的)
- 为进程分配资源,为程序和数据以及用户栈分配必要的内存空间(若资源不足,则处于阻塞态,等待资源)
- 初始化PCB,包括初始化标识信息、处理机状态信息、处理机控制信息,设置进程的优先级
- 若进程就绪队列可以接纳新进程,进程就进入就绪队列
2. 进程的终止——撤销原语
- 引起终止的事件
- 正常结束:进程任务已完成并准备退出运行
- 异常结束:发生了异常事件,使程序无法继续运行
- 外界干预:进程应外界的请求而终止运行
- 结束过程
- 根据被终止进程的标识符,检索PCB,读取进程状态
- 若进程处于运行态,终止运行,剥夺处理机
- 其子进程要么一并被终止,要么被init进程领养
- 将该进程所拥有的全部资源还给父进程或者操作系统
- 将PCB从队列中删除
3. 进程的阻塞–阻塞原语
- 执行时间:正在执行的进程,由于期待的某些事件(如:请求系统资源失败、等待某种操作的完成)未发生,由系统自动执行阻塞原语
- 阻塞原语——被阻塞进程自我调用
- 找到要被阻塞进程标识号对应的PCB
- 若该进程处于运行态,保护其现场,将其状态转为阻塞态,停止运行
- 把该PCB插入相应事件的等待队列,将处理机资源调度给其他就绪进程
4. 进程的唤醒–唤醒原语
- 执行时间:被阻塞进程所期待的事件发生时,由有关进程调用唤醒原语
- 唤醒原语
- 找到等待队列中进程相应的PCB
- 将其从等待队列中移出,置其状态为就绪态
- 将PCB插入就绪队列,等待调度程序调度
5. 进程切换
- 注: 进程切换是在内核态下完成的
- 过程
- 保存处理机上下文,包括程序计数器和其他寄存器
- 更新PCB信息
- 把进程的PCB移入相应的队列
- 选择另一个进程执行,更新其PCB
- 更新内存管理的数据结构
- 恢复处理机上下文
- 引起进程切换的事件
- 当前进程时间片用完
- 有更高优先级的进程到达
- 当前进程主动阻塞
- 当前进程终止
- 进程切换与处理机模式切换(变态)
- 处理机模式切换:CPU逻辑上可能还在同一进程中运行。若进程因中断或异常进入核心态运行,执行完后又回到用户态刚被中断的程序运行,则操作系统只需恢复进入内核时所保存的CPU现场,而无须改变当前进程的环境信息
- 进程切换:运行进程改变,故当前进程的环境信息也需要改变
- 调度和切换的区别——先有调度,再有进程切换
- 调度:决定资源分配给哪个进程的行为,是一种决策行为
- 切换:实际分配的行为,是执行行为
4. 进程的组织
- 进程:一个独立的运行单位,也是操作系统进行资源分配和调度的基本单位(未引入线程)
1. 组成
- PCB——核心(进程存在的唯一标志,常驻内存,随时都可存取)
- 进程描述信息
- 进程标识符:标志进程(唯一)
- 用户标识符:进程归属的用户,主要为共享和保护服务
- 进程控制和管理信息
- 进程当前状态:描述进程状态信息(处理机分配调度的依据)
- 进程优先级:描述进程的优先级
- 代码运行入口地址
- 进入内存时间
- 处理机占用时间
- 信号量使用
- 资源分配清单
- 用以说明有关内存地址空间或者虚拟地址空间状况
- 代码段指针、数据段指针、堆栈段指针、文件描述符、键盘、鼠标
- 处理机相关信息
- 处理机中各寄存器的值
- 通用寄存器值、地址寄存器值、控制寄存器值、标志寄存器值、状态字
- 进程描述信息
- 程序段 :能被进程调度程序调度到CPU执行的程序代码段
- 数据段 :进程对应的程序加工处理的原始数据或者程序执行时产生的中间结果
2. PCB的组织方式
- 链接方式:按照进程状态将PCB分为多个队列
- 特点:操作系统持有指向各个队列的指针
- 特点:操作系统持有指向各个队列的指针
- 索引方式:根据进程状态的不同,建立几张索引表
- 特点:操作系统持有各个索引表的指针
5. 进程的通信
- 定义:进程之间的信息交换
- 高级通信方式:以较高的效率传输大量数据的通信方式
1. 共享存储
- 定义:通信进程之间存在一块可以被直接访问的共享空间
- 分类
- 低级方式:基于数据结构共享
- 高级方式:基于存储区共享
- 注意
- 操作系统负责为通信进程提供可共享的存储空间和同步互斥工具
- 数据交换则由用户自己安排读/写命令完成
- 进程运行期间一般不能访问其他进程的空间(需通过特殊的系统调用实现)
- 进程内的线程是自然共享进程空间的
2. 消息传递
- 定义:进程间的数据交换是以格式化的消息为单位的,进程通过系统提供的发送消息和接收消息的两个原语进行数据交换
- 适用情况:通信进程间不存在可直接访问的共享空间
- 两种方式
- 直接通信方式:发送进程直接发送消息给接收进程,并把它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息
- 间接通信方式:发送进程把消息发送给某个中间实体(信箱),接收进程从中间实体获得消息
- 直接通信方式:发送进程直接发送消息给接收进程,并把它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息
3. 管道通信
- 定义:发送进程以字符流形式将大量数据送入写管道,接收进程从管道中接收数据
- 管道:用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件
- 管道机制必须具备的条件
- 互斥
- 同步
- 确定对方的存在
- 管道不同于文件的地方
- 限制管道的大小:一个固定大小的缓冲区。管道满的时候阻塞写进程
- 管道变空的时候阻塞读进程
- 特点
- 半双工通信
- 只要管道没空,读进程就可以从管道读数据
- 只要管道没满,写进程就可以往管道写数据
- 管道中的数据被读取后会马上丢弃
- 缓冲区只允许一边写入,另一边写出
- 半双工通信
6. 线程概念和多线程模型
1. 线程基本概念
- 定义
- 是一个基本的CPU执行单元,也是程序执行流的最小单元
- 由线程ID、程序计数器、寄存器集合和堆栈组成
- 不拥有系统资源,只拥有一点在运行中必不可少的资源
- 可与同属一个进程的其他线程共享进程所拥有的全部资源
- 引入目的
- 进程:更好地使多道程序并发执行,提高资源利用率和系统吞吐量
- 线程:减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能
2. 线程与进程的比较
3. 线程属性
- 多线程操作系统:线程作为独立运行(调度)的基本单位,进程不再是一个可执行实体,但仍具有与执行相关的状态(进程处于运行态,实质是该进程中的某线程正在执行)
- 线程的主要属性
- 不拥有系统资源,拥有唯一标识符(ID)和线程控制块(TCB)(线程控制块记录了线程执行的寄存器和栈等现场状态)
- 不同的线程可以执行相同的程序,同一个服务程序被不同用户调用时,操作系统将其创建为不同线程
- 同一进程中的各个线程共享该进程所拥有的资源
- 线程是处理机的独立调度单位,多个线程可以是并发执行的
- 多CPU系统中,各线程可同时占用不同的CPU
- 线程也有生命周期,阻塞、就绪、运行等状态
- 由于共享内存地址空间,同一进程中的线程间通信甚至无需系统干预
4. 线程的实现方式
- 用户级线程
- 有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在
- 线程切换在用户态下即可完成
- 内核级线程——处理机分配的单位
- 线程的管理工作全部由内核完成
- 线程切换要从用户态–>核心态
5. 多线程模型
1. 多对一模型
- 定义:将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成,用户级线程对操作系统不可见
- 优点:线程管理是在用户空间进行的,效率比较高
- 缺点:一个线程阻塞全部线程都会阻塞,多个线程不能并行运行在处理机上
2. 一对一模型
- 定义:每个用户级线程映射到一个内核级线程上
- 优点:并发能力强
- 缺点:创建线程开销大,影响应用程序的性能
3. 多对多模型
- 定义:n个用户级线程映射到m个内核级线程上(m<=n)
- 特点:结合上述两种,既提高了并发性,又适当的降低了开销
2. 处理机调度
1. 调度的概念
- 定义: 合理的对进程进行处理机分配
- 层次
- 作业调度(高级调度):从辅存中选择作业送入内存,每个作业只调入一次,调出一次
- 内存调度(中级调度):提高内存利用率和系统吞吐量,将已经具备运行条件的进程调入内存,修改其状态为就绪态
- 进程调度(低级调度):按照某种策略从就绪队列中选取一个进程,将处理机分配给它
- 三级调度的联系
- 七状态模型
- 挂起态与阻塞态的区别
- 两种状态都是暂时不能获得CPU的服务
- 挂起态将进程映像调到外存,阻塞态进程映像还在内存中
2.调度的时机、切换与过程
- 进程调度和切换程序是操作系统内核程序
- 进程调度
- 广义的进程调度
- 选择一个进程
- 进程切换
- 狭义的进程调度
- 选择一个进程
- 广义的进程调度
- 进程切换
- 特点
- 调度完成后立刻发生
- 保存元进程当前切换点的现场信息,恢复被调度进程的现场信息
- 特点
- 不能调度和切换的情况
- 处理中断过程中:中断处理逻辑上不属于某一进程,不应被剥夺处理机资源
- 进程在操作系统内核程序临界区中
- 进程在普通临界区时是可以调度、切换的
- 内核程序临界区是用来访问某种内核数据结构的
- 其他需要完全屏蔽中断的原子操作过程:如:加锁、中断现场保护、恢复现场
- 能切换的情况
- 发生引起调度条件且当前进程无法继续进行
- 中断处理结束或者自陷处理结束后,返回被中断进程的用户态程序执行现场前
3. 进程调度方式
1. 非剥夺调度方式
- 定义:即使某个更高优先级的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或发生某种事件进入阻塞态时
- 优点:实现简单、开销小,适合大多数批处理系统
- 缺点:不适用于分时系统和大多数实时系统
2. 剥夺调度方式
- 定义:若有更高优先级的进程请求处理机,立即暂停正在执行的进程,将处理机分配给更高级进程
- 优点:提高系统吞吐率和响应效率
- 注意:剥夺不是一种任意行为,必须遵守一定的原则(优先权、短进程优先、时间片原则等)
4. 调度的基本准则
- CPU利用率:尽可能保持CPU忙碌状态
- 系统吞吐量:单位时间内CPU完成作业的数量
- 调度算法和方式会对吞吐量造成较大影响
- 周转时间:作业提交到作业完成的时间
- 周转时间 = 作业完成时间 - 作业提交时间
- 平均周转时间 = 总周转时间 / N个作业
- 带权周转时间=作业周转时间 / 作业实际运行时间
- 值越小越好
- 平均带权周转时间 = 总带权周转时间 / N个作业
- 值越小越好
- 等待时间:衡量一个算法优劣,只需要简单的考察等待时间
- 对进程:等待处理机的时间
- 对作业:建立进程后的等待时间 + 作业在外存后备队列中等待的时间
- 响应时间:从用户提交请求到系统首次产生响应所用的时间
5. 调度算法
1.先来先服务算法(FCFS)
- 对象:作业调度、进程调度
- 作业调度:从后备作业队列中选择最先进入该队列的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列
- 进程调度:从就绪队列中选择最先进入该队列的进程,将处理机分配给它,使之投入运行,直到完成或因某种原因而阻塞时才释放处理机
- 适用性:适用于批处理系统
- 算法思想:先来的先分配处理机
- 优点:算法简单,对长作业有利,对CPU繁忙型作业有利
- CPU繁忙型作业:长时间占有CPU的作业
- I/O繁忙型作业:频繁访问I/O端口,每次访问都会放弃CPU,访问完后等待下一次调用
- 缺点:效率低,不利于短作业和I/O繁忙型作业
- 特点
- 无饥饿现象
- 非抢占式
2. 短作业优先算法(SJF)
- 对象:作业调度(SJF)、进程调度(SPF)
- SJF:从后备队列中选择一个或若干估计运行时间最短的作业,将它们调入内存运行
- SPF:从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机
- 适用性:适用于批处理系统
- 算法思想:优先选择预计运行时间最短的进程
- 优点:各进程同时到达时,平均等待时间、平均周转时间最短
- 缺点:对长作业不利,没有考虑作业的紧迫性,用户可以缩短作业预估时间,使得无法做到短作业优先
- 特点
- 有饥饿现象
- SJF和SPF是非抢占式的算法,但是也有抢占式的版本——最短剩余时间优先算法(SRTN)
3. 最高响应比调度算法(HRRN)
- 对象:主要用于作业调度
- 适用性:适用于批处理系统
- 响应比:响应比=(等待时间 + 要求服务时间)/ 要求服务时间
- 特点:
- 等待时间相同,要求服务时间越短响应比越大,有利于短作业进程
- 要求服务时间相同,作业响应比由等待时间决定,等待时间越长响应比越高,实现先来先服务
- 对于长作业,作业的响应比可以随等待时间的增加而提高,等待时间足够长时,其响应比可以升到很高,从而获得处理机
- 不会导致饥饿
- 非抢占式
4. 优先级调度算法
- 对象:作业调度、进程调度
- 作业调度:从后备作业队列中选择优先级最高的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列
- 进程调度:从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行
- 适用性:适用于实时操作系统
- 分类
- 剥夺性:立即停止当前运行进程,将处理机分配给更高优先级进程
- 非剥夺性:等待当前进程运行完后,然后将处理机分配给更高优先级进程
- 优先级分类
- 静态优先级:进程创建后无法对优先级进行修改
- 动态优先级:根据进程运行状态动态调整优先级
- 主要依据:CPU时间的长短、就绪进程等待CPU时间的长短
- 优先级设置原则
- 系统进程 > 用户进程
- 交互性进程 > 非交互性进程
- I/O型进程 > 计算型进程
- 特点
- 有饥饿现象
- 有抢占式的,也有非抢占式的
5. 时间片轮转算法
- 对象:进程调度
- 将就绪队列中的进程按先来先服务的原则依次调度,使用完一个时间片后,必须释放处理机给下一个就绪进程,返回到就绪队列的末尾重新排队,等待再次运行
- 适用性:适用于分时系统
- 时间片影响因素
- 系统响应时间
- 就绪队列中的进程数目
- 系统的处理能力
- 特点
- 时间片的大小对系统性能的影响很大
- 时间片过大—>先来先服务
- 时间片过小,处理机在进程间频繁切换,增大处理机的开销
- 不会导致饥饿
- 抢占式
- 时间片的大小对系统性能的影响很大
6. 多级反馈队列调度算法
- 对象:进程调度
- 适用性:适用于分时系统
- 实现思想
- 设置多个就绪队列,每个队列的优先级依次递减,时间片递增
- 进程首先放入第1级队列的末尾,按FCFS原则排队等待调度
- 轮到该进程执行时,若第一个时间片未完成,则放入第2级队列末尾(FCFS原则)·····
- 只有高级队列为空时,低级队列才能开始调度
- 若处理机正在执行第i级队列中的某进程,这时有新进程进入优先级高的队列,则此时新进程将抢占正在运行进程的处理机
- 优点
- 终端型作业用户:短作业优先
- 短批处理作业用户:周转时间较短
- 长批处理作业用户:经过前面几个队列得到部分执行,不会长期得不到处理
- 特点
- 有饥饿现象
- 抢占式
3. 进程同步
1. 基本概念
1. 临界资源
- 定义:一次只允许一个进程使用的资源
- 访问过程
- 进入区:(上锁)检查进程是否可以进入临界区,若能进入,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区
- 临界区:进程中访问临界资源的那段代码
- 退出区:(解锁)将正在访问临界区的标志清除
- 剩余区:代码中的剩余部分
2. 同步
- 定义:也称直接制约关系,为完成某种任务而建立的多个进程,相互合作,所以要进行通信同步
- 遵循的原则(前三个必须遵守,第四个可不必遵守)
- 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
- 忙则等待:已有进程进入临界区后,其他试图进入临界区的进程必须等待
- 有限等待:对于请求访问临界区的进程,在有限时间内进入临界区
- 让权等待:进程不能进入临界区的时候,应当立即释放处理机
3. 互斥
- 也称间接制约关系,当一个进程访问临界资源的时候,其他进程不能访问
2. 实现临界区互斥的基本方法
1. 软件实现方法
- 单标志法
- 定义:两个程序进程交替进入临界区
- 优点:实现简单
- 缺点:违背“空闲让进”,资源无法充分利用
- 定义:两个程序进程交替进入临界区
- 双标志法先检查
- 定义:每个进程访问临界区资源前,先检查临界资源是否被访问,如果空闲才进入
- 优点:不用交替进入,可以连续使用
- 缺点:违背“忙则等待”,不能保证互斥进入
- 定义:每个进程访问临界区资源前,先检查临界资源是否被访问,如果空闲才进入
- 双标志法后检查
- 定义
- 先设置自己标志,表明自己想要进入
- 检查对方标志,如果对方也要进入,那么就等待,否则就进入
- 优点:不会导致两个进程都进入临界区
- 缺点:违背“空闲让进”、“有限等待”,会产生饥饿现象
- 定义
- Peterson算法
- 定义:在算法三的基础上增加一个标志位,从而防止饥饿
- 优点:解决了饥饿现象
- 缺点:违背“让权等待",算法复杂
- 定义:在算法三的基础上增加一个标志位,从而防止饥饿
2. 硬件实现方法
1. 中断屏蔽法
- 定义: 对中断进行屏蔽,用开、关中断实现
- 优点:关中断非常方便
- 缺点:限制了处理机交替执行程序的能力,适用于单处理机
2. 硬件指令法
- TestAndSet(TS指令/TSL指令)
- 定义:读出指定标志后,将该标志置为true
boolean TestAndSet(boolean *lock){ //共享变量lock boolean old; old=*lock; *lock=true; return old; }
- 执行流程
- 进程访问临界资源前,利用TestAndSet检查,修改标志lock设为true
- 若有进程在临界区,则循环检查,直到进程退出将lock设为false
- 利用该指令实现互斥的算法描述
while TestAndSet(&lock); 进程的临界区代码段; lock=false; 进程的其他代码;
- Swap指令(XCHG指令)
- 定义:交换两个字(字节)的内容
Swap(boolean *a,boolean *b){ boolean temp; temp=*a; *a=*b; *b=temp; }
- 执行流程
- 每个临界区资源设置一个共享布尔变量lock,初值为false,每个进程中再设置一个局部布尔变量key,用于与lock交换信息
- 先利用Swap指令交换lock与key的值
- 检查key的状态
- 有进程在临界区时,重复交换和检查过程,直到进程退出
- 利用该指令实现互斥的算法描述
key=true; while(key!=false) Swap(&lock,&key); 进程的临界区代码段; lock=false; 进程的其他代码;
- 定义:交换两个字(字节)的内容
3. 优缺点
- 优点
- 适用于任意数目的进程,适用于多处理机环境
- 简单且容易验证正确性
- 支持进程内有多个临界区
- 缺点
- 违背“让权等待”
- 可能会导致饥饿现象
3. 信号量机制
- 定义:只能被两个标准的原语wait(S)(P操作)和signal(S)(V操作)访问
1. 整型信号量
- 定义:一个用于表示资源数目的整型量S
- P、V操作
wait(S){ while(S<=0); //只要信号量S<=0,就会不断测试 S=S-1; } signal(S){ S=S+1; }
- 只能进行三种操作
- 初始化
- P操作
- V操作
- 缺点:违背“让权等待”
2. 记录型信号量
- 定义:需要一个代表资源数目的整型变量value和一个进程链表L,用于链接所有等待该资源的进程
- 数据结构
typedef struct{ int value; //剩余资源数目 struct process *L; //等待队列 }semaphore;
- P操作
wait(semaphore S){ //P操作,相当于申请资源 S.value--; if(S.value<0){ //资源已分配完 add this process to S.L; //插入等待队列 block(S.L); //进行自我阻塞 } }
- V操作
signal(semaphore S){ //V操作,相当于释放资源 S.value++; //已有资源可分配 if(S.value<=0){ //仍有进程被阻塞 remove a process P from S.L; wakeup(P); //唤醒S.L中的第一个等待进程 } }
- 优点: 遵循“让权等待”
3. 利用信号量实现同步
semaphore S=0; //初始化信号量
P1(){
...
x; //语句x
V(S); //告诉进程P2,语句x已经完成
}
P2(){
...
P(S); //检查语句x是否运行完成
y; //检查无误,运行y语句
...
}
- 执行流程
- 若P2先执行到P(S)时,S为0,执行P操作会把进程P2阻塞,并放入阻塞队列
- P1中的x执行完后,执行V操作,把P2从阻塞队列中放回就绪队列
- 注意
- “前操作之后V,后操作之前P”
- P2需要的资源只能由P1产生
4. 利用信号量实现互斥
- 互斥:不同进程对同一信号量进行P,V操作实现的
- 算法
- 思想:每次只允许一个进程进入临界区,将S的初值设为1(可用资源数为1)。只需把临界区置于P(S)和V(S)之间即可实现两个进程对临界资源的互斥访问
semaphore S=1; //初始化信号量 P1(){ ... P(S); //准备开始访问临界资源,上锁 进程P1的临界区; V(S); //访问结束,解锁 ... } P2(){ ... p(S); //准备开始访问临界资源,上锁 进程P2的临界区; V(S); //访问结束,解锁 ... }
- 执行流程
- 没有进程在临界区时,任意一个进程要进入临界区,就要执行P操作,把S的值减为0,然后进入临界区
- 有进程在临界区时,S的值为0,再有进程要进入临界区,执行P操作时被阻塞,直至在临界区中的进程退出
- 注意:P、V操作要紧夹要使用互斥资源的那个行为,中间不能有其它冗余代码
5. 利用信号量实现前驱关系
- 实现原理
- 每一对前驱关系各设置一个同步信号量
- 在“前操作”之后对相应的同步信号量执行V操作
- 在“后操作”之前对相应的同步信号量执行P操作
- 本质:多级同步问题
6. 经典同步问题
- 分析步骤
- 画图理解题目
- 判断题目类型(生产者-消费者问题 60%、读者-写者问题 30%、哲学家进餐问题)
- 分析进程数目,填写进程模板
Process(){ while(1){ } }
- 补充基本代码(具体操作)
- 补充P、V操作(分析各操作的同步、互斥关系)
- 同步:动作的前后关系
- 互斥:独占资源(缓冲区)
- 检查调整代码
1. 生产者-消费者问题
- 问题描述
- 代码实现
- 注意
2. 多生产者-多消费者问题
-
问题描述
-
代码实现
- 注意:即使不设置专门的互斥变量mutex,也不会出现多个进程同时访问盘子的现象
- 原因:此时缓冲区大小为1,在任何时刻,apple,orange,plate三个同步信号量中最多只有一个是1。因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区
-
总结
3. 读者-写者问题
- 问题描述
- 读者优先
- 读写公平
4. 哲学家进餐问题
- 问题描述
- 预防死锁——三种思路
- 对哲学家进程施加一些限制条件,比如:最多允许四个哲学家同时进餐,这样可以保证至少有一个哲学家可以拿到左右两支筷子的
- 要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一支筷子,另一个会直接阻塞,这就避免了占有一支再等待另一支的情况
- 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子
- 总结
5. 吸烟者问题
- 问题描述
- 代码实现
6. 叫号-取号问题
- 面包师有很多面包,由n名销售人员推销。每名顾客进店后取一个号,并且等待叫号,当一名销售人员空闲时,就叫下一个号。试设计一个使销售人员和顾客同步的算法
-
int i = 0, j = 0; semaphore mutex_i = 1, mutex_j = 1; Customer(){ //顾客 进入面包店; P(mutex_i); //互斥访问i i++; V(mutex_i); //释放对i的访问 等待叫号i并购买面包; } Seller(){ while(1){ P(mutex_j); //互斥访问j if(j < i){ 叫号j; //号j已有顾客取走并等待 j++; V(mutex_j); //释放对j的访问 销售面包; }else{ V(mutex_j); //暂时没有顾客在等待 休息片刻; } } }
4.管程
-
目的:为了解决信号量机制编程麻烦/易出错的问题
-
定义:代表共享资源的数据结构以及对该数据结构实施操作的一组过程所组成的资源管理程序,这组操作能初始化并改变管程中的数据和同步进程
- 系统中的各种硬件资源和软件资源,均可用数据结构抽象地描述其资源特性,而忽略其内部结构和实现细节
- 对该数据结构实施的操作定义为一组过程,进程对共享资源的申请、释放都通过这组过程实现
-
组成(类比Java里的类)
- 管程的名称
- 局部于管程的共享数据结构说明
- 对该数据结构进行操作的一组过程(函数)
- 对局部于管程的共享数据设置初始值的语句
-
基本特性
- 局部于管程的数据只能被局部于管程内的过程所访问
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据
- 每次仅允许一个进程在管程内执行某个内部过程
-
条件变量:阻塞原因(当一个进程进入管程后,直到阻塞的原因解除时,在此期间,如果该进程不释放管程,那么其他进程无法进入管程)
- 每个条件变量保存了一个等待队列,用于记录因该条件变量而阻塞的所有进程
-
两种操作
- x.wait:当x对应的条件不满足时,正在调用管程的进程调用x.wait将自己插入x条件的等待队列,并释放管程
- x.signal:x对应的条件发生了变化,则调用x.signal,唤醒一个因x条件而阻塞的进程
-
条件变量和信号量的比较
- 相同点:条件变量的wait/signal操作类似于信号量的P/V操作,可以实现进程的阻塞/唤醒
- 不同点
- 条件变量没有值,仅实现了排队等待功能
- 信号量有值,其值反映了剩余资源数 (管程中,剩余资源数用共享数据结构记录)
4. 拓展(不考)
1. 用管程解决生产者消费者问题
monitor ProducerConsumer
condition full,empty; //条件变量用来实现同步(排队)
int count=0; //缓冲区中的产品数
void insert(Item item){ //把产品item放入缓冲区
if(count == N)
wait(full);
count++;
insert_item(item);
if(count == 1)
signal(empty);
}
Item remove(){ //从缓冲区中取出一个产品
if(count == 0)
wait(empty);
count--;
if(count == N-1)
signal(full);
return remove_item();
}
end monitor;
//生产者进程
producer(){
while(1){
item = 生产一个产品;
ProducerConsumer.insert(item);
}
}
//消费者进程
consumer(){
while(1){
item = ProducerConsumer.remove();
消费产品item;
}
}
- 管程中设置条件变量和等待/唤醒操作,以解决同步问题
- 由编译器负责实现各进程互斥地进入管程中的过程
4. 死锁
1. 死锁的概念
- 定义:多个进程因为竞争资源造成的一种僵局,没有外力作用,这些进程都无法向前继续推进
- 产生的原因
- 系统资源的竞争:对不可剥夺资源的竞争才可能产生死锁
- 进程推进顺序非法
- 请求和释放资源的顺序不当
- 信号量使用不当
- 注意:循环等待为出现死锁的必要条件
- 死锁产生的必要条件
- 互斥条件:进程对分配的资源进行排他性控制(一般不可破坏)
- 不可剥夺条件:进程获得资源在未使用完之前,不能被其他进程强行夺走
- 请求并保持条件:进程已经保持了至少一个资源,提出新的资源请求,而该资源已经被其他进程占有,此时该进程被阻塞,但是自己已获得的资源保持不放
- 循环等待条件:你等我释放,我等你释放
2. 死锁的处理策略
- 严格程度:避免 > 预防 > 检测与解除
- 并发性排序:检测与解除 > 预防 > 避免
1. 死锁预防–静态策略
- 原理:破坏死锁产生的四个必要条件中的一个或几个
- 破坏互斥条件:有些资源只能被互斥访问,并且某些情况下必须保护互斥性
- 破坏不剥夺条件
- 方法——剥夺资源法
- 策略:使进程释放已经占有的资源
- 特点
- 增加系统开销
- 实现复杂
- 降低吞吐量
- 使用场景:常用于状态易于保存和恢复的资源,如CPU的寄存器
- 破坏请求并保持条件
- 方法——一次性分配
- 策略:一次性申请完所需要的全部资源
- 特点
- 实现简单
- 资源被严重浪费
- 可能导致饥饿现象
- 破坏循环等待条件
- 方法——资源有序分配
- 策略:采用顺序资源法,给系统中的资源编号,进程必须按递增的顺序请求资源,且同类资源一次申请完
- 特点
- 资源编号必须稳定,限制了新类型设备的增加
- 造成资源浪费
- 不利于用户编程
2. 避免死锁–动态策略
- 原理:在资源动态分配过程中,防止系统进入不安全状态
1. 系统安全状态
- 按照某种方式分配资源后,是否会导致死锁,如果会导致死锁,就是不安全状态,反之就是安全状态
2. 银行家算法
- 思想:通过计算当前资源的不同分配方式,从而预测系统是否会进入不安全状态
- 数据结构
- 可利用资源向量(Available):含有m个元素的数组,其中每个元素代表一类可用的资源数目,Available[j]=K表示系统中现有j类资源K个
- 最大需求矩阵(Max): n × m n\times m n×m矩阵,定义系统中n个进程中的每个进程对m类资源的最大需求(一行代表一个进程,一列代表一类资源),Max[i,j]=K表示进程i需要j类资源的最大数目为K
- 分配矩阵(Allocation): n × m n\times m n×m矩阵,定义系统中每类资源当前已分配给每个进程的资源数,Allocation[i,j]=K表示进程i当前已分得j类资源K个
- 需求矩阵(Need): n × m n\times m n×m矩阵,表示每个进程最多还需要多少资源,Need[i,j]=K表示进程i还需要j类资源K个
- 算法描述——设Request是进程P的请求向量,Request[j]=K表示进程P需要j类资源K个
- 若Request[j] <= Need[i,j],则转向步骤2;否则认为出错,因为它所需要的资源数已超过它所宣布的最大值
- 若Request[j]>=Available[j],则转向步骤3;否则表示尚无足够资源,进程P必须等待
- 系统试探着把资源分配给P,并修改下面数据结构中的数值
- Available = Available - Request
- Allocation[i,j] = Allocation[i,j] + Request[j]
- Need[i,j] = Need[i,j] - Request[j]
- 系统执行安全性算法,检查此次资源分配后,系统是否处于安全状态,若安全,才分配资源,否则恢复原来的资源状态,让进程P等待
- 安全性算法(核心)——设置工作向量Work,有m个元素,表示系统各种的剩余可用资源数目。执行安全性算法开始时,Work=Available
- 初始时安全序列为空
- 从Need矩阵中找出符合下面条件的行,找到后,把对应的进程加入安全序列;若找不到,则执行步骤4
- 该行对应的进程不在安全序列中
- 该行小于等于Work向量
- 进程P进入安全序列后,可顺利执行,直至完成,并释放分配给它的资源,因此应执行Work = Work + Allocation[i],返回步骤2
- 若此时安全序列中已有所有进程,则系统处于安全状态,否则系统处于不安全状态
3. 死锁的检测及解除
1. 资源分配图
- 圆圈——进程
- 框——一类资源
- 框中的圆——一个资源
- 请求边——进程到资源的有向边
- 分配边——资源到进程的有向边
2. 死锁定理
- 在资源分配图中,找出满足分配的进程,然后消去其请求边与分配边,如果最后所有边都可被消去,那么就是可以简化的不存在死锁,反之存在死锁
3. 死锁的检测和解除
- 资源剥夺法:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他死锁进程,但是要防止挂起时间过长
- 撤销进程法:强制撤销部分甚至全部死锁进程并剥夺他们的资源。撤销原则可以根据优先级和撤销进程的代价进行
- 进程回退法:让一个或者多个进程回退到足以回避死锁的地步,进程回退时自愿放弃资源而非被剥夺。要求系统保持进程历史信息,设置还原点