简介面向操作系统课程学习者的杭电操作系统实验源代码包适合本科生对照课程要求完成实验、复习核心概念或准备课程设计。压缩包包含二十八份文件以C语言源文件、头文件、主程序入口和Makefile构建脚本为主并附有少量TXT说明、脚本与配置信息整体体积约五十六KB轻量紧凑便于快速阅读源码结构与编译运行。资源按实验一至实验五划分多个实验模块包含系统调用、进程同步、简单文件系统等经典主题其中simplefs相关代码展示了简易文件系统的实现思路多个带编号的版本目录可用于对照不同阶段的写法与排错。已有二百四十三人下载学习适合正在修读操作系统课程、需要实验参考或想借鉴完整代码结构的同学。代码量虽小但组织清晰配合构建脚本可直接编译调试便于二次修改和深入学习。1. 拿到 HDU 操作系统实验 zip 后先别急着解压这份压缩包到底能帮你什么收到一份 HDU 操作系统实验.zip多数人的第一反应是双击解压点开某个 .c 文件看一眼再合上。我的建议相反头二十分钟该花在确认包里有什么、文件完不完整、在哪种 Linux 环境下能跑而不是急着读代码。这类课程实验 zip 通常装着四类源码进程与 shell、线程同步、内存页面置换、文件系统模拟外加 Makefile 和报告模板。它真正能帮你的是把操作系统原理翻译成能编译、能调试、能改的工程骨架。适合正在跟课程设计较劲的本科生、批改实验的助教以及想拿旧题做教学案例的新老师。这篇笔记按解压到复现一条线走中间夹参数说明和踩坑记录。2. 解压 HDU 操作系统实验.zip 之前完整性与伪加密检查2.1 用 unzip/7-Zip 先看目录再算哈希从网盘、群文件或者课程平台下来的 zip第一件事不是双击而是先列目录。列目录不碰文件内容能看到每个实验的源码文件名、目录结构、总大小还能顺手判断打包的人用的是 Windows 还是 Linux如果大量出现.exe、.dll或者中文目录名后续解压就要额外处理编码和换行符。# 列出 zip 里的全部内容不真正解压 unzip -l HDU操作系统实验.zip | head -60 # 习惯 7-Zip 的话用这条输出更接近文件管理器 7z l HDU操作系统实验.zipunzip -l输出里每一行包含长度、日期和文件名。重点看两个地方一是有没有实验报告.pdf这类你真正需要的交付物二是有没有奇怪的隐藏目录比如__MACOSX/这种 macOS 打包残留后面解压出来全是垃圾文件。若看到__MACOSX解压后直接删除即可不影响实验源码。接下来算哈希。群文件传了好几手中间可能被重传、损坏甚至被改过。算一次 SHA-256和群公告或老师发的原始值比对能排除下载损坏的情况。sha256sum HDU操作系统实验.zip把输出的一长串十六进制存到文本文件里解压之前再算一次对比。这个习惯花十秒钟能省掉后面“代码明明没错但就是编译不过”的一整晚排查。2.2 解压命令参数编码、目标目录与中文目录名Windows 下打包的中文文件名用的是 GBK 编码Linux 的 unzip 默认按 UTF-8 解释直接解压会出现文件名乱码。常见做法是加-O指定编码mkdir -p ~/oslab unzip -O gbk HDU操作系统实验.zip -d ~/oslab如果你的 zip 是用 7-Zip 在 Windows 上打的、文件名本身就是 UTF-8就不要加-O gbk否则会把正常名字转成乱码。判断方法很简单先unzip -l看输出如果列表里中文正常直接解压如果列表里是乱码再用-O gbk试一次。解压后建议立刻把顶层目录整理成固定结构。常见的整理方式是按实验编号拆目录cd ~/oslab mkdir -p exp1_process exp2_thread exp3_memory exp4_filesystem reports find . -maxdepth 2 -type d | sort注意一个细节zip 格式对 Unix 可执行权限的保存本来就不完整。如果你后面发现解压出来的.sh脚本或编译好的二进制提示“Permission denied”用chmod x补上就行这不算文件损坏。2.3 zip 伪加密与忘记密码识别脚本和修复脚本实验资料在传播过程里经常被二次打包加密码最烦人的还不是真加密而是伪加密。伪加密的意思是文件数据本身没有加密只是 zip 目录项里的通用标志位General Purpose Bit第 0 位被置成 1解压器一看到这个位就弹出密码框。你输入什么密码都解不开但文件其实就在里面躺着。zip 里有两处标志位一个在本地文件头PK\x03\x04里另一个在中央目录头PK\x01\x02里。正常加密的 zip这两个位置的加密位都是 1伪加密往往是只改了其中一个造成解压器误判。下面这个脚本能逐个文件对比这两个标志位#!/usr/bin/env python3 # 检查 zip 伪加密对比本地文件头与中央目录头的通用标志位 import struct import sys def collect_flags(path): local_flags [] central_flags [] with open(path, rb) as f: data f.read() i 0 n len(data) while i n - 4: sig data[i:i 4] if sig bPK\x03\x04: # 本地文件头 flags struct.unpack(H, data[i 6:i 8])[0] local_flags.append(flags) name_len, extra_len struct.unpack(HH, data[i 26:i 30]) i 30 name_len extra_len # 跳到下一个头 elif sig bPK\x01\x02: # 中央目录头 flags struct.unpack(H, data[i 8:i 10])[0] central_flags.append(flags) i 4 else: i 1 return local_flags, central_flags local_flags, central_flags collect_flags(sys.argv[1]) for idx, (l, c) in enumerate(zip(local_flags, central_flags)): local_enc l 0x0001 central_enc c 0x0001 if local_enc and not central_enc: print(fentry {idx}: local0x{l:04x} central0x{c:04x} - 伪加密) else: print(fentry {idx}: local0x{l:04x} central0x{c:04x} - 正常/真加密)脚本逻辑不难本地文件头的通用标志位偏移是 6中央目录头的是 8各占 2 字节小端序。比较第 0 位是否一致即可。跑通这个脚本后如果是伪加密修复也很快。下面这段代码把两个头里的加密位都清掉输出一个新的 zip#!/usr/bin/env python3 # 修复伪加密清掉本地文件头与中央目录头的 bit0 import struct import sys def clear_bits(path, out): with open(path, rb) as f: data bytearray(f.read()) i 0 cnt 0 n len(data) while i n - 4: sig data[i:i 4] if sig in (bPK\x03\x04, bPK\x01\x02): offset i 6 if sig bPK\x03\x04 else i 8 flags struct.unpack(H, bytes(data[offset:offset 2]))[0] if flags 0x0001: data[offset] 0xFE # 清掉 bit0保持其他位不变 cnt 1 i 4 else: i 1 with open(out, wb) as f: f.write(data) print(f已清理 {cnt} 个标志位结果写入 {out}) clear_bits(sys.argv[1], sys.argv[2])修复后重新解压如果还要密码说明是真加密不是伪加密。真加密就老老实实找发放资料的人要密码或者套用学号、课程名这类规则去试。常见的实验包密码就几种课程名缩写、老师工号、学期年份。如果确实是自己的历史文件忘了密码可以用字典工具跑一遍常见组合但暴力破解不是第一选择先找分发者比什么都快。3. 实验源码长什么样进程、线程、内存、文件系统的四类骨架3.1 进程实验fork/wait 和 mini shell 的最小可跑骨架进程实验最常见的目标是写一个 mini shell循环打印提示符、读入命令、创建子进程执行、父进程等待回收。很多同学的代码一上来就卡在“子进程跑完但是提示符不回来”这是没搞清waitpid放在哪里。#include stdio.h #include stdlib.h #include string.h #include sys/wait.h #include unistd.h #define CMD_MAX 256 int main(void) { char line[CMD_MAX]; while (printf(mysh ), fgets(line, CMD_MAX, stdin) ! NULL) { line[strcspn(line, \n)] \0; if (strcmp(line, exit) 0) break; pid_t pid fork(); if (pid 0) { perror(fork); continue; } if (pid 0) { // 子进程执行命令后必须退出否则会继续循环 execl(/bin/sh, sh, -c, line, (char *)NULL); perror(execl); _exit(127); } int status; waitpid(pid, status, 0); printf(exit status: %d\n, WEXITSTATUS(status)); } return 0; }fork()返回值是最关键的分水岭小于 0 是创建失败等于 0 是子进程大于 0 是父进程拿到的子进程 PID。子进程里execl直接用sh -c执行整行命令省去手工切分参数但如果你想练字符串解析就得自己写空格和引号处理。_exit(127)里的 127 是 shell 惯例表示命令找不到WEXITSTATUS(status)取出子进程真正的退出码。如果父进程只fork不wait子进程会变成僵尸进程在/proc里状态栏能看到一个Z。3.2 线程实验pthread 条件变量实现生产者消费者线程同步实验十有八九是生产者消费者考察点是条件变量和互斥锁的配合。最容易翻车的写法是用if判断缓冲区满不满足但正确做法必须是while因为pthread_cond_wait返回后条件不一定成立。#include pthread.h #include stdio.h #define N 8 static int buf[N], head, tail, count; static pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t not_empty PTHREAD_COND_INITIALIZER; static pthread_cond_t not_full PTHREAD_COND_INITIALIZER; void *producer(void *arg) { for (int i 0; ; i) { pthread_mutex_lock(mtx); while (count N) // 不能写成 if (count N) pthread_cond_wait(not_full, mtx); buf[tail] i; tail (tail 1) % N; count; pthread_cond_signal(not_empty); pthread_mutex_unlock(mtx); } return NULL; } void *consumer(void *arg) { for (int i 0; ; i) { pthread_mutex_lock(mtx); while (count 0) pthread_cond_wait(not_empty, mtx); int val buf[head]; head (head 1) % N; count--; pthread_cond_signal(not_full); pthread_mutex_unlock(mtx); printf(consumed %d\n, val); } return NULL; }while (count N)之所以必须是循环是因为pthread_cond_wait的内部实现会临时释放互斥锁等被唤醒时重新拿锁这段时间里另一个线程可能已经把缓冲区再次填满。实验报告里如果能写出这个“虚假唤醒”场景分数基本稳了。编译时注意加-pthread否则链接阶段会报undefined reference to pthread_create后面第 5 章会专门说。3.3 内存实验页面置换算法模拟器该调哪几个参数内存管理部分一般是写页面置换算法模拟输入一串页面引用序列和物理页框数输出缺页次数。用 C 写链表比较繁琐很多实验包允许用任意语言我习惯先用 Python 把算法验证对再翻译回 C 提交。def lru(page_seq, frames): mem [] # 当前驻留页 last_use {} # 页号 - 最近访问时间 faults 0 for t, p in enumerate(page_seq): if p in mem: last_use[p] t continue if len(mem) frames: victim min(mem, keylambda x: last_use.get(x, -1)) mem.remove(victim) mem.append(p) last_use[p] t faults 1 return faults这个模拟器有三个参数要调page_seq长度、frames数量、引用串的生成规则。实验要求里通常会规定引用串范围是 0 到 9、长度 30 到 100自己测试时可以生成局部性明显的序列比如先密集访问一段连续页号再跳到另一段这样 LRU 和 FIFO 的差距才看得出来。要验证实现是否正确最简单的方法是拿 OPT 算法提前看整个引用串当基准LRU 的缺页数不可能比 OPT 少如果出现了说明实现有 bug。FIFO 还有个特征叫 Belady 异常增大frames时缺页数反而上升这可以作为排水实验的观察点写进报告。3.4 文件系统实验FAT 链和块分配模拟的结构文件系统模拟实验大多要求实现一个极简磁盘若干数据块一张 FAT 表记录链式分配关系。难点不在代码量而在于把“文件数据分布在哪些块上”这条链子想清楚。typedef struct { unsigned short fat[BLOCK_NUM]; // fat[i] 指向文件的下一个块0xFFFF 表示文件结束 unsigned char data[BLOCK_NUM][BLOCK_SIZE]; } SimDisk;写文件时从 FAT 表里找一个空闲块把 data 写进去并让前一个块的 fat 项指向它读文件时从起始块开始沿着 fat 链一路找到0xFFFF。常见翻车点是忘了初始化 fat 表为全空闲态fat[i] 0到底代表空闲还是使用中不同实验包定义不同。建议在头文件里用宏定义比如#define FAT_FREE 0x0000和#define FAT_EOF 0xFFFF避免魔法数字满天飞。文件系统实验最好加一个fsck函数遍历所有文件的 FAT 链检查有没有链断裂或循环这是验收时老师最爱问的问题之一。4. 把实验跑起来虚拟机选择、Linux 发行版与 Makefile 最小配置4.1 选虚拟机而不是双系统的三个理由操作系统实验的代码要跑在 Linux 上但这不等于你得把 Windows 卸了装双系统。虚拟机的好处有三点快照可以当后悔药编译内核模块挂了也不影响宿主机环境想换发行版随时重开一台。常见选择是 VMware Workstation Player 或 VirtualBox两者跑 Ubuntu、麒麟、统信 UOS 这类 Debian 系系统都很顺。只有当你需要真实硬件性能比如做时钟中断实验、测磁盘 IO时才值得换物理机。装虚拟机有个前置条件宿主机 BIOS 里必须开启 CPU 虚拟化。检查命令# 有 vmx 或 svm 输出就说明虚拟化已开启 grep -E vmx|svm /proc/cpuinfo如果一条输出都没有进 BIOS 找 Intel VT-x 或 AMD-V 的开关打开。这个步骤不做后面虚拟机一启动就报错最常见的提示是“客户机操作系统已禁用 CPU. 请关闭或重置虚拟机”第 5 章避坑手册里会专门说。4.2 Ubuntu 22.04 下装好工具链的实验环境Ubuntu 22.04 是当前比较稳的选择软件源里的 gcc 是 11 版对 C11 和 POSIX 接口支持都很好。装完系统第一件事是更新软件源并安装编译工具链sudo apt update sudo apt install -y build-essential gdb make file # 如果实验包里的旧代码是 32 位还要装 multilib sudo apt install -y gcc-multilib libc6-dev-i386build-essential包含 gcc、g、make 和基础库头文件gdb用来调试断点和查看内存file用来判断二进制文件是 32 位还是 64 位。实验目录建议放在用户主目录下不要放/opt或者/root否则后面普通用户编译会遇到权限问题。mkdir -p ~/oslab/exp1_process ~/oslab/exp4_filesystem国产环境下如果你想在麒麟或者统信 UOS 上复现命令基本一样因为它们都基于 Debian 系软件包管理器同样是 apt。唯一要注意的是软件源镜像地址可能需要改成对应厂商的源。4.3 Makefile 模板把四个实验一次编译到位实验包自带 Makefile 的概率不高更多时候是一堆.c文件让你自己编译。与其每次敲gcc -o lab1 lab1.c -pthread不如写一个统一 MakefileCC gcc CFLAGS -Wall -Wextra -g -pthread TARGETS lab1 lab2 lab3 lab4 all: $(TARGETS) lab1: lab1.c $(CC) $(CFLAGS) -o $ $^ lab2: lab2.c $(CC) $(CFLAGS) -o $ $^ lab3: lab3.c $(CC) $(CFLAGS) -o $ $^ lab4: lab4.c $(CC) $(CFLAGS) -o $ $^ clean: rm -f $(TARGETS) *.o .PHONY: all clean-Wall -Wextra打开所有常见警告编译器提示的未使用变量、类型不匹配都是潜在的扣分点。-g保留调试信息没有它 gdb 里看不到源码行号。-pthread同时处理预处理宏和链接线程库比老写法-lpthread更稳它放在编译和链接两个阶段都不会丢。.PHONY: all clean防止目录里出现名为 clean 的文件时 make 行为异常。4.4 内核模块类实验headers 版本必须与内核一致部分学校的操作系统实验会涉及内核模块比如写一个简单的字符设备驱动或者 hook 系统调用。这类实验的编译不是用普通 gcc而是要用当前内核的 build 目录# 先确认当前内核版本 uname -r # 安装匹配的内核头文件 sudo apt install -y linux-headers-$(uname -r) # 在模块源码目录里编译 make -C /lib/modules/$(uname -r)/build M$PWD modulesuname -r和内核头文件必须严格一致否则insmod加载时会报invalid module format原因是模块里的 vermagic 字符串和正在运行的内核不匹配。如果你更新过内核没重启uname -r显示的是新内核但系统实际跑的还是旧内核这时候先重启再编译别硬扛。5. 操作系统实验避坑手册5 个让我浪费过整晚的翻车点5.1 编译通过运行却报“No such file or directory”现象./lab1执行时bash 提示No such file or directory但ls -l lab1明明显示文件存在。原因最常见两种情况。第一lab1是 32 位可执行文件你的系统是纯 64 位环境缺 32 位动态链接器/lib/ld-linux.so.2第二脚本文件第一行#!/bin/bash带了 CRLF内核把整行当成解释器路径找不到就报同样提示。解决先file lab1看输出。如果是ELF 32-bit装gcc-multilib后用-m32重新编译如果是脚本用第 5.5 条的dos2unix转一下行尾。别在这上面熬夜先file永远是对的。5.2 链接时报 pthread_create 未定义-pthread 放错位置现象编译报undefined reference to pthread_create但pthread.h头文件明明已经 include 了。原因pthread 不是 libc 的一部分在较新的 glibc 里它并到 libc 了但编译多线程程序仍然需要-pthread这个选项来定义正确的宏并链接。很多人只在gcc -c编译阶段加了链接阶段忘了加。解决统一在 CFLAGS 里带上-pthread或者链接命令行直接写gcc -o lab2 lab2.o -pthread。注意 Makefile 里如果分开写了编译规则和链接规则-pthread两处都要有。5.3 insmod 报 invalid module formatheaders 与内核版本错位现象编译内核模块没报错但insmod mymod.ko失败dmesg尾部显示invalid module format或version magic mismatch。原因模块编译时的内核头文件和当前运行的内核不是同一版本。apt install linux-headers-$(uname -r)看起来装对了但如果系统里有多个内核/lib/modules/$(uname -r)/build软链接可能指向了旧文件。解决先uname -r记下版本再ls -l /lib/modules/$(uname -r)/build确认软链接指向的目录存在、且里面有Makefile。每次升级内核后都重新编译模块这是内核模块开发的日常不算坑。5.4 虚拟机一启动就报“客户机操作系统已禁用 CPU. 请关闭或重置虚拟机”现象VMware 或 VirtualBox 里启动 Ubuntu、麒麟、统信这类 Linux 虚拟机弹窗提示“客户机操作系统已禁用 CPU. 请关闭或重置虚拟机”然后虚拟机卡死。原因三个方向排查。最常见是宿主机 BIOS 没开 VT-x/AMD-V其次是在虚拟机里套虚拟机嵌套虚拟化时没有把硬件虚拟化透传给客户机还有可能是新建虚拟机时选了 32 位系统类型但镜像其实是 64 位CPU 模式不匹配。解决先在宿主机执行grep -E vmx|svm /proc/cpuinfo没输出就去 BIOS 开启。VMware 里在“处理器设置”勾选“虚拟化 Intel VT-x/EPT/AMD-V/RVI”。如果是在虚拟机里再开虚拟机只有 VMware 支持嵌套虚拟化VirtualBox 对嵌套支持较弱建议直接换物理机跑第二层。镜像位数和虚拟机类型不一致时删掉虚拟机重选 Linux 64 位不要手动硬改配置文件。5.5 解压出来的源码带 CRLFgcc 报 stray ‘\r’ 与 make 报 missing separator现象解压一个来自 Windows 的实验包make直接报missing separator或者 gcc 疯狂提示stray \r in program。原因zip 里的.c和Makefile是 Windows 下保存的行尾是 CRLF。make 对行尾极其敏感\r会让 tab 开头的规则行失效gcc 则会把这字符当成非法字符。解决整棵树做一次换行符转换sudo apt install -y dos2unix find ~/oslab -name *.c -o -name *.h -o -name Makefile | xargs dos2unixdos2unix会把 CRLF 转成 LF保留文件内容不变。如果是.sh脚本带 CRLF执行时往往报bad interpreter同样处理。这个坑碰到一次就长记性Linux 下收到 Windows 打包的 zip先转行尾再编译。6. 验证实验做没做对页面置换对比脚本和 strace 跟踪技巧6.1 用同一组引用串对比三种算法锁死正确性页面置换实验交上去之前我一般会跑一个自动化对比用同一份引用串和页框数分别执行 FIFO、LRU、OPT输出各自的缺页数。LRU 的缺页数必须小于等于 FIFO且大于等于 OPT这是硬约束不满足就是实现有 bug。# 假设 lab3 接受三个参数算法名、引用串文件、页框数 ./lab3 lru ref.txt 4 ./lab3 fifo ref.txt 4 ./lab3 opt ref.txt 4如果实验包只给了单一算法你可以自己写个几十行的 OPT 脚本当基准。把引用串固定在 100 位、页框数从 2 调到 8观察缺页数的变化曲线。这组数据一边能验证代码一边能当实验报告的图表素材一举两得。6.2 用 strace 观察系统调用序列确认进程和线程行为进程实验最怕的是代码逻辑上看着对但子进程根本没有按预期创建。这时候用 strace 直接抓系统调用比 printf 插桩高效得多sudo apt install -y strace strace -f -e traceclone,fork,execve,wait4 ./lab1-f跟随子进程-e trace只显示你关心的系统调用。看到clone或fork出现两次、execve执行了/bin/sh说明进程创建和命令执行都走通了。如果只有fork没有execve说明子进程根本没执行命令就退出了回去检查execl的参数。线程实验也能用这个思路把clone看成线程创建来排查。6.3 一点个人习惯我自己的习惯是每次改代码前git init一下哪怕只是本地仓库不往远端推。实验做到半夜改崩了还能回退到上一版这比任何检查都管用。编译时开-Wall -Wextra并把警告当错误改积累下来你对内存越界和类型转换的敏感度会明显提升。这套流程从解压、校验、编译到验证走下来操作系统实验基本不会再有玄学问题。希望帮到你。本文还有配套的精品资源点击获取