银行家算法详解:死锁避免与C语言实现
简介:操作系统银行家算法实验报告及配套C++源代码,面向操作系统课程学习者与并发编程入门者,以经典的Dijkstra银行家算法为例,完整演示资源分配与死锁避免的实现过程。资源共5个文件,包括docx格式实验报告、cpp源文件、h头文件与txt数据文件,其中实验报告系统讲解算法原理、四个关键数据结构(最大需求矩阵、可用资源向量、已分配矩阵、需求矩阵)及安全性检查流程;源代码则实现进程管理、资源申请与释放、安全序列判定等核心模块,并配有初始化数据文件,整体压缩包仅452KB,便于下载使用。已有5244人学习下载。通过这份资源,读者不仅能掌握银行家算法避免死锁的机制,还能动手运行程序观察资源分配过程,理解不安全状态下的请求挂起处理。实验报告与代码相互对照,并包含常见问题如资源分配不当、安全性检查误判的分析思路,适合作为操作系统课程设计或实验的参考。 操作系统这门课,十个学生九个绕不开死锁,而银行家算法几乎是每一届操作系统实验的保留项目。我第一次提交这份实验报告的时候,代码只跑通了教科书上的那一组数据,换成自己随便敲的两组数据就当场翻车,后来才彻底搞明白,死锁避免看似只是“能不能找到一条安全序列”,真正写代码时坑全藏在数据结构的设计和回滚动作里。这篇文章把我那版的实验报告和源代码整理出来,附上完整的测试过程和踩坑记录,给正在做这个实验或者复习操作系统期末考的同学参考。
1. 银行家算法到底在解决什么问题
1.1 死锁怎么来的,算法又凭什么能避免
死锁产生的四个必要条件:互斥、持有并等待、不可抢占、循环等待。银行家算法的思路很直白——在每次资源分配之前,先模拟一下“如果把这些资源分出去,系统还能不能让所有进程都顺利完成”。如果能,就分配;如果不能,宁可让进程等着,也不冒这个险。这相当于提前把循环等待的可能掐死在摇篮里,属于死锁避免策略,和死锁预防、死锁检测是不同路线的方案。
这个算法的名字来源于银行家放贷的逻辑:银行不会把全部资金都贷给一个客户,而是会留一部分准备金,保证即使所有客户同时来取钱,银行也不至于资金链断裂。操作系统里的“资金”就是各类资源,每个进程在开始前必须声明自己对每类资源的最大需求量,系统记录当前谁手里有多少资源、还剩多少资源,再按照“是否还能找到一条让所有进程都跑完的顺序”来决定是否批准新的请求。
1.2 四个核心数据结构,一个都不能少
写代码之前,先把模型建立起来。系统有 m 类资源、n 个进程,需要维护四张表:
| 数据结构 | 含义 | 作用 |
|---|---|---|
| Available | 每类资源的可用数量 | 表示系统现在还能分配多少资源 |
| Max | 每个进程对每类资源的最大需求 | 进程启动时声明,运行中不可修改 |
| Allocation | 每个进程当前已分配的资源数 | 记录资源当前归属 |
| Need | 每个进程还需要的资源数 | Need = Max - Allocation,判断请求是否合法 |
我在第一次写程序时,最大的问题就是没有把 Need 单独拎出来,而是每次用到的时候临时算 Max - Allocation。看起来没什么区别,但试分配的时候要同时修改 Allocation 和 Need,如果只改一张表,后续的检测就会出错。建议直接用一个二维数组保存 Need,每一步分配时同步维护,代码反而更简单。
2. 实验整体设计与流程拆解
2.1 实验目标与程序运行流程
这次实验的目标是:用 C 语言实现一个银行家算法的模拟程序,能够根据用户输入的进程数、资源数、各类资源总量和每个进程的 Max、Allocation,计算当前系统状态是否安全;然后接受某个进程的资源请求,判断该请求是否合法、是否可以立即分配、是否会导致系统进入不安全状态。
整体流程分为六步:
- 输入资源数量和进程数量。
- 输入每类资源的现有数量 Available。
- 输入每个进程的 Max 矩阵和 Allocation 矩阵,程序自动计算 Need 矩阵。
- 打印当前系统状态表,并调用安全性检测算法,输出当前状态的安全序列。
- 进入请求处理循环:用户输入请求的进程编号和请求向量。
- 程序依次检查 “请求是否小于 Need” 和 “请求是否小于 Available”,如果都满足,则试分配并重新执行安全性检测,根据检测结果决定接受请求还是回滚。
这里有一个值得注意的设计决策:把“安全性检测”做成一个独立的函数,它不依赖全局状态之外的任何东西。这样后面处理请求时,可以先修改全局状态,调用检测函数,再根据返回值决定保留修改还是恢复现场。如果安全性检测和试分配逻辑耦合在一起,调试起来会非常痛苦。
2.2 安全性检测的核心原理
安全性检测算法是整套代码的心脏,思路可以描述为“用模拟运行来找安全序列”:
- 令 Work = Available,所有进程标记为未完成。
- 从未完成的进程里,找到一个 Need[i] 的每一类资源数都小于等于 Work 的进程。
- 如果找到了,就假设该进程执行完毕,释放它占用的全部资源,即 Work = Work + Allocation[i],并标记该进程已完成,把它加入安全序列。
- 重复步骤 2,直到找不出满足条件的进程。
- 如果所有进程都已完成,说明系统当前处于安全状态;否则就是不安全状态。
这个逻辑看起来简单,但在实现上有几个容易出错的地方。最典型的是:检查 Need[i][j] <= Work[j] 时,一定要对每一类资源都做比较,不能只看某几类。我当初写循环时漏掉了对所有资源的遍历,导致一个本该不安全的系统被误判为安全,跑测试用例时才发现问题。另外,Work 数组在每次查找时是累加的,一旦某个进程被选中,它释放的资源必须立刻加到 Work 上,这会影响后续进程能否被选中。
3. 源码实现与核心代码解析
3.1 数据结构的定义与初始化
下面是完整的 C 语言实现,我把它拆成几块来说。首先是全局数据结构和初始化部分:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_PROC 10
#define MAX_RES 10
int proc_cnt; // 进程数
int res_cnt; // 资源类型数
int available[MAX_RES]; // 每类资源当前可用数
int max_need[MAX_PROC][MAX_RES]; // 最大需求矩阵
int allocation[MAX_PROC][MAX_RES]; // 已分配矩阵
int need[MAX_PROC][MAX_RES]; // 仍需矩阵 = max - allocation
void init_system() {
printf("请输入进程数量: ");
scanf("%d", &proc_cnt);
printf("请输入资源类型数量: ");
scanf("%d", &res_cnt);
printf("请输入每类资源的可用数量:\n");
for (int j = 0; j < res_cnt; j++) {
printf(" 资源 %c: ", 'A' + j);
scanf("%d", &available[j]);
}
printf("请输入每个进程的最大需求 Max:\n");
for (int i = 0; i < proc_cnt; i++) {
printf(" 进程 P%d: ", i);
for (int j = 0; j < res_cnt; j++) {
scanf("%d", &max_need[i][j]);
}
}
printf("请输入每个进程的已分配资源 Allocation:\n");
for (int i = 0; i < proc_cnt; i++) {
printf(" 进程 P%d: ", i);
for (int j = 0; j < res_cnt; j++) {
scanf("%d", &allocation[i][j]);
}
}
for (int i = 0; i < proc_cnt; i++) {
for (int j = 0; j < res_cnt; j++) {
need[i][j] = max_need[i][j] - allocation[i][j];
}
}
}
这里的 need[i][j] = max_need[i][j] - allocation[i][j] 是必须放在输入完成之后进行的,不能在输入 Allocation 之前算。还有一个细节:输入的 Max 必须大于等于 Allocation,如果进程当前已分配的资源超过了它的最大需求,这个输入本身就是非法的。实验程序里可以加一个校验,防止这种错误数据进入后续逻辑。
3.2 安全性检测函数的实现
安全性检测函数接受当前的三张表,返回是否安全,并通过指针输出一个安全序列。这样设计的好处是:既能检查初始状态,也能在试分配后直接调用它来验证新状态。
int is_safe(int *avail, int alloc[][MAX_RES], int need_mat[][MAX_RES], int safe_seq[]) {
int work[MAX_RES];
int finish[MAX_PROC] = {0};
int done = 0;
for (int j = 0; j < res_cnt; j++) {
work[j] = avail[j];
}
while (done < proc_cnt) {
int found = -1;
for (int i = 0; i < proc_cnt; i++) {
if (!finish[i]) {
int can = 1;
for (int j = 0; j < res_cnt; j++) {
if (need_mat[i][j] > work[j]) {
can = 0;
break;
}
}
if (can) {
found = i;
break;
}
}
}
if (found == -1) {
return 0;
}
finish[found] = 1;
safe_seq[done++] = found;
for (int j = 0; j < res_cnt; j++) {
work[j] += alloc[found][j];
}
}
return 1;
}
这段代码里用了一个 while 循环配合 found 标志位,每次从头扫描所有未完成的进程。时间复杂度是 O(n^2 * m),对实验规模来说完全够用。我见过有同学用递归去枚举所有可能的执行顺序,那就过于复杂了,而且没必要。
3.3 资源请求处理与试分配回滚
处理请求是另一个重点。请求向量 req[] 需要经过三重检查:
-
req[j] > need[pid][j],直接判定为非法请求,因为进程不可能需要超过自己声明的最大需求。 -
req[j] > available[j],说明资源暂时不足,让进程等待。 - 前两项都通过,先试分配,再调用
is_safe(),如果安全就接受,否则回滚。
int request_resources(int pid, int req[]) {
for (int j = 0; j < res_cnt; j++) {
if (req[j] > need[pid][j]) {
printf("错误: 请求超过该进程声明的最大需求!\n");
return -1;
}
if (req[j] > available[j]) {
printf("资源不足, 进程 P%d 需要等待\n", pid);
return 0;
}
}
int backup_avail[MAX_RES];
int backup_alloc[MAX_PROC][MAX_RES];
int backup_need[MAX_PROC][MAX_RES];
memcpy(backup_avail, available, sizeof(available));
memcpy(backup_alloc, allocation, sizeof(allocation));
memcpy(backup_need, need, sizeof(need));
for (int j = 0; j < res_cnt; j++) {
available[j] -= req[j];
allocation[pid][j] += req[j];
need[pid][j] -= req[j];
}
int safe_seq[MAX_PROC];
if (is_safe(available, allocation, need, safe_seq)) {
printf("分配成功! 安全序列: ");
for (int i = 0; i < proc_cnt; i++) {
printf("P%d ", safe_seq[i]);
}
printf("\n");
return 1;
} else {
memcpy(available, backup_avail, sizeof(available));
memcpy(allocation, backup_alloc, sizeof(allocation));
memcpy(need, backup_need, sizeof(need));
printf("分配会导致系统进入不安全状态, 已回滚, 请求被拒绝\n");
return 0;
}
}
回滚是整个程序里最容易写错的地方。我最早的做法是手动把 available、allocation、need 逐个恢复,结果漏了一个字段,程序跑第二次请求时状态就全乱了。后来学乖了,在试分配之前用 memcpy 把三张表完整备份,回滚时直接整体恢复,这样可以避免“恢复不完整”的问题。
3.4 主函数与状态展示
主函数负责把整个流程串起来,同时提供一个打印系统状态表的函数,方便观察每次分配前后的变化:
void print_state() {
printf("\n当前系统状态:\n");
printf("进程 Max Allocation Need\n");
for (int i = 0; i < proc_cnt; i++) {
printf("P%d ", i);
for (int j = 0; j < res_cnt; j++) printf("%2d ", max_need[i][j]);
printf(" ");
for (int j = 0; j < res_cnt; j++) printf("%2d ", allocation[i][j]);
printf(" ");
for (int j = 0; j < res_cnt; j++) printf("%2d ", need[i][j]);
printf("\n");
}
printf("Available: ");
for (int j = 0; j < res_cnt; j++) printf("%d ", available[j]);
printf("\n");
}
int main() {
init_system();
print_state();
int safe_seq[MAX_PROC];
if (is_safe(available, allocation, need, safe_seq)) {
printf("初始状态安全, 安全序列: ");
for (int i = 0; i < proc_cnt; i++) printf("P%d ", safe_seq[i]);
printf("\n");
} else {
printf("初始状态不安全!\n");
return 0;
}
int pid, req[MAX_RES];
while (1) {
printf("\n请输入要请求资源的进程编号(-1 退出): ");
scanf("%d", &pid);
if (pid == -1) break;
printf("请输入请求向量 (共 %d 个数): ", res_cnt);
for (int j = 0; j < res_cnt; j++) scanf("%d", &req[j]);
request_resources(pid, req);
print_state();
}
return 0;
}
4. 测试用例与运行结果分析
4.1 经典测试数据:初始安全状态
我采用操作系统教材上的经典数据来做测试。系统有 5 个进程、3 类资源(A、B、C),初始 Available 为 (3, 3, 2),各进程的 Max 和 Allocation 见下表:
| 进程 | Max | Allocation | Need |
|---|---|---|---|
| P0 | 7 5 3 | 0 1 0 | 7 4 3 |
| P1 | 3 2 2 | 2 0 0 | 1 2 2 |
| P2 | 9 0 2 | 3 0 2 | 6 0 0 |
| P3 | 2 2 2 | 2 1 1 | 0 1 1 |
| P4 | 4 3 3 | 0 0 2 | 4 3 1 |
运行程序后,安全性检测会找到一个安全序列,例如 P1 -> P3 -> P4 -> P0 -> P2。这个序列的含义是:让 P1 先跑完,释放它占的资源,然后 P3、P4、P0、P2 依次跑完,整个过程中每个进程都能在截止前拿到足够的资源。
4.2 场景一:合法请求且系统保持安全
假设此时 P1 请求 (1, 0, 2)。检查发现 (1,0,2) <= Need[P1]=(1,2,2) 且 (1,0,2) <= Available=(3,3,2) ,于是进入试分配:
- Available 变为 (2, 3, 0)
- Allocation[P1] 变为 (3, 0, 2)
- Need[P1] 变为 (0, 2, 0)
再次执行安全性检测,可以找到一个安全序列 P1 -> P3 -> P4 -> P0 -> P2,因此这个请求被接受。注意,此时 P1 的 Allocation 应该是 (3, 0, 2),而不是 (3, 0, 0)。很多同学在试分配时只修改了 available 和 need,忘记了同步更新 allocation,导致后续 Work 累加时资源数对不上。
4.3 场景二:请求超过可用资源直接拒绝
继续在刚才的状态下,P4 请求 (3, 3, 0)。当前 Available 是 (2, 3, 0),显然 (3, 3, 0) <= (2, 3, 0) 不成立。程序应该直接返回“资源不足,进程需要等待”,而不需要进入试分配流程。这里有一个容易被忽略的优先级问题:先检查请求是否超过最大需求,再检查资源是否够用,这个顺序不能反过来,否则一个本来非法的请求会被误判为“资源不足”,掩盖了程序设计上的错误。
4.4 场景三:试分配后不安全则回滚
还是在 P1 请求成功后的状态,假设 P0 请求 (0, 2, 0)。检查合法性和资源充足性都通过,试分配后:
- Available 变为 (2, 1, 0)
- Allocation[P0] 变为 (0, 3, 0)
- Need[P0] 变为 (7, 2, 3)
此时安全性检测发现,没有任何一个进程的 Need 所有分量都小于等于 Work=(2, 1, 0),因为可用资源里 C 类已经是 0,而所有未完成的进程都还需要 C 类资源,找不到安全序列。程序必须回滚到试分配之前的状态,并输出“请求被拒绝”。
这个场景特别能检验代码的回滚逻辑是否完备。如果备份恢复不彻底,下一次循环打印状态表时会发现 available 或者 allocation 和预期不一致,调试时很难定位。
5. 常见问题与排查技巧实录
5.1 安全性检查总是误判的几个原因
我在完成实验的过程中,以及帮学弟学妹排查代码时,发现安全性检测误判主要来自三个地方。
第一个是 Need 矩阵没有单独维护,或者初始化之后没有再更新。每次试分配时都需要同时修改 available、allocation、need 三张表,少改一个,状态就错了。建议用一个备份函数把三张表整体复制,而不是手动逐项恢复。
第二个是 Work 累加时机不对。安全性检测的 while 循环里,找到可执行的进程后必须立刻把它的 allocation 加到 work 中,然后再扫描下一个进程。如果先扫描完一轮再统一释放,就会漏掉“前一个进程释放资源后,后续进程才能执行”的依赖关系。
第三个是循环扫描时没有正确跳过已完成进程。忘记设置和检查 finish 标志位,会让同一个进程被反复选中,安全序列长度超过进程总数,最终越界访问。我在代码里用 finish[found] = 1 来标记,并在每次查找前判断 !finish[i] ,这样就不会重复处理。
5.2 输入数据边界带来的坑
银行家算法的输入要非常小心。首先是 Max 必须大于等于 Allocation,如果输入时反过来,Need 就成负数了,后面的检测全部没有意义。建议在 init_system 里加一个 check 函数,发现非法输入直接提示重新输入。
其次是资源数量不能为 0。如果某类资源总数为 0,而某个进程又需要这类资源,那这个进程永远无法完成。这个场景在测试时很容易遇到,所以输出安全序列时要留意进程的 Need 是否存在 0 之外的数。
还有一个体验层面的建议:交互式输入时打印清晰的提示,标明当前是在输入 Max 还是 Allocation,并且用进程编号 P0、P1 这种方式提示用户。我最初版本只打印“请输入第几个进程”,测试时老是对不上号。后来改成格式化输出,错误率立刻降下来了。
5.3 如何快速验证程序是否正确
一个实用的验证思路是:先运行初始状态的安全性检测,对比理论分析得到的安全序列,比如教材上的 (P1, P3, P4, P0, P2),看输出是否一致。然后手动画一张表,模拟一次请求,一步步算出试分配后的状态,再和程序打印的状态表对比。如果全流程能对上,说明核心逻辑基本没问题。
还可以设计一个必现死锁的状态来测试:让系统中所有进程都还差一个资源,而可用资源刚好全部分完,比如 3 个进程、2 类资源,每个进程都是已分配 (1, 0),最大需求 (2, 0),Available 为 (0, 0)。此时系统已经死锁,安全性检测应该返回不安全。这种“异常数据”往往比正常数据更容易暴露代码里的 bug。
6. 我做完这个实验后的一些想法
银行家算法在实际操作系统里用得并不多,因为它要求进程在开始前就知道自己的最大资源需求,这在很多真实场景下很难做到。但作为理解死锁避免的入门模型,它依然是非常好的教学样例。做完这个实验,至少能让你真正理解“安全状态”和“不安全状态”的区别,而不是停留在背概念的层面。
如果学有余力,可以考虑扩展几个方向:一是把单次请求改成多线程并发请求,模拟多个进程同时请求资源的竞争场景;二是加入资源释放逻辑,也就是进程结束后主动归还资源;三是做一个图形界面,动态展示安全性检测每一步 Work 的变化和进程调度顺序。我个人觉得第三个扩展对理解算法帮助最大,因为看着 Work 一点点变大、进程一个个被选中,整个“安全性”的概念会变得特别直观。
最后分享一个实验报告的小技巧:虽然实验报告要求的是代码能运行、结果正确,但评分的点睛之笔通常在“实验分析”部分。把某次请求被拒绝的过程仔仔细细写清楚,画出试分配前后的状态变化,再解释为什么找不到安全序列,让老师看到你不仅会调代码,还真正理解了算法背后的思想。这就和本文开头说的一样,银行家算法的代码本身不难,难的是每一步分配背后那根“安全底线”的把握,代码写多了,自然就稳了。
更多推荐



所有评论(0)