简介:操作系统银行家算法实验报告及配套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,计算当前系统状态是否安全;然后接受某个进程的资源请求,判断该请求是否合法、是否可以立即分配、是否会导致系统进入不安全状态。

整体流程分为六步:

  1. 输入资源数量和进程数量。
  2. 输入每类资源的现有数量 Available。
  3. 输入每个进程的 Max 矩阵和 Allocation 矩阵,程序自动计算 Need 矩阵。
  4. 打印当前系统状态表,并调用安全性检测算法,输出当前状态的安全序列。
  5. 进入请求处理循环:用户输入请求的进程编号和请求向量。
  6. 程序依次检查 “请求是否小于 Need” 和 “请求是否小于 Available”,如果都满足,则试分配并重新执行安全性检测,根据检测结果决定接受请求还是回滚。

这里有一个值得注意的设计决策:把“安全性检测”做成一个独立的函数,它不依赖全局状态之外的任何东西。这样后面处理请求时,可以先修改全局状态,调用检测函数,再根据返回值决定保留修改还是恢复现场。如果安全性检测和试分配逻辑耦合在一起,调试起来会非常痛苦。

2.2 安全性检测的核心原理

安全性检测算法是整套代码的心脏,思路可以描述为“用模拟运行来找安全序列”:

  1. 令 Work = Available,所有进程标记为未完成。
  2. 从未完成的进程里,找到一个 Need[i] 的每一类资源数都小于等于 Work 的进程。
  3. 如果找到了,就假设该进程执行完毕,释放它占用的全部资源,即 Work = Work + Allocation[i],并标记该进程已完成,把它加入安全序列。
  4. 重复步骤 2,直到找不出满足条件的进程。
  5. 如果所有进程都已完成,说明系统当前处于安全状态;否则就是不安全状态。

这个逻辑看起来简单,但在实现上有几个容易出错的地方。最典型的是:检查 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[] 需要经过三重检查:

  1. req[j] > need[pid][j] ,直接判定为非法请求,因为进程不可能需要超过自己声明的最大需求。
  2. req[j] > available[j] ,说明资源暂时不足,让进程等待。
  3. 前两项都通过,先试分配,再调用 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 一点点变大、进程一个个被选中,整个“安全性”的概念会变得特别直观。

最后分享一个实验报告的小技巧:虽然实验报告要求的是代码能运行、结果正确,但评分的点睛之笔通常在“实验分析”部分。把某次请求被拒绝的过程仔仔细细写清楚,画出试分配前后的状态变化,再解释为什么找不到安全序列,让老师看到你不仅会调代码,还真正理解了算法背后的思想。这就和本文开头说的一样,银行家算法的代码本身不难,难的是每一步分配背后那根“安全底线”的把握,代码写多了,自然就稳了。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

更多推荐