第11章:死锁和进程通信
- 死锁概念
- 死锁处理方法
- 死锁预防(Deadlock Prevention)
- 死锁避免(Deadlock Avoidance)
- 死锁检测和恢复(Deadlock Detection & Recovery)
- 死锁检测和恢复(Deadlock Detection & Recovery)
- 由应用进程处理死锁
- 银行家算法
- 死锁检测
- 进程和通信概念
- 信号和管道
- 消息队列和共享内存
11.1 死锁概念
-
defs:死锁是进程之间由于共享资源所导致的一种无限期等待的情况
- 由于竞争资源或者通信关系,两个或更多线程在执行中出现永远相互等待,只能由其他进程引发的事件
进程访问资源的流程
-
资源类型R1,R2,…,Rm
- CPU执行时间,内存空间,I/O设备等
每类资源Ri有Wi个实例
-
进程访问资源的流程
-
请求/获取
- 申请空闲资源
-
使用/占用
- 进程占用资源
-
释放
- 资源状态由占用变成空闲
-
资源分类
-
可重用资源(Reusable Resource)
资源不能被删除且在任何时刻只能有一个进程使用
进程释放资源后,其他进程可重用
-
可重用资源示例
- 硬件:处理器、I/O通道、主和副存储器、设备等
- 软件:文件、数据库和信号量等数据结构
-
可能出现死锁
- 每个进程应用一部分资源并请求其他资源
-
消耗资源(Consumable resource)
资源创建和销毁
-
资源消耗示例
- 在I/O缓冲区的中断、信号和消息等
-
可能会出现死锁
- 进程间相互等待接收对方的消息
出现死锁的必要条件
-
互斥
- 任何时刻只能有一个进程使用一个资源实例
-
持有并等待
- 进程保持至少一个资源,并正在等待获取其他进程特有的资源
-
非抢占
- 资源只能在进程使用后自愿释放
循环等待
11.2 死锁处理方法
-
死锁预防(Deadlock Prevention)
- 确保系统永远不会进入死锁状态
-
死锁避免(Deadlock Avoidance)
- 在使用前进行判断,只允许不会出现死锁的进程请求资源
-
死锁检测和恢复(Deadlock Detection & Recovery)
- 在检测到运行系统进入死锁状态后,进行恢复
-
由应用进程处理死锁
-
通常操作系统忽略死锁
- 大多数操作系统(包括UNIX)的做法
-
(一)死锁预防:限制申请方式
预防是采用某种策略,限制并发进程对资源的请求,使系统在任何时刻都不满足死锁的必要条件
-
互斥
- 把互斥的共享资源封装成可同时访问
-
持有并等待
- 进程请求资源时,要求它不持有任何其他资源
- 仅允许进程在开始执行时,一次请求所有需要的资源
- 资源利用率低
-
非抢占
- 如进程请求不能立即分配的资源,则释放已有资源
- 只在能够同时获得所有需要资源时,才执行分配操作
-
循环等待
- 对资源排序,要求进程按顺序请求资源
(二)死锁避免
-
利用额外的先验信息,在分配资源时判断是否会出现死锁,只在不会死锁时分配资源
- 要求进程声明需要资源的最大数目
- 限定提供与分配的资源数量,确保满足进程的最大需求
- 动态检查资源分配状态,确保不会出现环形等待
系统资源分配的安全状态
当进程请求资源时,系统判断分配后是否处于安全状态
-
系统处于安全状态
- 针对所有已占用进程,存在安全序列
序列
安全状态与死锁的关系
系统处于安全状态,一定没有死锁
-
系统处于不安全状态,可能出现死锁
- 避免死锁就是确保系统不会进入不安全状态
11.3 银行家算法
- 银行家算法是一个避免死锁产生的算法,以银行借贷发配策略为基础,判断并保证系统处于安全状态
银行家算法:数据结构
n = 线程数量, m = 资源类型数量
-
Max(总需求量):n×m矩阵
- 线程Ti最多请求类型Rj的资源 Max[i,j]个实例
-
Available(剩余空闲量):长度为m的向量
- 当前有Available[i]个类型Rj的资源实例可用
-
Allocation(已分配量):n×m矩阵
- 线程Ti当前分配了 Allocation[i,j]个Rj的实例
-
Need(未来需要量):n×m矩阵
- 线程Ti未来需要Need[i,j]个Rj资源实例
Need[i,j] = Max[i,j] - Allocation[i,j]
银行家算法:安全状态判断
1. Work 和 Finish 分别是长度为m和n的向量初始化:
Work = Available //当前资源剩余空闲量
Finish[i] = false for i:1,2,...,n //线程i没结束
2. 寻找线程Ti:
(a)Finish[i] = false //接下来找出Need比Work小的线程i
(b) Need[i] <= Work
没有找到满足的条件Ti,转4
3. Work = Work + Allocation[i] //线程i的资源需求量小于当前剩余空闲资源量,所以配置给它再回收
Finish[i] = true
转2
4. 如所有线程Ti满足Finish[i] == true,则系统处于安全状态 //所有线程的Finish为True,表明系统处于安全状态
银行家算法
初始化:Requesti 线程Ti的资源请求向量
Requesti [j] 线程Ti请求资源Rj的实例
循环:
1\. 如果Requesti <= Need[i],转到2。否则拒绝资源申请,因为线程已经超过了其最大要求
2\. 如果Requesti <= Available,转到3。否则,Ti必须等待,因为资源不可用
3\. 通过安全状态来确定是否分配资源给Ti:
生成一个需要判断状态是否安全的资源分配环境
Available = Available - Requesti;
Allocation[i] = Allocation[i] + Requesti;
Need[i] = Need[i] - Requesti;
调用安全状态判断
如果返回结果是安全,将资源分配给Ti
如果返回结果不安全,系统会拒绝Ti的资源请求
11.4 死锁检测
- 允许系统进入死锁状态
- 维护系统的资源分配图
- 定期调用死锁检测算法来搜索图中是否存在死锁
- 出现死锁时,用死锁恢复机制进行恢复
死锁检测:数据结构
-
Available:长度为m的向量
- 每种类型可用资源的数量
-
Allocation:一个 n×m矩阵
- 当前分配给各个进程每种类型资源的数量
- 进程Pi拥有资源Rj的Allocation[i,j]个实例
死锁检测算法
1. Work 和 Finish 分别是长度为m和n的向量初始化:
Work = Available //Work为当前资源剩余空闲量
Allocation[i] > 0 时,Finish[i] = false //finish为线程是否结束
否则,Finish[i] = true
2. 寻找线程Ti满足:
(a)Finish[i] = false //寻找没有结束的线程,且此线程将需要的资源量小于当前空闲资源量
(b) Requesti <= Work
没有找到满足的条件Ti,转4
3. Work = Work + Allocation[i] //把找到的线程拥有的资源释放回当前空闲资源中
Finish[i] = true
转2
4. 如某个 Finish[i] == false,则系统处于死锁状态 //如果有Finish为false,表明系统处于死锁状态
- 算法需要O(m×n^2)操作检测是否系统处于死锁状态
死锁检测算法的使用
-
死锁检测的时间和周期选择依据
- 死锁多久可能会发生
- 多少进程需要回滚
-
资源可能有多个循环
- 难于分辨“造成”死锁的关键进程
死锁恢复:进程终止
终止所有的死锁进程
一次只终止一个进程直到死锁消除
-
终止进程的顺序应该是
- 进程的优先级(由低到高)
- 进程已运行时间以及还需运行时间(保留运行时间较长者)
- 进程已占用资源
- 进程完成需要的资源
- 终止进程数目(越少越好)
- 进程是交互还是批处理(优先让用户交互的进程执行下去)
死锁恢复:资源抢占
-
选择被抢占进程
- 最小成本目标
-
进程回退
- 返回到一些安全状态,重启进程到安全状态
-
可能出现饥饿
- 同一进程可能一直被选择被抢占者
11.5 进程通信概念
进程通信(IPC, Inter-Process Communication)
进程通信是进程进行通信和同步的机制
-
IPC提供两个基本操作
- 发送操作:send(message)
- 接收操作:receive(message)
-
进程通信流程
- 在通信进程间建立通信链路
- 通过send/receive交换信息
-
进程链路特征
- 物理(如,共享内存、硬件总线)
- 逻辑(如,逻辑属性)
直接通信
-
进程必须正确的命名对方
- send(P, message) - 发送信息到进程P
- receive(Q, message) - 从进程Q接收信息
-
通信链路属性
- 自动建立链路
- 一条链路恰好对应一对通信进程
- 每对进程之间只有一个链接存在
- 链接可以是单向的,但通常是双向的
间接通信
-
通过操作系统维护的消息队列实现进程间的消息接收和发送
- 每个消息队列都有一个唯一的标识
- 只有共享了相同的消息队列的进程,才能够通信
-
通信链路的属性
- 只有共享了相同的消息队列的进程,才建立连接
- 连接可以是单向或双向
- 消息队列可以与多个进程相关联
- 每对进程可以共享多个消息队列
-
通信流程
- 创建一个新的消息队列
- 通过消息队列发送和接收消息
- 销毁消息队列
-
基本通信操作
- send(A, message) - 发送消息到队列A
- receive(A, message) - 从队列A接收消息
阻塞与非阻塞通信
进程通信可划分为阻塞(同步)或非阻塞(异步)
-
阻塞通信
-
阻塞发送
- 发送者在发送消息后进入等待,直到接收者成功收到
-
阻塞接收
- 接收者在请求接收消息后进入等待,直到成功收到一个消息
-
-
非阻塞通信
-
非阻塞发送
- 发送者在消息发送后,可立即进行其他操作
-
非阻塞接收
- 没有消息发送时,接收者在请求接收消息后,接收不到任何消息
-
通信链路缓冲
-
进程发送的消息在链路上可能有3种缓冲方式
-
0容量
- 发送方必须等待接收方
-
有限容量
- 通信链路缓冲队列满时,发送方必须等待
-
无限容量
- 发送方不需要等待
-
11.6 信号量和管道
(一)信号
信号
- 进程间的软件中断通知和处理机制
- 如,SIGKILL, SIGSTOP, SIGCONT等
信号的接收处理
-
捕获(Catch)
- 执行进程指定的信号处理函数被调用
-
忽略(Ignore)
-
执行操作系统指定的缺省处理
- 例如:进程终止、进程挂起等
-
-
屏蔽(Mask)
-
禁止进程接收和处理信号
- 可能是暂时的(处理同种类型的信号)
-
不足
- 传送的信息量小,只有一个信号类型
(二)管道
管道(pipe)
-
进程间基于内存文件的通信机制
- 子进程从父进程继承文件描述符
- 缺省文件描述符:0 stdin , 1 stdout, 2 strerr
进程不知道(或不关心)另一端
可能从键盘、文件、程序读取
可能写入到终端、文件、程序
与管道相关的系统调用
-
读管道:read(fd, buffer, nbytes)
- scanf()是基于它实现的
-
创建管道:pipe(rgfd)
-
rgfd是2个文件描述符组成的数组
- rgfd[0]是读文件描述符
- rgfd[1]是写文件描述符
-
11.7 消息队列和共享内存
(一)消息队列
-
消息队列是由操作系统维护的以字节序列为基本单位的间接通信机制
- 每个消息(message)是一个字节序列
- 相同标识的消息按先进先出顺序组成一个消息队列(Message Queues)
消息队列的系统调用
-
msgget(QID, buf, size, flags)
- 发送消息
-
msgrcv(QID, buf, size, type, flags)
- 接收消息
-
msgctl(…)
- 消息队列控制
(二)共享内存
- 共享内存是把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制
进程
- 每个进程都有私有地址内存空间
- 每个进程的内存地址空间需要明确设置于共享内存段
线程
- 同一进程中的线程总是共享相同的内存地址空间
优点
- 快速、方便的共享数据
不足
- 必须用额外的同步机制来协调数据访问
特点
最快的方法
一个进程写另一个进程立即可见
没有系统调用干预
没有数据复制
-
不提供同步
- 由程序员提供同步
共享内存的系统调用
-
shmget(key, size, flags)
- 创建共享段
-
shmat(shmid, *shmaddr, flags)
- 把共享段映射到进程地址空间
-
shmdt(*shmaddr)
- 取消共享段到进程地址空间的映射
-
shmctl(…)
- 共享段控制
完成共享关系的建立,而正常共享数据的访问只需读写指令,不需要专门的系统调用
需要信号量等机制协调共享内存的访问冲突