☰
操作系统银行家算法:可复现实验报告与源代码实现
2026/11/2 9:33:43 网站建设 项目流程

简介:围绕操作系统课程中的银行家算法实验,这份资料把实验报告与可运行源码打包在一起,面向正在学习进程管理与死锁避免的本科生及自学者。包内共 5 个文件,以 2 个 cpp 源文件与 1 个 h 头文件构成算法主体,分别承载最大需求矩阵、可用资源向量、已分配与需求矩阵的初始化、安全性检查与资源请求分配流程;1 个 docx 为完整的实验报告,1 个 txt 提供初始化数据,方便直接导入测试。压缩包约 452KB,体积轻便,便于随课程作业分发与本地调试。目前已有 5251 人学习下载。读者可据此对照报告中的四个数据结构与安全序列构造思路,逐步跟踪请求、试探分配、回滚与资源释放的执行路径,并借助源码注释理解进程管理与模拟循环的组织方式;报告中还归纳了资源分配不当、资源泄露、安全性检查出错等常见问题,可作为调试时的排查线索。

1. 从一张资源分配表说起:这套银行家算法方案要交付什么

操作系统课设里,银行家算法几乎年年出现:给出一张 Available、Max、Allocation 三张表,要求程序判断当前状态是否安全、某个进程发起的请求能否被满足、安全序列长什么样。常见交付物是一份实验报告加一份源代码,但真正卡住人的往往不是写不出代码——程序能跑,却说不清输出为什么对;换了测试数据,又对不上了。所以这套围绕「操作系统银行家算法 实验报告+源代码」展开的方案,主线只有两个字:可复现。数据结构怎么摆、试探分配怎么回滚、安全序列怎么记录、测试用例怎么设计到能对上手算,每一块都按能交差、能答辩的标准来做。它适合正在做操作系统课程设计、需要同时交报告和代码的同学,也适合想把死锁避免这块内容完整讲一遍的人。

2. 原理落到数据结构:四个向量与一组不能破的不变量

课本上银行家算法是一段伪代码,看着简单,落到工程里第一件事是把几个数组的边界和同步关系固定下来。这一步做不对,后面所有的安全性判断都是空中楼阁。这一章先把状态表示和安全性检查这两件事讲透,代码放到第 4 章。

2.1 四个核心结构、一张状态表和三条不变量

系统状态由四个结构描述,关系是固定的:

结构维度含义维护方式
Available1×m每类资源当前可用实例数分配时减、回收时加
Maxn×m进程对每类资源声明的最大需求初始化后只读
Allocationn×m已分配给每个进程的资源数与 Need 同步增减
Needn×m还需要多少才能跑完,Need = Max - Allocation由另两张表推导

m 是资源种类数,n 是进程数。很多同学图省事,把 Need 当成一个独立输入从文件里读,短时间没问题,一旦连续处理多个请求,就会出现 Max、Allocation、Need 三张表互相打架的情况。稳妥的做法是 Need 不单独存储输入,永远由 Max 减 Allocation 算出来,写入时三处一起改。按这个约束,状态在任何时刻都满足三条不变量:Need[i][j] 恒等于 Max[i][j] 减 Allocation[i][j] 且不小于 0;Available[j] 等于资源总量减去所有进程在该类资源上的 Allocation 之和;任何进程的 Allocation 不会超过它声明的 Max。实验报告里把这三条写成「数据一致性约束」,答辩时被问「你怎么保证状态不乱」就有话可答。

拿一组课上常用的算例把表填出来,5 个进程、3 类资源,初始 Available = (3,3,2):

进程Allocation (A,B,C)Max (A,B,C)Need (A,B,C)
P00,1,07,5,37,4,3
P12,0,03,2,21,2,2
P23,0,29,0,26,0,0
P32,1,12,2,20,1,1
P40,0,24,3,34,3,1

这组数据的手算结果后面会反复用到,最终安全序列是 P1、P3、P4、P0、P2,五步走完 Work 一路涨到 (10,5,7)。注意它不是唯一解,P1 之后如果先试 P3 之外的其他进程会发现都过不去,所以这一步的选择其实是被迫的——这种「被迫」正是手算和程序结果容易对不上的地方,也是后面写测试用例时必须盯住的位置。

2.2 安全性检查的执行步骤与指针复位这个细节

安全性检查是一段可以背下来的流程:令 Work 等于 Available、Finish 全为 false,反复在未完成的进程里找一个 Need 不大于 Work 的,让它「跑完」并把资源还回来(Work 加上它的 Allocation),标记完成、记入序列;某一轮里一个都找不到就停下,此时所有进程都完成则状态安全,否则不安全。

复杂度是 O(n²m):外层最多 n 轮,每轮内层要扫 n 个进程、每个进程比较 m 类资源。规模小的时候无所谓,但如果把这段逻辑当成一个可复用的检查器,被主流程在每次请求时调用一次,就要意识到它会被调 n 次以上,写报告时把这条复杂度写进去,比只贴代码显得扎实得多。

绝大多数实现翻车的地方不是思路,而是扫描指针的处理。有些写法图省事,用一个游标 i 从头扫到尾,成功一个就 i++ 继续往后,扫描过的位置不再回头。这在某些数据上恰好能得到正确结果,看上去能用,但遇到「前面的进程要等某个后面的进程释放资源」的排列就会误判为不安全。负责的做法是每次找到一个可完成的进程就 break,回到外层 while 再从头扫,因为本次释放的资源可能刚好让之前扫过而失败的进程变得可完成。这个细节建议在实验报告里单独写一段,配一组能触发误判的反例数据——答辩时拿出这组数据,比讲十句原理更有说服力。

还有一个常被忽略的约定:Need 全为 0 的进程应当视为已完成,可以直接进入序列,不必参与资源比较。如果实现里没处理,它也能被正常匹配(0 不大于任何非负的 Work),但如果 Work 里出现负数(意味着前面某步分配出了错),这个进程会被漏掉,暴露出更深的问题。所以留个断言比留个注释管用。

3. 总体架构设计:输入层、决策层、展示层的职责划分

算法本身只有几十行,但实验报告要写得像一份完整方案,代码结构就不能是一坨 main 函数。按输入、决策、展示三层拆,每一层都能对应到报告里的一节,也能对应到答辩时的一个问题。

层次职责关键产出出错时的表现
输入层读三张表、校验维度和数值合法性一个初始状态对象维度不一致、出现负数
决策层请求合法性检查、试探分配、安全性检查、回滚分配结论 + 安全序列状态被改坏、结果不可复现
展示层输出安全序列、前后对比、回滚原因控制台表 + 运行日志报告里写不出过程

3.1 输入层:把实验报告里的三张表读成数据结构

输入格式建议直接跟着报告走。报告里画的是三张矩阵表,那就按三张表来读,别为了省事把数据硬编码进代码——硬编码在演示时最省事,也是答辩现场被要求「换个数据跑一下」时最容易崩的。推荐用两种输入方式并存:结构化文件(JSON 或自定义文本段)给正式用例,交互式逐行输入给现场演示。结构化格式里只放 Available、Max、Allocation 三部分,Need 由程序算,这一点在第 2 章已经说过,落到格式设计上就是少一个字段、少一个出错入口。

输入层要做四类校验,全部在进入决策层之前完成:资源种类数与每个向量的长度是否一致;Allocation 的每一行是否逐元素不大于 Max;所有数值是否非负;按资源种类汇总的 Allocation 是否超过声明的资源总量。前三条是必须的,第四条很多实现漏掉,但它是唯一能发现「表本身填错」的检查——如果各进程已分配之和超过了系统声称的总量,后面的安全序列再漂亮也是错的。

3.2 决策层:请求进来之后要走的四道关

一个资源请求从进入到返回结论,固定走四步,顺序不能乱。第一步是合法性检查,请求向量长度对不对、有没有负数、是否超过该进程的剩余需求(超过说明它违约了)、是否超过当前可用资源(超过则只能等待,不算错误)。第二步保存快照,这是回滚的唯一依据。第三步试探分配,把可用资源扣掉、已分配加上、需求减掉。第四步调安全性检查,安全就确认,不安全就用快照恢复原状并给出拒绝理由。

这里的设计取舍值得在报告里写一句:为什么不先检查「申请是否超过 Available」就直接分配。因为银行家算法的核心不是当下够不够分,而是分完之后系统是否仍然存在安全序列。一个请求即使能立刻满足,也可能把系统推进不安全状态,此时拒绝才是正确行为。反过来,如果某次请求超过 Available,程序应当返回「暂时无法满足、需要等待」,而不是「非法请求」,这两种结论在报告里要分开表述,否则测试用例的预期输出会写错。

3.3 展示层:让实验报告有过程可写

结果输出不该只有一行「安全」或「不安全」。既然报告里需要一个安全序列,那就把每一步的 Work、当前选中的进程、更新后的 Work 全部打出来,形成一张过程表。这样做有两个直接好处:手算核对时不用猜程序是怎么走的;报告里的验证章节可以直接引用这段输出,省掉自己重画一遍的功夫。

日志里还要记录被拒绝的请求及其原因。常见情况是学生只打印最终结果,报告里写「经检验系统不安全」,却拿不出是哪一步导致的,老师一问就露怯。把试探分配前后的 Available 和 Need 打出来,配合快照恢复的时机,整条链路就完整了。展示层不参与任何状态修改,这是硬规矩——日志代码顺手改一下状态,是最隐蔽的一类 bug。

4. 关键实现:试探分配、失败回滚与安全序列输出

结构定下来之后,代码量其实不大,难点集中在两处:回滚要彻底,检查要正确。下面这份 Python 实现按第 3 章的分层组织,可以直接对照改成 C 或 Java 版本——数据结构换成数组加下标,判断逻辑一模一样。

4.1 状态类:快照、应用与三层校验

# banker/core.py class BankerState: """一次性状态快照,持有 Available / Max / Allocation / Need 四张表""" def __init__(self, available, max_demand, allocation): self.n_res = len(available) # 资源种类数从输入推导,不写死 self.n_proc = len(max_demand) # 进程数 self.available = list(available) self.max_demand = [list(r) for r in max_demand] self.allocation = [list(r) for r in allocation] # Need 只在构造时算一次,此后由 apply/restore 同步维护 self.need = [ [self.max_demand[i][j] - self.allocation[i][j] for j in range(self.n_res)] for i in range(self.n_proc) ] def snapshot(self): """试探分配前保存,回滚只认这一份拷贝,别的都不算数""" return (list(self.available), [list(r) for r in self.allocation], [list(r) for r in self.need]) def restore(self, snap): avail, alloc, need = snap self.available = list(avail) self.allocation = [list(r) for r in alloc] self.need = [list(r) for r in need] def apply(self, pid, request): """三张表一起改,任何一张漏改后面必然对不上""" for j in range(self.n_res): self.available[j] -= request[j] self.allocation[pid][j] += request[j] self.need[pid][j] -= request[j] def check_request(self, pid, request): """四道关里的第一道,全部通过才进试探分配""" if len(request) != self.n_res: return False, "请求维度与资源种类数不一致" if any(x < 0 for x in request): return False, "请求量不能为负" if any(request[j] > self.need[pid][j] for j in range(self.n_res)): return False, f"P{pid} 的请求超过其声明的剩余需求" if any(request[j] > self.available[j] for j in range(self.n_res)): return False, "当前可用资源不足,请求需等待" return True, "ok"

snapshot返回的是三个容器的深拷贝,这一点不能省。如果写成return self.available之类的引用,回滚时恢复出来的还是被改过的对象,等于没回滚——这是这个实验里最高频的错误,第 5 章还会展开。apply里三张表同步修改,对应第 2 章的三条不变量。check_request的四个判断按顺序返回不同措辞,是为了让展示层能区分「非法请求」和「资源不足需等待」,这两种情况在报告的测试用例表里属于不同的预期结果。

4.2 安全性检查:每次命中都从头再扫

def is_safe(self): """返回 (是否安全, 安全序列)。找不到可完成进程就停,全部完成才算安全。""" work = list(self.available) finish = [False] * self.n_proc order = [] while True: found = False for i in range(self.n_proc): # 每轮从 0 开始扫,不复用上次游标 if finish[i]: continue if all(self.need[i][j] <= work[j] for j in range(self.n_res)): work = [work[j] + self.allocation[i][j] for j in range(self.n_res)] finish[i] = True order.append(i) found = True break # 命中后回到 while 开头重扫 if not found: # 一整轮都没命中,收敛退出 break return (all(finish), order)

这段是整份代码的核心,三个地方值得盯。work从可用资源复制而来,绝不能在 self.available 上原地加减,否则一次检查就会污染真实状态。内层 for 循环每次命中都break,靠外层 while 重新发起一轮完整扫描,这就是第 2 章说的指针复位,写成游标一路向前的版本在部分数据上会漏解。返回值把order一起带出来,即使不安全也返回部分序列,展示层可以据此打印「已经跑到哪一步卡住」,报告里这一段就是现成的失败分析材料。

顺带解释一下复杂度在实现中的体现:外层 while 最多执行 n+1 次(每次至少完成一个进程),内层 for 每轮最多 n 次、每次比较 m 个元素,所以整体是 O(n²m)。当 n 只有五六个时,这点开销可以忽略,但如果主流程要拿它做多次试探,写报告时明确写清这一点,比含糊地说「算法效率较高」更站得住。

4.3 请求主流程与运行输出

def request(self, pid, req): """一次请求的完整生命周期:校验 -> 快照 -> 试探 -> 检查 -> 确认或回滚""" ok, msg = self.check_request(pid, req) if not ok: return False, msg, None snap = self.snapshot() self.apply(pid, req) safe, order = self.is_safe() if safe: return True, "分配成功", order self.restore(snap) # 不安全,恢复快照,一步都不能少 return False, "试探分配后系统进入不安全状态,已回滚", None

调用侧的命令行入口按第 3 章的分层写,读文件、构造状态、循环处理请求、打印过程表,各占一个函数。跑一组基线数据:

# 输入文件里只有 available / max / allocation 三段,need 由程序计算 python -m banker.cli cases/classic5x3.json --request 1:1,0,2

输出的过程表大致如下,每行对应安全性检查中的一次命中,work 是更新后的值:

步骤选中进程NeedWork 更新后
1P11,2,25,3,2
2P30,1,17,4,3
3P44,3,17,4,5
4P07,4,37,5,5
5P26,0,010,5,7

这张表就是实验报告里最值钱的一页:手算一遍、程序跑一遍,两边逐行对齐,验证过程就完成了大半。请求参数写成进程号:资源向量的形式,是为了能在同一条命令里指定多个请求顺序执行,方便构造「连续请求、中途被拒」这类复合用例。参数里的资源向量长度必须和输入的资源种类数一致,否则会在第一道校验被拦下,这属于预期行为,不是程序出错。

5. 常见问题与排查:五个最容易翻车的点

这段内容来自踩坑经验。银行家算法的代码量不大,出问题的地方很集中,下面五条按「现象 → 原因 → 解决」组织,对照自己的实现看,基本能覆盖大部分调试场景。

5.1 一个请求被拒之后,后续结果全乱:回滚不彻底

现象是第一次拒绝还正常,再发一个能通过的请求,输出的安全序列和手算对不上,甚至出现 Need 为负。根因是回滚只做了一半,常见写法是只把 Available 加回去,忘了把 Allocation 和 Need 减回来;另一种是快照只拷贝了 Available,另外两张表存的是引用。解决办法是把快照和恢复写成一对函数,快照里三个容器全部深拷贝,恢复时三个一起还原,中间不允许有别的代码修改状态。再补一层保险:在 restore 之后断言 Need 满足不变量,一旦为负立刻报错,别等到后面几步才发现问题。

5.2 明明有安全序列却报不安全:扫描指针没复位

现象是某些用例判不安全,手动排一遍却有解。根因就是第 2 章提到的游标写法,扫过的进程不再回头看,恰好错过「释放资源后前一个进程变得可完成」的组合。解决方式是把内层循环的外层套成 while,每次命中就 break 回到 while 开头重新从 0 扫,收敛条件是整整一轮没有任何命中。验证办法很简单:构造三进程用例,让 P0 依赖 P2 释放的资源、P2 排在后面,正确实现能通过,游标版本会失败。这组数据建议直接写进报告的问题分析里。

5.3 换一组测试数据直接崩:维度和容量写死

现象是把资源种类从 3 类改成 4 类,程序报数组越界或者输出乱码。根因是初始化时写了available = [0]*3或者循环里用常量 3 当上界。解决办法是所有维度都从输入推导,资源种类数取 Available 的长度,进程数取 Max 的行数,循环边界全部用这两个变量,代码里不出现任何数字常量。改完之后再补一条一致性校验:Max 行数、Allocation 行数、每个向量的长度三者必须匹配,不匹配就立刻退出并打印明确错误,别让程序带着错误状态往下跑。

5.4 请求超过可用资源却返回「不安全」:错误分类没做对

现象是提交一个远超当前可用资源的请求,程序走了试探分配、安全性检查,最后给出「不安全、已回滚」。结论虽然也是拒绝,但理由错了,报告的测试用例对不上。根因是把合法性检查和安全性检查混成一步。解决办法是把校验拆成四道关,长度、非负、是否超过剩余需求、是否超过可用资源,全部在快照之前判完;超过可用资源的情况返回「需等待」而不是「不安全」。这两种结论在报告里要分开列,答辩时老师很可能就问你「资源不足和安全状态是什么关系」,答得清楚是加分项。

5.5 报告写得漂亮但复现不了:过程数据没落盘

现象是报告里写了一个安全序列,重新跑程序得到的是另一个序列,自己都对不上。根因是安全序列本身不唯一,程序选哪一个取决于扫描顺序,而报告只记了最终结果没记过程。解决办法是把每次检查的 Work 变化、选中进程、更新后的 Work 全部输出并保存成日志文件,报告里引用日志原文,顺便注明「安全序列不唯一,以下为按进程号升序扫描得到的解」。再往前一步,把几组固定用例的输入和预期输出一起放进代码仓库,谁跑都是同一个结果,复现问题就地解决。

6. 验收与答辩:用暴力枚举给安全性检查做对照测试

安全性检查这段逻辑,人眼盯代码很难确认它对不对,光靠两三组手算数据也只是抽样。有个更硬的验证手段:小规模时把进程的全排列穷举一遍,看是否存在一个排列能让所有进程依次完成,这个结果是「标准答案」,再拿它和安全性检查的判定逐例对照。规模控制在 n≤6 时,720 种排列跑起来毫无压力,几百组随机用例几秒就能跑完。

# tests/test_safety_by_bruteforce.py import itertools, random from banker.core import BankerState def brute_force_safe(state): """穷举全排列判断存在性,作为安全性检查的对照基准""" for perm in itertools.permutations(range(state.n_proc)): work = list(state.available) ok = True for pid in perm: if any(state.need[pid][j] > work[j] for j in range(state.n_res)): ok = False break work = [work[j] + state.allocation[pid][j] for j in range(state.n_res)] if ok: return True, list(perm) return False, [] def random_case(n_proc=4, n_res=3, seed=None): rnd = random.Random(seed) allocation = [[rnd.randint(0, 3) for _ in range(n_res)] for _ in range(n_proc)] max_demand = [[allocation[i][j] + rnd.randint(0, 4) for j in range(n_res)] for i in range(n_proc)] available = [rnd.randint(0, 5) for _ in range(n_res)] return BankerState(available, max_demand, allocation) # 两边结论必须一致,不一致就打印用例,方便直接粘进报告当反例 for seed in range(200): st = random_case(seed=seed) assert st.is_safe()[0] == brute_force_safe(st)[0], f"seed={seed} 判定不一致"

这段测试的价值在于它的独立性:暴力枚举不关心 Need 怎么维护、指针怎么移动,它只回答「有没有一条路走通」,因此能反过来检验主实现。跑通两百组随机用例,再补上第 2 章那组经典数据,验收就有了量化依据,报告里可以写成表格形式:用例编号、资源种类数、预期结论、实际结论、安全序列是否被暴力枚举确认存在。

我一般还会在报告里单列一张「拒绝路径」验收表,专门验证回滚是否正确,因为正常路径跑通不代表异常路径没留坑:

验收项操作期望结果检查方式
安全序列正确性跑经典 5×3 用例输出合法安全序列暴力枚举确认存在
拒绝后状态复原连续提交「被拒请求 + 正常请求」第二个请求的判定与单独提交一致对比两次输出的过程表
等待与非法区分提交超过 Available 的请求返回「需等待」而非「不安全」检查返回措辞
维度自适应换成 4 类资源的数据正常运行,无越界输入校验 + 运行日志
不变量保持任意操作序列后Need 非负、三表一致restore 后的断言

答辩现场最常被问的三个问题,其实都能从这套验收里找到答案:安全序列为什么不唯一(扫描顺序决定)、拒绝一个请求的依据是什么(试探后无安全序列)、怎么证明你的程序是对的(暴力枚举对照加回滚一致性检查)。提前把日志和用例准备好,被要求「换个数据现场跑一下」时不至于手忙脚乱。

这套流程走下来,最大的收获不是银行家算法本身,而是一个习惯:凡是「判定类」的逻辑,都要找一个独立于它实现的基准去对照,手算只能抽样,暴力枚举才叫覆盖。从那以后我每次写这类带判断的算法作业,都强制先把对照测试搭起来再动主逻辑,回滚和边界的问题基本在写完当天就暴露完了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询