文章目录
- 1.1 什么是死锁
- 1.2 死锁的类型
- 1.2.1 竞争资源引起的死锁
- 1.2.2 进程间通信引起的死锁
- 1.2.3 其他原因引起的死锁
- 1.3 死锁产生必要条件
- 1.4 死锁的处理策略
- 1.5 死锁的预防
- 1.5.1 破坏资源独占条件
- 1.5.2 破坏不可剥夺条件
- 1.5.3 破坏保持申请条件
- 1.5.4 破坏循环等待条件
- 1.6 死锁的避免
- 1.6.1 什么是安全序列
- 1.6.2 安全序列、不安全状态、死锁的联系
- 1.6.3 银行家算法
- 1.6.3.1 安全性检查算法
- 1.6.3.2 资源分配算法
- 1.7 死锁的检测与消除
- 1.7.1 资源分配图
- 1.7.2 资源分配图的约简
- 1.7.3 死锁检测算法
- 1.7.4 死锁检测时刻
- 1.7.5 死锁的恢复
- 1.8 鸵鸟算法
- 1.9 饥饿与活锁
- 1.9.1 饥饿与饿死
- 1.9.2 活锁
- 1.10 死锁的例子
- 1.10.1 过河问题1
- 1.10.2 过河问题2
- 1.11 可复用资源的静态分析
- 1.11.1 相关概念
- 1.11.2 可复用资源死锁的静态分析算法
- 1.12 同种组合资源死锁的必要条件
1.1 什么是死锁
哲学家进餐问题中,如果5位哲学家进程并发执行,都拿起了左手边的筷子……
semaphore chopstick[5]={ 1, 1, 1, 1, 1 };
Pi (){ W//i号哲学家的进程
while(1){
P(chopstick[i]);//拿左
P(chopstick[(i+1)号5]);//拿右
吃饭...
V(chopstick[i]);//放左
V(chopstick[(i+1)号5]);//放右
思考...
}
}
那么根据上面这段代码逻辑,五个哲学家同时阻塞,程序无法推进,这种由于进程竞争资源而引起的僵持称为”死锁“。
一组进程中的每个进程均等待此组进程中其他进程所占有的、因而永远无法得到的资源,这种现象称为进程死锁,简称死锁。
死锁 VS 饥饿 VS死循环
- 死锁:各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。
- 饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象。比如:在短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而发生长进程“饥饿”。
- 死循环:某进程执行过程中一直跳不出某个循环的现象。有时是因为程序逻辑bug 导致的,有时是程序员故意设计的。
1.2 死锁的类型
1.2.1 竞争资源引起的死锁
这种类型的死锁是由于进程争夺使用系统中有限的资源而引起的,因而称为竞争资源引起的死锁,如定义所述。这种类型的死锁是本文接下来要讨论的核心。也就是说,在接下来的讨论中如未特别指明均假定死锁是由于进程竞争资源而引起的。
1.2.2 进程间通信引起的死锁
假设在一个基于消息的系统中,进程P1等待进程P2发来的消息,进程P2等待进程P3发来的消息,进程P3等待进程P1发来的消息,如此3个进程均无法继续向前推进,亦即发生死锁这种类型的死锁在基于消息的系统中比较常见。
1.2.3 其他原因引起的死锁
例如,假设有一扇门,此门较小,一次只能通过一个人。又设有两位先生M1和M2,他们都要通过这扇门。显然,门相当于一个独占型资源。如果M和M,竞争地使用这一资源,则他们都能通过。但是假如他们都很谦虚,在门口处M1对M2说“您先过”,M2对M1说“您先过”,则二者均无法通过,造成僵持。这就是“After You / Afer You”问题。如果程序设计得不合理,也可能会发生类似的现象,在广义上也称为死锁。
1.3 死锁产生必要条件
假设死锁是由于进程竞争资源而引起的,下面给出死锁发生的4个必要条件。这4个必要条件是由Coffman于1971年首先提出的,所以也称为Coffman条件。
- 资源独占。一个资源在同一时刻只能被分配给一个进程。如果某一进程申请某一资源,而该资源正在被另外的某一进程所占有,则申请者需要等待,直到占有者释放该资源。
- 不可剥夺。资源申请者不能强行地从资源占有者手中夺取资源,即资源只能由其占有者在使用完后自愿地释放。
- 保持申请。进程在占有部分资源后还可以申请新的资源,而且在申请新资源的时候并不释放它已经占有的资源。
- 循环等待。存在一个进程等待序列{ p1,p2,……,pn},其中p1等待p2所占有的某一资源,p2等待p3所占有的某一资源……pn等待p1所占有的某一资源。
**当且仅当上述4个条件同时满足时,死锁才会发生。**换言之,只要破坏上述4个条件中的任意一个,死锁就不会发生。
1.4 死锁的处理策略
- 不允许死锁发生
- 预防死锁(静态)。破坏死锁产生的四个必要条件中的一个或几个。是指对于进程有关资源的活动按某种协议加以限制,如果所有进程都遵守此协议,即可保证不发生死锁;
- 避免死锁(动态)。是指对于进程有关资源的申请命令加以实时检测,拒绝不安全的资源请求命令,以保证死锁不会发生。(如银行家算法)
- 允许死锁发生
- 死锁的检测和解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁。
1.5 死锁的预防
1.5.1 破坏资源独占条件
资源独占条件:只有对必须互斥使用的资源的争抢才会导致死锁。
如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。比如:SPOOLing技术。操作系统可以采用 SPOOLing 技术把独占设备在逻辑上改造成共享设备。比如,用SPOOLing技术将打印机改造为共享设备……
该策略的缺点:并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏资源独占条件。
1.5.2 破坏不可剥夺条件
**不可剥夺条件:**进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
破坏不可剥夺条件:
- 方案一:**当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。**也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件。
- 方案二:**当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。**这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)
该策略的缺点:
- 实现起来比较复杂。
- 释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。
- 反复地申请和释放资源会增加系统开销,降低系统吞吐量。
- 若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿。
1.5.3 破坏保持申请条件
保持申请条件:进程在占有部分资源后还可以申请新的资源,而且在申请新资源的时候并不释放它已经占有的资源。
可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源
该策略实现起来简单,但也有明显的缺点:有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。另外,该策略也有可能导致某些进程饥饿(旱的旱死涝的涝死)。
1.5.4 破坏循环等待条件
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。
原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按此规则,已持有大编号资源的进程不可能逆向地回来申请小编号的资源,从而就不会产生循环等待的现象。
如果我们存在一个环,那么环上拥有最大编号的进程没有资格申请小编号资源,我们一定可以消环。
该策略的缺点:
- 不方便增加新的设备,因为可能需要重新分配所有的编号
- 进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费
- 必须按规定次序申请资源,用户编程麻烦。
1.6 死锁的避免
1.6.1 什么是安全序列
你是一位成功的银行家,手里掌握着100个亿的资金……
有三个企业想找你贷款,分别是企业B、企业A、企业T,为描述方便,简称BAT
B表示:“大哥,我最多会跟你借70亿……“
A表示:“大哥,我最多会跟你借40亿……”
T表示:“大哥,我最多会跟你借50亿……”
然而……江湖中有个不成文的规矩:如果你借给企业的钱总数达不到企业提出的最大要求,那么不管你之前给企业借了多少钱,那些钱都拿不回来了……
刚开始,BAT三个企业分别从你这借走了20、10、30亿……
最大需求 | 已借走 | 最多还会借 | |
---|---|---|---|
B | 70 | 20 | 50 |
A | 40 | 10 | 30 |
T | 50 | 30 | 20 |
此时,A还想进一步借20亿,你敢借吗?
假如给了A20亿:
最大需求 | 已借走 | 最多还会借 | |
---|---|---|---|
B | 70 | 20 | 50 |
A | 40 | 10 | 30-20=10 |
T | 50 | 30 | 20 |
此时,我们发现如果以后借钱顺序按照T->B->A,那么我们可以依次从三者收回贷款,给T20亿,完事T还回来50亿,然后给B50亿,完事B还回来70亿,然后再满足A,最后100亿全回来了。
或者先给A10亿,A还钱后手里有50亿,再给T20亿,T还钱后就有80亿,最后再给B借……
像上面两种借钱序列:T->B>A 或者 A->T->B 我们是可以满足所有企业的贷款并且成功收回贷款的,这样的序列其实就是安全序列。
1.6.2 安全序列、不安全状态、死锁的联系
所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就处于安全状态。当然,安全序列可能有多个。
如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法”的核心思想。
1.6.3 银行家算法
银行家算法是荷兰学者 Dikstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁。
**核心思想:**在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
银行家算法可以分为——资源分配算法 & 安全性检查算法
数据声明:假设系统中有n个进程,申请m个资源
可用资源:int Available[m];
进程所需最大资源量:int Claim[n][m];
各进程已经分配的资源量:int allocation[n][m];
各进程仍需的资源量:int Need[n][m],显然初始时有Need = Claim
当前各进程资源申请:int Request[n][m]
1.6.3.1 安全性检查算法
算法目标——检测当前是否存在安全序列
算法流程
- Finish[i] 代表 进程i是否可被满足, res[m]代表各资源可用量
- 不断寻找进程i,满足Finish[i] = false, Need[i] <= res
- 找到,更新res[] = res[] + allocation[i], Finish[i]= true
- 否则,跳出循环
- 如果所有i,都满足Finish[i] = true,那么系统处于安全状态
- 否则,系统处于不安全状态
算法时间复杂度O(n^2 m)
1.6.3.2 资源分配算法
当进程i发出资源请求Request[i]
算法流程
-
Request[i] > Need[i], 申请量大于了声明最大需求量,返回false
-
Available = Available - Request[i], Allocation[i] = Allocation[i] + Request[i], Need[i] = Need[i] - Request[i]
-
执行安全性检查算法
-
安全,分配资源,进程i继续执行
-
不安全,将进程i阻塞
Available = Available + Request[i], Allocation[i] = Allocation[i] - Request[i], Need[i] = Need[i] + Request[i]
-
例: 设资源集合 R = { A, B, C }, 资源类 A 中含有 11 个 实例, 资源类 B 中含有 5 个实例, 资源类 C 中含有 8 个实例. 进程集合 P = { p0, p1, …, p4}. 某时刻系统状态如下:
Claim Allocation Need Available A B C A B C A B C A B C p0 8 5 3 1 1 2 7 4 1 3 2 3 p1 3 2 2 2 0 0 1 2 2 p2 9 0 3 3 0 0 6 0 3 p3 2 3 2 2 1 1 0 2 1 p4 4 4 3 0 1 2 4 3 1 我们此时尝试寻找安全序列, 找到: { p1, p3, p4, p2, p0 }, 因而断言系统此时处于安全状态.
现进程 p2 发出新的资源申请, Request[2] = (1, 0, 2), 请你判断该资源是否可以分配.
解:
首先有Request[2] < Need[2]
然后假定分配资源后状态变为
Claim Allocation Need Available A B C A B C A B C A B C p0 8 5 3 1 1 2 7 4 1 2 2 1 p1 3 2 2 2 0 0 1 2 2 p2 9 0 3 3 0 0 5 0 1 p3 2 3 2 2 1 1 0 2 1 p4 4 4 3 0 1 2 4 3 1 运行安全性检测算法, 找到安全序列: <p3, p1, p4, p0, p2>, 因而上述状态是安全的.
所以我们可以为p2分配资源.
1.7 死锁的检测与消除
1.7.1 资源分配图
进程的死锁问题可以用有向图更加准确地加以描述,这种有向图称为系统资源分配图(resource allocation graph),经过对资源分配图的约简将得到重要的死锁定理。
资源分配图的定义
一个系统资源分配图是一个二元组 G = (V,E),其中V是结点集,E是边集。结点集定义为V = P U R,
其中
- p = { p1,p2,…,pn }为系统中所有进程所构成的集合
- R={ r1,r2,…,rn }为系统中所有资源类所构成的集合。
- 边集E = { (pi,ri) } U { (rj,pj) },其中pi ∈ P,rj ∈ R。
- 如果(pi,rj) ∈ E,则有一条由进程
pi
到资源类rj
的有向弧,表示进程pi
申请资源类rj
中的一个资源实例。 - 如果(rj,pi) ∈ E,则有一条由资源类
rj
到进程pi
的有向弧,表示资源类rj
中的一个资源实例被进程pi
占有。 - 将形如(pi,rj)的边称为申请边,将形如(rj,pi)的边称为分配边。
根据上述关于资源分配图的定义,容易证明:
如果图中没有环路,则系统中没有死锁; 如果图中存在环路,则系统中可能存在死锁。
存在环路不一定有死锁是因为一个资源类内的资源实例可以有多个。比如下面的例子
存在环路: p1 -> r2 -> p3 -> r1 -> p1
但是p2是有可能释放一个r1的资源实例的, 所以当p2释放资源后, p3 可以 获得一个r1 资源实例, 此时环路解除, 死锁消除.
可见环路是死锁存在的必要条件而非充分条件。
例:进程集P、资源类集R、边集E:
P = { p1, P2, P3 }
R = { r1(2), r2(3), r3(1), r4(3) }
E = { (r1, p1), (r1, p2), (r2, p1), (r2, p1), (r2, p2), (r3, p3), (r4,p3), (p1, r1), (p2, r3) }其中资源类后括号内的数字均代表资源实例的个数
我们尝试画出上述描述的资源分配图:
黑色边是由已分配资源实例指向分配进程
橙色边是由资源申请进程指向资源实例
显然无环,无死锁发生。
1.7.2 资源分配图的约简
通过资源分配图的约简来检测系统是否处于死锁状态. 资源分配图的约简方式如下:
- 寻找非孤立且无请求边的进程节点pi, 如果不存在这样的结点, 算法结束
- 否则, 去除pi的所有分配边, 回收资源
- 寻找所有请求边都能被满足的进程pj, 将其请求边全部转化为分配边
- 转到1
算法结束时, 如果所有结点均为孤立结点, 那么称资源分配图是可以完全约简的. 否则是不可完全约简的.
死锁定理
S 为死锁状态的充分必要条件是 S 的资源分配图不可完全约简.
1.7.3 死锁检测算法
死锁检测算法由 Shoshani 和 Coffman 所提出, 所用数据结构如下:
- Available: 长度为 m 的向量, 记录当前各个资源类中空闲资源实例的数目.
- Allocation: m × n 的矩阵, 记录当前每个进程占有各个资源类中资源实例的个数.
- Request: m × n 的矩阵, 记录当前每个进程申请各个资源类中资源实例的个数.
为了简洁表达, 我们把 矩阵Allocation 和 Request的行看作向量, 分别表示为 Allocation[i] 和 Request[i].
算法流程:
-
令 Work 和 Finish 分别是长度为 m 和 n 的向量, 分别表示可用资源和进程完成是否完成, 初始化:
- Work = Available
- 对于所有的 i = 1, 2, …, n, 如果 Allocation[i] != 0, 则 Finish[i] = False, 否则 Finish[i] = true.
-
寻找满足下述条件的下标 i:
- Finish[i] = false
- Request[i] <= Work
- 如果不存在上述 i , 则转到步骤4
-
Work = Work + Allocation[i]
Finish[i] = True
-
如果存在 i , 1<= i <= n, Finish[i] = False, 则系统处于死锁状态. 且进程 pi 参与了死锁
不难发现, 死锁检测算法和我们上面的资源分配图约简过程一致.
时间复杂度 O ( n 2 m ) O(n^2m) O(n2m)
定理: P是进程集合, P’ 是 所有占有资源的进程集合, 则 P 中存在死锁和 P’ 中存在死锁等价
证明很简单, 这里略.
1.7.4 死锁检测时刻
死锁检测时刻取决于:
- 死锁发生频率
- 死锁涉及的进程数目
我们一般可以在如下时刻进行死锁检测:
- 进程等待时刻检测
- 每当进程申请资源不能被满足发生等待时我们就进行检测, 显然代价很高
- 定时检测
- 资源利用率低时检测
- 因为死锁会使得更多进程等待, 导致CPU利用率低, 因此我们设个界限如45%, 此时进行死锁检测.
1.7.5 死锁的恢复
- 系统重启动法
- 最现实、最简单也是代价最高的一种
- 终止进程法
- 终止那些参与死锁的进程, 并回收其资源.两种策略:
- 一次撤销所有参与死锁的进程. 但是代价较高, 有些不占有资源的进程也会被撤销
- 逐一撤销参与死锁的进程, 如果撤销一个进程后仍然存在死锁, 就选择下一个进程进行撤销. 对于撤销进程的选择可以自己定优先级
- 终止那些参与死锁的进程, 并回收其资源.两种策略:
- 剥夺资源法
- 剥夺死锁进程占用的全部或者部分资源. 也分两种实现:
- 逐步剥夺, 一次剥夺死锁进程的一个或一组资源, 如果死锁尚未解除就再继续剥夺, 直到死锁解除
- 一次剥夺
- 剥夺死锁进程占用的全部或者部分资源. 也分两种实现:
- 进程回退
- 类似于我们用虚拟机时经常在VMWare中打快照, 我们不断的按照时间顺序回退到之前的快照, 直到没有死锁
1.8 鸵鸟算法
鸵鸟遇到危险时将头埋进沙子.
鸵鸟算法(ostrich algorithm)选择对死锁视而不见. 这实际上不能算一个算法, 但却是目前操作系统中采用最多的策略, 比如UNIX和Windows. 那么出现死锁怎么办呢? —— 重启吧x_x
实际上这个买卖是很划算的, 死锁检测开销太大, 而死锁事实上发生的频率很低, 偶尔发生一次还是可以接受的.
1.9 饥饿与活锁
1.9.1 饥饿与饿死
饥饿(starvation): 当等待时间给进程的推进和响应带来明显的影响时,就称发生进程饥饿。当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义时, 称该进程被饿死(starve to death)
饿死和死锁都是由于竞争资源而引起的, 但是又有明显差别:
- 从进程状态考虑: 死锁进程都处于等待态. 忙式等待(处于运行态或就绪态)的进程并非处于等待态, 但是却可能被饿死.(如前面章节进程互斥算法中忙等的进程)
- 死锁进程等待永远不会被释放的资源, 饿死进程等待会被释放但却不会分配给自己的资源. 其等待时限没有上界(排队等待或忙式等待)
- 死锁一定是发生了循环等待, 而饿死则不一定. 这也说明资源分配图可以检测死锁却不能检测进程饿死.
- 死锁一定涉及多个进程, 而饥饿或被饿死的进程可能只有一个.
1.9.2 活锁
当进程申请资源不能满足时,便进入等待状态,待资源得到时被唤醒,这种等待称为排队等待。与忙式等待相比,排队等待的进程不会分派CPU,因而减少了对处理器不必要的占用时间.
当资源竞争不是很激烈的情况下, 进程等待时间往往很短, 此时如果等待时间和进程切换开销相差不大, 我们不如让其忙等, 而非排队等待.
忙式等待条件下发生的饥饿称为活锁(live lock)
例如: UNIX下, 我们假设 进程表proc 共有50个表项, 10个进程每个进程都要创建5个子进程, 每个进程执行如下代码段
如果创建失败, 就会睡眠一段时间然后再次创建
for (int i = 1; i <= 5; i ++ )
while (fork() == -1) //创建失败
sleep(5);
那么当每个进程都创建4个子进程时, proc表用完, 发生活锁. 与死锁不同, 活锁可能会解除, 如上例, 只要有进程运行结束, proc有表项空闲, 忙等的进程就可能结束忙等.
1.10 死锁的例子
1.10.1 过河问题1
有南北走向的河流如下图所示,河中有用石块搭成的便桥,每个石块上最多容纳一个过河者,两个相邻石块的间距恰好为一步。西岸过河者经过石块1、2、…、n 到达东岸, 东岸过河者经过石块n、n - 1、…、1到达西岸。试分析可能发生的死锁情况,给出一个无死锁、无饿死、并行度高的解法,并用信号量和PV操作实现。
int west_crossing, east_crossing; int west_wait, east_wait; //两边等的人 semaphore wq, eq; //两边是否能过河 semaphore mutex; //互斥锁 void WestMan() { P(mutex); if (east_crossing > 0) { west_wait = west_wait + 1; V(mutex); P(wq); } else { west_crossing = west_crossing + 1; V(mutex); } 过河 P(mutex); west_crossing = west_crossing - 1; if (west_crossing == 0) { east_wait = east_wait - 1; east_crossing = east_crossing + 1; V(eq); } V(mutex); } void EastMan() { P(mutex); if (west_crossing > 0) { east_wait = east_wait + 1; V(mutex); P(eq); } else { east_crossing = east_crossing + 1; V(mutex); } 过河 P(mutex); east_crossing = east_crossing - 1; if (east_crossing == 0) { west_wait = west_wait - 1; west_crossing = west_crossing + 1; V(wq); } V(mutex); }
上述解法其实问题很明显,如果某方向源源不断的有人到来, 那么另一边的人就会饿死。
那么如何设计无饥饿解法?
考虑读者写者问题中的读写公平解法,增加一个信号量代表过河权,每个人只有拿到这个信号量才能过河,过完河再释放。这样就能够保证两边都不会有人饿死了。
代码略。
1.10.2 过河问题2
有南北走向的河流如下图所示,河中有用石块搭成的便桥,每个石块上最多容纳一个过河者,两个相邻石块的间距恰好为一步。西岸过河者经过石块1、2、5、6、4、3 到达东岸,东岸过河者经过石块3、4、7、8、2、1到达西岸。试分析可能发生的死锁情况,给出一个无死锁、无饿死、并行度高的解法,并用信号量和PV操作实现。
分析最大并行度:8个格子都占满:如左边两人在5、6上,右边的人都占满了,此时能够推进下去
避免死锁:对于一遍正在过河的人数大于等于3,另一边过河人数不能超过2
防止被对方堵住:西边的人先申请3再申请4,东边的人先申请1,再申请2
显然不会饿死
semaphore mutex; //互斥锁 int ec, wc; //两侧过河人数 int ew, ww; //两侧等待人数 semaphore S1, S2, S3, S4, S5, S6, S7, S8; //8块石头 void WestMan() { P(mutex); if (ec >= 3 && wc == 2) { ww ++; V(mutex); P(wq); } else { wc ++; V(mutex); } P(S1); P(S2); V(S1); P(S5); V(S2); P(S6); V(S5); P(S3); P(S4); //先3再4 V(S6); V(S4); V(S3); P(mutex); wc --; if (wc == 2) while (ew > 0) { ew --; ec ++; V(eq); } else if (wc == 1 && ww > 0){ ww --; wc ++; V(wq); } V(mutex); } void EastMan() { P(mutex); if (wc >= 3 && ec == 2) { ew ++; V(mutex); P(eq); } else { ec ++; V(mutex); } P(S3); P(S4); V(S3); P(S7); V(S4); P(S8); V(S7); P(S1); P(S2); //先3再4 V(S8); V(S2); V(S1); P(mutex); ec --; if (ec == 2) while (ww > 0) { ww --; wc ++; V(wq); } else if (ec == 1 && ew > 0){ ew --; ec ++; V(wq); } V(mutex); }
1.11 可复用资源的静态分析
1.11.1 相关概念
**可复用资源:**一次只能分配给一个进程使用的资源,其使用后仍存在,且可再被分配给其他进程使用。
如:打印机、磁带都是可复用资源。
由相对独立的若干资源构成的资源集合称为组合资源,其中每个相对独立的资源称为子资源。
由相同类型的子资源构成的组合资源,称为同种组合资源。
1.11.2 可复用资源死锁的静态分析算法
-
以每个进程占有资源又申请资源作为一个状态,记作 ( p i : a j : a k 1 , a k 2 , … , a k n ) (p_{i}:a_{j}:a_{k_{1}},a_{k_{2}},\dots ,a_{k_{n}}) (pi:aj:ak1,ak2,…,akn)=(进程:当前请求:已占有资源)
-
以每个状态作为一个结点
-
如果 s 1 s_{1} s1所申请的资源为 s 2 s_{2} s2所占有,则由 s 1 s_{1} s1 向 s 2 s_{2} s2画一条有向弧
-
找出所有环路
-
判断环路上的状态是否能同时到达。如是则有死锁的可能性,否则无死锁的可能性。
事实上,如果环路上出现了相同的进程,说明该环路的状态不能同时到达(因为一个进程不可能同时处于多个状态)
类似地,如果环路上出现了相同的已占有资源,也说明环路状态不能同时到达。
不难发现,如果上面两种情况都没出现的话,我们就要进一步分析该环路的状态是否可以同时到达了。
例:设有可复用资源R = { A, B, C, D, E, F, G },进程P1, P2, P3 有关资源的活动如下:
P 1 : c d c ˉ a b d ˉ a ˉ b ˉ P_{1}: c\ d\ \bar c\ a\ b\ \bar d\ \bar a\ \bar b P1:c d cˉ a b dˉ aˉ bˉ
P 2 : d e d ˉ b f e ˉ b ˉ f ˉ P_{2}: d\ e\ \bar d\ b\ f\ \bar e\ \bar b\ \bar f P2:d e dˉ b f eˉ bˉ fˉ
P 3 : c e e ˉ f a c ˉ f ˉ a ˉ P_{3}: c\ e\ \bar e\ f\ a\ \bar c\ \bar f\ \bar a P3:c e eˉ f a cˉ fˉ aˉ
(带-代表释放资源,否则代表申请资源)
那么一共有9个状态;
画有向弧:
找到环路:(P1: b: ad) -> (P2: f: eb) -> (P3: a: cf)
环路上既没有相同进程也没有相同的占有资源,所以我们需要进一步分析:
环路上状态对应三个进程推进到下面的线①
我们假设①可以到达,那么④也可以到达,因为a、b、f这三个资源三个进程都没有,从④跨到①谁先来无所谓
那么我们来看④是否可以同时到达
由于P3需要c,而P1要释放c,所以P1比P3早到达,P1 < P3
P2需要e,P3释放e,所以P3比P2早到达,P3 < P2
因为P2释放d,P1需要d,所以P2 < P1
P1 < P3 < P2 < P1,矛盾,故④不可同时到达,进而得出①不可同时到达
1.12 同种组合资源死锁的必要条件
设某类资源共有M个实例,使用这种资源的进程共有N个,这N个进程中的每个进程至少需要一个资源实例。如果一个资源实例也不需要,就不算在N个进程之中。这N个进程并发执行时发生死锁的必要条件如下:
所有进程所需资源总量
∑
≥
M
+
N
所有进程所需资源总量\sum \ge M + N
所有进程所需资源总量∑≥M+N
证明如下:
$$
\begin{align}
&假定已经发生死锁,且n个进程(2\le n \lt N)被卷入死锁。由于死锁已经发生,系统M个资源均已被n个死锁进程所占有,\ \
& 而且还不够,每个死锁进程至少还需要一个资源实例,n个进程至少还需要n个资源实例。有 \ \
& 死锁进程所需要的资源总量 \ge M + n \ \
& 每个未死锁(已经执行完毕)进程至少需要一个资源实例,N - n个进程至少需要N - n个资源实例,则 \ \
& 未死锁进程所需要的资源总量 \ge N-n \ \
& 则 所有进程所需资源总量\sum \ge M + n + N - n = M + N \ \
\end{align}
$$
例: 某类资源共有16个实例,进程P需要5个实例,进程P需要6个实例,进程P需要4个实例,进程P需要4个实例。请你判断是否会产生死锁。
5 + 6 + 4 + 4 < 16 + 4 = 20,故没有死锁