简介这是一份吉林大学软件工程《操作系统》实验大作业的完整实验报告文档面向操作系统课程学习者及备考者。报告以进程与线程概念为理论基础聚焦两大经典实验一是利用pipe()创建管道并通过fork()生成两个生产进程与两个消费进程完成进程间通信二是利用clone()创建四个线程借助共享内存和pthread_mutex互斥锁模拟生产者-消费者问题实现对共享存储区的互斥访问。文档包含完整的源代码、头文件说明、运行流程分析及实验总结并特别指出管道读写端的关闭顺序和wait()等待子进程等关键细节能帮助读者深入理解进程线程区别、IPC机制与线程同步技术同时积累并发编程的调试经验。资源包共含1个doc文档压缩包大小1.12MB内容紧凑且可直接查阅。已有1768人学习下载。 翻电脑翻出个老文件名字就叫“吉林大学软件工程操作系统实验大作业.doc”修改了几十遍的痕迹全躺在标题上了。当时做这门课大作业的时候我最大的一个错觉是“把调度算法写出来就跑完事了”结果后来看到评分标准才明白操作系统实验大作业真正考的不是某段代码而是你面对一个开放问题时能不能像做软件工程那样把需求、设计、实现、测试、汇报整个链条走通。这篇文章想分享的是如果把“操作系统实验大作业”当成一个完整项目来做从选题、架构、编码到测试、报告、答辩每一步应该怎么拆、怎么选、怎么避开我踩过的坑。适合正在写操作系统课程设计、或者刚开始准备但还一头雾水的同学参考。1. 课程大作业和平时小实验的本质区别从老师怎么打分倒推该做什么1.1 小实验验证技能大作业考验交付很多同学在操作系统课上做过不少小实验比如填一个调度函数、实现一次页面置换、跑通一个生产者消费者demo。这类小实验的代码框架是现成的你要做的是把某个核心函数补全跑出正确结果实验报告一交就算完事。但大作业不一样。大作业通常只给一个主题范围比如“实现一个进程调度模拟器”或者“模拟内存分配与回收”。没有现成脚手架没有单测样例连界面要不要做、参数怎么录入都完全开放。这时候“能不能把代码跑起来”反而是最低要求老师更关注的是你有没有把问题当成一个软件工程问题来处理数据怎么设计、模块怎么划分、结果怎么验证、报告怎么写。换句话说小实验验证的是“你会用这个知识点”大作业考验的是“你能不能交付一个完整的东西”。1.2 常见选题与难度评估操作系统的大作业选题范围其实很广不同选题的代码量、理解难度、可展示性差别很大。我根据自己见过的情况整理了一个对照表可以帮你快速判断该选哪个方向。选题方向核心考察点适合什么情况最容易翻车的点进程调度模拟调度算法、进程状态、时间片、周转时间想兼顾代码量适中、算法对比清晰的同学只写调度函数没做输入输出和统计页面置换算法模拟缺页率、置换策略、局部性原理想聚焦单一概念、数学统计比较直观的同学测试序列太简单看不出算法差异内存分配与回收模拟分区管理、碎片、首次/最佳适配对数据结构操作感兴趣的同学内存释放时的合并处理容易漏掉文件系统模拟目录结构、索引节点、磁盘布局想做点偏工程、偏设计感的同学很容易陷入代码量失控的泥潭生产者消费者问题信号量、互斥、同步、死锁想展示并发能力的同学多线程运行结果不稳定难复现银行家算法死锁避免、资源分配想快速做完、逻辑偏纯算法的同学界面和演示做得太简陋答辩吃亏如果还没有特别倾向我的建议和不少同学的选择一致优先考虑进程调度模拟器。它把进程状态、就绪队列、调度算法、时间片、性能指标这些操作系统核心概念都串起来而且结果好用图表展示答辩时能讲的东西很丰富。1.3 从评分维度倒推设计重点不同学校、不同老师的大作业评分标准不完全一样但我观察下来基本都会看四个维度功能完整性、结构设计、结果分析、报告与表达。这四个维度的重要性常常出乎很多人的预料。功能完整性当然是底线程序要能运行参数要能调结果要能输出。但结构设计往往才是拉开差距的地方代码是不是堆在一个几百行的文件里模块有没有清晰的职责边界数据结构选型有没有说明理由结果分析则要看你是不是真的用实验数据说了事——三个算法跑同一组输入你不仅给结果还要解释为什么这个算法在这个指标上更好。报告与表达考验的是你能否把“做过的事情”讲成“工程决策”这需要你在写代码的时候就为报告和答辩留证据。看清这四个维度之后大作业的做法就变了不是上来就写代码而是先设计方案再动手实现最后认真做测试。下面我用进程调度模拟器作为主线把完整过程拆开讲。2. 以进程调度模拟器为例把需求翻译成架构与数据结构2.1 为什么进程调度模拟器是好的主线样例进程调度模拟器几乎覆盖了操作系统课程里最经典的一整块内容进程的状态切换就绪、运行、阻塞、完成、就绪队列的组织方式、FCFS/SJF/优先级/时间片轮转这些调度算法、以及周转时间、等待时间、带权周转时间这些性能指标。更关键的是它的逻辑闭环很清晰输入一批进程的到达时间和服务时间通过不同的调度算法决定谁先运行最终输出各个进程的完成时间、周转时间等指标。代码量大概在500到1000行之间对软工的同学来说既不会太简单显得敷衍也不会复杂到需要花一个月去看源码。当然选题没有标准答案。如果你对文件系统更感兴趣或者对并发同步更擅长选别的方向完全没问题。这里用进程调度模拟器展开是为了让下面的设计思路有一个具体的载体。2.2 模块划分别把调度算法和统计逻辑揉在一起拿到题目之后第一步不是找调度算法代码而是先想清楚一个模拟器需要哪些模块。我当时把整个项目划分成了这么几个部分数据定义模块、输入解析模块、调度核心模块、时间推进模块、统计输出模块以及一个可选的可视化模块。数据定义模块负责定义进程控制块PCB和就绪队列的结构是所有逻辑的公共基础。输入解析模块负责读入参数无论是从命令行还是配置文件目的是把“创建几个进程、每个进程什么时间到达、需要跑多久”这些信息变成内部数据结构。调度核心模块是重中之重它实现FCFS、SJF、优先级调度、时间片轮转这些算法但只做一件事从就绪队列里选下一个应该运行的进程。时间推进模块负责维护全局时钟处理“当前进程运行多久”“下一个进程什么时候到达”这类时间问题。统计输出模块记录每个进程的完成时间、等待时间最后算出平均周转时间、平均带权周转时间。把统计逻辑从调度逻辑里拆出来是我后来觉得非常值得的一个决定——因为算法的替换变得特别干净不会改一处逻辑结果到处出问题。2.3 PCB设计和就绪队列的数据结构选型PCB是操作系统眼里一个进程的“档案袋”。写模拟器的时候不需要把真实Linux的task_struct全套复刻但核心字段一定要有。我当时用C语言定义的结构体长这样typedef struct pcb { int pid; // 进程标识 int arrive_time; // 到达时间 int need_time; // 需要的CPU服务时间 int remain_time; // 剩余服务时间 int priority; // 优先级数字越大越优先 int start_time; // 首次开始运行时间 int finish_time; // 完成时间 int state; // 0-未到达 1-就绪 2-运行 3-完成 struct pcb *prev; struct pcb *next; } pcb_t;这个结构里比较容易被忽略的是start_time。计算等待时间时如果一个进程被抢占它的实际运行时间会被拆成很多段只有记录“第一次开始运行的时间”才能正确算出响应时间进而分析交互性。就绪队列我最终用的是双向链表而不是数组。原因很实际调度算法在运行过程中要不断从队列头取进程、把新到达或时间片耗尽的进程插回队列按照SJF还要插入到合适的位置双向链表的插入和删除都是常量级操作代码写起来也比数组搬移清晰很多。虽然本科实验模拟的进程数可能很有限数组也够用但双向链表带来的边界条件处理经验本身就是这个实验想让你练的东西。3. 核心实现里容易掉头发的几个点事件驱动、队列插入、随机复现3.1 用事件驱动推进时间而不是逐时间片扫描初学者写调度模拟器最直觉的做法是这样用一个for循环从时间0跑到总时长每个时间单位都检查有没有进程到达、要不要切换。这个思路没错但当模拟总时长很长、进程很多时每个时间片扫描效率很低而且代码会陷入各种“到点了要检查什么”的细节里。我当时采用的方案是事件驱动只在需要处理事件的时间点工作事件就是“新进程到达”和“当前进程的时间片结束或者运行完成”。用一个简化伪代码表示就是while (还有未完成的进程) { if (就绪队列为空) { // 当前没有可运行的进程直接跳到下一个进程到达时刻 time next_arrive_time; 将到达进程加入就绪队列; } else { 从就绪队列取出一个进程; 计算本次应该运行多久; // 取剩余时间、时间片、下个到达时间的最小值 模拟运行; 更新全局时钟; 如果进程完成记录完成时间; 如果时间片用完且未完成重新插入就绪队列; } }这种写法最大的好处是CPU空闲的时间段直接被跳到下一个到达时刻跳过去了不用一秒一秒地空转。同时所有状态切换都集中在这几个判断里调试起来思路非常清晰。我记得自己第一次用扫描方式写的时候为了处理“同一时刻进程到达和时间片到期谁先谁后”的边界改了好几轮换成事件驱动之后这个问题从根源上简单了很多。3.2 就绪队列插入策略同样是链表不同算法插入位置不同就绪队列看起来都是“把进程插进去”但不同的调度算法插入策略完全不同这是实现时最容易搞混的地方。FCFS的思路最简单新到达进程一律插到队尾队首就是先到达的进程。SJF则不是按到达时间排队而是按服务时间排队——新进程来的时候要插到第一个比它服务时间长的进程前面如果剩余时间相同就保持稳定顺序。优先级调度类似按优先级字段插入。时间片轮转比较特殊它还是“按到达顺序排队”但时间片用完且还没运行完的进程要重新插到队尾而不是继续待在队列里。这个“插到队尾还是插到中间”的差别直接决定了算法对不对。我当时写SJF的时候下意识用了尾插法跑出来的结果怎么都不像SJF后来加日志才发现新到达的短进程被排到了队末根本没起到“短作业优先”的作用。所以写队列插入逻辑前先想清楚每种算法在就绪队列上的行为比急着写代码重要得多。另外还有一个细节双向链表的边界处理。队首插入、队尾插入、空队列插入这三个case最好都单独测一遍否则运行到一半出现野指针或者漏节点的概率相当高。3.3 随机进程生成与结果可复现性为了演示方便很多同学的模拟器会在启动时自动生成一批随机进程随机到达时间、随机服务时间、随机优先级。这个功能很方便但有一个隐含的坑——随机生成后每次运行的数据都不一样你写报告时无法对着一个固定的实验数据详细分析答辩时如果老师让你现场复现某个结果你也解释不清楚。我当时处理这个问题的办法是引入随机种子。启动时如果设置了种子就按种子生成固定的进程集不设置种子才走系统随机数。这样平时调试可以随便跑但报告和答辩准备阶段我会固定同一个种子保证文档里的数据都是能一键复现的。代码上就是初始化随机数发生器时调用srand(42); // 固定种子的情况下除此之外进程数量、到达时间的分布范围、服务时间的范围最好做成启动参数而不是写死在代码里。这样设计对比实验的时候不用每改一次就重新编译一次。强烈建议你把这个小细节做进去后面整理报告会感激当时的自己。3.4 别让main函数膨胀成两万行的怪物我在前面把模块划分得很清楚实际编码时就要真的按文件来组织。我当时建了大概这么几个文件pcb.h定义结构体queue.c实现双向链表的相关操作sched_fcfs.c、sched_sjf.c这些文件分别实现对应的调度算法实现simulator.c负责整体调度循环main.c负责参数解析和调用入口。这样组织之后切换调度算法只需要在main里选择调用哪个调度函数完全不用动其他模块。我当时还配了一个Makefile一条make命令就能编译出可执行文件。这里想多说一句很多同学觉得单文件也能跑为什么要拆开。实验课的评分未必会因为“拆分了文件”就多给分但你自己调试的时候会发现模块化代码极大降低了出错的排查范围。比如统计结果不对你很清楚问题出在统计模块而不是调度模块。这种模块思维本身就是软件工程这个专业要锻炼的东西。4. 测试与验证别让辛苦做出来的模拟器只跑过一次4.1 边界与异常场景比算法本身更值得花时间很多人的大作业程序只准备了一个正常输入样例但真实实验验证远不止“能跑就行”。我在给模拟器做测试时会额外构造这样几类边界场景。第一类是空任务队列进程数为0时程序直接退出并且给出清晰提示不能报数组越界。第二类是所有进程在同一时刻到达这会消除到达时间差异造成的影响能更干净地比较不同调度算法的行为。第三类是进程稀疏到达——比如前一个进程运行完了下一个还要等很久才来这种情况专门考验空闲时间的处理逻辑。第四类是极端服务时间某个进程需要0个时间单位表示它瞬间完成或者服务时间远大于其他进程用来观察长进程对周转时间的影响。第五类是多个进程优先级相同保证调度算法在优先级相等时仍然稳定。这些边界场景我是一开始就写进测试用例里的它们帮我发现了很多逻辑漏洞。比如空队列时如果时间推进代码没有处理好“就绪队列为空且未来没有进程到达”的状态程序就会陷入死循环。又比如服务时间为0的进程如果处理顺序不对可能出现完成时间早于到达时间这种明显不合理的结果。边界测试其实不会花很多时间但对提高代码质量的作用非常大。更重要的是报告里如果写上一段“我做了哪些边界测试、发现并修复了什么问题”老师一眼就能看出你是认真做了实验的而不是凑了一个能跑的结果。4.2 设计对比实验同一组输入跑全部调度算法进程调度模拟器最有价值的部分就是用同一组进程数据跑不同的调度算法然后把指标放在一起对比。我当时设计了一个基准测试用例包含5个进程进程到达时间服务时间优先级P1073P2245P3411P4544P5832用FCFS、SJF、抢占式优先级调度、时间片轮转时间片2跑出来的典型结果对比如下。不同实现对于“同一时刻多个事件先后顺序”的约定可能有细微差别但整体趋势可供参考调度算法平均周转时间平均带权周转时间平均等待时间FCFS9.23.535.4SJF8.22.554.4优先级调度9.84.546.0RR时间片210.22.606.4这张表里最值得在报告里展开的是带权周转时间。FCFS看起来平均周转时间和RR差不多但带权周转时间却明显偏大原因在于P1这种长进程排在前面导致后面短进程的等待时间被拉长短进程相对自己的服务时间而言“等得特别久”。SJF的带权周转时间最小符合理论预期。优先级调度的带权周转时间很差是因为P3这种低优先级短进程被活活拖到最晚完成服务时间只有1周转时间却高达15。对比实验做出来之后不要只放一张表就完事。挨个解释“为什么这个算法在这个指标上表现出这个特征”这才是操作系统实验大作业真正想看到的东西。把算法理论和实验数据结合起来说比单纯背课本定义有说服力得多。4.3 输出完整的调度甘特图为报告和答辩留证据光有统计指标还不够我当时还让程序输出了进程运行的甘特图类似这样P1 [0-2] P2 [2-4] P1 [4-6] P3 [6-7] ...甘特图的好处是能直观看到进程什么时候被调度、什么时候被抢占、什么时候完成。写报告时放一张小规模用例的甘特图再配合统计表整个实验分析就显得特别完整。而且答辩时如果老师盯着某个指标问你“为什么这个进程等了这么久”你可以直接指着甘特图解释它被谁抢占了、等在哪一段比空口解释半天都有效。另一个建议是把调度过程的完整日志存成文件。我当时每次运行都会生成一个日志文件记录了每个时间点上谁在运行、谁到达了、谁重新进入队列。后来整理报告遇到“这个数据是怎么来”的疑问直接翻日志就能校准省了很多力气。5. 报告和答辩决定最终评价的另一半分数5.1 报告结构不是把代码粘贴一遍而是讲清楚一个工程故事实验报告是很多同学容易轻视的部分觉得代码都写出来了报告随便写写就行。但实际上报告是老师了解你整个思考过程的唯一途径它的重要性完全不亚于代码。我当时总结的报告结构大概是这样的问题背景与需求描述、总体设计、详细设计与实现、测试与分析、总结与思考。问题背景和需求描述不需要长篇大论重点是明确“要做什么、输入是什么、输出是什么、有哪些约束条件”。总体设计里放模块划分图、模块职责说明让读者一眼看到整个程序的骨架。详细设计里写核心数据结构、关键算法流程这一部分的重点是解释“为什么这么设计”而不是贴一大段代码。测试与分析是最容易出彩的部分把正常的测试用例、边界测试、算法对比都放进来配合图表说明结论。最后的总结与思考可以写自己遇到了什么问题、怎么解决的、还想往哪个方向继续做。这份报告从头到尾都在讲“我做了什么决策、为什么这么做”而不是复述课本知识。老师看报告时想看到的不是你会背概念而是你面对问题时的工程判断。5.2 画图要点模块结构图和调度时序图实验报告里图是最直观的说明方式。模块结构图通常放在总体设计部分把前面说的几个模块之间的关系画清楚。调度时序图则适合放在详细设计部分展示一个具体的调度过程。画图不需要用什么高级工具简洁清晰最重要。图的布局要统一线条不要交叉模块或节点命名要与报告正文保持一致图下方要有编号和图题正文引用图时要能对应上。有一个容易被忽视的细节图里的所有符号或者箭头如果你在报告中引用了就要在正文里解释清楚。比如进程状态转换图的箭头代表什么状态迁移、什么条件触发如果不写图就失去了意义反而显得是为了凑图而画。5.3 答辩高频问题能自圆其说才算真弄懂了答辩是紧张刺激的环节但也是把你的工作完整展示出来的机会。根据我的经验围绕进程调度模拟器老师常问的问题有以下几类每一类我都建议提前准备清楚。“为什么就绪队列用双向链表而不是数组”这个问题考察你对数据结构选型的理解你可以从插入删除操作的时间复杂度、实现SJF时需要按服务时间排序的便利性、以及链表节点和PCB结合的直观性来回答。“RR的时间片大小是怎么定的如果改成1或者改成5会怎样”这其实是在考察你对时间片轮转本质的理解。时间片太短上下文切换开销占比变大时间片太长RR就退化成FCFS。比较稳妥的回答是设计时把时间片设置成变量并跑了不同时间片下的对比测试用数据说话而不是拍脑袋。“为什么这组进程里FCFS的带权周转时间明显更大”如果准备充分你可以直接靠数据回答指出长进程排在前面导致后续短进程等待过长再用P1的例子具体说明。如果被问到细节手头日志里的甘特图就是你的底牌。“同时有多个进程到达你的程序按什么顺序处理”这类问题没有标准答案但你要能讲清楚自己实现时的规则并且说明这个规则在哪些场景下更合理。关键是自洽不要前后矛盾。另外想提醒一点答辩时不要只盯着自己的代码文件讲。先花一两分钟把大作业的背景、目标、整体设计串一遍让老师了解你“为什么这么做”再深入细节。这样即使中间某个细节回答得不完美老师也能感受到你的整体思路是清晰的。写到这里我翻着那个doc文件回想当时为了一个链表边界条件折腾一下午为了画一个能看懂的甘特图反复改输出格式。现在回头看收获最大的其实不是某个调度算法怎么写而是建立了一套“先设计再编码、先测试再写报告”的做事习惯。如果你正在被操作系统实验大作业折磨我的建议很简单别急着写代码先把问题拆清楚把数据结构想明白。花在设计上的时间后面一定会在编码、调试、测试、写报告甚至答辩的每个环节加倍还给你。最后再给你一个小技巧把你的模拟器设计成交互式命令行工具支持键盘输入参数而不是只靠硬编码数据。答辩现场临时换一组数据演示效果会好得多。本文还有配套的精品资源点击获取