简介:围绕操作系统课程中的银行家算法实验,这套资料面向正在学习死锁避免与资源分配策略的高校学生及自学者,提供可直接参考的实验报告与配套实现,帮助理清最大需求矩阵、可用资源向量、已分配矩阵和需求矩阵之间的关系,并完成安全性检查与安全序列构造。压缩包共5个文件,约452KB,以两份C++源文件和一个头文件承载进程管理、资源申请释放及安全性判断逻辑,另附实验报告文档与初始化数据文本,便于对照原理运行和调试。已有5251人学习,说明其在课程实验场景中具有一定参考价值。读者可从中获得算法流程梳理、数据结构设计、模拟循环组织方式以及资源分配不当、资源泄露、安全检查错误等常见问题的排查思路,适合用作课程实验复现与报告撰写参考。
1. 从一次误判说起:为什么这包东西值得先跑再读
一位同学交操作系统实验前自己先验了一遍:输入 5 个进程、3 类资源,程序冷冰冰打印「系统不安全」。可他纸上推演,P1、P3、P4、P0、P2 明明走得通。折腾到后半夜才发现,是他手工敲 Need 矩阵时错了一格——银行家算法本身没错,错在喂给它的数据。
这套「银行家算法 实验报告 + 源代码」解决的就是这个场景:源码把安全性检查、请求分配、回滚整套逻辑跑通,报告把资源分配表和状态转换讲清楚。它适合赶操作系统实验的、想真正搞懂死锁避免的、以及要拿去做课程设计底稿的人。下面按「结构 → 算法 → 跑通 → 避坑 → 改」的顺序拆。
2. 项目结构拆解:报告与源码两条线怎么咬合
2.1 典型目录骨架与文件职责
这类实验包常见两种形态:单文件几百行的速成交作业版,和拆成 src / data / docs 的课程设计版。后者更适合二次开发,我一般会按下面的方式整理,源码和报告的对应关系一目了然。
banker/ ├── src/ │ ├── main.c # 菜单循环、输入解析、结果打印 │ ├── banker.c # 安全性检查、请求分配、资源回收 │ └── banker.h # 矩阵声明与函数原型 ├── data/ │ ├── case_safe.txt # 初始状态安全用例 │ ├── case_request.txt # 请求被允许 / 被拒用例 │ └── case_bad.txt # 非法输入用例 ├── docs/ │ ├── report.md # 实验报告正文:算法描述 + 流程图 + 截图 │ └── matrix.md # 四张表的字段说明与示例数据 └── Makefile关键是banker.c和report.md的对齐:报告里画的「安全性检查流程图」,每一步都应该能在banker.c里找到对应函数。我见过不少报告写得花团锦簇,代码里却是一个 200 行的main全塞死,答辩一问「Work 数组在哪重置」就露馅。把「计算 Need」「安全性检查」「请求试探」「回滚」拆成四个函数,报告直接按函数名写章节,改起来也不容易牵连。
data/目录常被忽略,但它决定调试效率。三组用例分开存,跑一次就能看出「是我改坏了算法,还是这组数据本来就该判不安全」。文本格式建议一行一个矩阵行,空格分隔,读取时用fscanf直接进数组,比手敲交互输入快得多。
2.2 技术选型:C 控制台 + 静态数组的取舍
为什么不用链表、不用动态分配?因为银行家算法的输入规模是固定的:进程数 n 通常 3~10,资源类数 m 通常 2~4,上限写死成宏完全够用,还能省掉一整套内存管理的调试成本。常见做法是:
#define MAX_PROC 16 /* 进程上限,实验一般 5 个,留余量 */ #define MAX_RES 8 /* 资源类别上限,实验一般 3 类 */这两个宏是整个工程的「地基」。写小了,输入 6 个进程就直接越界写坏栈,程序可能在打印安全序列时莫名其妙崩掉,报错位置和真正出错的位置差了十万八千里,这类玄学问题我踩过不止一次。写大了只是多占几百字节,静态数组不心疼。
选控制台而不是图形界面,理由同样务实:算法实验的评分点是「逻辑对不对」,不是「界面好不好看」。控制台程序几十行就能把输入、输出、菜单串起来,编译只要一条gcc,交作业时老师也能直接跑。真要演示,后面第 6 章再挂个简单的可视化,那时候算法已经稳了,改界面不影响核心。
如果原始代码是 Java 或 Python 版本,结构其实一样:Java 用int[][]加一个Banker类,Python 用嵌套 list 加几个函数,本章的函数划分和数据结构说明照样适用,换语言只是换了语法糖。
3. 核心算法实现:安全性检查与请求分配的两段关键代码
3.1 四张表的数据结构与初始化
银行家算法的全部状态就压在四个数组上:Available(系统当前可用)、Max(每个进程声明的最大需求)、Allocation(已分配)、Need(还差多少)。其中 Need 必须是算出来的,不能让人手输。
int n, m; /* 实际进程数、资源类数 */ int Available[MAX_RES]; /* 系统当前可用资源 */ int Max[MAX_PROC][MAX_RES]; /* 每个进程的最大需求 */ int Allocation[MAX_PROC][MAX_RES]; /* 已分配给各进程的资源 */ int Need[MAX_PROC][MAX_RES]; /* 尚需资源 = Max - Allocation */ /* 输入完 Max 和 Allocation 后立刻算 Need,杜绝手输不一致 */ void build_need(void) { for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) Need[i][j] = Max[i][j] - Allocation[i][j]; }build_need只有六行,却是整份代码里最该存在的函数。原报告里如果写了「Need 由用户输入」,改掉它——手工输入四个矩阵,只要有一格对不上,后面所有结论都是错的,而且错得很隐蔽:程序不报错,只是判定结果和手算对不上。
参数上有个细节:Available只用m个元素,Max这些用n × m,所以初始化时循环边界不能写混。i < n处理进程维度,j < m处理资源维度,这两个变量名在整个工程里保持一致,别在一个函数里叫i/j,另一个函数里叫p/r,否则回滚代码里迟早写反。
另外建议加一层输入校验:任何Need[i][j] < 0(说明 Allocation 超过了 Max)或者Available[j] < 0,直接打回。这类脏数据在同学互相测试时出现频率极高。
3.2 安全性算法:Work / Finish 双数组的循环逻辑
安全性检查是整个实验的核心,逻辑一句话能说完:拿当前可用资源去试,谁能跑完就把资源还回来,看最后是不是所有进程都能跑完。但落到代码上有两个必须写对的地方——Work 必须是副本,外层必须是循环。
bool is_safe(int safe_seq[]) { int work[MAX_RES]; bool finish[MAX_PROC] = {false}; int count = 0; for (int j = 0; j < m; j++) work[j] = Available[j]; /* 副本,绝不改原表 */ bool progress = true; while (progress && count < n) { /* 一轮没进展才停,不能只扫一遍 */ progress = false; for (int i = 0; i < n; i++) { if (finish[i]) continue; bool ok = true; for (int j = 0; j < m; j++) { if (Need[i][j] > work[j]) { ok = false; break; } /* 任一资源不够即出局 */ } if (!ok) continue; for (int j = 0; j < m; j++) work[j] += Allocation[i][j]; /* 假设它跑完,资源全部归还 */ finish[i] = true; safe_seq[count++] = i; /* 记进安全序列 */ progress = true; } } return count == n; /* 全部标记完成才算安全 */ }逐段说明。开头work是Available的拷贝,这个副本的意义是「推演」,推演结果不能污染真实系统状态,否则一次检查就把 Available 改乱了,后面再来一次请求就全错。finish数组同理,是布尔标记,count兼作安全序列的下标和完成计数,省一个变量。
中间的双层结构是关键。内层for扫描所有未完成进程,ok的判断必须是「所有资源类都满足」,写成「任一资源满足」是最常见的逻辑反转,症状是几乎什么输入都判安全。找到能跑的进程后,work[j] += Allocation[i][j]把这部分资源还回去,progress = true宣告本轮有进展。
外层while (progress)是很多人翻车的地方。假设进程顺序是 P0 卡住、P1 能跑,只扫一遍的话,扫到 P0 时资源不够,直接跳过,扫到 P1 才让它跑,但此时不会再回头检查 P0 了,于是误判为不安全。加progress循环重扫,直到某一轮一个都推进不了才收手,才算真正收敛。返回值用count == n,语义比一堆布尔与运算清楚。
3.3 请求资源处理:试探、判定、回滚三步
请求处理是「先假装给你,看看会不会出事,出事就撤回」。撤回这一步必须三个数组一起动,少一个就留下脏状态。
/* 返回值:0 允许,-1 超过最大需求,-2 超过当前可用,-3 会导致不安全 */ int request_resources(int pid, const int request[]) { for (int j = 0; j < m; j++) /* 门一:不能超过自己声明的 Max */ if (request[j] > Need[pid][j]) return -1; for (int j = 0; j < m; j++) /* 门二:不能超过系统现有 */ if (request[j] > Available[j]) return -2; for (int j = 0; j < m; j++) { /* 试探性分配 */ Available[j] -= request[j]; Allocation[pid][j] += request[j]; Need[pid][j] -= request[j]; } int seq[MAX_PROC]; if (is_safe(seq)) return 0; /* 安全,分配正式生效,不回退 */ for (int j = 0; j < m; j++) { /* 不安全,原路退回 */ Available[j] += request[j]; Allocation[pid][j] -= request[j]; Need[pid][j] += request[j]; } return -3; }三段结构对应三道门。第一道门检查Request ≤ Need,防止进程狮子大开口,注意是拿Need比,不是拿Max比——这里也是常见错点,拿 Max 比会放过「累计请求超过声明上限」的非法情况。第二道门检查Request ≤ Available,资源不够直接拒,不用进安全性检查。
第三段试探分配,三个数组同步改:Available 减少,Allocation 增加,Need 减少。改完之后调is_safe,返回 true 就return 0让分配生效,此时什么都不能还原;返回 false 就三个数组同步反向操作,一个都不能漏。
漏还原的症状很有迷惑性:单次请求测试看起来正常,连着请求三次之后结果就完全不可信了,因为 Available 被悄悄扣掉了却没恢复,越跑系统越「穷」,最后什么都判不安全。这就是我前面说的黑匣子问题,光看输出根本看不出来。建议在is_safe前后各打一行 Available 快照,调试时对比一眼就知道还原有没有做全。
4. 跑通与验收:编译、输入格式和三组自测用例
4.1 编译命令与输入格式约定
工程用 Makefile 管理,核心就一条编译命令。如果你的环境没有 make,直接敲 gcc 也一样。
# 方式一:走 Makefile make # 产出 ./banker ./banker # 方式二:手动编译(无 make 环境) gcc -Wall -Wextra -std=c99 -o banker src/main.c src/banker.c ./banker-Wall -Wextra强烈建议加上。这类矩阵代码里「未初始化变量」和「循环边界写错」占了错误来源的一大半,编译器给的警告往往比调试器还准。-std=c99是为了for (int i = ...)这种写法在旧编译器上也能过。
输入顺序建议固定成「n m → Available → Max 逐行 → Allocation 逐行」,交互模式下先让用户选「手动输入 / 从文件读取」。文件读取用fscanf(fp, "%d", &x)循环灌数组,比交互敲键盘快十倍,也方便反复回归。
4.2 三组用例:安全、不安全、被拒
拿经典的五进程三资源数据验收,先看初始状态:
进程数 5,资源类数 3 Available = [3, 3, 2] Max P0..P4 = [7,5,3] [3,2,2] [9,0,2] [2,2,2] [4,3,3] Alloc P0..P4 = [0,1,0] [2,0,0] [3,0,2] [2,1,1] [0,0,2] Need P0..P4 = [7,4,3] [1,2,2] [6,0,0] [0,1,1] [4,3,1]第一组:直接做安全性检查,期望输出「安全」,安全序列形如P1 → P3 → P4 → P0 → P2(P1 P3 P4 P2 P0也合法,取决于扫描顺序,两种都算对)。如果你跑出来是不安全,八成是 Need 算错或者循环只扫了一遍。
第二组:让 P1 请求[1,0,2]。逐项比对:1≤1、0≤2、2≤2,Need 通过;1≤3、0≤3、2≤2,Available 通过。试探后 Available 变成[2,3,0],此时P1的 Need 降为[0,2,0],它能立刻跑完归还,链条继续,检查通过,期望输出「允许分配」。这组用例专门验证「试探分配后不回退」的分支。
第三组:让 P4 请求[3,3,0]。Need 是[4,3,1],这道门能过;Available 是[3,3,2],也能过。但试探后 Available 只剩[0,0,2],任何进程的 Need 都凑不齐,安全性检查失败,期望输出「拒绝并回滚」。跑完这一组要额外确认:Available 打印出来仍然是[3,3,2],如果变成[0,0,2],就是回滚没写全。
提示:每组用例都从初始数据重新起一遍程序,不要在第一组改完状态后直接跑第三组,否则期望值全乱。
4.3 输出怎么读,报告里该放哪些图表
程序的输出建议固定成四块:当前四张表的快照、安全性检查结论、安全序列、请求处理结果(允许 / 拒绝 / 拒绝原因)。拒绝原因要写具体,比如「超过当前可用资源」「分配后将进入不安全状态」,别只打印一个 false,答辩时被问「为什么拒」会很难看。
实验报告里最有说服力的三样东西:一张资源分配表(把四个矩阵排成表格)、一张安全性检查流程图(Work 和 Finish 的循环判断)、三组用例的终端截图。截图里要能看到输入数据和输出结论,别只截最后一行。如果报告里要写算法复杂度,安全性检查是O(n²m),外层最多 n 轮、内层 n 个进程、每个进程比 m 类资源,写清楚这个推导比抄一段定义管用。
5. 避坑记录:这套实现里五个高频翻车点
5.1 数据录入阶段的两个坑
坑一:Need 矩阵让用户手输。现象是程序对同一组 Max 和 Allocation 时而判安全、时而判不安全,重跑一次结果又变了。原因是人手工敲矩阵必然出错,而程序没有任何一致性校验,错的数据算出来的当然是对的结果——只是对的是错题。解决很简单,加一个build_need(),Need 一律由Max - Allocation算出来,输入环节只收 Max 和 Allocation,用户想输 Need 都没入口。
坑二:MAX_PROC定得太小。现象是输入第 6 个进程时程序直接崩溃,或者打印出一串乱码地址。原因是静态数组越界写,覆盖了相邻变量的内存,崩在哪一行完全看运气。解决是把上限宏留足余量(16 起步),并在读入n之后立刻判断if (n <= 0 || n > MAX_PROC) { 报错退出 },同样校验m。这道校验只要三行,能省掉几小时的玄学排查。
5.2 算法逻辑里的三个坑
坑三:安全性检查只扫一遍。现象是某组数据明明存在安全序列,程序却报不安全,而且换一种进程输入顺序结论又变了。原因是内层循环扫一遍就结束,前面因资源不足被跳过的进程不会被重新考虑。解决是加progress标志和外层while,一轮一个都没推进才算收敛结束。
坑四:回滚只改了 Available。现象是单次请求测试正常,连续请求几轮之后系统判定越来越保守,最后全部拒绝。原因是试探分配改了 Available、Allocation、Need 三处,回滚时只把 Available 加回去了,Allocation 和 Need 还留着试探值,进程的已分配资源和需求全乱。解决是把三个数组的还原写成紧挨着的一组语句,改的时候一起改,并在调试版本里打印还原前后的快照做对比。
坑五:资源判断写成「任一满足」。现象是几乎所有请求都被判安全,安全性检查形同虚设。原因是if (Need[i][j] <= work[j]) ok = true;这种写法把「所有 j 都满足」写成了「存在 j 满足」。解决是默认ok = true,一旦有任一资源不满足就ok = false; break;,把逻辑反过来写。
6. 二次开发:改成能自动跑用例的死锁检测小工具
作业交完,这套代码其实还能再往前走一步。我通常加一个随机用例生成器,让程序自己造数据、自己检查、自己比对不变量,比手工敲十组数据靠谱得多。
/* 生成一组随机但合法的资源分配数据,并做基本不变量校验 */ void gen_random_case(int max_proc, int max_res) { n = 2 + rand() % (max_proc - 1); m = 1 + rand() % max_res; for (int j = 0; j < m; j++) Available[j] = rand() % 6; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { Max[i][j] = rand() % 8; /* 声明的最大需求 */ Allocation[i][j] = rand() % (Max[i][j] + 1); /* 保证不超过 Max */ } } build_need(); /* Need 永远算出来,不随机 */ /* 不变量:Allocation + Available 不能为负,Need 不能为负 */ for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) if (Need[i][j] < 0) { printf("用例非法\n"); exit(1); } }Allocation那行用rand() % (Max[i][j] + 1)是关键,保证已分配不超过声明上限,否则生成的用例本身就违反约束,测出来的「不安全」全是假阳性。build_need()直接复用,杜绝随机数据里出现自相矛盾的 Need。
再往前一步是请求队列回放:把一串请求按顺序喂进去,记录每一步的结论和回滚后的 Available,最后导出成 CSV。验证方法就三条——请求被允许后 Available 必须恰好减少 Request 的量;被拒绝后 Available 必须和请求前一模一样;任意时刻所有进程的Allocation + Need必须等于Max。前两条查回滚,第三条查矩阵一致性,跑一千组随机用例,只要有一条不成立就停下。
如果要做演示,把安全序列的求解过程打成逐轮日志(第几轮、Work 是多少、选中了谁),再接到一个简单的文本进度条上,效果就出来了。
从那以后我每次写完这类矩阵算法,都先拿全零矩阵和单进程矩阵跑一遍边界自检,再看正常用例——先确认「不该出事的地方真没出事」,剩下的才叫调算法。这套习惯帮我省了不少返工。希望帮到你。
本文还有配套的精品资源,点击获取