☰
银行家算法实验报告避坑指南:从ba.cpp实现到安全序列验证
2026/10/7 18:56:36 网站建设 项目流程

简介:这份资源面向操作系统课程学习者与备考者,聚焦死锁避免与银行家算法这一经典难点,提供可运行的实验实现与配套说明。包内共3个文件,以cpp源码、doc实验报告和txt参考说明为主:源码完整呈现进程与资源的数据结构初始化、资源申请与释放模拟、安全性检查及安全序列输出;文档则梳理实验步骤、算法原理与结果分析,便于对照代码理解每一步判断逻辑。资源包仅33KB,轻量易携带,适合在课程实验、期末复习或面试准备中快速搭建验证环境。目前已有650人学习下载,读者可借此掌握最大需求、已分配、当前需求与可用资源四类状态的维护方式,理解预检查如何避免系统进入不安全状态,并学会用深度优先或工作集思路实现安全性检查,从而加深对并发资源分配策略的认识。

1. 银行家算法到底在算什么:从 BA.rar 里那份实验报告说起

很多人第一次接触死锁,是在操作系统课设里拿到一个叫BA.rar的压缩包,里面躺着ba.cpp和一份实验报告模板。打开一看,要求实现银行家算法,输入进程数、资源数、各类资源总量、最大需求矩阵、已分配矩阵,然后判断当前状态是否安全,给出一个安全序列。看起来就是个矩阵运算题,但真正动手写ba.cpp的时候,翻车点比想象中多得多。

银行家算法解决的核心问题是:系统在分配资源之前,先假装分配一次,然后检查是否存在一条让所有进程都能顺利跑完的路径。如果存在,这次分配就是安全的;如果不存在,就拒绝分配,让进程等待。它不预防死锁,也不检测死锁,而是在分配前做一次“预演”,用安全性检查把可能导致死锁的请求挡在门外。这套逻辑在BA.rar的实验报告里通常要求画出安全性检查的每一步,但很多人只贴了代码,没把“为什么这样算”讲清楚。

这篇文章面向三类人:正在做银行家算法实验、需要把ba.cpp写对并写出能过答辩的实验报告的学生;工作中遇到线程死锁或数据库死锁,想回头理解经典算法思想的工程师;以及需要把银行家算法讲给别人听、但自己还没完全理顺的开发者。我会从数据结构定义、安全性检查流程、代码实现、参数调试、常见翻车点一路写到怎么验证结果,尽量让每一步都能直接抄作业。

2. 银行家算法的数据结构与安全性检查:矩阵怎么摆、指针怎么走

2.1 四个矩阵和三个向量到底存什么

银行家算法的输入通常用四个矩阵和三个向量描述。以ba.cpp里最常见的定义为例:

名称含义维度典型初始化方式
Available当前各类资源剩余可用量1 × m系统总量减去已分配总量
Max每个进程对各类资源的最大需求n × m题目给定或从文件读入
Allocation每个进程已分配到的资源量n × m题目给定或从文件读入
Need每个进程还需要的资源量n × mMax 减 Allocation
Work安全性检查时的工作向量1 × m初始等于 Available
Finish进程是否已完成1 × n初始全 false
SafeSeq安全序列1 × n记录被选中的进程编号

这里最容易搞混的是Need和Work。Need是每个进程自己的缺口,Work是系统在安全性检查过程中动态变化的“假设可用量”。每选中一个进程,就把它的Allocation加到Work上,表示它跑完后释放了资源。Finish用来标记进程是否已经被加入安全序列,避免重复选中。

在ba.cpp里,我一般用vector<vector<int>>存矩阵,用vector<int>存向量。这样比固定数组灵活,也方便从文件读入。如果实验报告要求用 C 风格数组,那就把MAX_PROCESS和MAX_RESOURCE定义成常量,但要注意题目给的进程数和资源数不能超过这个上限。

2.2 安全性检查的完整流程:从 Work 到 SafeSeq

安全性检查的伪代码在实验报告里通常写成这样:

// 安全性检查核心逻辑 bool isSafe(vector<int>& safeSeq) { vector<int> work = available; // 工作向量初始等于当前可用资源 vector<bool> finish(n, false); // 所有进程初始未完成 int count = 0; // 已加入安全序列的进程数 while (count < n) { bool found = false; for (int i = 0; i < n; i++) { if (!finish[i]) { bool canRun = true; for (int j = 0; j < m; j++) { if (need[i][j] > work[j]) { // 需求大于可用,不能跑 canRun = false; break; } } if (canRun) { for (int j = 0; j < m; j++) { work[j] += allocation[i][j]; // 进程跑完释放资源 } safeSeq[count++] = i; finish[i] = true; found = true; } } } if (!found) return false; // 一轮扫描没有任何进程能跑,不安全 } return true; }

这段代码的逻辑说明:外层while保证最多扫描 n 轮,每轮尝试找一个Need <= Work且未完成的进程。找到后把它加入安全序列,释放它占有的资源到Work,标记Finish。如果某一轮扫描没有任何进程能被选中,说明剩余进程互相等待,系统不安全,直接返回 false。

参数说明:available是当前可用资源向量,need和allocation是 n × m 矩阵,n是进程数,m是资源种类数。safeSeq是输出参数,用来记录安全序列。注意work是局部变量,每次安全性检查都重新从available拷贝,不要用全局变量,否则多次检查会互相污染。

2.3 资源请求的处理:先假装分配,再回滚

银行家算法对外暴露的接口通常是request(pid, requestVec)。处理流程分四步:

  1. 检查requestVec是否超过该进程的Need,超过就报错,因为进程不能请求超过它最大需求的资源。
  2. 检查requestVec是否超过当前Available,超过就让进程等待。
  3. 假装分配:Available -= requestVec,Allocation[pid] += requestVec,Need[pid] -= requestVec。
  4. 调用安全性检查。如果安全,正式分配;如果不安全,回滚刚才的三步修改,让进程等待。

回滚这一步是很多ba.cpp翻车的地方。有人只写了分配,忘了回滚,结果一次不安全的请求把系统状态改乱了,后面再检查全是错的。我一般会把修改前的Available、Allocation[pid]、Need[pid]先备份,不安全就恢复。

// 资源请求处理与回滚 bool requestResources(int pid, vector<int>& req) { for (int j = 0; j < m; j++) { if (req[j] > need[pid][j]) return false; // 超过最大需求 if (req[j] > available[j]) return false; // 当前资源不足 } // 备份 vector<int> oldAvailable = available; vector<int> oldAlloc = allocation[pid]; vector<int> oldNeed = need[pid]; // 假装分配 for (int j = 0; j < m; j++) { available[j] -= req[j]; allocation[pid][j] += req[j]; need[pid][j] -= req[j]; } vector<int> safeSeq(n); if (isSafe(safeSeq)) { return true; // 安全,正式分配 } else { // 回滚 available = oldAvailable; allocation[pid] = oldAlloc; need[pid] = oldNeed; return false; } }

这段代码的关键在于备份和回滚。oldAvailable、oldAlloc、oldNeed分别保存修改前的状态,不安全时逐项恢复。注意allocation[pid]和need[pid]是行向量,备份时直接拷贝整行。

3. 把 ba.cpp 跑起来:从文件读入到输出安全序列的完整实现

3.1 输入格式设计与文件读入

实验报告里通常要求从文件读入数据,但没规定具体格式。我一般用这样的文本格式:

3 3 10 5 7 7 5 3 3 2 2 0 1 0 2 0 0 3 0 2

第一行是进程数 n 和资源数 m。第二行是各类资源总量。接下来 n 行是 Max 矩阵,再 n 行是 Allocation 矩阵。这样读入的代码比较直接:

// 从文件读入银行家算法初始状态 ifstream fin("input.txt"); int n, m; fin >> n >> m; vector<int> total(m); for (int j = 0; j < m; j++) fin >> total[j]; vector<vector<int>> maxNeed(n, vector<int>(m)); for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) fin >> maxNeed[i][j]; vector<vector<int>> allocation(n, vector<int>(m)); for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) fin >> allocation[i][j]; // 计算 Need 和 Available vector<vector<int>> need(n, vector<int>(m)); vector<int> available(m, 0); for (int j = 0; j < m; j++) { int sumAlloc = 0; for (int i = 0; i < n; i++) { need[i][j] = maxNeed[i][j] - allocation[i][j]; sumAlloc += allocation[i][j]; } available[j] = total[j] - sumAlloc; }

逻辑说明:先读总量,再读 Max 和 Allocation,然后逐列计算Need和Available。Available等于总量减去该列所有进程已分配之和。注意Need不能为负数,如果出现负数说明输入数据有误,Max 小于 Allocation,需要检查题目数据。

参数说明:n和m从文件第一行读取,total是资源总量向量,maxNeed和allocation是 n × m 矩阵。need和available是计算出来的,不需要从文件读。

3.2 输出格式:安全序列和矩阵打印

实验报告通常要求打印初始状态、安全性检查过程和安全序列。我一般用表格形式输出:

// 打印当前系统状态 void printState() { cout << "Process\tMax\tAllocation\tNeed\n"; for (int i = 0; i < n; i++) { cout << "P" << i << "\t"; for (int j = 0; j < m; j++) cout << maxNeed[i][j] << " "; cout << "\t"; for (int j = 0; j < m; j++) cout << allocation[i][j] << " "; cout << "\t"; for (int j = 0; j < m; j++) cout << need[i][j] << " "; cout << "\n"; } cout << "Available: "; for (int j = 0; j < m; j++) cout << available[j] << " "; cout << "\n"; }

输出安全序列时,用P0 -> P1 -> P2这样的格式,方便实验报告截图。如果系统不安全,打印“当前状态不安全,无安全序列”。

3.3 主流程:初始化、检查、请求处理

主函数把前面几块串起来:

int main() { readInput(); // 读入并计算 Need、Available printState(); // 打印初始状态 vector<int> safeSeq(n); if (isSafe(safeSeq)) { cout << "系统安全,安全序列: "; for (int i = 0; i < n; i++) { cout << "P" << safeSeq[i]; if (i != n - 1) cout << " -> "; } cout << "\n"; } else { cout << "系统不安全,无安全序列\n"; } // 可选:处理一次资源请求 int pid; vector<int> req(m); cout << "输入请求进程号和请求向量: "; cin >> pid; for (int j = 0; j < m; j++) cin >> req[j]; if (requestResources(pid, req)) { cout << "请求已分配\n"; printState(); } else { cout << "请求被拒绝,进程需等待\n"; } return 0; }

逻辑说明:先读入并打印初始状态,然后做一次安全性检查并输出安全序列。接着从标准输入读一个资源请求,调用requestResources处理,根据返回值打印分配成功或拒绝。注意requestResources内部已经做了安全性检查,如果安全会修改全局状态,所以分配成功后要重新打印状态。

参数说明:pid是进程编号,从 0 开始。req是请求向量,长度等于资源种类数 m。输入时要求用户按空格分隔输入。

4. 银行家算法实验报告避坑:从矩阵越界到安全序列不唯一

4.1 现象:安全性检查死循环,程序卡住不输出

原因:while (count < n)循环里,如果某一轮没有任何进程被选中,found保持 false,但循环没有退出条件,会一直重复扫描。更隐蔽的情况是finish数组没有正确初始化,或者need矩阵计算错误导致永远找不到Need <= Work的进程。

解决:在while循环里加if (!found) return false;,确保一轮扫描无结果时立即退出。同时检查need是否出现负数,负数会导致比较逻辑异常。初始化finish时用vector<bool> finish(n, false),不要用memset对vector操作。

4.2 现象:安全序列输出重复进程或漏掉进程

原因:finish标记和safeSeq写入不同步。有人在找到可运行进程后只写了safeSeq[count++] = i,忘了finish[i] = true,下一轮扫描又选中同一个进程。或者count递增和safeSeq索引不一致,导致覆盖。

解决:选中进程后立即设置finish[i] = true,并且safeSeq[count] = i; count++;分两步写,避免count++和数组索引混用。输出安全序列时用count作为长度,不要用n,因为不安全时count可能小于n。

4.3 现象:资源请求被拒绝后,系统状态被改乱

原因:requestResources里做了假装分配,但安全性检查失败后没有回滚,或者回滚不完整。常见的是只恢复了Available,忘了恢复Allocation[pid]和Need[pid]。

解决:在修改前备份三个量,不安全时逐项恢复。备份用值拷贝,不要用引用。如果Allocation是二维向量,备份allocation[pid]这一行即可,因为其他进程的行没有改动。

4.4 现象:实验报告里的安全序列和标准答案不一致

原因:安全序列不唯一。银行家算法的安全性检查只要求找到一条安全序列,不要求唯一。不同的扫描顺序会得到不同的安全序列,但只要所有进程都能被加入,就是正确的。

解决:在实验报告里说明“安全序列不唯一,以下为其中一条”。如果老师要求特定顺序,通常按进程编号从小到大扫描即可。不要因为和同学的结果不同就反复改代码,先确认双方的安全序列都满足Need <= Work的约束。

4.5 现象:多资源类型时 Available 计算错误

原因:Available是按资源类型逐列计算的,不是按进程逐行。有人把每行的Allocation加起来当成Available,导致维度错乱。

解决:用双重循环,外层遍历资源类型 j,内层遍历进程 i,累加allocation[i][j],然后用total[j]减去这个和。这样得到的available[j]才是第 j 类资源的剩余量。

5. 进阶验证:用随机测试和边界用例确认 ba.cpp 的鲁棒性

5.1 构造随机测试用例验证安全性检查

手工造数据容易覆盖不全,我一般写一个随机测试生成器,自动生成满足Allocation <= Max且sum(Allocation) <= total的输入,然后跑ba.cpp看是否崩溃或输出异常。

# 随机生成银行家算法测试用例 import random def gen_case(n, m, seed): random.seed(seed) total = [random.randint(5, 20) for _ in range(m)] max_need = [[random.randint(1, 8) for _ in range(m)] for _ in range(n)] allocation = [[0] * m for _ in range(n)] for j in range(m): remain = total[j] for i in range(n): alloc = random.randint(0, min(max_need[i][j], remain)) allocation[i][j] = alloc remain -= alloc return n, m, total, max_need, allocation n, m, total, max_need, allocation = gen_case(4, 3, 42) print(n, m) print(*total) for row in max_need: print(*row) for row in allocation: print(*row)

逻辑说明:先生成资源总量,再生成每个进程的最大需求,然后逐列分配资源,保证每列已分配之和不超过总量。这样生成的用例一定满足银行家算法的输入约束。把输出重定向到input.txt,再跑ba.cpp,观察是否所有进程都能被加入安全序列。

参数说明:n是进程数,m是资源种类数,seed是随机种子,方便复现。total每类资源在 5 到 20 之间,max_need每个元素在 1 到 8 之间。如果生成的用例导致Need为负,说明max_need小于allocation,需要调整生成逻辑。

5.2 边界用例:全零分配、单进程、单资源

除了随机测试,几个边界用例必须手动跑一遍:

用例输入特征预期结果
全零分配Allocation 全 0Available 等于 total,安全序列任意顺序
单进程单资源n=1, m=1只要 Need <= Available 就安全
资源刚好够sum(Allocation) == totalAvailable 全 0,只有 Need 全 0 的进程能跑
死锁状态每个进程都持有部分资源且 Need 大于 Available不安全,无安全序列

全零分配时,Need等于Max,Available等于total。如果每个进程的Max都不超过total,安全序列就是 0 到 n-1。单进程单资源时,安全性检查退化成一次比较。资源刚好够时,Available全 0,只有Need全 0 的进程能被选中,跑完后释放资源,再检查下一个。死锁状态用来验证不安全分支是否正确输出。

5.3 用日志追踪安全性检查的每一步

实验报告要求画出安全性检查过程,我一般在isSafe里加日志输出:

// 在 isSafe 循环内加日志 cout << "Work: "; for (int j = 0; j < m; j++) cout << work[j] << " "; cout << "| 选中 P" << i << "\n";

这样每次选中进程时打印当前Work和进程号,实验报告里直接截图就是安全性检查过程。注意日志不要加在正式提交的代码里,否则输出太乱。可以加一个DEBUG宏控制:

#ifdef DEBUG cout << "Work: "; for (int j = 0; j < m; j++) cout << work[j] << " "; cout << "| 选中 P" << i << "\n"; #endif

编译时用g++ -DDEBUG ba.cpp -o ba开启日志,不加-DDEBUG就是干净输出。

5.4 从银行家算法到线程死锁排查的迁移

银行家算法的思想在工程里对应的是“资源分配前先检查”。数据库死锁排查时,SHOW ENGINE INNODB STATUS输出的LATEST DETECTED DEADLOCK段落,本质上是在告诉你哪些事务互相等待。线程死锁用jstack或gdb抓栈,看哪些线程持有锁并等待对方释放。这些场景里,银行家算法的矩阵和向量变成了锁持有表和等待图,但核心逻辑一样:找一条所有线程都能跑完的路径,找不到就是死锁。

我自己的习惯是,写完ba.cpp后不急着交实验报告,先用随机测试跑 100 组,再用边界用例跑一遍,最后把DEBUG日志打开,对着输出手算一遍安全序列。这样即使老师临时改数据,也能当场验证。希望帮到你。

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

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

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

立即咨询