北交大操作系统实验包:进程调度到文件系统完整复现与避坑指南
发布时间:2026/10/5 7:16:17 作者:尧图编辑部 阅读量:1,286

简介这份资源是北京交通大学操作系统课程的实验答案与报告合集面向正在修读操作系统实验课、需要参考实现思路与报告写法的本科生。内容覆盖进程管理、内存管理、文件系统、死锁与资源分配、页面置换算法及权限管理等核心实验模块每个实验均配有可运行的代码实现与对应的实验报告文档便于对照理解调度算法、地址转换、缺页中断、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 worldhello_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(vectorint frames, vectorint ref, int capacity) { int faults 0; vectorint 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 或 4ref是页面引用串实验报告里一般会给固定序列。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返回 -1perror输出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 5capacity3FIFO 缺页 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 traceclone,open,read,write ./2-2 # 跟踪 FIFO 通信看 open 的阻塞行为 strace -f -e traceopen,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 磁盘镜像再写代码不然改了半天不知道数据落在哪。希望这套实验包能帮你把操作系统的几个核心模块真正跑通报告也有实打实的验证数据可写。本文还有配套的精品资源点击获取