操作系统(9) 死锁

220 阅读6分钟

本文引用代码及图片均来自 李治军: 操作系统32讲

死锁的形成

考虑下面的例子

semaphore full = 0;             // 缓冲区数据数量
semaphore empty = BUFFER_SIZE;  // 缓冲区空闲容量
semaphore mutex = 1;            // 缓冲区锁
P()                             // 消费资源
V()                             // 生产资源

image.png

生产者只能在缓冲区不满且获取了锁的情况下生产数据,否则会阻塞直到相应的信号量(emptymutex)可用,生产数据后会唤醒阻塞在相应信号量(fullmutex)上的进程

消费者只能在缓冲区非空且获取了锁的情况下消费数据,否则会阻塞直到相应的信号量(fullmutex)可用,消费数据后会唤醒塞在相应信号量(emptymutex)上的进程

以上流程在缓冲区满缓冲区空缓冲区非空非满的情况下都能正常工作,但是如果工作流改为如下的样子:

image.png

缓冲区满的时候,假设现在调度生产者,生产者获取了mutex然后阻塞在empty上。调度后到消费者运行,消费者直接阻塞在了mutex

此时,生产者需要消费者消费数据释放empty,而消费者则需要生产者释放mutex以消费数据。生产者无法获取empty所以无法释放mutex,生产者则获取不了mutex无法消费数据而不能释放empty

此时生产者和消费者互相等待谁也执行不了,这种互相等待对方持有的资源而造成谁也执行不了的情况就叫死锁

死锁成因可归结为以下:

  1. 资源互斥使用,一旦占有别人无法使用
  2. 进程占有某些资源不释放的情况下去申请别的资源
  3. 各自占有的资源和申请的资源形成环路等待

image.png

进一步可以总结死锁形成的4个必要条件:

  1. 资源互斥
  2. 不可抢占
  3. 请求和保持
  4. 循环等待

死锁的处理

死锁处理一般划分为4种方案

  1. 死锁预防
  2. 死锁避免
  3. 死锁检测与恢复
  4. 死锁忽略

死锁预防

死锁预防主要有两种思路

  1. 一次性申请所有资源 进程在执行前就把需要的资源都申请下来,确保在接下来的执行中不需要等待别人的资源,如果某资源申请不到则放弃所有已申请到的资源并等待下一次机会

该方法需要进程预估执行过程中需要用到的所有资源,在实际操作中有一定难度,而且执行前就申请的资源有可能在执行快结束时才使用,申请到的资源闲置同时别的进程又用不了造成资源利用率低

  1. 对资源按类型排序,申请资源必须按序进行 该方法可以避免A进程拿着2号资源等待1号资源而B进程拿着1号资源等待2号资源之类的循环等待,因为A进程拿到1号资源之前是无法申请2号资源的。该方法缺点同样是资源利用率低,本来进程先用2号资源再用1号资源,但是因为顺序限制只能提前把1号资源占着

死锁避免

死锁避免方案中系统在分配资源之前先评估该分配是否会导致死锁,不会就分配否则拒绝分配进程阻塞

系统判断是否会导致死锁其实是通过判断分配后系统是否处于安全状态来完成的,安全状态是指系统中所有进程可以按照一定的顺序执行完成

image.png

如上图中各个进程可以按照A序列完成运行,A序列是一个安全序列,系统处于安全状态

1. 系统给P1分配2B资源,P1执行完成后收回已分配资源,系统剩余5A、3B和2C的资源
2. 系统给P3分配1B资源,执行完成后收回...
3. 系统给P2分配6A资源...
4. ...

我们可以判断系统是否存在安全序列来判断系统状态,著名的银行家算法就是用于查找安全序列

int Available[1..m];         //每种资源剩余数量
int Allocation[1..n, 1..m];  //已分配资源数量
int Need[1..n, 1..m];        //进程还需的各种资源数量
int Work[1..m];              //工作向量
bool Finish [1..n];          //进程是否结束

银行家算法伪代码:
1. 初始化 work = Available, Finish[1..n] = false
2. 按编号顺序找到满足 Need[i] < work && Finish[i] == false 的进程Pi,否则跳到步骤4
3. 分配资源后Pi可以顺利完成,收回资源 work = work + Allocation[i], Finish[i] = true, 重复步骤2
4. 检查是否所有Finish[i] == true, 是则存在安全序列,否则不存在

具体例子可参考本小节第一张图,系统先判断P0的需求,无法满足,然后按序找到下一个进程P1,可以满足...

在实际应用中,当进程发出资源请求,系统先假设分配,然后调用银行家算法判断分配后系统是否存在安全序列,存在则实际分配,否则拒绝请求

死锁检测与恢复

在银行家算法中,每次判断m{m}个资源是否满足需要m{m}次操作,重复在n{n}个进程中寻找总共需要1+2+3+...+n{1+2+3+...+n}次操作,所以时间复杂度为O(mn2){O(mn^2)}

银行家算法的效率还是比较低的,如果每次分配都检查一次会消耗大量的资源。所以我们可以采用定时检测的机制,隔固定时间或固定次数分配后再做一次检测,如果之前的分配被检测到会造成死锁那么就撤销分配回滚进程

定时检测可以减少资源消耗但是又带来了另外的问题——哪个进程回滚?如何回滚?

回滚需要一个复杂机制的支撑,这本身就需要消耗很多资源,现有系统的架构可能也要跟着改变,代价太大。基于以上种种问题,检测与恢复的方案在实际中并不常见

死锁忽略

直接忽略死锁是处理死锁最简单的方法了,什么叫忽略呢?就是你想发生就发生吧,我懒得预防和检测更不要说恢复了。发生了怎么办?重启!

死锁忽略是基于死锁出现是小概率的以及可以通过重启来解决死锁这两个特征的一种方案,在非专用操作系统中(Windows,Linux等)都采用死锁忽略的方案

参考文献