简介:一份吉林大学软件工程操作系统实验大作业报告,面向正在学习进程线程机制、需要完成通信与同步实验的高校学生。压缩包内含一个doc格式的文档,大小约一点一二兆字节。报告围绕两个实验展开:第一,创建管道,利用进程派生生成两个生产者和两个消费者,实现字符串数据的跨进程传递;第二,创建四个线程,借助共享内存模拟生产消费行为,并使用互斥锁保证共享存储区的互斥访问。内容涵盖实验设计思路、完整可运行的源代码、关键代码逐段分析、执行流程和实验总结,特别说明了关闭管道端、等待子进程结束等易错细节。已有1770人浏览学习,可作为操作系统实验报告的参考范例,也适合备考复习时对照理解进程线程与同步理论。
1. 吉林大学软件工程操作系统实验大作业:这门课到底要你交什么
“吉林大学软件工程操作系统实验大作业”这个标题背后,其实是操作系统课程里最硬核的交付物:一份可运行的实验代码、一组能说明问题的测试数据,和一份按软件工程规范写出来的报告。不少同学在《计算机操作系统》(汤小丹版)上能把进程、死锁、虚拟内存背得滚瓜烂熟,真到了实验课,却连“为什么生产者消费者的信号量要设成 3 而不是 1”都答不上来——因为教材里的伪代码和编译器里的真实执行之间,隔着缓冲区、调度顺序和一堆说不清的竞态条件。这篇文章就按你从拿到题目到提交文档的完整路径,把 Linux 环境搭建、代码框架选择、报告写法、踩坑记录和验收方式讲清楚。适合正在选实验题、代码跑不通、或者报告不知道写多深的软工学生,也适合想带学生做实验的助教。
2. 先把实验环境立住:Linux 怎么选、怎么装、代码框架怎么定
2.1 为什么操作系统实验首选 Linux,而不是 Windows 原生跑
操作系统实验的代码,十有八九要跟进程、信号量、页表、文件系统打交道。Windows 上不是不能写,但 Win32 API 的进程模型、同步对象和 Linux 的 fork()、pthread、sem_t 完全是两套语言。你用 Windows 写完拿去 Linux 上交叉编译,第一步就会在头文件上翻车。常见做法是:直接用 Linux。发行版选 Ubuntu 的 LTS 版本,比如 22.04,别追最新的 24.x 或者滚动发行版,实验需要稳定,不需要尝鲜。
如果你对 Linux 基础操作还不熟,先补一遍“linux操作系统基础知识”:文件权限、vim 基本编辑、gcc 编译参数、make 和 gdb,这四个够用。我会建议你在正式写实验代码前,先花一个晚上把这几样过一遍。这个投入很值,因为后面调试代码时,90% 的时间都用得上。操作系统实验最忌讳一边翻教程一边赶代码,环境装到一半就放弃了,后面所有实验都会受拖累。
2.2 最小可用环境:虚拟机、WSL2 还是双系统
选环境,其实是在选你愿不愿意为“接近真实内核”付出额外成本。下面这个对比可以直接抄:
| 方案 | 内核真实度 | 调试验收成本 | 适用位置 |
|---|---|---|---|
| VMware/VirtualBox 虚拟机 | 真实内核 | 略高,重启慢 | 大多数同学的默认选择 |
| WSL2 | 接近真实 | 低,启动快 | 有 GUI 需求、嫌虚拟机卡 |
| 双系统 | 完全真实 | 高,装系统有风险 | 想做内核模块深度实验、有闲置电脑 |
| 云主机 | 真实内核 | 中,按小时付费 | 本地机器太老跑不动 |
我一般推荐虚拟机。内存分 4GB 给 Ubuntu,CPU 给 4 核,磁盘 40GB,大部分实验都够。别贪多,分太多宿主机卡,分太少编译内核模块时直接内存不足报错。WSL2 也可以,文件系统跨 Windows 和 Linux 共享时会有一点性能损耗,实验规模小到无所谓。真正要注意的是:如果用虚拟机,每次关机前把快照做好,尤其是编译内核前——这个后面避坑章会细说。
2.3 用户态模拟实验的代码框架:一个能跑通的最小骨架
操作系统实验大作业常见两套题型。一套是“改内核”,比如往 Linux 源码里添加系统调用,或者改调度器,这种实验的判据是 dmesg 输出和内核编译是否通过,难度高、回报也高。另一套是“用户态模拟”,也就是用 C 语言把进程调度、内存置换这些机制仿真出来,不碰真实内核。多数软工方向的大作业属于后者,因为它能同时考察数据结构和操作系统原理。
无论是哪种,我建议你先把代码框架定成“输入文件 + 核心算法 + 统计输出”三段式。下面是一个进程调度模拟的最小骨架,用 C 写的,可以直接抄来改:
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_PROC 64 // 进程控制块,实验里用结构体模拟真实的 PCB typedef struct { int pid; // 进程编号 int arrive; // 到达时间 int need; // 需要的 CPU 时间 int finish; // 完成时间,算周转时间用 } PCB; int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "用法: %s <输入文件>\n", argv[0]); return 1; } PCB procs[MAX_PROC]; int n = 0; FILE *fp = fopen(argv[1], "r"); if (!fp) { perror("打开输入文件失败"); return 1; } // 输入文件每行格式:pid 到达时间 需要时间 while (fscanf(fp, "%d %d %d", &procs[n].pid, &procs[n].arrive, &procs[n].need) == 3) { procs[n].finish = 0; n++; } fclose(fp); // 后续在这里接入具体的调度算法,比如时间片轮转 printf("读入 %d 个进程,等待调度算法实现。\n", n); return 0; }这段代码的逻辑不复杂:把 PCB 定义成结构体数组,从文件读入进程集合,再留一个接口给调度算法。之所以不让你直接写在 main 里,是因为后面每种算法都要跑同样的输入,输出还要做对比,如果数据是从文件里读的,换算法时完全不用改输入。这里的三个参数 pid、arrive、need 是调度实验的标配,不管作业要求的是 FCFS、SJF 还是时间片轮转,都要用到这三个字段。等写完算法,再把 finish 和周转时间统计出来,放进报告表格里就行。
这套框架的好处是:调试时不需要反复手工输入数据,你只需要维护输入文件和算法函数,中间环节不会出错。等大作业做到后半程你就会明白,能稳定复现的数据才是报告里最有说服力的部分。
3. 把四个经典实验逐个拆解:调度、同步、内存、文件系统
3.1 进程调度实验:时间片轮转与多级反馈队列的代码骨架
进程调度是整个操作系统实验里最好拿分、也最容易做浅的一道题。常见要求是:用 C 语言实现 FCFS、短作业优先(SJF)、时间片轮转(RR),并比较平均周转时间。FCFS 和 SJF 本质是两个队列排序,唯一要小心的是 SJF 要考虑进程到达时间,不能简单按 need 排序——很多同学的翻车点就在这里。
时间片轮转稍微有一点复杂度。核心是一个就绪队列和时钟变量。伪代码式的 C 骨架我这么写:
// 时间片轮转调度的核心循环,time_slice 是时间片长度 int current = 0; // 当前时间 int remaining[MAX_PROC]; // 每个进程剩余还需要的 CPU 时间 memcpy(remaining, procs, sizeof(remaining)); // 用一个简单的循环代替真实队列,实验规模下足够 while (1) { int done = 1; for (int i = 0; i < n; i++) { if (remaining[i] > 0) { done = 0; if (procs[i].arrive <= current) { if (remaining[i] > time_slice) { current += time_slice; remaining[i] -= time_slice; } else { current += remaining[i]; remaining[i] = 0; // 这里记录完成时间,用于计算周转时间 } } } } if (done) break; }这段逻辑是按“每次找到第一个到达且未完成的进程”来模拟轮转的。真实内核里用的是环形链表,但实验规模下用 for 循环 + 时间累加的写法更容易调试,也不容易出现指针错误。要注意的坑是:时间片不是越大越好。时间片设成 1 时,每个进程都能高频获得 CPU,但上下文切换次数暴涨;设成 100 时,RR 退化成近乎 FCFS。报告里要分别跑时间片 1、5、10 三组数据,对比周转时间,这个对比本身就是一个很好的实验结论。
多级反馈队列(MLFQ)如果作业要求做,你可以把它理解成多个带有不同时间片的队列:新进程先进头队列,队列 1 没跑完就降级到队列 2,以此类推。数据结构上用数组代替链表能省很多调试时间,只要在数组里维护每个队列的队头和队尾指针,规模在 64 个进程以内不会有性能问题。MLFQ 的核心参数有三个:各级队列的时间片、降级策略(立刻降还是跑完一轮再降)、以及是否在队列间做优先级抢占。这三个参数你在报告里交代清楚,老师一眼就能看出你是真的理解了算法,而不是对着网上代码改了个名字。
3.2 同步互斥实验:生产者消费者里信号量的三个必调参数
同步互斥是大作业里最容易“看起来对了但跑起来就死锁”的部分。最常见的题目是生产者消费者,其次是读者写者。以生产者消费者为例,正确写法是维护三个信号量:mutex 保护共享缓冲区,empty 表示空槽位,full 表示已填的槽位。很多同学栽在初始值上。
#include <pthread.h> #include <semaphore.h> #define BUFFER_SIZE 10 sem_t mutex, empty, full; void *producer(void *arg) { for (int i = 0; ; i++) { sem_wait(&empty); // 空槽减少,没有空槽就阻塞 sem_wait(&mutex); // 进入临界区 // 往环形缓冲区写入 i sem_post(&mutex); // 离开临界区 sem_post(&full); // 已填充的槽增加 } return NULL; } void *consumer(void *arg) { for (;;) { sem_wait(&full); // 没有数据就阻塞 sem_wait(&mutex); // 从环形缓冲区读取 sem_post(&mutex); sem_post(&empty); } return NULL; }这段代码里三个信号量的初始值非常关键:mutex 初值为 1,保证只有一个线程进入临界区;empty 初值为 BUFFER_SIZE,表示缓冲区全空;full 初值为 0,表示还没有数据。如果调成 full = BUFFER_SIZE,生产者永远不会阻塞,消费者一上来就读空数据,结果完全错误。如果 mutex 初值调大,临界区同时进多个人,那就不是互斥了。这是“信号量设成 3 而不是 1”的血泪教训:只有 mutex 必须为 1,empty 和 full 初始值取决于缓冲区容量,不是拍脑袋填的。
读者写者实验里的经典坑是“写者饥饿”:如果读者源源不断进来,写者可能永远拿不到锁。常见解决是用一个 reader_count 计数,配合一个写者优先的信号量。你在报告里重点比较“读者优先”和“写者优先”两种策略下的完成时间,这能体现你对公平性的理解。测试时注意多跑几轮,同步 bug 是概率性的,一次成功不代表没问题,我一般会连续跑 50 轮,统计死锁次数和超时次数。
3.3 内存管理实验:用数组模拟物理帧,LRU 置换怎么不翻车
内存管理实验通常有两种规模:一是模拟页表和地址转换,二是做页面置换算法。后者更常见,因为能用到经典算法 FIFO、LRU、Clock,并对比缺页率。我的建议:先按“物理帧大小固定为 F,虚拟页号序列从文件读入”这个思路设计,不要一上来就用链表做 LRU,容错率太低。
LRU 的“翻车点”在于很多人会把 LRU 和频率混淆。LRU 记录的是“最近一次被访问的时间”,不是“被访问了多少次”。用计数器实现最直接:每次访问就把该页的时间戳设为当前访问序号,替换时找时间戳最小的页。下面是个能用的核心片段(假设帧数量为 frame_count,页表项存在数组 page_table 里):
int frame[MAX_FRAMES]; // 帧数组,存当前的页号 int last_used[MAX_FRAMES]; // 每帧最近一次被访问的时刻 int free_cnt = frame_count; // 空闲帧数 int find_lru_frame(void) { int min_idx = 0; for (int i = 1; i < frame_count; i++) { if (last_used[i] < last_used[min_idx]) { min_idx = i; // 找最近访问时间最小的帧 } } return min_idx; }这段代码里的 last_used 数组是 LRU 的灵魂。每次命中时更新对应帧的 last_used,每次缺页时需要替换时调用 find_lru_frame。别用“统计访问次数”的变量去替代,那是 LFU。报告中要对比同一访问序列下 FIFO 和 LRU 的缺页次数,并且要解释为什么 FIFO 会出现 Belady 异常。这部分是拿高分的关键:不止跑结果,还要说明原因。
3.4 文件系统实验:先写磁盘布局,再谈 inode 和目录项
文件系统实验在软工大作业里出现频率稍低,但一旦出现,多数要求是“设计并实现一个简易文件系统”,包括磁盘块的分配、文件的创建删除和目录遍历。别一上来就写代码,先用一张布局表把磁盘定下来。常见做法是:把磁盘模拟成一个大数组,前 N 块作为元数据区,中间放 inode 表,后面是数据块和空闲块位图。
我一般定义如下的磁盘布局:
| 区域 | 起始块 | 大小 | 内容 |
|---|---|---|---|
| 超级块 | 0 | 1 | 魔数、块数、inode 数、空余块指针 |
| 位图区 | 1 | 2 | 数据块分配位图 |
| inode 表 | 3 | 16 | 每个 inode 记录文件大小、数据块指针 |
| 数据区 | 19 | 剩余 | 文件内容实际存放位置 |
这个布局不是教科书标准,但足够支撑一个微型文件系统。实现时核心是三个函数:alloc_block() 从位图找空闲块,alloc_inode() 分配一个空闲 inode,和 dir_add_entry() 在目录项里加入文件。目录项就用定长结构体数组,文件名最多 28 字节,加 4 字节的 inode 号,不要做哈希目录,那是加分项不是必选项。写完这三个函数后,再写 create、delete、list 三个命令,一个能交差的文件系统实验就出来了。报告里贴一下“创建文件后的目录遍历输出”以及“删除文件后位图变化”两张图,比贴一大段代码有说服力得多。
4. 实验报告写到什么程度才算“软件工程级”
4.1 报告的章节结构:需求分析、设计、测试结果对齐
操作系统实验大作业的文档,最容易犯的毛病是“代码为主、分析为辅”。大多数课程给的模版是 .doc 格式,标题写“吉林大学软件工程操作系统实验大作业.doc”,里面也就几页要求,但评分老师真正想看到的是:你能否把操作系统概念和实现细节对应起来。我建议报告按下面这个结构写:
| 章节 | 写什么 | 篇幅建议 |
|---|---|---|
| 需求分析 | 实验做什么、输入输出是什么、边界条件 | 1 页 |
| 设计 | 数据结构、模块划分、核心算法选择理由 | 2-3 页 |
| 核心实现 | 关键函数代码片段,附逻辑说明 | 2 页 |
| 测试与结果 | 测试用例、对比数据、结果截图 | 2 页 |
| 遇到的问题 | 2-4 个典型 bug 和排查过程 | 1 页 |
| 总结 | 实验收获,尽量具体到某个参数 | 半页 |
其中“遇到的问题”一节最容易被忽略,却最能体现你的独立工作量。写 bug 时按“现象 → 原因 → 解决”三段写,不要只写结果。比如“通过调试发现是缓冲区未加锁导致的数据竞争”——这句话本身就有价值,比“本次实验顺利完成”强一百倍。软件工程课程设计里反复强调的可追溯性,在实验报告里就是“每个结论都能对上一条测试记录”。
4.2 测试数据怎么给:吞吐量、CPU 占用和边界用例一张表说清
操作系统实验最容易拿到高分的地方,是别人只给一张输出截图,你给了一张可以直接复现的测试表。以调度实验为例,最少要有三组数据:常规负载、极限负载、相同进程数不同到达间隔。表格格式可以直接抄下面这个:
| 算法 | 进程数 | 到达间隔 | 平均周转时间 | 平均等待时间 |
|---|---|---|---|---|
| FCFS | 10 | 均匀 | xxx | xxx |
| SJF | 10 | 均匀 | xxx | xxx |
| RR 时间片=1 | 10 | 均匀 | xxx | xxx |
| RR 时间片=5 | 10 | 均匀 | xxx | xxx |
边界用例也很重要:进程数为 1 时所有算法应该等价;所有进程同时到达时 SJF 和 FCFS 应该结果一致;某个进程的 need 为 0 时不能出异常。这些用例能帮你发现隐性问题,也是在报告里体现严谨性的最好方式。如果实验涉及同步,额外给一组“多轮运行无死锁”的测试记录,写清跑了多少轮、每轮多长时间。
4.3 代码附录的装订方式:让老师 10 分钟看完核心逻辑
很多同学把整个工程文件复制进文档,造成二三十页的代码海洋,评分老师根本看不进去。代码附录的正确姿势是:只放核心文件,并且每个文件前加一小段说明,指出本文件在工程中的职责。比如调度实验,附录放 main.c、schedule.c、schedule.h 三个文件就够了,输入输出模块不用放。每个函数前用一行注释说明参数和返回值,这就是软件工程的“可读性”要求。
另外,既然模板是 .doc 格式,提交前记得检查排版:代码用等宽字体如 Courier New,字号用五号;行距固定值 20 磅;所有代码块保持在页面内,不要跨页截断。提交前把 Word 的“打印预览”过一遍,确保页码正常。这一套做下来,老师不用翻二十页源码就能确认你代码结构清楚,分数自然不一样。
5. 操作系统实验避坑指南:5 个真实翻车现场与排查顺序
5.1 程序结果和预期不符:先查同步条件还是先查调度顺序
现象:调度算法写好了,跑测试用例,发现输出顺序和教材例子对不上,CPU 时间累加结果也偏大。
原因:多数不是算法逻辑错,而是“时间推进”的口径不统一。时间片轮转里,一个进程可能在时间片中途到达,你如果把整个时间片都算给当前进程,后面到达的进程就全部后移。
解决:先把“时间推进”单独封装成一个函数,所有调度逻辑都只通过这个函数推进时间。推荐先按“每个循环单位=时间片单位”来写,把到达检查放在每个时间片开始前,再逐步验证。这个排查顺序同样适用于信号量问题:先确认临界区范围,再加锁排除,逐层缩小范围,不要一开始就怀疑编译器。
5.2 printf 输出顺序错乱:标准输出缓冲在给你“加戏”
现象:生产者消费者实验里,加了 printf 做日志,明明代码逻辑没问题,输出顺序却是乱的,有时候日志还会缺行。
原因:printf 是行缓冲(终端下)或全缓冲(重定向到文件下)的,多线程条件下,两个线程的输出可能交错到同一个缓冲区,刷新时机不一致时,打印顺序和实际执行顺序根本不能对应。
解决:日志输出统一加 fflush(stdout);比较日志顺序时,不要在 printf 里观察真实调度顺序,应该用一个带时间戳的日志数组记录事件,程序结束后统一打印。这条经验也适用所有“打印结果和别人不一样”的排查,先怀疑缓冲,再怀疑同步。
5.3 内核模块编译报错:多半是头文件版本不匹配
现象:照着网上的例子写了一个内核模块,make 的时候报“无法打开 linux/xxx.h”,或者一大堆 redefinition。
原因:虚拟机里装的 Ubuntu 内核版本和 linux-headers 包版本不一致。这是 Linux 内核模块实验最常见的翻车现场,和代码本身没关系。
解决:在实验前先执行 sudo apt update && sudo apt install linux-headers-$(uname -r),然后 uname -r 确认 Makefile 里指定的版本和你当前内核一致。再一个预防性操作:编译内核前必须打虚拟机快照,否则一次错误的 make install 可能让你花半天重装系统。这条建议价值很高,别省。
5.4 截图放进文档变模糊:分辨率、字号和缩放三个细节
现象:截图在终端里很清楚,放进 .doc 文档后变成一团马赛克,老师看不清楚输出结果。
原因:终端窗口的默认字号小,截图分辨率不够;或者 Word 里图片被缩放得太小;还有一种情况是截图带了窗口边框,放到文档里占地方又模糊。
解决:截图前把终端字号调大,至少用 14pt;截取内容区域而不是整个屏幕;插入 Word 后,图片宽度控制在 14 厘米左右。命令行输出比较多时,优先把结果用重定向写进 txt 文件,再以等宽字体粘贴到文档里,这样既清晰又能复制。截图里如果想标注关键数据,用深色矩形和高亮箭头,别用浅色画笔。
5.5 最后一天才发现用例没过:测试要留一半时间
现象:代码写完到提交只剩一天,跑全量测试时发现某个边界用例(比如进程数为 1)输出异常,慌乱之中不知道改哪里。
原因:这不是技术问题,是项目管理问题。操作系统实验的调试成本比普通软件工程大作业高,任何一次环境重建、内核编译、代码重跑都可能耗掉数小时,测试时间永远比你以为的多。
解决:我的习惯是“代码写完不算完,测试跑通才算完”。具体操作是:核心代码写完后,先跑最小用例——1 个进程、2 个进程、全部同时到达,这组用例十几分钟就能全跑完;确认它们通过后再扩充到真实负载。这样任何边界问题都提前暴露,不会拖到最后一天。如果你已经到提交前夜才发现问题,那就做增量修复:先修最小用例,再修关键路径,记录每个问题的影响范围,不追求一次全搞定。
6. 验收前最后一道检查:用一条命令重跑所有实验用例
6.1 把实验流程固化成 run.sh,自动比对结果
大作业到了验收阶段,最怕的不是代码有 bug,而是“换了一台机器/换了一个终端,跑出来的结果和报告不一致”。所以验收前,我会把整个实验流程固化成一条命令。
#!/bin/bash # 实验验收脚本:编译、运行、对比期望输出 set -e # 编译调度实验 gcc -o sched scheduler.c main.c -Wall # 跑三组输入文件,每次重定向到结果文件 for input in cases/small.txt cases/medium.txt cases/all_same.txt; do ./sched "$input" > "out/$(basename "$input").out" done # 和期望输出做 diff,任何差异都会让脚本非零退出 diff -r out/ expected/ && echo "全部用例通过"这个脚本的逻辑很简单:编译 → 跑用例 → diff 比对。它强制你提前准备“期望输出”,这会逼着你自己把正确的实验现象想清楚。参数上,这里的 cases/ 目录放输入文件,out/ 放新生成的结果,expected/ 放你确认正确的旧结果。以后改一次算法代码,跑一遍脚本就能确认没有引入回归。这是软件工程里“可复现性”最朴素但最有效的落地方式。
6.2 报告里只贴一张“可复现结果表”,胜过十张截图
最后的验收文档,除了代码和 .doc 报告之外,我还会单独放一页“运行说明”,四行字:环境版本(Ubuntu 22.04 + gcc 版本)、编译命令、运行命令、实验结果存放路径。然后报告里的测试章节只贴一张统一格式的结果表,标注“可在上述命令下复现”。这样做的好处是,即使验收现场换了一台机器,老师照着运行说明就能重建环境,结果一致说明你的实验是扎实的;结果不一致,也能迅速定位是环境差异还是代码问题。
把实验做成自动化脚本这个习惯,我是在操作系统实验上吃了一次教训才养成的——那一次我改了调度算法的一个小细节,结果漏跑了一个边界用例,当场被老师问住。从那以后我每次交实验,都先写 run.sh 再碰业务代码,先保证能复现,再追求功能完整。操作系统实验的核心目的不是让你把调度算法背下来,而是让你体会“真实系统里一点小改动会产生什么连锁反应”。如果你按这篇文章把环境、代码、报告、验收四步走完,哪怕只做了其中两个实验,你的收获也会比刷十遍期末复习题更实在。希望帮到你。
本文还有配套的精品资源,点击获取