
简介杭电操作系统实验工程包是一份覆盖操作系统核心实验的完整代码与构建资源面向计算机专业本科生、考研复试准备者以及自学操作系统的开发者。资源按实验2进程调度、实验3同步互斥、实验5简单文件系统等模块划分从系统调用到文件存储结构逐层递进便于对照课程进度边读代码边验证。压缩包共28个文件以12个C源码和4个头文件为主体配合makefile构建脚本、main入口程序及txt说明文档整体仅56KB结构紧凑、定位明确。内容包含可编译运行的C语言示例、进程管理/同步互斥的参考实现以及简单文件系统simplefs的磁盘布局与文件操作源码并附有CMake工程配置和Linux下的辅助脚本方便直接编译运行或二次修改。目前已有241人学习/下载资源虽小巧但实验要点齐全适合备考或正在完成操作系统实验的同学参考对照、查漏补缺。 大二下学期那门操作系统课说实话我一开始以为就是背背进程、死锁、页面置换的概念期末能混过去就行。结果实验课第一个星期拿到实验指导书看到要求用C语言在Linux下写一个模拟文件系统整个人直接懵了。后来那门课我前后写了几个通宵踩了无数坑也收获特别多。这份“HDU操作系统实验”整理了我大学期间实验课的全部代码、报告和参考资料覆盖进程管理、同步互斥、内存管理、文件系统等经典模块。如果你是正在修这门课、不知道怎么动手的同学或者考研复试想拿个项目出来讲清楚的准研究生这份内容应该能帮你少走很多弯路。1. 实验课程的整体脉络与设计思路1.1 一套操作系统实验到底在考察什么很多同学拿到实验题就想着查代码、抄学长、改改名字交上去。我当年也有这种想法但后来发现如果不懂实验背后的设计逻辑验收时老师随便问一个问题就露馅了。操作系统实验的核心不是让你把某个算法写出来而是让你理解操作系统在真实环境下是怎么工作的。所以HDU的实验安排基本遵循“从用户态到内核态、从单一功能到综合设计”的思路。比如第一次实验往往是进程创建与控制让你用fork、exec这一层系统调用感受进程是怎么产生的第二次可能就是生产者消费者让你理解同步互斥机制到底解决什么问题往后才是页面置换算法、磁盘调度、文件系统这类偏模拟和设计的实验。从验收标准来看老师更看重三件事第一程序能不能稳定跑通不能只是“大部分情况正常”第二你用的数据结构、核心流程讲不讲得清楚第三面对随机输入或边界输入时程序会不会崩溃。这三个标准背后对应的其实是对操作系统的核心数据结构进程控制块、信号量、页表、索引节点等有没有真的理解而不只是会调API。1.2 为什么教材用经典理论实验却非要用C《计算机操作系统》这类教材我们当时用的是汤小丹老师的版本讲的都是抽象的算法和原理比如P操作、V操作、页表映射、电梯调度。看着好像很好懂但你真要实现一遍才会发现大量“教材不会告诉你”的细节。实验要求用C语言在Linux环境下完成原因也很直接C是贴近系统接口的语言Linux内核本身也是C写的系统调用层直接用C就可以调。fork、wait、pthread_create、open、read、mmap这些接口全是C函数。相比之下Java、Python虽然也能模拟但它们在语言层面帮你屏蔽了底层细节你就很难体会到“进程就是一个task_struct结构体”“文件描述符就是一个整数索引”这种本质。所以当时我们实验的要求都很硬核不限制你用哪个编译器但程序必须能在Linux命令行下编译运行文件系统实验不允许用现成的数据库或专门的文件系统库必须自己设计磁盘块和索引结构。这样搞完一轮实验你对“内存”“进程”“文件”这些词的理解会和只背概念时完全不一样。2. 实验环境准备别在第一周就劝退2.1 虚拟机、Linux发行版和基本工具链如果到现在你还在Windows上用Dev-C写操作系统实验我建议你立刻停下来。不是说不可以而是很多实验涉及进程隔离、共享内存、信号量这些机制Windows的API和Linux差别很大课程验收标准一般也是按Linux来的。最省事的方案是装一个虚拟机VirtualBox或者VMware都行跑一个Ubuntu 20.04或22.04 LTS。为什么不用最新版本因为LTS版本稳定软件源里该有的工具基本都有很多教程也是基于这个版本对新手最友好。这里提醒一下如果你在VMware里启动Ubuntu时遇到“客户机操作系统已禁用 CPU请关闭或重置虚拟机”这类报错往往不是系统坏了而是虚拟机的CPU虚拟化设置或硬件兼容版本不对。进虚拟机设置里检查“虚拟化引擎”相关的选项或者把虚拟机硬件兼容版本调低一点通常就能解决。这种问题看起来吓人其实只是环境的锅不是你的代码有问题别被它劝退。安装完系统后在终端里执行下面这一行把基础工具装齐sudo apt update sudo apt install -y build-essential vim gdb strace make manpages-devbuild-essential里包含了gcc、g、make等必要的编译工具。gdb是调试器后面排查段错误、死锁全靠它。strace可以跟踪程序发起的系统调用用来理解自己的程序到底调用了哪些内核接口非常直观。还有一个容易被忽略的manpages-dev装上之后你就能随时随地查看系统调用和C库函数的帮助文档了。2.2 让procfs和gdb成为你的眼睛真正开始写实验代码后你会发现一个问题程序能不能跑是一回事跑起来之后系统里发生了什么是另一回事。这时候除了在代码里加printf更专业的做法是看/proc目录。Linux把正在运行的进程信息都暴露在/proc下面。你启动一个程序后可以到/proc/[pid]/status里面看到进程的状态、内存占用、父进程PID等信息。如果担心进程变成僵尸直接ps -ef或者top里看Z状态就行。实验里要求“验证子进程确实存在过”与其自己在代码里脑补不如让程序睡眠时去/proc目录看一圈效果超级直观。gdb的调试核心命令其实不多我用的最频繁的就是下面这几个gdb ./a.out break main # 在main入口打断点 run # 启动程序 next # 单步跳过 step # 单步进入 print 变量名 # 查看变量 bt # 查看调用栈 info threads # 查看线程信息很多同学调试段错误一上来就盯着代码看半天其实用gdb运行一下崩了之后进入(gdb)提示符敲一个bt看到调用栈问题基本就定位了。这个习惯能帮你省掉大量无效的盯代码时间。3. 核心实验的实操拆解与代码要点3.1 进程控制实验fork、exec和wait的迷宫第一个正式实验通常是进程控制。这个实验强推先画图再写代码。因为fork一次调用返回两次的逻辑太反直觉了很多新手在代码里画了四五层fork之后连自己有几个子进程都数不清。理解fork核心其实就一句话调用后当前进程复制一份副本父进程得到子进程的PID子进程返回0出错返回-1。我自己的习惯是在fork之后马上判断返回值并且两边分支写清楚形成一个类似下面的标准骨架#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main(void) { pid_t pid fork(); if (pid 0) { perror(fork error); exit(1); } if (pid 0) { // 子进程分支 printf(子进程运行, PID%d, 父进程PID%d\n, getpid(), getppid()); execlp(/bin/echo, echo, hello from exec, (char *)NULL); perror(exec failed); exit(1); } else { // 父进程分支 int status; wait(status); // 等待子进程结束 if (WIFEXITED(status)) { printf(子进程退出码: %d\n, WEXITSTATUS(status)); } printf(父进程继续运行, PID%d\n, getpid()); } return 0; }这个实验里我踩过最大的坑就是忘了调用wait。不调用wait的话子进程结束之后会变成一个僵尸进程系统的进程表中还留着它的记录。虽然实验时表面上不影响运行但如果你多开几个子进程不回收很快能看到Z状态进程堆成一片严重的会让系统资源被白白占着。另一个值得自己动手去验证的点是exec前后进程的变化。a.out调exec之后用户态代码被替换成新程序但进程PID不变。这个就是教材里说的“exec不创建新进程只是改变进程映像”。课堂上听一百遍不如自己跑一遍体会深刻。3.2 同步互斥实验生产者消费者不是背模板进程同步第一次上机的时候我在终端里写过最经典的生产者消费者模型。那个实验的验收要求是代码有随机延迟、缓冲区大小可配置、能连续跑很久不死锁。看着简单实操起来全是细节。核心思想是用三个信号量配合empty信号量记录缓冲区空位full信号量记录已使用的位置mutex保护临界区中的buffer操作。标准的加减法如下生产者侧sem_wait(empty); // 申请一个空位 sem_wait(mutex); // 进入临界区 buffer[in] item; // 放入缓冲区 in (in 1) % BUFFER_SIZE; sem_post(mutex); // 离开临界区 sem_post(full); // 已满位置加1消费者侧是对称的不过要把empty和full反过来。这个实验最大的难点不是信号量的使用而是搞清楚“多个锁顺序不一致会导致死锁”。如果生产者在持有mutex的情况下再去等待empty而消费者正好持有空闲的缓冲槽在等mutex两边就互相卡住了。教材里会说这是“破坏了互斥等待条件”实验里你会真实地看到程序卡死不动的效果。我当时在pthread版本里还遇到过另一个问题编译的时候忘了加-lpthread导致一堆“undefined reference to pthread_create”的报错。另外注意信号量相关函数实际使用时可能需要加上-pthread编译选项同时sem_init等函数的头文件要包含semaphore.h。这道题如果让我重新做一遍我一定会先画一张带状态箭头的手绘图把互斥和同步分开标清楚写代码的时间能少一半。3.3 内存管理和虚拟内存页面置换算法从拍脑袋到严谨对比内存管理实验最常出现的是页面置换算法模拟。经典的三个算法是FIFO、LRU、OPT。这个模拟实验的实现思路其实不算难生成一个长度随机的页面访问序列按顺序模拟页框装入记录每次缺页中断。三种算法优劣可以用一个表格直观对比算法实现难度缺页率水平适用场景典型坑点FIFO最简单通常偏高教学演示、简单系统存在Belady异常LRU中等较低通用操作系统近似使用需要记录访问时间或维护链表OPT简单但依赖未来理论最低评估基准需要知道整个访问序列第一次写这个实验的时候我直接把FIFO、LRU、OPT三个算法的代码各写了一版。做到后面我发现很多同学把LRU用“访问次数最多/最少”来实现这是错误的。LRU关注的是“多久没被访问”不是“被访问了几次”。这和读书是一个道理你最近读的书你记得牢但不代表你读得最多的那本书就一定是最新的。实际实现LRU可以维护一个计数器或双向链表每次访问就更新最近访问顺序。为了验证算法正确性我建议大家把每次缺页的页面编号、页框内容都打印出来然后用小规模的访问序列手动演算一遍。这个习惯帮我在验收时发现问题比如某个页明明还在内存中却被认为缺页归根结底是因为每次地址转换时没有更新页表项而不是置换策略本身错了。打印每一步的状态转换是这种模拟类实验最可靠的调试手段。3.4 文件系统实验在数组上建一个“能用的磁盘”文件系统实验如果要求自己设计一个迷你文件系统是一个真正能拉开差距的题目。思路核心其实不复杂用一个普通文件或者一块大数组模拟整块磁盘划分成若干个磁盘块再用一个位图记录哪些块占用、哪些块空闲。接着实现创建文件、删除文件、读取文件、写入文件这几个基本操作。我当时用一个大数组模拟磁盘结构类似这样#define BLOCK_SIZE 512 #define MAX_BLOCKS 1024 unsigned char disk[MAX_BLOCKS][BLOCK_SIZE]; // 模拟磁盘 unsigned char block_bitmap[MAX_BLOCKS]; // 空闲块位图然后在磁盘开头分配一个“超级块”记录文件数和位图位置每个文件用一个inode结构保存起始块号、文件大小等信息。真正做起来你会发现这和真实文件系统相比只是简化版但读写过程中涉及的核心机制——分配块、回收块、记录文件元信息——全都体会到了。我当时卡了很长时间的坑是磁盘块的碎块问题。删除文件后位图对应的块被释放但文件分配表里还有旧的映射关系如果不做整理下次新建文件时可能复用到了还没清干净的块。说明白点就是释放这一环节比分配更重要这其实反映了文件系统设计里“回收”要处理各种边界情况的复杂性。写完这个实验再看课本里讲的“索引节点”“目录项”你会觉得那真的不是一个抽象概念而是你亲手设计过的数据结构。4. 常见问题与排查技巧实录4.1 编译、运行阶段的高频坑速查表很多问题不是你的逻辑错了而是环境、配置或者小细节上的疏忽。我把实验期间遇到最多的几类问题整理成一个表方便你排查时对照现象常见原因解决办法printf内容重复打印两次fork复制了进程的用户空间缓冲区输出加\n或使用fflush(stdout)编译时报pthread_create未定义忘记链接线程库gcc编译时加-pthread子进程结束后出现僵尸进程父进程没调用wait父进程调用wait/waitpid程序运行一段时间后卡死信号量或互斥锁等待顺序导致死锁gdb attach查看线程栈段错误但代码检查不出问题指针越界或空指针用gdb运行bt查看调用栈文件系统模拟时新文件内容异常释放块时位图没清零或重复分配检查位图更新逻辑其中printf重复打印这个问题我印象特别深。因为fork会完整复制进程的地址空间包括标准I/O缓冲区。如果你在fork之前用printf输出了内容但没有换行也就是没有触发冲洗那么这个缓冲区会被复制到子进程里去导致子进程退出时又把这些内容输出了一遍。解决办法是输出字符串时习惯性地带上\n或者主动fflush。这也是我在实验报告中专门写过的细节之一。4.2 我自己怎么排查“程序卡死”的有一次在做生产者消费者实验时程序跑着跑着就没有任何输出了。当时我第一反应是循环条件写错了看了半天代码也没发现问题。后来用两个工具组合排查才找到了真正的根因。先用了strace跟踪系统调用strace -f -o trace.log ./producer_consumer打开trace.log之后我看到线程调用futex相关系统调用后卡住陷入等待状态。之后我用gdb定位gdb -p 进程号进入gdb后执行info threads看到两个线程同步阻塞在pthread_mutex_lock上。这说明根本不是死循环而是锁互相等待。再进一步打印线程栈发现生产者和消费者在申请不同的锁时顺序相反。问题出在我写代码时把“获取互斥锁”和“等待缓冲区信号量”的顺序搞反了。排查这个问题的全过程大概花了半小时但收获很大。现在对于“卡住”类的问题我已经养成了“先看系统调用再看线程栈”的习惯而不是上来就无脑加printf。你把排查过程写进实验报告里老师其实非常认可因为这正好说明你对系统机制的理解是到位的。5. 让实验变成简历上能讲的亮点5.1 在“能跑”之后继续加东西很多同学实验做完就扔了这挺可惜的。操作系统实验是少数几个能让你直接接触系统底层的机会稍微花点心思扩展一下就能变成简历上一个很有区分度的项目。比如页面置换实验完全可以不只是跑三个算法你可以把缺页率随页框数量变化的曲线用gnuplot画出来直观展示Belady异常文件系统实验可以在基本的创建/删除文件之外增加格式化、碎片整理甚至简单权限控制生产者消费者可以改成多生产者多消费者并统计不同线程数下的吞吐量。这些扩展点都很小但写进项目描述里比单纯写“实现了生产者消费者算法”要具体得多面试时也有真实数据支撑。如果你打算把实验代码作为项目展示强烈建议用Git管理从第一次实验就提交提交信息写得清楚一点比如“实现LRU页面置换算法”“修复释放磁盘块时位图更新错误”。这本身也是一个很好的工程习惯。5.2 学不动的时候教材、手册和公开课怎么搭配这门课学起来硬核但把它拆开看就还好。我的经验是以学校教材为主线以Linux man手册为工具书以公开课为补充。教材当时用的是汤小丹老师的《计算机操作系统》里面的重点章节进程描述与控制、调度算法、同步互斥、内存管理、文件系统尽量做到每一个小节都能用自己的话讲清楚。遇到库函数或系统调用看不懂时直接查man手册比上网找零散博客靠谱得多。公开课可以看一些经典的操作系统课程视频比如“30天自制操作系统”这本书的实战性很强适合想动手做一个小操作系统的人再看一些公开的OS网课配合着课本理解调度、中断、内存管理这些概念会有很多“原来如此”的时刻。但注意看视频不是主要任务动手写实验才是主菜。6. 最后再分享一点我的个人体会操作系统实验是我大学四年里少有的“刚开始想混过去后来越做越上头”的课。如果你现在正在被fork、死锁、页表折腾别慌每个人都是这么过来的。实验代码不要求一版完美先写一个能跑的版本再一步步优化这个过程本身就是最好的学习。做完之后把代码和报告都存好以后复试、面试拿出来讲真的会比临时准备一个项目强太多。本文还有配套的精品资源点击获取