1.引出死锁
再看生产者-消费者的信号量解法,之前的例子:
//用文件定义 共享缓冲区
int fd = open("buffer.txt");
write(fd, 0, sizeof(int));//写in
write(fd, 0, sizeof(int));//写out
//信号量的定义和初始化
semaphore full = 0;//生产的产品的个数
semaphore empty = BUFFER_SIZE;//空闲缓冲区的个数
semaphore mutex = 1;//互斥信号量
//生产者
Producer(item)
{
P(empty);//生产者先判断 缓存区个数 empty是否满了,empty == 0,阻塞
P(mutex);//操作文件前,用mutex防止其他进程操作该文件
读入in,将item写到in的位置上
V(mutex);//使用完文件,释放文件资源
V(full);//生产者生产产品,增加full,消费者就可以消费了
}
//消费者
Consumer()
{
P(full);//当full == 0,缓冲区空了,阻塞
P(mutex);
读入out,从文件中的out位置读出到item,打印item;
V(mutex);
V(empty);//消费者消耗产品,增加缓冲区个数,增加empty,生产者就可以继续生产了
}
上边的例子 Producer 先调用的 P(empty) 后调用的 P(mutex),如果换个位置呢?
// 设 mutex初值是1,empty初值是0,缓冲区取满了
Producer(item)
{
P(mutex); // P(mutex) 会把 mutex 变成 0
P(empty); // P(empty) 会把 empty 变成 -1,生产者阻塞
读入in,将item写到in的位置上
V(mutex);
V(full);
}
P(semaphore s)
{
s.value--;
if(s.value < 0){
sleep(s.queue);
}
}
//消费者
Consumer()
{
P(mutex); // mutex是0,执行P(mutex)将mutex变成 -1,消费者阻塞,此时 消费者 和 生产者都阻塞了
P(full);
读入out,从文件中的out位置读出到item,打印item;
V(mutex);
V(empty);
}
此时 消费者 和 生产者都阻塞了,
生产者要执行,需要有人把empty释放了,即 消费者要执行 V(empty),当前消费者卡在 P(mutex)
消费者要执行,需要生产者把 mutex释放了,生产者要执行 V(metux),生产者卡在 上边的 P(empty)
生产者在P(empty)往下执行,依赖于消费者,消费者要往下执行,又依赖生产者P(empty)下边的指令。形成环路等待,死锁。
如果很多进程都没法推进,会导致计算机不工作,CPU利用率低
2.死锁的成因
以车辆占用道路为例:
车辆 A 行驶在 道路1(上边 左右方向的线) 上,想申请 道路2 (右边 上下方向的线)
道路2 被 车辆B 占用着,车辆 B 要申请 道路3(下边 左右方向的线)
道路3 被车辆C 占用着,车辆C 要申请 道路4(左边 上下方向的线)
道路4 被车辆D 占用着,车辆D 要申请 道路1,然而 道路1 被 车辆A 占用着
死锁的成因:
资源互斥(比如信号量、打印机),进程占有资源,同时 又再去申请其他资源,造成环路等待,形成死锁
3.死锁的必要条件
- 互斥使用(Mutual exclusion)
- 资源的固有特性,如道口
- 不可抢占(No preemption)
- 资源只能自愿放弃,如 车开走以后
- 请求和保持(Hold and wait)
- 进程必须占有资源,再去申请
- 循环等待(Circular wait)
- 在资源分配图中存在一个环路
4.死锁的处理
4.1 死锁处理方法概述
- 死锁预防
- 破坏死锁出现的条件,不要 占有资源 又申请其他资源
- 死锁避免
- 检测每个资源请求,如果造成死锁就拒绝
- 死锁检测 + 恢复
- 检测到死锁出现时,让一些进程回滚,让出资源
- 死锁忽略
- 好像没有出现死锁一样
4.2 死锁预防的例子
4.2.1 在进程执行前,一次性申请所有需要的资源,不会占有资源再去申请其他资源
比如 代码第3句 需要一个信号量,第10句需要一个信号量,在程序开始,就获取这2个信号量
缺点:
- 需要预知未来,变成困难。要知道所有用到的资源,如果代码 if 中用到了 信号量,在程序开始 也要申请该信号量。如果本次执行,不走这个 if,占用的这个信号量就没有被利用
- 许多资源分配后 很长时间才使用,资源利用率低
4.2.2 对资源类型进行排序,资源申请必须按序进行,不会出现环路等待
缺点: 造成资源浪费
对资源进程排序,如果需要10号资源,需要把 10号之前的资源 全部申请了,造成资源的浪费
4.3 死锁避免
4.3.1 安全序列
安全状态: 如果系统中的左右进程 存在一个可完成的执行序列 P1,… ,Pn,则称系统处于安全状态
安全序列:所有进程都可以执行完的序列
上图含有 5个 进程:P0-P4
Allocation :占有的资源,以P1 为例,占用资源A 3个,资源B 0个,资源C 2个
Need:需要的资源
Available:系统中 剩余的资源
什么方法能让 这些进程都执行完?
右边的问题中 选项 A 是安全序列
当前系统 剩余资源时 ABC = 230,给 P1,P1可以执行完,P1 执行完 Available ABC = 532
P3 可以执行,P3 执行完,Available ABC = 743
其他进程都可以执行
4.3.2 银行家算法
int Available[1..m]; //每种资源剩余数量
int Allocation[1..n, 1..m]; //已分配资源数量
int Need[1..n, 1..m]; //进程还需的各种资源数量
int Work[1..m]; //工作向量
bool Finifh[1..n]; //进程是否结束
Work = Available;
Finifh[1..n] = false;
while(true)
{
for(i = 1; i <= n; i++)
{
// Need[i] <= Work 这个任务是可以完成的
if(Finish[i] == false && Need[i] <= Work)
{
Work = Work + Allocation[i]; // Work 累加系统曾分配给 i 的资源
Finish[i] = true;
break;
} else {
goto end;
}
}
}
End: for(i = 1; i <=n; i++)
if(Finish[i] == false)
return "deadlock";
时间复杂度是 T(n) = O(m* n^2),m是资源数,n是进程数
系统中的 资源和 进程都很多,执行的代价还是很大的
银行家算法实例:
应用时,首先假装分配,然后调用 银行家算法,如果给进程1分配资源,进程1执行完,其他进程都不能执行,则拒绝 进程1 的资源申请
4.4 死锁检测 + 恢复:发现问题再处理
- 定时检测 或者是 发现资源利用率低时检测
- 选择哪些进程回滚?回滚的依据是什么,是优先级,还是占有资源多少
- 如何实现回滚?那些已经修改的文件怎么办?
回滚容易出错,比如存款程序,用户已经往账户里打钱,信息已经写到一个文件里了,现在要回滚,这个钱已经在银行了。回滚会出错。
4.5 死锁忽略
Windows 和 Linux 都 采用了 死锁忽略的方法
- 死锁忽略的处理代价最小
- 这种机器上出现死锁的概率比其他机器低
- 死锁可以用重启来解决,PC重启造成的影响小
- 死锁预防让编程变得困难
4.6 总结
- 死锁预防
- 引入太多不合理因素
- 死锁避免
- 每次申请都执行银行家算法,效率太低
- 死锁检测 + 恢复
- 恢复很不容易,进程造成的改变很难恢复
- 死锁忽略
- 死锁出现是不确定的,可以用重启来处理死锁
- 大多数非专门的操作系统都用 死锁忽略,如 UNIX,Linux,Windows