1.死锁的原理
一组相互竞争资源或进行通信的进程间的“永久”阻塞。
当一组进程中的每一个进程都在等待某个事件(典型情况是等待请求资源的释放),而只有在这组进程中的其它被阻塞的进程才可以触发该事件。
所有的死锁都涉及两个或多个进程之间对资源需求的冲突。
1.1 可重用资源
资源通常分为两种:可重用的和可消耗的
可重用资源:一次只能供一个进程使用,并且不会由于使用而耗尽的资源,在使用后释放依然可被其它进程使用的资源。
如处理器、I/O通道、内存和外存、设备以及诸文件、数据库和信号量之类的数据结构
1.2 可消耗资源
可以被创建和销毁的资源。如中断、信号、消息和I/O缓冲区中的消息
通常对某种类型可消耗资源的数目没有限制,一个无阻塞的生产进程可以创建任意数目的这类资源。
1.3 资源分配图
使用资源分配图,它是一种有向图,它阐述了系统资源和进程的状态。
在资源分配图中,存在进程和资源的环,从而导致了死锁。
1.4 死锁的条件
死锁的三个必要条件:
1)互斥 :一次只有一个进程可以使用一个资源
2)占有且等待 :当一个进程等待其他进程时,继续占有已分配的资源
3)不可抢占 :不能强行占有进程已占有的资源
当设计数据资源时,需要设置数据回滚恢复机制rollback recovery mechanism以支持资源抢占
4)循环等待 :存在一个封闭的进程链,使得每一个进程至少占有此链的下一个进程所需要的一个资源
处理死锁的三个办法:
1)预防
采用某种策略来消除条件1-4中的一个来预防死锁
2)避免
基于资源分配的当前状态做动态选择来避免死锁
3)检测并恢复
试图检测死锁的存在并试图从死锁中恢复出来
6.2 死锁预防
间接的死锁预防方法,即防止死锁的三个必要条件:
1)互斥:
互斥条件是不可能禁止的
2)占有且等待
可以要求进程一次性请求所需要的资源,并阻塞这个进程直到所有的请求都同时满足,但是表现的很低效:
# 可能需要很长时间才能等待到需要的所有资源,但是进程也未必事先直到它需要的每一个资源
# 即使等到了,有些资源也可能很久不会使用
3)不可抢占
方案1:如果占有资源的一个进程进行进一步的资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求
方案2:如果一个进程请求一个被另一个进程所占有的资源,则操作系统可以抢占另一个进程,要求它释放资源,需要优先级机制的支持
直接的死锁预防方法,即防止循环等待的发生:
4)循环等待
可以通过定义资源类型的线性顺序来预防
即如果已经进程已经分配到了R类型的资源,那么接下来请求的资源只能是R类型以后的资源,这里需要一个排序机制
6.3 死锁避免
在死锁预防中,通过约束资源请求,防止4个死锁条件中至少一个的发生,这些都会导致低效的资源使用和低效的进程执行。
死锁避免则相反,它允许三个必要条件的发生,但通过明智的选择确保永远不会到达死锁点,因此死锁避免比死锁预防允许更多的并发。
在死锁避免中,是否允许当前的资源分配请求时通过判断该请求是否能够导致死锁来决定的,所以,需要知道将来的进程资源请求的情况。
死锁避免的方法:
1)如果一个进程的请求会导致死锁,则不启动此进程,即进程启动拒绝
2)如果一个进程增加的资源请求会导致死锁,则不允许此分配,即资源分配拒绝
3.1 进程启动拒绝
考虑一个有着n个进程和m种不同类型的资源的系统,定义一下向量和矩阵:
向量Resource = R = (R1,R2,....,Rm); //系统中每种资源的总量
向量Availiable = V = (V1,V2,.....Vm); //系统中可用的每种资源的总量
矩阵Claim = C //Cij表示进程i对资源j的请求,这个矩阵信息由进程事先申明
矩阵Allocation = A //Aij表示分配给进程i的资源j,显示每个进程当前的资源分配情况
存在这样的关系式:
1) Rj = Vj + 所有的Aij;
2) Cij <= Rij //进程所申请的资源总是少于资源的总量
2) Aij <= Cij //分配给进程的资源总是少于进程所申请的
有了这些矩阵表达式和关系式,就可以定义一个死锁避免策略:如果一个进程的资源需求会导致死锁,则拒绝启动这个进程。
即当且仅当所有的j,Rj >= C(n +1)j + 所有的i < =n的Cij的和,才启动新进程P(n+1)。
3.2 资源分配拒绝方法,又称为银行家算法
银行家算法:
想从银行借钱的顾客对应于进程,借出的钱对应于资源。作为银行业务问题,银行可以借出的钱是有限的,每个顾客都有一定的银行信用额。顾客可以选择借一部分,但不能保证顾客在取走大量贷款后一定偿还。可如果银行有风险,没有足够的基金提供更多的贷款让顾客最后偿还,则银行家会拒绝贷款给顾客。
需要定义状态和安全状态,状态是当前给进程分配的资源情况,安全状态是指至少有一个资源分配序列不会导致死锁。
死锁避免策略:
确保系统中进程和资源总是处于安全的状态,当进程请求一组资源时,假设同意该请求,从而改变了系统状态,然后确定其结果是否还是处于安全状态。
如果是,同意这个请求;如果不是,阻塞该进程。
全局数据结构
strnct state{
int resource[m];
int available[m];
int claim[n][m];
int alloc[n][m];
}
资源分配方法:
if(alloc[i,*] + request[i,*] > claim[i,*]) error; //如果总申请超过相应的资源总数,出现错误
else if(request[*] > available[*]) suspendProcess; //如果请求的量超过了剩余资源总量,阻塞进程
else{
define newstate by:
alloc[i,*] = alloc[i,*] + request[*]; //定义新的状态,假定分配请求资源
avilable[*] = available[*] - request[*]; //剩余资源总量减少
}
if(safe(newstate)){ //判断是否是安全状态,如果是,进行资源分配
carry out allocation;
}
else{ //如果否,则重置原始状态,阻塞进程
restore original state;
suspend process;
}
测试安全算法(银行家算法)
boolean safe(state s){
int currentavail[m];
process rest[number of processes];
currentavail = available;
rest = {all process};
possible = true;
while(possible){ //试图去尽可能地满足所有rest中的进程的请求,用possible做标记,为真,则可以继续为进程分配资源,为假则表示rest中没有找到一个可以分配足够资源的进程了
find a process Pk in rest such that
claim[k,*] - alloc[k,*] <= currentavail; //找到一个有足够资源分配给它的进程
if(found){ //如果找到了更新状态,资源分配出去,且将此进程踢出剩余进程
currentvail = currentvail + alloc[k,*];
rest = rest -Pk;
}
else possible = false;
}
return (rest == null); //如果为空,则表示所有申请资源的进程都可以通过一个顺序得到满足,而不会出现死锁
}
如果是没有死锁的资源分配方式,那么可以认为是safe的了
死锁避免比死锁预防的限制少,但在使用上还是有很多限制:
1)必须事先申明每个进程申请的最大资源
2)考虑的进程必须是无关的,没有同步要求限制
3)分配的资源数目必须是固定的
4)在占有资源时,进程不能退出
4. 死锁检测deadlock dectection
只要有可能,被申请的资源就被授权给进程
4.1 死锁检测算法
死锁的检测可以在非常频繁地在每个资源请求时进行,也可以进行得少一些,具体取决于发生死锁的可能性。
好处是尽快地检测死锁情况,并且由于此方法基于系统状态的逐渐变化情况,因而算法比较简单。
数据结构:
Allocation矩阵和Available向量,还定义了一个请求矩阵Q,其中Qij表示进程i请求资源j的量。
算法:
一个标记没有死锁的进程的进程。
最初所有进程都是未标记的,然后执行以下操作:
1)标记Allocation矩阵中一行全为零的进程(没有分配资源的进程不会死锁的)
2)初始化一个临时向量W,令其等于Available向量
3)查找下标i,使进程i当前未标记且Q的第i行小于等于W,即对所有的1<=k<=m,Qik<=Wk。如果找不到这样的行,终止算法
找到一个没有死锁的进程。
4)如果找到了这样的行,标记进程i,并把Allocation矩阵中的相应行加到W中,也就是说,对所有的1<=k<=m,令Wk = Wk + Aik。
策略:
查找一个进程,使得可用资源可以满足该进程的资源请求,然后假设同意这些资源,让该进程运行直至结束,释放所有资源,即Wk = Wk + Aik。
然后算法继续找寻,不断标记。算法相当简单
算法的结果是有没有标记的进程时存在死锁,每个未标记的进程都是死锁的。
算法不能保证可以防止死锁,这要取决于将来请求的次序,它所做的一切是确定当前是否存在死锁。
4.2 恢复
一旦检测到死锁,就需要某种策略以恢复死锁:
1)取消所有的死锁进程,最常用的
2)把每个死锁进程回滚到前面定义的某些检查点,并且重新启动所有进程
要求在系统中构造回滚和重启机制
3)连续取消死锁进程直到不再存在死锁
选择取消进程的顺序基于某种最小代价原则,每次取消后,必须重新调用死锁检测方法,已验证是否仍存在死锁
4)连续抢占资源直到不再存在死锁
和3)一样,需要使用一种基于代价的选择方法,且需要重新调用死锁检测
5 综合的死锁策略
1)把资源分成几组不同的资源类
2)为预防在资源类之间由于循环等待存在死锁,可以使用定义的线性排序策略,
即不同的资源,分配的顺序不同
3)在一个资源类中,使用该类资源最合适的算法
1)可交换空间:在进程交换中所使用的外存中的存储块
如果已知最大存储请求,可以通过要求一次性分配所有请求的资源来预防死锁
2)进程资源:可分配的设备,如磁带设备和文件
死锁避免策略常常是很有效的,因为进程可以事先申明它们将需要的这类资源
3)内存:可以按页或段分配给进程
对于内存,基于抢占的预防是最合适的策略,被抢占的进程被换到外存
4)内部资源,如I/O通道
基于资源排序的预防策略
6.哲学家就餐问题
问题描述:
有5位哲学家住在一个房子里,在他们面前有一个餐桌。每位哲学家的生活就是思考和吃饭。
吃饭需要两把叉子,在哲学家盘子的两侧都有叉子,一共五个盘子,五个叉子。
算法需要保证互斥(不能共用叉子),同时还要避免死锁和饥饿。
6.1 使用信号量解决方案
方案一:
每位哲学家首先拿起左边的叉子,然后拿起右边的叉子,用餐结束后放下左右的叉子。
此方案会导致死锁和饥饿。
semphore fork[5] = {1}; //五个叉子互斥使用,使用二元信号量
int i;
void philosopher(int i){
while(true){
think();
wait(fork[i]);
wait(fork[(i + 1) mod 5]);
eat();
sigal(forkfork[(i + 1) mod 5]);
sigal(fork[i]);
}
}
void main(){
parbegin(philosopher[0],....,philosopher[4]);
}
改进的办法:
增加一个服务员,一次只允许最多四个哲学家就坐,那么需要一个等于4的计数信号量
semphore fork[5] = {1};
semphore desk = 4;
int i;
void philosopher(int i){
think();
wait(desk); //这也是唯一的改变
wait(fork[i]);
wait(fork[(i + 1) mod 5]);
eat();
sigal(forkfork[(i + 1) mod 5]);
sigal(fork[i]);
signal(desk);
}
6.2 管程解决方案
所谓管程的解决方案,提供一种互斥机制,使管程中的数据变量每次只能被一个进程访问到。
并维持着多个条件变量队列。
为每一把叉子对应一个条件变量,有一个布尔向量记录每把叉子的使用状态。
monitor dining_controller;
cond ForkReady[5] = {true}; //可以理解为五个条件队列
void get_forks(int pid){
int left = pid;
int right = (pid + 1)% 5;
if(!fork(left)) cwait(ForkReady[left]);
fork(left) = false;
if(!fork(right)) cwait(ForkReady[right]);
fork(right) = false;
}
void release_forks(int pid){
int left = pid;
int right = (pid + 1)% 5;
if(empty(ForkReady[left]))
fork(left) = ture;
else signal(ForkReady[left]);
if(empty(ForkReady[right]))
fork(right) = ture;
else signal(ForkReady[right]);
}
void philosopher(int i){ //i 从0到4
while(true){
think();
get_forks(i);
eat();
release_forks(i);
}
}
6.7 UNIX的并发机制
管道、信息和共享内存提供了进程间传递数据的方法,而信号量和信号则用于其他进程的触发行为。
7.1 管道
管道是一个环形缓冲区,允许两个进程以生产者/消费者的模型进行通信。因此,是一个先进先出的队列,由一个进程写,另一个进程读。
管道被创建时获得一个固定大小的字节数。
在尝试写操作时,如果有足够的控件,则写请求被立即执行,否则该进程被阻塞。
同样,读操作时,如果试图读取多余当前管道的字节数时,它也被阻塞,否则读请求立即执行。
操作系统强制实施互斥,即一次只能有一个进程访问管道。
两种管道:
1)命名管道:
不相关的进程只能共享命名管道
2)匿名管道:
只有有血缘关系的进程可以共享匿名管道
7.2 消息Message
消息是有类型的一段文本。
UNIX为参与信息传递的进程都提供msgsnd和msgrcv系统调用。
每一个进程都有一个与之相关联的消息队列,其功能类似于信箱。
消息发送者指定发送的每个消息的类型,类型可以被接收者用做选择的依据。
接收者可以按照先进先出的原则的顺序接收消息,或者按照其类型接收。
当进程试图给一个满队列发送消息时,它将被阻塞;
当进程试图从一个空队列读取消息时,它将被阻塞。
但是如果进程读取某一特定类型的队列失败时,该进程是不会阻塞的。
7.3 共享内存
共享内存是UNIX提供的进程间通信手段速度最快的一种。这是虚存中由多个进程共享的一个公共内存块。
进程读写共享内存所使用的机器指令与读写虚拟内存空间的其他部分所使用的指令相同。
每个进程有一个只读或读写权限。
互斥约束属于共享内存机制的一部分,但必须由使用共享内存的进程提供。
7.4 信号量
UNIX System V的信号量系统调用是对semwait和semsignal的推广,在它们上面可以同时进行多个操作,并且增量和减量操作的值可以大于1。
内核自动完成所需要的操作,且在所有操作完成前,其他任何进程都不能访问该信号量。
一个信号量包含以下元素:
1)信号量当前值
2)在信号量上操作的最后一个进程的进程ID
3)等待给信号量的值大于当前值的进程数
4)等待该信号量的值为零的进程数
与信号量相关联的是阻塞在该信号上的进程队列。
信号量实际上是以集合的形式创建的,一个信号量集合有一个或多个信号量。
sem_ctl系统调用允许同时设置集合中所有信号量的值。
sem_op系统调用把一系列信号量操作作为参数,每个操作定义在集合中的一个信号量上,每个操作定义在集合上的一个信号量上。
7.5 信号
信号是用于向一个进程通知发生异步事件的机制。
信号类似于硬件中断,但没有优先级,即内核平等地对待所有的信号。即使对于同时发生的进程,一次只给进程一个信号,没有次序。
进程间可以相互发送信号,内核也可能在内部发送信号。
信号的传递是通过修改信号要发送到的进程所对应的进程表中的一个域来完成的,也就没有队列排队的概念了。
只有在进程被唤醒继续运行时,或者进程准备从系统调用中返回时,才能处理信号。
进程可以通过执行某些默认行为(如终止进程)、执行一个信号处理函数或者忽略该信号。
小结:
死锁是指一组争用资源或相互通信的进程被阻塞的现象。
阻塞是永久的,除非操作系统采取某些非常的行动,如杀死一个或多个进程,或者强迫一个或多个进程进行回滚。
死锁可能涉及可重用资源和可消耗资源。
可重用资源是指不会因为使用而被耗尽或销毁的资源,如I/O通道或一块内存空间。
可消耗资源时指当被一个进程获得时就销毁了的资源,这类资源的例子有消息和I/O缓冲区中的信息。
处理死锁常用的三个方法:
1)死锁预防
通过确保死锁的一个必要条件不会满足,保证不会发生死锁
2)死锁避免
涉及分析新的资源请求,以确定它是否会导致死锁,并且只有当不可能死锁时才同意该请求
3)死锁检测
如果操作系统总是同意资源请求,则需要周期性地死锁检测,并采取行动打破死锁。