简介:这份资源是北京交通大学操作系统课程的实验答案与报告合集,面向正在修读操作系统实验课、需要参考实现思路与报告写法的本科生。内容覆盖进程管理、内存管理、文件系统、死锁与资源分配、页面置换算法及权限管理等核心实验模块,每个实验均配有可运行的代码实现与对应的实验报告文档,便于对照理解调度算法、地址转换、缺页中断、LRU替换策略、银行家算法等关键知识点。压缩包共41个文件,以26个C语言源文件和5个C++源文件为主体,另有汇编、头文件、目标文件及Markdown报告文档,整体约73KB,结构按lab_1至lab_5分目录组织,便于按实验模块检索。目前已有201人学习下载,适合需要完成实验提交、复盘实现细节或准备操作系统相关考核的学习者参考借鉴。
1. 北交大操作系统实验包:从进程调度到文件系统的完整复现路径
如果你正在搜“操作系统作业”或者“操作系统实验”,大概率是两种情况:要么课设临近截止,需要一份能跑通的参考实现;要么想系统地把进程、内存、文件系统这几块串起来,但课本上的伪代码落不了地。北京交通大学这套操作系统实验包,覆盖了从汇编入门、进程控制、进程通信、页面置换到文件系统实现的完整链路,每个 lab 都配了源码和实验报告。它解决的不是“看懂概念”,而是“动手写出来、跑起来、报告能写清楚”。适合正在上操作系统课、需要交实验的学生,也适合想补操作系统底层实践但不知道从哪下手的开发者。下面按 lab 拆开讲,重点放在每个实验怎么编译、参数怎么调、容易在哪翻车。
2. lab_1 到 lab_2:从汇编入口到进程控制的落地细节
2.1 lab_1 的汇编与 C 混合编译链路
lab_1 目录里有hello_linux.asm、hello_linux.c、huibian.c、mem.c、thread.c、cpu.c、getpid.c。这个实验的核心目的是让你理解一个 C 程序从源码到进程的完整链路,以及系统调用在汇编层长什么样。hello_linux.asm是纯汇编版本的 hello world,hello_linux.c是对应的 C 版本,huibian.c大概率是混合编程的示例。
编译汇编文件时,32 位和 64 位环境的差异是第一个坑。北交大实验环境通常是 32 位 Linux 或需要加-m32参数。常见做法是:
# 编译纯汇编版本,生成 32 位可执行文件 nasm -f elf32 hello_linux.asm -o hello_linux.o ld -m elf_i386 hello_linux.o -o hello_linux # 编译 C 版本,注意加 -m32 保持位数一致 gcc -m32 hello_linux.c -o hello_linux_c # 混合编译:汇编目标文件 + C 目标文件链接 gcc -m32 -c huibian.c -o huibian.o ld -m elf_i386 huibian.o hello_linux.o -o huibian_mix这里-f elf32指定输出 32 位 ELF 格式,-m elf_i386让链接器按 32 位模式链接。如果系统没装 32 位库,会报cannot find -lc或skipping incompatible,需要先装gcc-multilib。getpid.c和thread.c是用来观察进程 ID 和线程创建的,cpu.c和mem.c可能是模拟 CPU 调度和内存访问的辅助代码。这个 lab 的验收点通常是:你能说清楚int 0x80和syscall的区别,以及为什么 64 位系统跑 32 位汇编需要额外链接参数。
2.2 lab_2 进程创建与同步的代码结构
lab_2 目录下有2-2.c、2-3.c、2-4.c、2-3_m.c。从命名看,这是四个递进的进程控制实验。2-2.c大概率是fork()基础用法,2-3.c涉及进程同步或信号,2-4.c可能是管道或共享内存,2-3_m.c是2-3.c的修改版(m 代表 modified)。
以fork()为例,最常见的翻车点是父子进程的输出顺序和文件描述符继承:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { perror("fork failed"); return 1; } else if (pid == 0) { // 子进程:pid 返回 0 printf("child pid=%d, parent pid=%d\n", getpid(), getppid()); _exit(0); // 用 _exit 避免刷新父进程的 stdio 缓冲区 } else { // 父进程:pid 返回子进程 ID printf("parent pid=%d, child pid=%d\n", getpid(), pid); wait(NULL); // 等待子进程结束,避免僵尸 } return 0; }关键参数说明:fork()返回值三次含义——负数是失败,0 是子进程,正数是父进程里拿到的子进程 PID。wait(NULL)的参数是子进程退出状态指针,传 NULL 表示不关心。_exit(0)和exit(0)的区别在于前者不刷新 stdio 缓冲区,在 fork 场景下能避免输出重复。2-3_m.c如果是修改版,通常改的是同步方式,比如从wait()改成信号量或sleep()轮询。这个 lab 的验收点是你能画出进程树,并解释fork()后父子进程的地址空间是写时复制。
3. lab_3 进程通信:Sender/Receiver 与 FIFO 的四种组合
3.1 共享内存与消息传递的代码对照
lab_3 是这套实验包里文件最多的部分:Sender_1.c、Receiver_1.c、Sender_2.c、Receiver_2.c、Sender_3.c、Receiver_3.c、fifo_send.c、fifo_rcv.c、pipe.c、Client.c、Server.c、3-1.c、3-2_1.c、3-2_2.c、3-3_1.c、3-3_2.c。从命名规律看,Sender/Receiver 成对出现,编号 1/2/3 对应三种 IPC 机制:管道、FIFO、共享内存或消息队列。Client.c和Server.c是 C/S 模式的 socket 通信。
先看 FIFO 的发送端和接收端:
// fifo_send.c:写端 #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> int main() { // 创建命名管道,权限 0666 mkfifo("/tmp/myfifo", 0666); int fd = open("/tmp/myfifo", O_WRONLY); // 阻塞直到有读端打开 char msg[] = "hello from sender"; write(fd, msg, sizeof(msg)); close(fd); return 0; }// fifo_rcv.c:读端 #include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("/tmp/myfifo", O_RDONLY); // 阻塞直到有写端打开 char buf[64]; read(fd, buf, sizeof(buf)); printf("received: %s\n", buf); close(fd); unlink("/tmp/myfifo"); // 用完删除,否则下次 mkfifo 报 File exists return 0; }参数说明:mkfifo的第二个参数是权限位,实际权限还受 umask 影响。open的O_WRONLY和O_RDONLY默认阻塞,如果只想非阻塞打开,加O_NONBLOCK,但那样在无对端时会直接返回失败。unlink是必须的,否则第二次运行mkfifo会报文件已存在。pipe.c是匿名管道版本,只能在有亲缘关系的进程间用,不需要mkfifo和unlink。
3.2 编译顺序与运行配对
Sender/Receiver 必须成对运行,且顺序有讲究。FIFO 的open是阻塞的,如果先跑发送端,它会卡在open直到接收端也打开同一个 FIFO。常见做法是两个终端分别跑:
# 终端 1 gcc fifo_rcv.c -o fifo_rcv && ./fifo_rcv # 终端 2 gcc fifo_send.c -o fifo_send && ./fifo_send如果先跑发送端,终端 2 会挂起,这是正常现象,不是死锁。Sender_1.c和Receiver_1.c如果是共享内存版本,需要先shmget创建共享段,再shmat挂载,最后shmdt卸载、shmctl删除。共享内存的坑在于:一个进程写完后,另一个进程不一定立刻看到,需要配合信号量或msync做同步。Client.c和Server.c是 socket 版本,编译时可能需要加-lpthread,运行时要先起 Server 再起 Client,端口号在代码里写死的话,被占用会报Address already in use,用SO_REUSEADDR可以缓解。
4. lab_4 与 lab_5:页面置换算法和文件系统的实现要点
4.1 页面置换算法的参数与命中率验证
lab_4 只有一个文件页面置换算法.cpp和报告操作系统实验四_页面置换算法.md。这个实验要求实现 FIFO、LRU、OPT 等页面置换算法,并统计缺页率和命中率。核心逻辑是模拟一个固定大小的物理块数组,按引用串逐个访问:
#include <iostream> #include <vector> #include <algorithm> using namespace std; // LRU:每次访问把页面移到最近使用位置,淘汰最久未用的 int lru(vector<int>& frames, vector<int>& ref, int capacity) { int faults = 0; vector<int> recent; // 最近使用顺序,尾部最新 for (int page : ref) { auto it = find(frames.begin(), frames.end(), page); if (it == frames.end()) { faults++; if ((int)frames.size() < capacity) { frames.push_back(page); } else { // 淘汰 recent 头部的页面 int victim = recent.front(); recent.erase(recent.begin()); replace(frames.begin(), frames.end(), victim, page); } } // 更新 recent:先删旧位置,再压入尾部 recent.erase(remove(recent.begin(), recent.end(), page), recent.end()); recent.push_back(page); } return faults; }参数说明:capacity是物理块数,通常取 3 或 4;ref是页面引用串,实验报告里一般会给固定序列。faults是缺页次数,命中率 = 1 - faults/ref.size()。这个实验的坑在于:LRU 的“最近使用”顺序维护容易写错,尤其是页面已在内存但需要更新顺序时。另一个坑是 OPT 算法需要预知未来引用串,实现时要扫描当前位置之后的所有引用,找最远才被访问的页面淘汰。报告里通常要求对比不同 capacity 下的命中率曲线,capacity 从 3 增到 5 时,FIFO 可能出现 Belady 异常(缺页率反而上升),这是报告里值得写的分析点。
4.2 lab_5 文件系统的模块划分与编译
lab_5 是文件系统实现,文件包括io.cpp、io.h、file.cpp、file.h、main.cpp、init_disk.cpp、init.o、main.o、ldisk.txt。这是一个模拟磁盘文件系统的 C++ 项目,ldisk.txt是虚拟磁盘文件,init_disk.cpp负责格式化,io.cpp封装底层读写,file.cpp实现文件操作,main.cpp是测试入口。
编译顺序很重要,因为init.o和main.o已经预编译了,但如果你改了源码需要重新生成:
# 重新编译所有模块 g++ -c init_disk.cpp -o init.o g++ -c io.cpp -o io.o g++ -c file.cpp -o file.o g++ -c main.cpp -o main.o # 链接 g++ init.o io.o file.o main.o -o fs # 先格式化虚拟磁盘,再运行 ./fs init ./fs参数说明:ldisk.txt是磁盘镜像文件,大小决定了文件系统的容量。init_disk.cpp里的init命令会清空ldisk.txt并写入超级块、inode 位图和空闲块位图。常见坑是:如果ldisk.txt不存在,init会创建;但如果已存在且格式不对,init可能不覆盖,需要手动删除再跑。io.h和file.h里的结构体定义要和报告里的设计一致,否则读写偏移对不上。这个实验的验收点通常是:能创建文件、写入内容、读回内容、删除文件,并且ldisk.txt的二进制布局和报告里的磁盘结构图能对应上。
5. 避坑与排查:这套实验包最容易翻车的五个地方
5.1 32 位与 64 位环境混用导致链接失败
现象:编译 lab_1 的汇编文件时报ld: i386 architecture of input file is incompatible with i386:x86-64 output,或者 C 文件链接汇编目标文件时报undefined reference to printf。
原因:汇编用nasm -f elf32生成 32 位目标文件,但gcc默认按 64 位链接,位数不匹配。另外 32 位链接需要 32 位的 libc,系统没装gcc-multilib时找不到。
解决:所有编译命令统一加-m32,汇编用-f elf32,链接用ld -m elf_i386。如果报缺库,执行sudo apt install gcc-multilib。实在不行就在 32 位虚拟机里跑,别在 64 位环境硬扛。
5.2 FIFO 文件残留导致 mkfifo 失败
现象:第二次运行fifo_send.c时,mkfifo返回 -1,perror输出File exists。
原因:上一次运行创建的/tmp/myfifo没有被删除,mkfifo要求路径不存在才能创建。
解决:在fifo_rcv.c末尾加unlink("/tmp/myfifo"),或者在fifo_send.c开头先unlink再mkfifo。更稳妥的做法是用mkfifo的返回值判断,如果失败且errno == EEXIST,就直接open复用已有 FIFO。
5.3 共享内存未同步导致读到脏数据
现象:Sender 写入共享内存后,Receiver 立刻读取,但读到的还是旧内容或全零。
原因:共享内存本身没有同步机制,写进程shmat后写入,读进程可能还在shmget或shmat阶段,或者 CPU 缓存没刷新。
解决:配合信号量(sem_open/sem_wait/sem_post)做读写同步,或者在写入后调用msync(addr, len, MS_SYNC)强制刷回。简单场景可以用sleep(1)临时规避,但报告里别这么写,会被扣分。
5.4 页面置换算法命中率统计口径不一致
现象:自己算的命中率和报告里的对不上,或者不同算法之间比较时结果反直觉。
原因:命中率的定义有两种——命中次数/总访问次数,或者 1 - 缺页次数/总访问次数。另外,首次访问空物理块算不算缺页,不同实现有差异。
解决:统一口径,在报告里写明“缺页次数包含首次装入”。验证时用一个小引用串手工算一遍,比如1 2 3 4 1 2 5,capacity=3,FIFO 缺页 6 次,LRU 缺页 5 次,对不上就检查代码里的计数逻辑。
5.5 文件系统 init 未清空旧磁盘镜像
现象:运行./fs init后,创建文件报“无空闲块”,但明明是刚格式化的。
原因:init_disk.cpp可能只写了超级块,没有重置位图;或者ldisk.txt是只读的,写入失败但没报错。
解决:先rm ldisk.txt再./fs init,确保从零创建。检查init_disk.cpp里是否对 inode 位图和空闲块位图做了memset清零。如果ldisk.txt权限不对,chmod 666 ldisk.txt再试。
6. 进阶验证:用 strace 和 gdb 把实验代码看透
这套实验包的价值不只是“能跑”,而是你能通过它看清系统调用的真实行为。我一般会拿strace跟一遍 lab_2 和 lab_3 的可执行文件,观察fork、clone、open、read、write的实际参数和返回值。比如跑strace ./2-2,你能看到clone系统调用创建子进程时传的标志位,比看fork()手册直观得多。
# 跟踪进程创建和文件操作,-f 表示跟随子进程 strace -f -e trace=clone,open,read,write ./2-2 # 跟踪 FIFO 通信,看 open 的阻塞行为 strace -f -e trace=open,read,write ./fifo_rcv-f是关键参数,不加的话子进程的系统调用看不到。-e trace=过滤只关心的调用,输出更干净。如果你想知道fork()之后父子进程的地址空间差异,用gdb在fork前后打印&变量的地址,会发现虚拟地址相同但物理页不同,这就是写时复制的直观证据。
对于 lab_5 文件系统,验证方法是把ldisk.txt用xxddump 出来,对照报告里的磁盘布局图,看超级块、inode 区、数据区的偏移是否一致:
# 查看磁盘镜像前 512 字节的十六进制和 ASCII xxd -l 512 ldisk.txt # 对比创建文件前后的差异 cp ldisk.txt ldisk_before.txt ./fs create test.txt xxd ldisk.txt > after.hex xxd ldisk_before.txt > before.hex diff before.hex after.hex这样能确认文件创建时到底改了哪些块,比只看代码可靠。从那以后我每次拿到文件系统实验,都强制先 dump 磁盘镜像再写代码,不然改了半天不知道数据落在哪。希望这套实验包能帮你把操作系统的几个核心模块真正跑通,报告也有实打实的验证数据可写。
本文还有配套的精品资源,点击获取