☰
操作系统课程设计:进程调度、银行家算法与C语言实现指南
2026/10/8 7:27:04 网站建设 项目流程

简介:这是一份西安电子科技大学计算机科学与技术专业的操作系统课程设计资料包,面向操作系统课程学生、毕业设计或大作业需要完整参考方案的学习者,可用于理解进程管理、内存调度、文件系统等核心实验的设计思路与实现流程。压缩包以zip格式发布,约41MB,核心内容包括课程设计报告、参考实验代码以及原始上机截图,其中报告可用于梳理实验原理和结论,代码便于对照调试与二次修改,截图则还原真实上机过程。目前已有604人学习下载。资料尤其适合已有一定编程基础、需要结合课程要求完成类似题目并自行扩展功能的学习者,建议将其作为思路参考与排错对照,而非直接照搬。整体结构围绕实验报告、源码、截图三个模块组织,便于按需查阅。

1. 操作系统课程设计:先搞清这份资料里有什么、能抄什么

计算机科学与技术专业到了高年级,操作系统课程设计基本是绕不过去的一道硬关卡。它不是单纯写个算法跑通就算完,而是要交一份完整的课程设计报告,配实验代码和上机截图,老师还会对着代码随机提问。西电这套操作系统课程设计资料,把报告、代码、原始截图放在一起,适合当作参考样本来学习课程设计报告怎么写、经典实验(进程调度、银行家算法、页面置换)的代码结构怎么组织。特别提醒:它是参考资料,不是成品作业,代码不能直接复制照搬,需要自己能看懂、会改、能讲。

2. 课程设计报告先读三遍:从文档反推代码架构和验收评分点

先讲一个经验:我不建议一上来就打开代码文件。操作系统的课程设计报告信息密度比代码高得多,老师验收时先翻报告,再让你跑程序,最后抽着问算法细节。报告里写清楚的模块划分,就是代码里结构体的设计依据;测试部分贴的截图,就是代码里输入输出的验收标准。先把报告读三遍,比自己埋头调试半天效率高。

2.1 报告里藏着实验要求:先看需求分析再动手

课程设计报告一般按需求分析、总体设计、详细设计、测试与分析四段走。需求分析不是抄教材定义,它约定的输入输出格式、进程数量上限、时间片大小、资源种类数,全都是代码的硬约束。常见误操作是拿着模板报告改个标题就交,老师问一句"你的输入文件格式是什么"就露馅。

报告里需求分析这节,通常藏着三个关键约束:进程数量范围(比如最大支持 10 个进程)、资源种类的数量(银行家算法里 M 的取值)、输出格式(要求打印安全序列还是只判断安全/不安全)。这三个参数决定了代码里数组开多大、循环怎么写。课前把这三个数标出来,再去看代码,事半功倍。

2.2 总体设计与数据结构:把模块图和 PCB 对应起来

报告的总体设计一般有一张模块结构图,标出主模块、调度模块、输入模块、输出模块之间的调用关系。这张图翻译成代码就是函数划分:main 负责读数据和打印结果,scheduler 负责选进程,queue 负责就绪队列操作。对照图找代码,比自己从 main 函数逐行往下跟要快。

数据结构是另一个重点。进程调度实验里最核心的是 PCB 结构体,字段通常包括进程名、到达时间、服务时间、剩余时间、完成时间、等待时间。如果你的报告里写的是"进程控制块包含进程标识符和状态信息",但代码里 struct 连到达时间都没有,这份报告就是抄的,经不起问。正确做法是把代码里真实的字段画进报告,让文档和实现严格一致。

提示:报告里的流程图不用画得花哨,能用标准符号把调度过程画清楚就行。老师更看重文档和代码的一致性,而不是美观。

2.3 测试截图是证据链:上机记录的四个信息点

原始上机截图的价值在于它是运行证据,而不是给你撑篇幅用的装饰。截图里至少要保留四个信息:终端或命令行环境(说明你在什么系统下跑通的)、输入参数(进程数量、时间片大小要和报告里写的一致)、输出结果(要和手算的理论值对得上)、代码路径或编译命令(证明这是你自己环境里编译的)。

很多同学吃亏在截图和代码环境对不上,比如代码是在 Linux 下写的,截图却是 Windows 命令行。老师一眼就能看出来是拼凑的。建议把代码在哪个系统编译的都写进报告,截图里带上终端窗口标题栏,这份证据链才算完整。操作系统课程设计通常要求在 Linux 环境下开发,报告里明确写"实验环境:Ubuntu 20.04 + gcc",截图里也能看到对应终端,这一项就稳了。

3. 进程调度实验复现:从 FCFS 到时间片轮转的 C 语言实现与参数调优

进程调度是操作系统课程设计里最常考的题目,也是报告里最好讲清楚的一块。实现难度不大,但很少有人把它写成"能回答追问"的状态。核心就是把 PCB 结构体设计好,再把调度算法的选择逻辑用代码讲明白,每一个分支都能说出理由。

3.1 为什么先写 FCFS 再扩展时间片轮转:算法选型逻辑

先来先服务(FCFS)是所有调度算法的基础,它按到达时间顺序执行,实现最简单,但缺点是平均等待时间受到达顺序影响很大,一个长作业会拖住后面所有短作业。短作业优先(SJF)能显著降低平均周转时间,但前提是系统能预知每个进程的服务时间,而且长作业可能被饿死。时间片轮转(RR)则兼顾响应时间和公平性,代价是上下文切换次数变多。

课程设计里比较讨巧的架构是先实现 FCFS 和 SJF 做对比,再在同一套 PCB 上扩展时间片轮转。调度算法只改选择逻辑,不改数据结构,这样报告里就能写"本设计通过统一 PCB 结构实现了多种调度算法,便于对比分析"。这一句话在验收时比堆代码更打动人。

数据结构统一之后,切换算法的成本非常低。FCFS 是找最早到达的进程,SJF 是找服务时间最短的进程,RR 是取队首进程运行一个时间片。这三种操作都在同一个 PCB 数组和就绪队列上完成,后面代码里你会看到,核心逻辑就是排序和队首操作的区别。

3.2 FCFS/SJF 的 C 语言实现:结构体、队列与排序

以 C 语言实现为例,PCB 结构体定义如下,注意把剩余时间单独拎出来:

#include <stdio.h> #include <stdlib.h> #define MAX_PROC 10 typedef struct { char name[8]; int arrive; // 到达时间 int serve; // 服务时间(运行总长) int remain; // 剩余时间,时间片轮转时使用 int finish; // 完成时间 int start; // 首次开始运行时间,用于计算等待时间 } PCB; int n; // 实际进程数 PCB proc[MAX_PROC]; // 按到达时间排序,FCFS 的第一步 int cmp_fcfs(const void *a, const void *b) { PCB *p1 = (PCB *)a, *p2 = (PCB *)b; return p1->arrive - p2->arrive; } // FCFS:到达时间排序后依次执行,模拟运行过程 void run_fcfs(void) { qsort(proc, n, sizeof(PCB), cmp_fcfs); int current_time = 0; for (int i = 0; i < n; i++) { if (current_time < proc[i].arrive) current_time = proc[i].arrive; // 处理空闲期 proc[i].start = current_time; // 首次运行时间 current_time += proc[i].serve; // 运行完成 proc[i].finish = current_time; // 完成时间 printf("%s start=%d finish=%d\n", proc[i].name, proc[i].start, proc[i].finish); } }

这段代码里最关键的是 current_time 的推进逻辑。当前时间小于下一个进程的到达时间时,CPU 处于空闲,直接把时间跳到到达时刻;调度算法只管选进程,不管推进时间,这个职责分离让后续扩展 SJF 和 RR 都变得很轻松。start 字段单独记录首次开始运行的时间,之后算等待时间就是 start 减去 arrive。

SJF 的改法是在 FCFS 的基础上改成每次从"已到达但未运行"的进程里选服务时间最短的。实现上可以继续用 qsort,但排序关键字变成:到达时间优先,服务时间次之。更朴素的做法是两层循环模拟"每一时刻选一个进程",这个写法对 RR 更有价值。先理解 FCFS 版的时间推进,再看 SJF 和 RR 就不晕了。

3.3 时间片轮转的改造点:就绪队列的循环与切换时机

时间片轮转的代码难点不在排序,而在什么时候切换。我常用一个简单的循环队列模拟就绪队列,每个进程用 remain 记录剩余时间,每次只运行一个时间片 q:

int rr_index = 0; // 当前运行的进程下标 int remain_proc = n; int current_time = 0; // 初始化 remain = serve for (int i = 0; i < n; i++) proc[i].remain = proc[i].serve; while (remain_proc > 0) { // 跳过未到达的进程 if (proc[rr_index].arrive > current_time) { current_time++; // 模拟时钟推进 rr_index = (rr_index + 1) % n; continue; } // 跳过已完成进程 if (proc[rr_index].remain == 0) { rr_index = (rr_index + 1) % n; continue; } // 运行一个时间片 int run_time = (proc[rr_index].remain < q) ? proc[rr_index].remain : q; proc[rr_index].remain -= run_time; current_time += run_time; if (proc[rr_index].remain == 0) { proc[rr_index].finish = current_time; printf("%s finish at %d\n", proc[rr_index].name, current_time); remain_proc--; } rr_index = (rr_index + 1) % n; }

这段实现的关键细节是 run_time 的取值:剩余时间不足一个时间片时,只跑剩下的部分,否则可能把时间扣成负数。跳过未到达进程时,我用的是逐单位推进 current_time,效率不高但逻辑直观,课程设计里数据规模小,完全够用。真正要理解的是切换时机——进程运行完一个时间片,不管有没有结束,都轮到队尾重新排队。

时间片 q 的取值直接决定实验结果,这也是报告里值得写的一组对比数据。我用三组实验数据说明:进程集合固定为 A(到达 0、服务 6)、B(到达 1、服务 4)、C(到达 2、服务 2),q=1 时平均周转时间最长,因为每个进程都要频繁切换;q=4 时 C 进程要等 A、B 各跑完 4 个时间片才轮到,响应时间明显变差。参数对比表在报告里非常加分。

时间片 q周转时间序列平均周转时间响应感受
1A=16, B=10, C=410.0响应快但切换频繁
2A=16, B=12, C=611.3均衡
4A=16, B=12, C=1012.7短作业等待明显变长

写报告时建议把上表做成"实验结果分析",并解释为什么 q 越小响应越好但吞吐下降。老师喜欢看到这种参数敏感性分析,而不是直接把代码截图贴上去充字数。

4. 银行家算法与死锁避免:安全性检测的数据结构和边界处理

银行家算法是操作系统课程设计里另一个高频题目,也是面试操作系统岗位常问的点。它比进程调度多一层抽象:调度是在已有进程集合里选,银行家算法要判断"这次分配资源会不会让系统进入不安全状态"。理解这个区别,报告的核心论点就立住了。

4.1 银行家算法的核心:四张表和一个安全性检测

银行家算法对应四张数据结构表:Available 表示各类资源当前可用数量,Max 表示每个进程对各类资源的最大需求,Allocation 表示每个进程已经占用的资源数,Need 表示每个进程还需要的资源数。它们之间的关系是 Need = Max - Allocation,这个公式在代码里必须用结构体字段体现,不要用魔法数组裸算。

报告里要把这四张表画成表格,然后描述算法流程:进程发起请求时,先判断请求量是否超过 Need,再判断是否小于 Available;通过后做试探分配,运行安全性检测算法,如果系统仍然安全才真正分配,否则回滚。这个"试探—检测—回滚"的过程,是银行家算法的灵魂,面试官问的细一点就会落到回滚实现上。

安全性检测的目标是找到一个进程完成序列,让每个进程都能依次获得所需资源并运行结束。找不到这个序列就说明系统进入不安全状态。注意不安全和死锁不是一回事:不安全状态不一定死锁,但死锁一定发生在不安全状态。报告里如果能写出这句话,并且代码里的安全性检测确实是在"预判"而非"检测死锁",这个知识点就算吃透了。

4.2 安全性检测的 C 实现:Work、Finish 与回溯顺序

安全性检测算法的输入是当前可用资源 Work(初始等于 Available)、每个进程的 Need 和 Allocation。它不断扫描进程集合,找一个未完成且 Need 不超过 Work 的进程,让它运行完毕并释放 Allocation,把资源加回 Work,直到所有进程都完成。代码实现如下:

#define N 5 // 进程数 #define M 3 // 资源种类数 int Available[M]; int Max[N][M]; int Allocation[N][M]; int Need[N][M]; int is_safe(int safe_seq[], int *seq_len) { int Work[M], Finish[N]; for (int j = 0; j < M; j++) Work[j] = Available[j]; // 初始可用资源 for (int i = 0; i < N; i++) Finish[i] = 0; // 所有进程未完成 int count = 0; while (count < N) { int found = 0; for (int i = 0; i < N; i++) { if (Finish[i]) continue; int can_run = 1; for (int j = 0; j < M; j++) { if (Need[i][j] > Work[j]) { can_run = 0; // 有一种资源不够就跳过 break; } } if (can_run) { for (int j = 0; j < M; j++) Work[j] += Allocation[i][j]; // 资源回收 safe_seq[count++] = i; // 记录安全序列 Finish[i] = 1; found = 1; } } if (!found) return 0; // 本轮没找到可执行进程,系统不安全 } *seq_len = count; return 1; // 所有进程都执行完,系统安全 }

这段代码里最容易被忽略的是 while 循环的退出条件。如果某一轮扫描完所有进程,都没有找到 Need 满足 Work 的进程,说明系统已经不安全,必须立刻返回 0,否则会陷入死循环。Finish 数组的作用就是避免重复处理同一个进程,它和 found 标志配合,构成了这个算法的终止条件。

资源回收的顺序也有讲究:进程运行结束后,要把 Allocation 归还到 Work 里,而不是归还 Need。归还 Need 是错的,因为进程能运行说明它已经拿到了所有需要的资源,已经消耗的不可能再还回去。我在确认代码时就吃过这个亏,结果安全序列算出来全是正数,但手算验证就是对不上。

4.3 资源请求处理与新进程加入:边界参数表

安全性检测只解决"判断当前状态是否安全",完整的银行家算法还要写资源请求处理函数。进程发出请求向量 Request[j],处理流程是:检查 Request 是否小于等于 Need,检查是否小于等于 Available,如果都满足,就做试探性分配,然后调用 is_safe 决定是否提交分配,否则拒绝并给出提示。回滚操作就是把刚才试探扣掉的数据加回去。

我建议把请求处理函数写成"先检查、再试探、后判断、不回滚就恢复"四步,每一步用一个独立函数封装。这样好处是报告里可以按函数逐个描述,验收时也扛得住追问。实际写代码时,很多同学把试探分配直接写进 main 里,一旦安全性检测不通过,恢复操作散落到各处,很容易漏恢复一个字段。

边界参数需要注意的点并不复杂,但漏掉任何一个都会出问题:

边界场景处理策略预期结果
Request 大于 Need直接拒绝,提示超出需求不允许分配
Request 大于 Available挂起等待,返回资源不足分配不成功
Need 全为 0 的进程安全性检测时可直接跳过不影响安全序列
资源种类 M 为 0算法失去意义,初始化时拦截提示参数错误

写测试用例时,要覆盖一个安全状态和一个不安全状态。课程设计里常见的翻车点是所有测试数据都是安全的,老师随便改一个进程的 Max 就露馅。报告里如果写"本算法能正确识别不安全状态",测试部分就必须有一组真实的不安全数据示例,我当时用的是让两个进程互相持有对方所需资源的一组数据,安全序列打印为空,这一步能看出报告是认真做过的。

5. 操作系统课程设计避坑:编译、算法与验收环节的五个高频翻车点

所谓血泪经验,都是自己踩出来的。这一章把我在操作系统课程设计里见过的高频问题集中记下来,每条都按现象、原因、解决三个角度拆,读者可以对照自己的代码检查。

5.1 编译运行环节:Linux 环境与内存错误的坑

现象:代码在 Windows 下用 Dev-C++ 编译通过,拿到 Linux 下用 gcc 编译报错,或者运行时直接段错误崩溃。

原因:Windows 下的一些头文件路径和库在 Linux 不一样,比如 conio.h 在 Linux 下不存在,getch 函数用不了。段错误多数是数组越界,进程数超过预设 MAX_PROC,或者 PCB 数组下标访问到负数。

解决:开发环境统一到 Linux,装 gcc 后用gcc -Wall -g -o scheduler scheduler.c编译,加上 -Wall 和 -g 能发现大部分警告和调试信息。代码里在访问 proc 数组前加边界判断,比如if (idx < 0 || idx >= n) continue;。从那以后我每写完一个模块,都会先检查所有数组下标,再跑编译。

5.2 算法逻辑环节:死循环与随机数据的坑

现象:银行家算法的安全性检测函数跑不结束,程序卡死;页面置换算法每次运行结果都不一样,和报告里手算的对不上。

原因:安全性检测没有设置 found 标志,每轮扫描都找到同一个进程,Work 永远不被更新,while 循环出不去。随机数据的问题一般是 srand 没有固定种子,每次生成的内存页编号序列不同。

解决:安全性检测里每一轮扫描必须有 found 标志,一轮结束发现 found 为假立即返回不安全。随机数实验要固定种子,比如srand(42),然后把生成的序列打印出来,手算和程序结果对照,一致性无误再写进报告。

5.3 报告验收环节:截图证据与代码注释的坑

现象:报告里的测试截图和代码运行结果对不上,注释全是从网上模板拷的,老师问某个字段的含义,回答不上来。

原因:截图是拿别人的结果拼的,或者代码改了但截图没更新。注释和实际逻辑脱节,说明没有逐行理解代码。

解决:截图必须用自己环境里最新代码跑出来的结果,运行前先清屏、把编译命令和输入数据都留在终端里再截图。代码注释要写成"这个字段记录什么、这个分支处理什么情况",不要写"// 这是一个变量"这种废话。操作系统课程设计验收时会随机抽代码提问,与其背别人的注释,不如把每个字段的理解写进报告里。

6. 进阶:把课程设计改造成能写进简历的进程调度模拟器

到这里,基础版本的实验都跑通了,但你如果想把这段经历写进简历,或者拿它参加工程实训,还需要做两个层面的升级:验证方法设计和结构化改造。

6.1 验证方法:用固定算例和边界输入确认算法正确

我习惯先准备一组手算过的固定数据,比如三个进程到达时间分别是 0、1、2,服务时间分别对应 5、3、1,手算 FCFS 的平均周转时间,再跑程序核对。确认一致后,再测试边界输入:所有进程同时到达、空进程集、时间片设为 1 等。这些边界用例能暴露算法里的逻辑漏洞,也是报告里"测试与分析"章节最扎实的内容。

6.2 结构化改造:参数化输入、日志输出与文本甘特图

改造方向有三个:一是把进程数据从命令行参数或文件读取,而不是写死在数组里;二是加日志输出,每切换一个进程就打印原因和当前时间,方便观察调度过程;三是输出文本格式的甘特图,用字符表示每个时间区间由哪个进程运行,这段输出放到报告里非常直观。

注意:这三个改造都建立在基础代码已经能稳定运行、并且你清楚每段逻辑含义的前提下。如果基础版本的代码自己还没看懂就急着加功能,反而会引入一堆新问题。

完成这些改造后,课程设计就不再是应付作业的代码,而是一个可以演示、可解释、有测试数据的工程项目。我最后一次做操作系统课程设计时,就是靠这份进程调度模拟器在期末验收里从"能跑"变成了"能讲",从那以后我每次写完课设代码,都会强制自己走一遍"固定算例手算对照、边界输入、日志输出验证"这三步,再考虑写报告。希望帮到你。

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

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

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

立即咨询