拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

银行家算法全解析:从死锁避免到安全性检测实战

银行家算法全解析:从死锁避免到安全性检测实战 [操作系统] 银行家算法从教材到实战的完整拆解我在操作系统这门课里摸爬滚打了十年前后带过好几届课程设计和毕业生项目。要说哪块内容是“教材里讲得清楚、考试也考、但真到面试和工作里最容易翻车”的银行家算法绝对排得上号。这门算法的名字听着很唬人其实就是想解决一个非常朴素的问题操作系统把资源分给多个进程时怎么保证不会出现“你等我、我等你”的僵局也就是死锁。这篇文章就把银行家算法彻底拆开——从它要解决的死锁问题出发讲透算法的核心思想和四个核心数据结构再带你手把手实现一遍完整流程最后聊聊考试里最常见的题型套路以及为什么这个算法在真实操作系统里几乎没人直接用。不管你是正在复习期末、准备考研复试还是面试遇到“如何避免死锁”这篇内容都能直接派上用场。1. 银行家算法到底在解决什么问题1.1 死锁是怎么发生的先说场景。假设系统里有三个进程A、B、C两个资源打印机和扫描仪。A占着打印机等扫描仪B占着扫描仪等打印机C呢又占着别的什么在等A手里的资源。这三个进程谁也推进不下去都在干等就形成了死锁。死锁发生的条件其实很严格四个条件得同时满足互斥资源一次只能给一个进程用、持有并等待进程占着自己的资源还存在那等别的资源、不可剥夺资源不能被系统强行抢走、循环等待进程之间形成一个等待环。四个条件缺一个死锁就成不了。这也是为什么实际工程里很多时候只需要破坏其中任何一个条件问题就能解决。银行家算法走的是另一条路它不去破坏这四个条件中的任何一个而是在资源分配之前先做一次“安全检查”---你要是敢把资源给某个进程系统还能不能让所有进程都顺利完成任务如果不能这次分配就拒绝。1.2 银行家算法这个怪名字哪来的这个名字源于银行放贷的思路。银行把钱贷给企业企业逐笔申请资金银行不会一次性把全部额度都批出去而是每次放款前都盘算一下我已经放出去多少、企业一共最多需要多少、剩下的款项是不是能在一个安全的顺序下收回来。如果有一笔钱贷出去之后可能导致某几家企业同时违约银行就宁可先不贷。操作系统就是银行进程就是要钱的企业资源就是现金。进程事先声明“我总共最多需要多少资源”并且保证“用完就会归还”。系统只要在每一次分配前做一次压力测试——按某种顺序模拟推进看能不能让每个进程都拿到它需要的最大量并最终完成任务——能就分配不能就让它等。你可能会觉得这有点保守没错它确实是偏保守的策略但换来的是“绝对不死锁”的保证。在强调安全的场合这个取舍是值得的。1.3 安全状态与安全序列这里必须把两个概念掰开说清楚安全状态和安全序列。安全状态是指系统按照现在的资源分配情况存在至少一个进程执行顺序使得每个进程都能在有限时间内获得所需资源并顺利完成。这个顺序就叫安全序列。比如有三个进程P1、P2、P3如果存在一种顺序让它们都能跑完系统就是安全的如果找不到系统就处于不安全状态。注意不安全状态不等于已经死锁它只是“存在死锁的风险”。银行家算法的核心逻辑就是只在分配后系统仍处于安全状态时才批准这次资源分配。用生活里的例子类比你手里有1000块流动资金三个朋友分别要借500、600、400他们承诺月底还。你判断一下先借给要500的那个人他月底还你500你再借给要600的他月底还600最后借给要400的。这个顺序能让你的钱永远够用这就是安全序列。如果三个人同时要借800、700、900你怎么排都排不出一个能收回来的顺序那就一个都不能借——至少不能全借。2. 四个核心数据结构与算法流程拆解2.1 四个表一个都不能少实现银行家算法需要维护四个核心数组教材上通常用矩阵或者向量表示。下面用n表示进程数m表示资源类型数。第一个是最大需求矩阵Maxn行m列Max[i][j]表示进程i在整个运行周期里最多需要第j类资源的数量。这个数字是进程启动时就声明好的相当于企业向银行申报的贷款总额度。第二个是已分配矩阵Allocation同样n行m列表示当前每个进程已经占用了多少资源。这个数字是动态变化的随着分配和释放不断更新。第三个是需求矩阵Need表示进程还缺多少资源才能达到它的最大需求。三者之间有非常固定的关系Need Max - Allocation。比如进程P0最多需要3台打印机已经拿了2台那它还缺1台。第四个是资源向量Available长度为m表示系统当前还有多少资源是可用的。每次分配和回收都要实时更新。这四个表缺一不可而且它们之间存在严格的约束。实现的坑也往往出在这里很多人写代码时不更新Need矩阵或者更新了Allocation但忘了同步Available导致后续检查结果完全错误。2.2 请求分配时的两步检查当一个进程发出资源请求Request时算法分两步走。第一步是合法性检查Request小于等于Need才合法否则说明进程要的超过了它事先声明的总量属于非法请求直接报错。同时Request必须小于等于Available如果当前可用资源不够进程就得等待。第二步是预分配与安全性检查。这一步先假装分配成功更新数据结构然后调用安全性检测算法判断分配后的系统是否仍然处于安全状态。需要特别提醒的是第二步的所有更新都是“模拟”的不是真正改了系统数据。只有安全性检查通过了才把这些预分配的数据正式写回。这个“先模拟后提交”的思路跟数据库里事务的概念一模一样。我见过很多初学实现直接就把预分配的数据覆盖到了真实数据上检查不通过也没法回滚整个系统状态就乱了。2.3 安全性检测核心中的核心安全性检测算法的输入是预分配后的状态输出是“安全”或“不安全”。如果安全还会顺带给出一个安全序列。算法维护一个工作向量Work初始等于Available再维护一个Finish数组初始都是false表示所有进程都还没完成。然后反复执行这样的操作找一个还没完成的进程Pi它的Need[i]每一类都不超过Work——也就是当前的资源足够满足它——那就认为它的所有资源都可以分配给它让它运行到结束并释放全部资源即Work Allocation[i]同时Finish[i]设为true。如此循环直到找不出任何可推进的进程。最后检查Finish数组如果所有进程的Finish都为true说明找得到安全序列系统安全否则不安全。这个过程的本质是一个贪心模拟每次选择当前资源能满足的进程让它执行完再回收资源。它不要求一次找到最牛的调度方案只要找到一个可行的推进顺序就够了。我习惯把这个过程跟“清空待办清单”类比你有一堆任务每个任务需要不同的工具和时间手上工具不够就先做那些能做起来的做完了工具会释放出来再用这些工具去做更重的任务。如果最终能把所有任务做完你今天就是安全的。3. 手写一次完整实现代码、执行与现场解析3.1 代码实现的核心设计我用Python写一个最小但完整的银行家算法实现包含资源请求处理和安全性检测。完整代码可以在任意Python 3环境运行不依赖第三方库建议你拿到手之后自己敲一遍光看不练分分钟就忘。import copy class Banker: def __init__(self, available, max_matrix, allocation): n len(max_matrix) m len(available) self.available available[:] self.max_matrix [row[:] for row in max_matrix] self.allocation [row[:] for row in allocation] self.need [[self.max_matrix[i][j] - self.allocation[i][j] for j in range(m)] for i in range(n)] self.num_processes n self.num_resources m def check_safety(self, work, allocation, need): n self.num_processes m self.num_resources finish [False] * n safe_sequence [] work work[:] while True: found False for i in range(n): if not finish[i] and all(need[i][j] work[j] for j in range(m)): # 第i个进程可以完成回收它的所有资源 for j in range(m): work[j] allocation[i][j] finish[i] True safe_sequence.append(i) found True break if not found: break return all(finish), safe_sequence def request_resources(self, process_id, request): m self.num_resources for j in range(m): if request[j] self.need[process_id][j]: return False, 请求超过该进程声明需求 if request[j] self.available[j]: return False, 可用资源不足进程需等待 # 预分配 temp_available self.available[:] temp_allocation [row[:] for row in self.allocation] temp_need [row[:] for row in self.need] for j in range(m): temp_available[j] - request[j] temp_allocation[process_id][j] request[j] temp_need[process_id][j] - request[j] safe, seq self.check_safety(temp_available, temp_allocation, temp_need) if not safe: return False, 分配后系统不安全拒绝分配 # 安全正式提交 self.available temp_available self.allocation temp_allocation self.need temp_need return True, f分配成功安全序列为 {seq}这段代码的核心是两点一是所有检查都基于Need和Available的合法性二是预分配与提交分离。安全性检测里的临时数组通过copy模块做深拷贝避免污染真实状态。3.2 跑一个教科书级例子以经典的5进程3资源场景为例。假设系统有A、B、C三类资源总量分别是10、5、7。五个进程的声明和已分配情况如下进程Max (A,B,C)Allocation (A,B,C)Need (A,B,C)P07,5,30,1,07,4,3P13,2,22,0,01,2,2P29,0,23,0,26,0,0P32,2,22,1,10,1,1P44,3,30,0,24,3,1已分配总量分别是7、2、5所以Available (10,5,7) - (7,2,5) (3,3,2)。初始化代码if __name__ __main__: available [3, 3, 2] max_matrix [ [7, 5, 3], [3, 2, 2], [9, 0, 2], [2, 2, 2], [4, 3, 3], ] allocation [ [0, 1, 0], [2, 0, 0], [3, 0, 2], [2, 1, 1], [0, 0, 2], ] banker Banker(available, max_matrix, allocation) safe, seq banker.check_safety(banker.available, banker.allocation, banker.need) print(safe, seq) # 输出 True [1, 3, 4, 0, 2]手动推演一遍Work(3,3,2)从P0开始看Need(P0)7,4,3不满足P1的Need1,2,2都小于等于Work让P1执行回收后Work(5,3,2)接着P3的Need0,1,1满足Work变成(7,4,3)P4的Need4,3,1满足Work变成(7,4,5)再回头看P0Need7,4,3满足Work变成(7,5,5)最后P2的Need6,0,0满足全部Finish。安全序列就是1-3-4-0-2。这个例子运行后你会看到输出就是True和对应的安全序列。建议你把每个进程的推进步骤跟代码一步步对上这个过程跑通了算法就算真正入门了。3.3 再测一次资源请求的完整流程假设现在P1请求 (1,0,2)。第一步检查Request(1,0,2) ≤ Need(P1)(1,2,2)合法≤ Available(3,3,2)可满足。预分配得到新状态Available(2,3,0)Allocation(P1)(3,0,2)Need(P1)(0,2,0)。再执行安全性检测这个状态同样存在安全序列于是分配通过正式提交。如果P0接着请求 (0,2,0)此时Available(2,3,0)Request(0,2,0) ≤ Need(P0)(7,4,3)也 ≤ Available。预分配后Available(2,1,0)Allocation(P0)(0,3,0)Need(P0)(7,2,3)。然后做安全性检测你会发现找不到任何一个进程的Need全部小于等于Work——P1的Need(0,2,0)其中A类需要0、B类需要2但Work中B类只剩1不满足P2、P3、P4也都卡死。此时系统不安全必须拒绝P0的请求。这个拒绝看起来有点“不近人情”因为P0只是要2个B类资源系统也不是完全枯竭可一旦给了未来就存在死锁的风险。银行家算法就是这么强硬宁可让一个进程多等一会儿也绝不让系统走进不安全的边缘。4. 考试与面试里最常见的题型与易错点4.1 三类高频考题逐个击破银行家算法在408统考和各大高校期末题里题型非常固定。第一类是给你初始矩阵让你判断当前状态是否安全并给出安全序列这类题占六成以上。解法就是套安全性检测算法从进程0开始逐个试探找到满足条件的就推进。要注意的是安全序列不唯一考试只要写对一条路径即可但阅卷时通常看步骤建议把每一步的Work变化都写清楚。第二类是给定一次资源请求让你判断能否分配。这类题要严格按两步走先合法性后安全性。很多同学会跳过合法性直接做安全性检测这在考试里会扣过程分。第三类是反问题给你一个不安全状态让你说明为什么找不到安全序列。这种题的关键是说出“不安全状态不一定死锁但存在死锁风险银行家算法通过拒绝分配来规避风险”。4.2 高频易错点清单第一个易错点是搞混Available的初始化。Available是当前剩余资源不是系统资源总量。好多同学直接用系统总量去做安全性检测结果永远都是安全的完全失去了检测意义。第二个易错点是在安全性检测里没注意进程一旦Finish置为true后续就不能再被选一次。有的实现会把已完成的进程又拉进来重复计算资源导致安全序列里出现同一个进程两次或Work被重复累加。第三个易错点是忽略了Need矩阵动态变化。每次请求成功后Need必须同步更新。我在批改课程设计时经常看到Allocation跟Available都改了Need却忘记更新导致下一次请求判断时数据全错。第四个易错点是请求量超过Need时依然继续做安全性检测。这属于非法请求应当直接终止或报错而不是继续算下去。实际工程里这种请求往往意味着进程逻辑有bug不能姑息。第五个易错点是预分配与正式提交不分。一旦安全性检测不通过必须回滚所有预分配变量回到分配前的状态。很多实现偷懒直接拿正式数据结构做预分配检查不通过就乱了套答辩时基本一眼被看穿。4.3 面试官想听到的技术面试如果问银行家算法大多不会只让你背定义。面试官更想听你说清楚三件事算法的本质是避免死锁而非检测死锁它发生在资源分配阶段通过预判规避风险安全性检测的时间复杂度是O(n²·m)n是进程数m是资源类型数循环里每次要扫描n个进程每轮最多推进一个这个算法在实际操作系统中很少直接用因为进程很难事先声明最大资源需求而且资源类型和进程数量一大开销就上去了。能把最后一点说到位面试官通常会觉得你是真的理解而不是考前背了两天书。5. 从教材到工程算法为什么在真实系统里少见5.1 理想假设与实际生产的差距银行家算法的前提是每个进程都能事先声明自己所需的最大资源量。这在批处理时代的作业调度里勉强可行因为作业的任务边界是明确的。但现代操作系统里进程是动态创建、动态申请资源的进程自己往往都不知道未来会用到多少内存、打开多少个文件、发起多少个网络连接。没有最大需求声明算法第一步就卡住了。再加上进程数量动辄成百上千资源类型从CPU、内存、磁盘到各种外设O(n²·m)的安全性检测开销在每一个系统调用点上执行代价是完全不可接受的。为了“绝对不死锁”付出这么大的性能代价在生产环境里得不偿失。5.2 真实系统用什么策略现代操作系统处理死锁的主流策略是鸵鸟算法——不处理真出了问题就重启。别觉得搞笑这在很多场景下确实是理性的选择死锁发生的概率很低而避免死锁的代价很高两害相权取其轻。在驱动开发或数据库引擎里更常见的是死锁检测与恢复。系统定期检查资源等待图看有没有环发现死锁就剥夺某个进程的资源或者直接终止它。比如MySQL InnoDB检测到死锁会选一个代价最小的事务回滚。这种“先放任出事了再收拾”的思路跟银行家算法刚好相反。还有一种思路是加锁顺序规范化也就是破坏循环等待条件。比如所有进程必须按固定的编号顺序申请资源A类锁永远在B类锁之前拿这样就不会出现AB和BA互相等待的环。工程上这种手段最常见因为实现简单、零额外开销。5.3 那学它还有什么用你可能会想既然真实系统不用学它干嘛第一它把“如何用预判来避免系统性风险”这个思想展现得特别清楚这种思维在分布式系统、并发编程、数据库事务里都一样适用。第二它是理解死锁的四个必要条件最直观的训练场把死锁问题从“概念上的认识”落实成“可操作的算法”。第三面试和考试确实会考这个理由很现实。从思维训练的角度讲我见过不少并发编程踩坑的同学回头补了银行家算法的课后再看死锁问题会本能地思考“这个资源分配顺序是不是安全”。这就是教材算法在思维层面留下的真正价值。做课程设计时我习惯要求学生除了完成代码还必须写清楚每一步的安全性推演过程并且至少设计两个测试用例一个安全的、一个不安全的。上机验证时你会发现运行通过并不难难的是输入一组精心构造的数据让算法在“看似可以分配”的边缘果断拒绝。能把这一步做到位银行家算法才算学明白了。
返回列表