
简介一套面向计算机专业学生的华中科技大学操作系统实验与课设资料包覆盖进程管理、内存管理、文件系统、设备管理、死锁预防、线程管理、调度策略、系统安全与网络编程等核心知识适合需要动手编写调度器、文件系统或设备驱动以深入理解系统原理的学习者。包内共249个文件压缩后约41.66MB以C/C与Java源码、class字节码、jar依赖库、makefile构建脚本、XML/JSON配置、内核模块文件及调试输出为主可支撑从内核模块编译到Java并发服务的多种实验场景。资料已有166人学习/下载。借助其中的示例程序、实验代码、工程配置与文档读者可系统梳理系统调用、进程同步与调度算法等关键机制并参考完整项目结构搭建自己的实验环境提升系统编程与问题排查能力。 每年到了操作系统实验课的中期课程群里总会出现同一类哀嚎这个进程调度为什么跑着跑着就卡死了内核编译到第6步又报错了有没有人能来看一眼说实在的这套围绕华中科技大学操作系统课程展开的实验与课设代码磨人程度远超大部分人的预期。作为完整跑通整套实验和课程设计的过来人我想把隐藏在HUST Operating System Codes背后的实战经验拆开讲讲实验地图怎么读、环境怎么搭、并发代码怎么调、课设怎么串以及那些文档里根本不会写的坑。无论你是正在为这门课头秃的本科生还是想靠实战理解操作系统底层原理的自学者这篇文章都能帮你把时间花在刀刃上。1. 先看懂实验地图再动手这套实验到底在考什么拿到压缩包就直奔代码是这堂课最大的策略失误。我先说结论操作系统实验和普通编程作业有本质区别它的每个实验都不是孤立的小任务而是在复现教材里某个核心机制。以HUST Operating System Codes为例压缩包里的内容通常按知识模块组织每个模块下既有实验代码也有对应的实验指导文档。很多同学跳进代码里一头雾水就是因为没有先把这份实验地图摊开来看。1.1 压缩包里有什么实验模块与课程知识点的对应关系我建议你拿到压缩包后做的第一件事不是打开任何源文件而是把目录结构完整列出来然后逐一对照课程教材的章节。操作系统理论课的经典教材很多学校用的是汤小丹那本《计算机操作系统》华科这套实验的模块划分基本和这本书的知识体系是对应的。我整理了一张对照表可以参考实验模块教材章节大致对应核心知识点进程管理进程的描述与控制PCB、fork/exec、进程状态转换同步与互斥进程同步信号量、条件变量、生产者消费者、死锁处理机调度处理机调度时间片轮转、优先级调度、多级队列内存管理存储器管理分页、页面置换算法、地址转换文件系统文件管理inode、目录项、磁盘布局、位图课程设计全部章节综合运用独立实现一个可运行系统这张表的意义在于它让实验和考试连在一起了。操作系统期末复习最痛苦的地方是概念太多、太抽象但如果你把每个实验代码当作概念的实物投影很多抽象问题一下子就具体了。比如PCB到底是什么你写了调度实验就明白它就是一个包含状态、优先级、上下文字段的结构体被挂在就绪队列里。期末复习时对着实验代码回忆概念效率比死记硬背高得多。1.2 先排序再动手实验之间的依赖逻辑第二个容易被忽略的点是实验之间的先后顺序。很多实验是有依赖关系的不是随便挑着做。我给你理一条典型的依赖链环境搭建和内核编译是所有实验的地基这块起不来后面什么都跑不了。接着是进程管理实验因为它要依赖你对内核进程模型的修改。再往后才是同步互斥实验和调度实验它们需要跑在进程能正常创建和切换的基础上。内存管理实验可以稍微独立一些但很多综合性题目会涉及进程地址空间所以也建议排在进程实验之后。文件系统实验和其他模块耦合度相对低可以放到最后突击。最后的课程设计则是把所有模块串起来的大汇总。这条链路的核心逻辑是操作系统的知识点是从进程如何产生到进程如何并发再到进程如何被调度、如何分配内存、如何读写文件层层递进的。跳着做实验你会在某个瞬间发现代码看不懂——不是因为你笨而是因为你跳过了前置实验里埋下的关键数据结构。2. 编译内核与添加系统调用环境准备本身就是第一道大作业如果你问十个做过这套实验的人最痛苦的是什么至少六七个会回答编译内核。这个环节不产生任何可见的算法成果但它卡住了无数人。原因很简单操作系统实验和其他课设不一样它不是在应用程序的维度写业务逻辑而是要在内核的维度修改行为。比如系统调用实验你需要在自己编译的内核里添加一个新的系统调用然后写一个用户态程序去调用它。如果用发行版自带的内核这种操作要么做不了要么危险系数极高。2.1 为什么操作系统实验必须碰内核有人会问原理课讲了那么多我就不能写个用户态程序模拟一下就行了吗答案是基本不行。因为操作系统最核心的行为——进程调度、内存分配、中断处理——都发生内核态。你在用户态能观察到的只有系统调用返回之后的结果。想要真正理解一次系统调用从用户态陷入内核态经历了什么你必须亲手修改内核、编译内核、再运行自己修改过的内核。华科这套实验里通常有一项是添加或修改系统调用。这一步的意义不只是会改代码而是让你把编译、链接、内核镜像生成、启动引导这整条链路走通。走通之后你再去看《计算机操作系统》里处理机的两级状态内核态与用户态这类章节理解会深一个层次。2.2 环境选型与编译参数少走两个月弯路的细节环境选型上我强烈建议用虚拟机而不是实体机。VMware或VirtualBox都行但有两个硬指标一定要满足硬盘空间至少给40GB内存至少给4GB。内核编译的过程本质上是一场资源消耗战源码解压、编译产生的中间文件、模块安装每一项都在吃硬盘和内存。我见过有同学给了20GB磁盘编译到一半直接报No space left on device然后心态爆炸。编译内核的步骤看起来也就是下载源码、make menuconfig、make、make modules_install、make install这几步但每一步都有细节。以Ubuntu环境为例我习惯的执行顺序是# 下载内核源码后进入源码根目录 make menuconfig make -j4 make modules sudo make modules_install sudo make install sudo update-initramfs -u这里有几个关键细节全是实测换来的经验第一make menuconfig这一步关键不是你配了什么选项而是你知不知道哪些是必须的。很多同学第一反应是全选图省事结果编译了四五个小时最后还是起不来系统。正确的做法是在默认配置基础上确认虚拟化驱动、内核模块支持、procfs、sysfs这些基础选项已经打开其他选项能不动就不动。第二编译并行度不要拉满。make -j$(nproc)看起来美好但在虚拟机里并行编译任务数超过实际分配的物理核数时编译效率反而会下降还容易触发gcc崩溃。我实测下来虚拟机里用make -j4比用make -j8稳定得多。第三编译完成之后先别急着重启检查三样东西是否齐全/boot下的vmlinuz、initrd.img、System.map以及/lib/modules/下是否有对应内核版本的模块目录。这三个文件加一个模块目录缺一个重启基本都是起不来的。2.3 编译过程中的常见报错与应对我这里列几个最常见的错误以及解决方向全是教材里不会写的内容错误现象常见原因解决思路undefined reference toxxx内核配置中某个选项未开启导致对应符号没编进去回到make menuconfig搜索相关选项并打开编译到一半OOM killed虚拟机内存不足关闭图形界面、增加内存、降低-j并行度重启后Kernel panic - not syncing: VFS忘了编译模块或initramfs未更新进入恢复模式补做make modules make modules_install update-initramfs -umake menuconfig报错缺ncurses缺少终端图形库依赖安装libncurses-dev后再执行这个环节的定位你自己要拎清楚它不是实验的前置工作而是实验的一部分。很多老师期末成绩里明确包含环境搭建的分数你哪怕算法写得再好环境一塌糊涂照样扣分。所以我建议环境搭建阶段不要赶进度每一步都弄清楚这个命令到底做了什么后面会省下无数返工时间。3. 进程与线程实验并发代码里那些看起来正常的bug环境跑通之后真正的算法实验就开始了。进程管理相关的实验通常要求你实现或修改一个简单的进程控制块管理、实现基本的调度逻辑。这里最核心的概念就是PCB它相当于进程的身份证记录着进程状态、程序计数器、寄存器上下文、调度信息、内存限制等。实现调度器时所有操作都是围绕就绪队列里的PCB展开的。3.1 PCB与进程调度最容易被结构体指针坑到的地方这里很容易出现的第一个问题就是把PCB当普通结构体随便加字段、随便改逻辑结果导致系统里多个进程的状态互相污染。比如你把一个进程从就绪队列里摘下来却忘了清空它的队列指针等下一次调度器遍历就绪队列时拿着一个指向已释放内存的野指针去遍历不崩才怪。这类bug最诡异的地方在于它在你的机器上可能跑一天都不崩在老师的测试环境里第一次运行就挂。我踩过的一个教训是进程状态切换时一定要先修改PCB中的状态字段再执行队列操作。顺序反了可能出现进程已经在运行但就绪队列里还残留着它的节点这种逻辑矛盾。这个问题用printf打点很难复现但用gdb在状态切换的代码处设置断点观察PCB字段和队列节点的一致性基本很快能定位。第二个常见坑是fork实验。很多同学误以为fork是把当前进程的内存复制一份于是在实现时直接memcpy整个地址空间。实际上教学内核里往往用了写时复制COW的简化版本。如果你的实验要求实现COW最核心的点在于页表项的只读标志和缺页异常处理要配合好。这里我建议多花时间理解页表项权限位和缺页异常这两个机制因为它们不仅是fork实验的根基也是后面内存管理实验的根基。3.2 线程同步三大翻车点锁粒度、死锁与条件变量同步互斥实验是整个实验包里最容易丢分的部分。信号量、互斥锁、条件变量理论课上讲得头头是道真正上手写并发代码你会发现三个经典问题反复出现。第一锁的粒度没想清楚。实现生产者消费者模型时很多同学给整个缓冲区加一把大锁生产者和消费者互斥倒是保证了但并发性也几乎没了性能测试一跑分数惨不忍睹。正确的思路是把缓冲区的空闲位置和缓冲区里的数据分开管理用两个信号量分别计数再用一把只有几行代码的小锁保护缓冲区的读写下标。锁的范围越小并发度越高这是并发编程最朴素的真理。第二死锁。最常见的就是多个线程以不同顺序去拿多把锁结果在某个时刻互相持有对方需要的锁。这种bug在代码里极难通过肉眼发现因为它不是必然发生而是概率性发生。我调试过最久的一个死锁是三个线程、两把锁的场景靠printf打点打了整整一下午才在日志里看出线程A持有锁1等锁2线程B持有锁2等锁1的循环等待。这里分享一个经验遇到疑似死锁不要靠读代码硬想直接用gdb attach到已经卡死的进程上然后执行thread apply all bt查看所有线程的调用栈。哪个线程在等哪把锁、锁被谁持有一眼就能看出来。几乎每个死锁问题都可以通过这种方式在几分钟内定位。第三条件变量的谓词判断写错。pthread_cond_wait的正确用法官方推荐是配合while循环来判断谓词而不是if。原因是wait返回后并不能保证条件一定满足可能发生虚假唤醒。很多同学在这里栽跟头一跑就出现数据错乱。你只要记住一个规则条件变量wait之后永远用while重新检查条件而不是用if就能避开这个经典大坑。4. 内存与文件系统实验把抽象概念变成能跑的代码相比进程线程实验内存管理和文件系统实验更抽象因为它俩涉及的东西都无法直接观察。很多同学对虚拟地址、页表、页框、置换算法这些概念背得滚瓜烂熟但让他用代码实现一个页面置换算法却不知道怎么下手。4.1 页面置换算法的模拟实现从时间戳到链表加索引我的经验是不要一上来就试图处理真实的页表而是先把问题简化成一个模拟器。比如LRU页面置换算法你先定义一个物理页框数组再定义一个访问序列然后按顺序模拟每一次内存访问如果访问的页面已经在物理页框中算命中否则算缺页并从页框中选一个牺牲页替换。牺牲页的选择依据就是最长时间未被使用的页面。实现LRU最直观的思路是给每个页框加一个时间戳每次访问就更新时间戳需要淘汰时找时间戳最小的。这种写法逻辑简单但性能不好。更常见的优化是使用链表/* 简化版LRU页面置换核心结构 */ struct page_node { int page_id; struct page_node *prev, *next; }; /* 假设已有 lru_head 和 lru_tail分别指向链表头和尾 */ void access_page(int page_id) { /* 若页面已在链表中先摘下来否则新建节点 */ struct page_node *node find_page(page_id); if (!node) { node alloc_page_node(page_id); /* 淘汰链表尾部节点也就是最久未使用的页面 */ evict_lru_tail(node); } else { unlink_node(node); } /* 把当前访问的页面插入链表头部 */ insert_at_head(node); }这个结构的关键就是每次访问的页面放链表头需要淘汰时踢链表尾。这里有一个性能细节链表的查找要做到O(1)就得配合一个数组或哈希表记录页面号到节点指针的映射否则每次访问都要遍历链表数据量一大就慢得没法看。如果你在实验报告里能把这一层优化写清楚老师对代码实现水平的评价会明显不一样。4.2 玩具文件系统的磁盘布局与inode设计文件系统实验也是同理。教学场景下通常不会让你去改真实的ext4而是让你实现一个玩具文件系统定义超级块、inode表、数据块位图、目录项然后实现文件的创建、删除、打开、读写。这套流程走下来你对磁盘布局的理解会远超上课听讲。具体来说一个简化文件系统的磁盘布局可以划分为引导块、超级块、inode位图、数据块位图、inode区、数据区。超级块记录文件系统的元信息比如块大小、inode数量等inode是文件的身份证记录文件类型、文件大小、权限、指向数据块的指针目录项则是一个名字到inode号的映射。本质上一个目录也是一个特殊文件里面存放着一系列目录项。实现时我特别想提醒一点inode的存储结构设计要预留好扩展性。很多同学把inode设计成只有几个直接数据块指针结果文件一旦超过这个容量读写就崩。课程代码里常见的设计是加入一级间接块、二级间接块模拟真实文件系统的多级索引。虽然实验文件一般不大但把这个机制实现出来你对文件系统如何支撑大文件存储的理解就会深刻很多。文件系统实验调试起来比进程同步还难受因为一旦把数据块指针写错整个磁盘镜像就可能被破坏而且错误现象不明显可能只是某个文件读到一半数据不对。我建议你在代码里加一个文件系统一致性检查函数在每次关键操作后检查数据结构是否合理比如位图记录的使用块数是否和inode里记录的块数一致。这种自查逻辑虽然费一点代码量但能帮你把无数隐藏bug暴露在早期。5. 课程设计完整链路从需求拆解到迷你系统跑起来如果把前面几个实验比作单个知识点的单元测试那课程设计就是把这些知识点合并起来的期末考试。华科这套操作系统课程设计通常要求你实现一个可以运行的迷你操作系统或操作系统的核心子系统。很多同学在这里犯的策略错误是一上来就开写写到一半发现模块之间完全对不上最后通宵修补。5.1 架构先行模块划分与接口约定决定联调命运课程设计的第一步不是写代码而是画模块图。我见过太多人把课程设计做成大杂烩把实验一、实验二、实验三的代码复制粘贴到一个工程里结果结构混乱、接口对不上能编译但运行必崩。一个典型的迷你操作系统核心模块至少包括内核初始化模块、进程管理模块、内存管理模块、中断处理模块、系统调用模块。每个模块对外只暴露几个函数接口内部实现尽量独立。做架构设计时你要问自己几个问题进程管理模块的创建进程函数参数是什么返回什么错误码内存管理模块分配一页内存接口签名长什么样中断处理模块怎么把当前的进程上下文保存下来这些问题在写代码之前想清楚远比写完之后再来统一省事。我的经验是先定好一组头文件把所有数据结构定义、函数声明、错误码规范全部写清楚。比如统一错误码内存不足返回-ENOMEM参数非法返回-EINVAL。后面模块集成时大家包括你自己都基于这组头文件开发联调阶段的痛苦会少一大半因为接口层面的问题在编译期就能暴露一半。5.2 集成与答辩为什么跑通不是终点模块集成阶段是最考验代码功力的。集成的过程中经常出现单模块测试没问题合在一起就崩的现象。这通常是因为模块之间的时序假设不一致。我调试过一个案例中断处理模块在保存上下文时假设内存管理模块还没有开启分页所以直接使用物理地址但集成后内存管理模块在系统启动早期就开启了分页导致中断处理里写的是虚拟地址自然崩得一塌糊涂。这类问题的排查思路是先看崩溃点的指令和数据地址判断访问的是物理地址还是虚拟地址再倒查是谁破坏了双方的约定。集成完之后还有一件事容易被忽略写文档和准备演示。操作系统课程的课设答辩老师经常问的不是你怎么实现的而是为什么这样实现。比如你为什么不选另一种调度算法你的内存分配算法在什么情况下会退化线程切换的时机是怎么确定的如果你只是把代码跑通了但不理解每个设计决策背后的理由这种问题基本答不上来。我建议你在写课设报告时把每个核心设计决策对应的备选方案也列出来简单说明为什么不选它。比如调度实验里你选了时间片轮转而没选优先级调度可能的理由包括时间片轮转公平性好、实现简单、适用于交互式场景。这样既让报告显得有深度也强迫你去思考实验背后的原理。6. 三个真实排错案例与给后来者的通关建议最后分享几个我印象最深的排错案例每个都对应一个常见的错误模式希望能帮你少走一次弯路。6.1 案例一内核编译成功却无法启动的连锁反应这个坑我在前面提过值得再展开一次。当时我编译完内核make install也执行了重启虚拟机结果卡在initramfs之后直接Kernel panic。刚开始我以为是内核配置出了问题反复make menuconfig重编了三遍浪费了一整个下午。后来才意识到我编译的是内核本体但忘了编译内核模块。没有模块文件系统驱动根本加载不了自然挂载不上根文件系统。这个案例最大的教训是编译成功只是第一步产物完整才算真正结束。我现在每编译完一个内核都会先ls /boot和ls /lib/modules确认所有产物齐全再执行update-initramfs -u最后才敢重启。这个习惯帮我躲过了后面好几次类似的问题。6.2 案例二条件变量与谓词被不相关线程偷偷修改做线程同步实验时我的程序总是运行到一半卡死。用gdb attach上去发现所有线程都停在pthread_cond_wait里而主线程在等一个子线程join。看了半天才反应过来条件变量等待的谓词条件在发出signal之前被另一个线程偷偷改了。根源在于我只用一把互斥锁保护条件变量加谓词的整体逻辑却忽视了另一个不相关的线程也会修改这个谓词变量。后来我把谓词变量的所有修改操作统一收口到同一把锁里问题立刻消失。这个案例说明并发编程中锁的覆盖范围一定是数据和谓词一起保护而不是只锁眼前那几行代码。你以为自己保护了条件变量但保护不了被多个线程共享的普通变量死锁和竞态依然会找上门。6.3 案例三inode指针编号冲突导致文件乱码文件系统实验里我实现的读取文件总是读到一半出现乱码。排查了很久才发现问题出在inode的直接块指针和间接块指针的编号冲突数据块地址从0开始编号而位图判断块是否已分配时也把0号块当成了有效数据块。结果文件系统创建一个超过间接块容量的文件时数据被错误地覆盖。解决办法是让数据块地址从1开始编号0号块专门用作空指针。这个案例属于典型的边界条件没想清楚。真实文件系统里0号块有很多特殊用途这个设计不是随意的。把边界条件当成一等公民来处理是所有系统软件开发的必修课。6.4 给后来者的四条建议第一时间分配上环境搭建和内核编译这块看起来不产生成果但一定不要压缩它对后续所有实验都有影响。第二不要一个人闷头死磕。操作系统实验里的很多坑你在课程群问一句可能就有同学踩过并知道解法。第三如果时间允许把每个实验的代码从头读一遍弄清楚每一行是干什么的。期末考试的很多分析题就从实验代码的设计细节里出。第四把每个实验的验收要点列成checklist提交之前逐项过一遍。很多时候你在报告里写实现了调度算法但老师真正检查的是切换进程时上下文是否正确保存和恢复。拿checklist对照验收标准能提前发现那些以为实现了但实际没实现的问题。这套实验磨人但确实是把操作系统从书本知识变成手上能力的最快路径。我自己做完这些实验之后的直观感受是再回头看那些内核源码和系统程序不再觉得它们是一堆天书般的宏和结构体而是能看出设计者的意图和取舍。这种看门道的能力就是这门课真正想给你的东西。如果你也想试试类似的挑战除了华科这套实验以外哈工大那个公开的操作系统实验课程也值得当补充练手——先把一套完整跑通再对比着看另一套收获会更大。本文还有配套的精品资源点击获取