☰
操作系统实验包实战:从解压到跑通答辩的全攻略
2026/10/12 13:45:31 网站建设 项目流程

简介:面向操作系统课程实验的本科生,这份杭电操作系统实验资料按实验一至实验五分模块整理,覆盖系统调用、进程调度、同步互斥、简单文件系统等典型主题,适合在课程设计或期末复习时对照阅读。从系统调用到文件系统,基本对应操作系统课程的核心实验环节,可帮助快速梳理各模块的代码框架。压缩包共有二十八个文件,其中十二个 C 源代码与四个头文件为主要内容,另配 main 程序、Makefile 构建脚本和 txt 说明文档,分别用于入口演示、自动编译和实验说明;整体仅五十六 KB,十分轻量。资料按实验目录分层,能清晰看到每次实验的代码组织与运行入口;simplefs 等实现代码有助于理解简单文件系统的关键写法,txt 文档可作为实验背景与关键步骤的参考,适合用来检查自己的实现思路。已有二百四十四人学习,对于正在做操作系统实验或准备实验答辩的同学,是一份轻量而实用的参考。

1. 操作系统实验包到手:先别急着交作业

你可能刚解压完一份名为“HDU操作系统实验.zip”的压缩包,这个代号在高校资源包里很常见,但比学校缩写更重要的是里面那几份实验代码。我的建议是:不论你是补作业还是补知识,都先花半小时把它读明白,而不是直接复制提交。操作系统实验包通常跑在一套老旧的教学框架上,代码能不能在你本机动起来,很多时候和算法本身没关系,只是环境没对上。

这篇文章要解决的是从拿到包到能演示、能答辩的那段路:解压乱码、编译报错、输入方式对不上、运行窗口一闪而过。这些坑我在给人辅导这类实验时见过太多次,它们大多不是算法难题,而是环境与代码风格问题。下面每章都会给可直接复制的命令、可调整的参数,以及能直接补进代码的辅助片段。

适合三类人:正在上操作系统实验课的学生,你需要的不是结论而是跑通;想补基础的开发新人,这类实验是理解进程与内存管理的最佳实践;想把它改造成自己版本的学习者,我会明确告诉你哪些地方值得动手。先看完拆包这章,把包里的结构摸清楚,再决定往下走哪条路。

2. 拆包:先认出操作系统实验包的六类标配实验

2.1 六类标配:为什么几乎所有实验包都长一个样

操作系统课程实验有一套约定俗成的菜单。处理器管理对应各类调度算法模拟;内存管理对应页面置换算法;进程同步与互斥对应生产者消费者或读者写者问题;死锁避免对应银行家算法;设备管理对应磁盘调度;再加上文件系统模拟,就构成了本科实验的主干。这套组合和操作系统教材的章节几乎一一对应,所以无论压缩包标注的院校缩写是哪个,内部内容大概率跑不出这个范围。

这些模拟程序不碰真实硬件。它们不需要驱动开发、不需要内核编译,本质就是把教材算法翻译成普通C程序:main函数里给一组测试数据,程序计算结果并打印。因此判断一个实验属于哪一类,不需要从头读懂全部代码,只需看输入输出格式就够了。

我常用下面这张表做快速判断,也建议你照这个思路去翻包内代码:

输入数据特征对应实验类型
Available / Max / Allocation银行家算法,死锁避免
进程号、到达时间、服务时间调度算法,如先来先服务、时间片轮转
页号序列、内存块数页面置换算法,如FIFO、LRU
生产者、消费者、缓冲区大小进程同步与互斥
柱面号、磁头位置磁盘调度算法
文件名、目录项、磁盘块号文件系统模拟

2.2 用 unzip 展开并识别编码:中文乱码的第一道坎

拿到 zip 后的第一个操作不是双击,而是在终端里用命令展开。命令行解压有两个好处:一是能显式指定文件名编码,避免中文乱码;二是你顺便能看清每个文件的大小和类型,做到心里有数。我一般在 Linux 上这样处理:

mkdir -p os-lab unzip -O GBK "HDU操作系统实验.zip" -d os-lab ls -l os-lab file os-lab/* 2>/dev/null | head -30

逻辑说明:unzip -O GBK告诉解压程序,压缩包里的文件名是用 GBK 编码的,这是 Windows 中文环境下压缩出来的老包最常用的编码方式。-d os-lab把内容统一解压到指定目录,避免文件散落在当前路径下难以管理。file命令用来识别每个文件的实际类型,区分 C 源文件、可执行程序、文本还是 VS 工程文件,不让后续处理走弯路。

参数说明:如果你在某个 zip 上执行unzip -O GBK后发现文件名依然是乱码,说明它可能是 UTF-8 或其他编码打包的,把参数换成-O UTF-8再试一次。macOS 自带的 unzip 不支持-O选项,遇到这种老包,我通常会换 7-Zip 解压,或在 Windows 上完成解压再拷贝过来。

注意:解压之前先用unzip -l列出包内内容,不解压就能看到文件名清单,这是排查乱码问题最快的方式。

2.3 定位 main 与 Makefile:入口不对,后面全是白费

六个实验通常不在同一个程序里,每个实验是独立目录或独立一组源文件。所以你编译之前必须确认每个目录的入口在哪里。常见打包习惯是每个目录下有一个主源文件加若干辅助文件,但命名权在打包者手里,最可靠的判断标准不是文件名,而是int main的位置。

用下面一组命令可以快速定位:

find os-lab -name "*.c" -o -name "*.cpp" | sort grep -rn "int main" os-lab --include="*.c" --include="*.cpp" | sed 's/:.*//' | sort -u ls os-lab/*/Makefile os-lab/*/makefile 2>/dev/null

逻辑说明:第一条列出全部源码文件,让你知道包内代码总量;第二条递归搜索int main,去掉行号信息后只保留文件路径,得到真正的程序入口;第三条检查是否存在 Makefile,决定下一步按哪种方式编译。如果你装了 ripgrep,第二条可以简写成rg -l "int main" os-lab,速度更快。

如果包里找不到 Makefile,说明工程是裸源文件或 IDE 工程格式。在 Windows 上可以用 PowerShell 同样定位入口:

Select-String -Path .\*.c, .\*.cpp -Pattern "int main" -List | Select-Object Path

锁定入口后,下一步是看 main 函数的输入方式:是控制台手工输入、从文本文件读入,还是直接在代码里写死测试数组。这个判断决定了你在第 3、4 章用哪种方式喂数据。我一般只看前 50 行就够,找scanf、fopen或初始化数组的赋值语句。

3. 跑通:进程调度实验从源码到可执行文件的最小命令

3.1 为什么第一个实验选进程调度

进程调度实验是整包内容里最适合用来建立信心的一部分。它的代码结构通常是一个 PCB 结构体、若干进程实例、一个调度函数、一个输出函数,依赖几乎只有 C 标准库,不需要额外的线程库或图形库。这意味着只要编译命令敲对,它就能跑;它跑不起来,问题一定出在环境而非算法上。

调度实验的验证也非常直观。输入是若干进程的到达时间和服务时间,输出是每个进程的开始时间、完成时间、等待时间,以及平均等待时间或平均周转时间。这些指标用手算就能验算,不需要像内存管理那样推断抽象状态。我第一次处理这类包时,就是先把进程调度跑通,用结果反向确认代码逻辑,再带着同样的工具链去啃银行家和多线程实验。

这里最容易翻车的是中断场景,比如把包里的文件原样拿过来,却忽略了它是按 Windows 控制台交互写的,scanf 的格式、清屏函数、延时函数全都不兼容。所以我把编译和运行的最小命令单列一节,你照着走一遍就能分清问题归属。

3.2 Linux 下编译运行:最小命令与辅助代码

假设你已经按第 2 章把包解压到 os-lab/sched 目录,进入该目录后的完整操作如下:

cd os-lab/sched gcc -Wall -o sched *.c ./sched < input.txt

逻辑说明:gcc -Wall -o sched *.c把当前目录下所有 C 文件编译成一个名为 sched 的可执行程序。-Wall打开常用警告,老代码里常隐藏未使用变量、隐式声明这类问题,这个选项能提前暴露。< input.txt把文件内容作为标准输入重定向给程序,适合需要固定数据集的场景。

参数说明:如果代码里用了 C99 之后的特性,比如在 for 循环内声明变量,需要加-std=c99;如果程序用到数学库函数,编译后报 undefined reference 到pow、sqrt这类,则要加-lm。我遇到更老的教学代码还会缺少返回语句,-Wall下会看到 warning,但程序多半能跑。

跑通之后,建议你在 main 函数适当位置加一个辅助输出函数,观察每次调度前的就绪队列状态。这里给出一个通用片段,按你包内 PCB 结构体的字段名调整即可:

void dump_pcb(const PCB *p, int n) { for (int i = 0; i < n; i++) { printf("pid=%d arrive=%d service=%d\n", p[i].pid, p[i].arrive_time, p[i].service_time); } }

逻辑说明:这个函数把内存中的 PCB 数组内容完整打印出来,放在读入数据之后调用一次,能确认数据有没有被正确解析;放在每次调度之后调用,能观察进程状态如何迁移。我习惯在读入函数返回后立刻调用,先排除数据解析错误,再谈算法正确性。

配合一组手算过的输入数据,输出对不对一眼就能看出来:

3 1 0 4 2 1 3 3 2 5

格式说明:第一行是进程数,之后每行依次是进程号、到达时间、服务时间。不同实验包的字段顺序可能不同,以你包内 scanf 的读取顺序为准,我这里只是给出通用约定。运行后如果在纸面上算出平均周转时间 8.33,而程序输出一致,说明代码逻辑基本可信。

3.3 Windows 下用 VS 或 gcc:相差的不只是编译器

Windows 上最常见的坑有三个:Sleep大写与sleep小写不通用、system("cls")清屏在非 Windows 平台没有实现、Visual Studio 把scanf视为不安全函数而要求改scanf_s。这些差异在 Linux 下编译时体现为 undefined reference,在 Windows 下恰好都是能跑的,所以反而坑了很多人。

我的做法是在代码里加一层条件编译,让同一份代码在两个平台都能编译:

#ifdef _WIN32 Sleep(1000); system("cls"); #else sleep(1); #endif

逻辑说明:_WIN32是 Windows 编译器预定义的宏,在 Windows 上编译时走上半支,调用大写的Sleep和system("cls");在 Linux 上走下半支,调用 POSIX 的sleep。这样你不用改动实验算法主体,只调整平台相关调用就能跨平台编译。

如果你手上是 Visual Studio 工程,命令行编译也可以做到不打开 IDE:

call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 cl /W4 /Fe:sched.exe *.c

参数说明:vcvarsall.bat是 VS 提供的环境初始化脚本,执行后cl命令才可用;/W4相当于 gcc 的-Wall,是最高级别的编译警告;/Fe:sched.exe指定输出文件名。如果不想折腾 VS,统一装一个 MSYS2 或 MinGW-w64,用第 3.2 节的 gcc 方式编译,会更省心。

4. 调参:银行家算法实验的参数设置与测试数据构造

4.1 数据文件格式:银行家算法读的是什么

银行家算法实验是整包中逻辑最完整、也最容易被“抄完就交”的一块。它做的事情是:系统有若干类资源,每个进程声明了自己最大需要量,已分配了一部分,剩下的可用资源集中管理。当进程申请资源时,系统先试探性分配,再检查剩余资源能否满足某个进程完成并回收资源的顺序,存在这样的顺序就是安全状态,允许分配,否则拒绝。

实验代码读取的数据必然是四个部分:资源种类数、进程数量、每个进程的最大需求矩阵 Max、每个进程的已分配矩阵 Allocation。可用资源向量 Available 通常单独给出,Need 矩阵则由 Max 减 Allocation 计算得到。我习惯把测试数据组织在一个文本文件里,结构如下:

# 第一行:进程数 资源种类数 # 第二行:Available 各类资源数量 # 接下来 N 行:每个进程的 Max # 再接下来 N 行:每个进程的 Allocation 5 3 10 5 7 7 5 3 3 2 2 9 0 2 2 2 2 4 3 3 0 1 0 2 0 0 3 0 2 2 1 1 0 0 2

逻辑说明:这组数据是教材中常见的经典用例,用于验证安全序列是否存在。Need 矩阵由 Max 减 Allocation 得到,比如第一个进程 Max 是 7 5 3,Allocation 是 0 1 0,则它的 Need 是 7 4 3。程序能不能正确算出安全序列,取决于它是否严格遵循“试探分配后做安全性检查”的流程。

必须强调一点:这段文本是我常用的等价表达,具体字段顺序以你包内代码里的fscanf或scanf顺序为准。有的包第一行先写资源种类数再写进程数,有的在代码里写死数组维度,改了数据文件却忘记同步数组大小,就会越界读写。拿到代码后第一件事应该是找读入函数,确认字段顺序。

4.2 三个必调参数与一组边界用例

实验代码跑通一次不算完,只有改过参数、换过用例才算真正理解。这个实验里最值得调的是三个参数,它们覆盖了算法的主要维度和常见陷阱:

参数作用建议调整方式
进程数量参与安全性检查的进程总数从 3 调到 5 或 8,观察安全序列查找是否仍然正确
资源种类数每类资源的可用数量与分配数量从 3 调到 4,验证代码是否存在写死的数组维度
Available 初始值各类资源初始可用量调小到刚好覆盖部分进程的 Need,验证拒绝路径

调整时注意看数据的合法性约束:每个进程的 Allocation 不能超过 Max,每个进程的 Need 不能为负,所有进程的 Allocation 之和不能超过系统资源总量。这些条件被违反时,好的实验代码应该报错退出,而不只是打印一个乱码的安全序列。

我建议至少跑三组边界用例。第一组是单进程,一个进程、两类资源,结果必然安全,用来验证最简路径;第二组是资源总量恰好等于各进程 Max 之和,安全序列依赖精确的资源回收顺序,能把“刚好够用”和“不够用”两种状态区分开;第三组是某个进程的 Allocation 超过 Max 的非法输入,用来观察代码有没有做合法性校验。三组跑完,算法主流程的健壮性基本见分晓。

这里有一个常见误区:只调大参数但不看对应的输出,以为跑不崩就代表正确。银行家算法的正确输出应该是每一步临时分配后的 Available 变化轨迹,以及最终的安全序列。你盯着输出里的 Avail 值变化,才能发现代码是不是重复计算了资源。

4.3 用 printf 插桩:比 gdb 更快看到安全序列

安全性检查是一个反复试探的过程,出错位置通常在三个地方:Need 矩阵计算错误、试探分配没有把已分配资源加回 Work、循环判断时把已完成的进程重复检查。用调试器设断点看变量也可以,但更省事的方式是直接加打印函数,把中间过程输出来。

我一般在安全性检查循环里加这样一个插桩:

void trace_check(int i, int *work, int *finish) { printf("[trace] P%d check: need ok, finish=%d, work now:", i, finish[i]); for (int j = 0; j < R; j++) { printf(" %d", work[j]); } printf("\n"); }

逻辑说明:这个函数在每次找到一个满足条件的进程时调用,输出当前进程编号、finish 状态以及更新后的 work 向量。它在试探分配成功的位置之后调用,能看到资源回收的顺序。如果 trace 里出现进程反复被选中,说明 finish 数组没有正确置位;如果 work 值不增反减,说明回收逻辑写反了。

参数说明:R是资源种类数,需要与包内代码里的全局变量名保持一致;work是当前可用资源数组,在算法中也叫Work或Available,按实际变量名替换。插桩代码只在调试时保留,确认无误后删掉即可。这一招比 gdb 高效,因为安全序列的生成过程本来就是顺序打印的,printf 的输出天然就是执行轨迹。

5. 常见问题排查:从解压到顺利运行的五个典型坑

这个章节是我处理实验包时被问得最多的部分。下面五条基本是血泪经验的浓缩版,每一条都按现象、原因、解决三个层次写,顺序也基本是操作前后关系,你可以按这个顺序排查。

5.1 解压后文件名乱码

现象:解压后所有目录和文件名变成“锟斤拷”或一排方框口口。原因:压缩包是在中文 Windows 下用 GBK 编码文件名打包的,而你在 Linux 或 macOS 上用默认 UTF-8 解压,两边编码对不上。解决:在 Linux 下解压时加-O GBK参数;macOS 下改用 7-Zip 或直接到 Windows 解压后拷贝过来。

5.2 编译报错:Sleep 未声明或 sleep 链接失败

现象:编译时报Sleep' undeclared,或者链接时报undefined reference to sleep。原因:Windows 下的延时函数是大写开头的Sleep,属于 Windows API;Linux 下是小写sleep,属于 POSIX 标准,两者声明头文件也不一样。解决:按第 3.3 节的方法用#ifdef _WIN32做条件编译,或者直接统一替换成空操作,因为教学实验里延时通常只是为了视觉效果,删掉不影响算法结果。

5.3 运行黑窗一闪而过

现象:在 Visual Studio 里按 F5 运行,窗口闪一下就关闭,什么都看不到。原因:控制台程序执行完 main 函数后立即退出,调试模式下窗口不会自动保持打开,代码里也没有等待按键的语句。解决:在 main 末尾加getchar();等待一个回车,或者直接到命令行里手动运行编译出来的 exe 文件。不要用system("pause")收尾,它不是标准库函数,换个环境又是个坑。

5.4 全角符号与源码编码导致的诡异编译错误

现象:编译报错位置在某个函数末尾,提示expected ';' before '}',但肉眼看着没问题,报错看起来像玄学。原因:源代码经历过 Windows 记事本编辑,文件编码是 GBK,且代码里混入了全角中文标点,比如全角分号、全角圆括号,编译器把它当成字符处理导致语法错乱。解决:把源码转成 UTF-8 无 BOM 格式;查找全角符号用编辑器替换功能,把全角分号、逗号、括号替换成半角。

5.5 pthread 相关实验链接失败

现象:编译线程同步实验时,报undefined reference to pthread_create。原因:pthread 库在 Linux 上不是默认链接的,gcc 需要显式指定。解决:编译命令最后加-pthread,注意不是-lpthread,-pthread会在编译和链接两个阶段都做正确设置。命令示例:gcc -Wall -pthread -o threads producer_consumer.c。这条命令同样适用于文件系统、磁盘调度等多文件实验的链接阶段。

6. 改造:把别人的实验包变成能答辩的自己的东西

6.1 换一组自己的数据,并保留输出截图

实验包自带的测试数据往往是最小规模的,只够展示“能跑”。答辩时老师最常问的一句就是:你换一组数据还能跑对吗?所以我拿到实验包后的第一个习惯,是构造一组比原始数据规模更大的输入。调度实验换到 8 个进程,银行家实验换到 6 个进程 4 类资源,页面置换换到 20 个页号序列。让程序用新人数据重新输出,把关键结果和预期值对照。这一步能暴露算法里写死规模的隐患,也能让你在演示时拿出一份“经过扩展验证”的输出结果。

6.2 加一个能讲清楚的小改动

完全照抄别人的代码,答辩时很难说出设计思路;但大改算法风险又高。折中的做法是保留原主体逻辑,只加一个能讲明白的小功能。比如调度实验里增加一个平均等待时间的统计输出,银行家实验里把安全性检查的每一步可用资源变化打印出来,页面置换实验里统计缺页次数并对比两种算法。我来演示一个最简单的改造:在进程调度输出结束后补一段统计。

printf("avg waiting time: %.2f\n", total_waiting_time / (double)process_count); printf("avg turnaround time: %.2f\n", total_turnaround_time / (double)process_count);

逻辑说明:这两个指标是书中评估调度算法的标准量。只要在每次进程完成时累加等待时间和周转时间,结束时除以进程数就能得到均值。改动量不到十行,但你已经把“会跑”升格成“会评估”,答辩时可以从两个算法的指标差异展开讲,而不是只描述代码在干什么。

6.3 先把要演示的样例手工算一遍

我最早拿到这类实验包时,第一件事是闷头把它跑通,结果答辩被老师一句“这个安全序列你自己算一遍”问住。从那以后我养成了一个习惯:任何要演示的输入样本,先拿纸笔手工过一遍流程,记录每一步的结果。调度样例就手写完成时刻和平均周转时间,银行家样例就手写安全序列和每次分配的 Available 变化。带着手算的结果去对比程序输出,既是验证代码,也是准备答辩腹稿。这样调试时的信心和演示时的底气都比直接贴代码要强得多。这是一个值得坚持的小习惯,希望帮到你。

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

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

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

立即咨询