1.死锁
1.1 基本概念
死锁:死锁指的是系统中并发执行的多个线程(进程)由于无法获所需的资源而永久阻塞的状态。
死锁产生的必要条件:
A.排它性互斥:指的是资源在任意时刻只能由一个任务(线程或进程)使用。如果此时还有其它任务请求该资源,则请求者只能等待,直至占有资源的任务释放资源。
B.不可抢占:指的是当一个任务拥有某种资源时,除非它主动释放它,否则无法让该任务失去该资源的拥有权。
C.持有和等待:指的是任务已经拥有了至少一种资源,然后又等待其它资源可用。
D.循环等待:指在发生死锁时,必然存在一个任务——资源的环形链,即任务集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
产生死锁时这4个必要条件都必须被满足,因而处理死锁就是要避免、预防这4个条件同时成立,或者在4个条件同时成立时破坏其中的任意一个条件。处理死锁的方法包括:
- 预防死锁
- 避免死锁
- 检测死锁
- 解除死锁
1.2 预防死锁
该方法是为资源申请设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。
使用的限制条件一般有:
A.消除持有和等待这个条件:任务要一次申请完它所有需要的资源,只有当所有资源都可以获得时它才能继续运行。由于要求任务一次申请完其所有的所有资源,因而就不存在持有和等待这种情况。但是其不足在于:在实际的系统中很难预测一个任务需要那些资源,而且即便可以预测出来任务所需的资源,任务在运行时也不一定就要用到这些资源,因为运行中真正需要的资源是由代码路径决定的,这就可能造成占有了不使用的资源从而导致出现浪费;更严重的问题在于,使用该防范隐含着一个要求,即任务所需要的资源必须同时被释放。这就意味着所有的资源都只能在任务已经不需要任何资源时才能被释放,这会造成极大的资源浪费。
B.消除不可抢占这个条件:如果任务申请新的资源的请求不能被满足,它就应该释放它已经持有的资源。随后任务再次尝试申请资源的时候应该申请以前已经持有的资源和新需要的资源。与第一种条件相比,这种条件不需要任务一次申请其所需的所有资源,而是根据需要申请。但是该方法也存在严重的不足,因而它在申请一种资源失败后,就会释放所有已经持有的资源,当重新尝试申请资源时就要申请这个时候所需的所有资源,这就需要追踪所需的资源,而且在再次尝试时,可能以前已经被持有过的资源也变的不可用了,这无疑加大了编程的复杂度。
C.消除循环等待这个条件: 该方法要求给资源进行统一编号,如果任务已经持有了编号为i的资源,则它只能申请编号大于i的资源。这就消除了循环等待这个条件。
1.3 避免死锁
该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。避免死锁算法中最有代表性的算法是Dijkstra E.W 于1968年提出的银行家算法:银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许任务动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。安全序列是指一个任务序列{P1,…,Pn}是安全的,即对于每一个任务Pi(1≤i≤n),它之后的任务所需要的资源量不超过系统当前剩余资源量与所有进程Pj (j < i )当前占有资源量之和。安全状态和不安全状态:
- 安全状态:如果存在一个由系统中所有进程构成的安全序列P1,…,Pn,则系统处于安全状态。安全状态一定是没有死锁发生。
- 不安全状态:不存在一个安全序列。不安全状态不一定导致死锁。
为了使用该策略,每个任务都需要预估它所需要的最大资源量,然后系统使用该信息建立一个资源需求表以供使用。但是通常这种预估都是比较困难的。而且由于预估的是最大资源量,而实际运行中一个任务并不一定会真正用到完它所预估的最大资源量,因而使用这种预估进行死锁避免时有可能导致不必要的阻塞。
1.4.检测死锁
死锁检测并不须事先采取任何限制性措施,它只用于在运行过程中发生死锁。并精确地确定与死锁有关的任务和资源,然后就可以采取适当措施,从系统中将已发生的死锁清除掉。
1.4.1 单实例资源的死锁检测
所谓单实例资源即该资源只有1个,因此也只能被一个任务所申请使用。
- 画出进行死锁检测时系统资源的使用、申请有向图graph。其中如果任务当前拥有某个资源,就画一条从该资源到该任务的有向边;如果任务正在申请某个资源,就画一条从该任务到该资源的有向边。
- 从该图中取出一个未遍历过的节点,将图graph中的该节点标记为已遍历,并创建一个空列表L用来容纳图graph的节点。
- 检查该节点是否已经存在于列表L中了,如果是,则就存在死锁;否则将该节点加入列表L的尾部
- 检查图graph中是否还存在未检查的从该节点发出的有向边,如果没有,则跳转到步骤6
- 从这些有向边中选取一条,在图graph中将该有向边标记为已检查,将该有向边的入节点当作当前节点,在图graph中将当前节点标记为已遍历,并跳转到步骤3
- 如果L非空,则将L中最后一个节点从L中删除,如果L还非空,则将L中最后一个节点设为当前节点,并跳转到步骤4
- 如果图graph中还有未遍历过的节点,则跳转到步骤2;否则算法结束,不存在死锁
1.4.2 多实例资源的死锁检测
所谓多实例资源即该资源有多个,因此在同一时间可能被多个任务所使用
- 列出进行死锁检测时每种资源的剩余量和每个任务需要使用的量
- 找出一个任务,它所需要的每种资源量小于或等于系统此时剩余的相应资源的量,即它的申请要求可以被满足;如果找不到这样的任务,则系统存在死锁
- 标记步骤2找出的任务为可运行,并将此时它所持有的资源量加到系统剩余资源量上
- 如果系统中还存在没有标记为可运行的任务,就回到步骤2;否则算法结束,系统不存在死锁
1.5 解除死锁
它一般与检测死锁一起使用。常用的实施方法是撤销或挂起一些任务,以便回收一些资源,再将这些资源分配给已处于阻塞状态的任务,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。
这些策略一般都是由系统实现和支持的,在应用程序的编写中也有一些原则可供参考:
A.不要在可能会对性能造成不良影响的长时间操作(如 I/O)中持有锁。
B.不要在调用模块外且可能重进入模块的函数时持有锁。
C.一般情况下,优先使用粗粒度锁,如果确定粗粒度锁对性能造成了很大影响,再使用细粒度锁。
D.使用多个锁时,尽量让所有任务都按照相同的顺序来上锁以避免死锁。
2.优先级逆转
2.1 基本概念
优先级逆转:优先级逆转指的是在可抢占系统中,高优先级任务由于等待低优先级任务所持有的资源而阻塞,同时低优先级优先运行的状态。它的基本表现有两种情形:
A.低优先级T1任务持有资源R运行;高优先级任务T2启动开始运行,高优先级任务需要使用资源R,但是无法获得该资源,因而阻塞;低优先级任务T1继续运行,看上去好像高优先级任务被低优先级任务抢占了
B.低优先级T1任务持有资源R运行;高优先级任务T2启动开始运行,高优先级任务需要使用资源R,但是无法获得该资源,因而阻塞;低优先级任务T1被调度运行;此时一个优先级小于T2但是大于T1的任务T3启动了,它就会抢占任务T1;最终看起来好像任务T3抢 占了高优先级任务T2,如果说情形1由于竞争资源还比较容易看出来的话,情形2则更具有迷惑性,T2和T3之间不存在竞争关系,T3竟然在T1之前运行了。
大部分情况下优先级逆转并不导致问题,因为高优先级任务只是延迟执行了,但是有时候这也会是个问题。
2.2 解决方法
由于优先级逆转和资源的共享使用有关,因而可以通过对资源添加访问控制协议来解决这个问题,可以通过如下三种资源访问控制协议来解决优先级逆转问题:
A.优先级继承协议
B.天花板优先级协议
C.优先级天花板协议
2.2.1 优先级继承协议
该协议的访问控制规则:如果一个任务T1由于无法获取资源R而阻塞,并且其优先级高于资源R的持有者T2的优先级,就提升其持有者T2的优先级到任务T1的优先级。具体的规则:
- 如果资源R不可用,申请该资源的任务T阻塞
- 如果资源R可用,就将其分配给申请者任务T
- 当有优先级高于任务T的任务申请资源R时,就提升T的优先级到该高优先级任务的优先级
- 当任务T释放资源R时,就恢复其优先级
2.2.2 天花板优先级协议
在天花板优先级中,所有任务所需要的所有资源都是已知的,所有任务的优先级也是已知的。每种资源都有一个天花板优先级,它等于所有需要使用该资源的任务的最高优先级。比如系统中有任务T1(优先级4)、T2(优先级6)、T3(优先级8)、T4(优先级10),资源R1、R2、R3、R4,其中任务T1需要使用资源R1、R3,任务T2需要使用资源R2,任务T3需要使用资源R2、R4,任务T4需要使用资源R1、R3、R4.则资源R1、R2、R3、R4的天花板优先级分别为:10,8,10,10。使用该协议时的规则为:
- 如果资源R不可用,申请该资源的任务T阻塞
- 如果资源R可用,就将其分配给申请者任务T,任务T的优先级被提升到资源的天花板优先级
- 当任务T释放拥有最高天花板优先级的资源时,就重新调整其优先级,即任意时刻都保证任务以它拥有资源中的最高天花板优先级运行
- 当任务释放所有所使用的资源的时候,它的优先级就回到系统分配给它的优先级
2.2.3 优先级天花板协议
类似于天花板优先级协议,在优先级天花板协议中所有任务所需要的所有资源都是已知的,所有任务的优先级也是已知的。每种资源都有一个天花板优先级,它等于所有需要使用该资源的任务的最高优先级。比如系统中有任务T1(优先级4)、T2(优先级6)、T3(优先级8)、T4(优先级10),资源R1、R2、R3、R4,其中任务T1需要使用资源R1、R3,任务T2需要使用资源R2,任务T3需要使用资源R2、R4,任务T4需要使用资源R1、R3、R4.则资源R1、R2、R3、R4的天花板优先级分别为:10,8,10,10。二者不同之处在于规则,在优先级天花板协议中有一个当前优先级天花板,当前优先级天花板等于当前正被使用的所有资源的最高天花板优先级,基于当前优先级天花板,该协议的规则为:
- 如果资源R不可用,申请该资源的任务T阻塞
- 如果资源R可用,并且任务T的优先级高于当前优先级天花板,则资源R就分配给T,否则看规则3
- 如果资源R可用,并且任务T持有的某个资源的天花板优先级等于当前优先级天花板,则资源R就分配给T,否则任务T将阻塞
- 如果任务T的优先级高于阻塞了它的任务T1的优先级,则T1将继承任务T的任务并以该优先级任务运行,T1将在它释放了所有天花板优先级高于T的任务优先级的资源后恢复其原有优先级
该协议结合了优先级继承协议和天花板优先级协议,用当前正在被使用的资源的优先级天花板计算出了一个当前天花板优先级,但是它又不同于运行任务的优先级;任务的运行优先级只有在任务阻塞了一个高优先级任务时才会改变,而且这个改变不是简单的设置为当前优先级变化板,而是继承它所阻塞的高优先级任务的优先级
3.确保程序只有一个运行实例
经常见到的一个需求是使得应用程序只有一个运行实例在运行。这可以通过记录锁来实现,实现方法是:
A.在启动应用后创建一个特殊文件,该文件的位置和名字是特定唯一的
B.尝试在该文件上上一个记录锁写锁,并锁住整个文件
C.如果上锁失败,说明有一个应用实例在运行,退出
D.继续执行
之所以记录锁可以实现该目的是因为:根据记录锁的特性,无论记录锁的持有者进程以何种方式结束,系统都会自动释放它所持有的所有记录锁--如果进程没有自己释放它的话。因而只要无法锁定就肯定表明有一个正在运行的实例正在持有这把锁。
由于命名信号量也具有类似的特性(系统会在进程终止时自动关闭其打开的所有命名信号量),因而命名信号量也可以用于该目的。