操作系统实验包实战:环境配置、调度算法与内存管理避坑指南 简介面向福州大学《操作系统原理》课程的教学实验资源包内含一套名为 epos-x86 的简化操作系统及配套工程文件。它面向计算机专业学生用于通过动手实验理解进程调度、内存分配、文件系统、设备中断、线程同步与死锁预防等核心机制。该实验操作系统在保持精简的同时覆盖了完整内核模块适合课堂配套实验与课后自学。资源共79个文件以36个C源文件和30个头文件为主配合4个汇编文件、Makefile构建脚本及说明文档压缩包仅271KB体积小巧但结构完整。包内包含内核代码、用户程序、构建配置与说明文档目录划分清晰便于按模块查阅。已有75人学习下载。实验代码覆盖关键知识点例如轮转与优先级调度、首次适应内存分配、基础文件系统实现、键盘与显示设备模拟、信号量互斥操作、银行家算法以及简单权限控制便于学生在真实代码中调试运行、加深对操作系统设计思想的理解。1. 从课程实验包到一份能跑的代码这袋东西到底值不值得接拿到“福州大学《操作系统原理》实验操作系统.zip”这个名字大概率不是普通的课件打包而是把整门课的实验框架、参考代码、实验指导书和评分说明一起压进了包里。对于正在修这门课的同学这个包的意义不是“下载完看一眼”而是能让你从“操作系统原理在讲什么”直接落到“我能不能在 Linux 里亲手写一个调度器、改一页页表、看内核怎么切换进程”。这篇笔记就围绕这个 zip 包展开讲清楚你拿到压缩包之后该怎么拆、怎么搭环境、怎么把每个实验从“看得懂”调到“能运行、能交作业、能拿分”顺带把我在教学和答疑里见过的典型翻车点都列出来。适合正在做操作系统实验的本科生也适合想用一批现成实验题快速补操作系统基本功的从业者。2. 拆包前的判断先搞清楚这个 zip 里装的是什么实验体系2.1 从文件后缀和目录命名反推实验环境在解压之前先别急着双击。常见做法是先看一眼压缩包内部的文件结构这能帮你判断实验是基于 Linux C、基于某个模拟器还是基于评测脚本。操作系统的本科实验一般有三种形态一种是在真实 Linux 内核里加模块、改系统调用用 QEMU 启动第二种是用大学自研的教学操作系统给你一个可编译的内核骨架让你补全进程调度、内存管理第三种是一批用户态小程序模拟 FIFO/LRU 页面置换、生产者消费者、银行家算法等。命名里带“实验操作系统”这种字样更像第二种或第三种给你一条能编译的内核或框架不是让你从头写一万行。解压时建议在 Linux 环境里做Windows 上双击解压很容易把符号链接、可执行权限和文件名编码搞坏。强烈建议用 bash 的 unzip 命令mkdir -p os-lab unzip 福州大学 《操作系统原理》实验操作系统.zip -d os-lab cd os-lab find . -maxdepth 2 -type f | head -50-d指定解压目录避免把所有文件直接摊在当前目录find只列两层够你快速看到顶层结构。执行完之后重点关注三类文件.c或.h源码、Makefile或.sh构建脚本、README或实验指导*.pdf实验要求。如果顶层还有schedule/、memory/、fs/这种按实验划分的子目录说明实验是模块化设计的每个目录对应一次作业这种组织方式最省心——交作业时按目录打包即可。2.2 用 README 和 Makefile 判断“能不能跑”拿到一个实验包最忌讳一上来就打开某个.c文件从头读到尾。先看构建系统构建系统能跑代码再烂都有机会调。打开 Makefile 或 README重点确认三件事需要哪个版本的 GCC、实验是否只支持 32 位编译-m32、内核实验是否需要 root 权限或额外工具链。grep -n -E gcc|g\\|m32|qemu|bochs|depmod|insmod Makefile 2/dev/null | head -20如果你的发行版是 64 位 Ubuntu/Debian而 Makefile 里频繁出现-m32恭喜你第一个大坑已经出现了——缺 32 位库。常见做法是安装多架构支持sudo dpkg --add-architecture i386 sudo apt update sudo apt install -y gcc-multilib g-multilib libc6-dev-i386gcc-multilib让 gcc 能输出 32 位程序libc6-dev-i386提供 32 位头文件和库。如果实验包基于 QEMU.qcow2 镜像、grub 配置、vmlinuz 文件你还得装qemu-system-x86。这些都配好之后直接执行make能出kernel可执行文件或.ko模块说明这条链路是通的后面所有实验都站在这个地基上。3. 把第一个实验跑通的完整路径从编译到看到输出3.1 最小可运行的编译与启动命令无论实验是内核还是用户态模拟第一关都是“编译通过”。我一般会把操作拆成四步读 Makefile 的默认目标、执行编译、处理第一个报错、运行产物。这里用一个典型的用户态实验骨架举例——假设实验包里有sched_demo.c模拟时间片轮转调度#include stdio.h #include stdlib.h #include string.h #define MAX_PROC 16 #define TIMESLICE 2 typedef struct { char name[16]; int need; // 剩余需要执行的 CPU 时间 int arrived; // 到达时间 } proc_t; static proc_t procs[MAX_PROC]; static int pcnt 0; // 模拟时间片轮转每次从就绪队列头取进程执行 TIMESLICE 后放回队尾 void round_robin(void) { int t 0, done 0; while (done pcnt) { for (int i 0; i pcnt; i) { if (procs[i].need 0 || procs[i].arrived t) continue; int run procs[i].need TIMESLICE ? procs[i].need : TIMESLICE; printf([t%2d] %s 运行 %d 单位\n, t, procs[i].name, run); procs[i].need - run; t run; if (procs[i].need 0) { printf([t%2d] %s 结束\n, t, procs[i].name); done; } } } } int main(void) { strcpy(procs[pcnt].name, A); procs[pcnt].need 5; procs[pcnt].arrived 0; pcnt; strcpy(procs[pcnt].name, B); procs[pcnt].need 3; procs[pcnt].arrived 1; pcnt; strcpy(procs[pcnt].name, C); procs[pcnt].need 2; procs[pcnt].arrived 2; pcnt; round_robin(); return 0; }这段代码的关键在于run procs[i].need TIMESLICE ? procs[i].need : TIMESLICE这行——它处理了“剩余时间小于时间片”的情况否则进程会被多扣时间。编译时用gcc -Wall -o sched_demo sched_demo.c-Wall开全部警告如果你在实验框架里改代码警告里藏着的未初始化变量会在运行时给你颜色看。跑起来后[t ]每行代表一个调度决策你可以对照理论课上的甘特图验证输出。3.2 改成你自己的调度算法参数与结构怎么动这套骨架最常见的实验要求是“把时间片轮转改成多级反馈队列”——多一个优先级数组每个优先级一个时间片长度新进程进最高优先级队列时间片耗尽且未完成就降级。改的部分从run ...那一行开始换成“按优先级找下一个进程”外层循环不再是简单轮转。这里给你一个一次性替换round_robin()函数的高优先级进程抢占版本#define LEVELS 3 static int prio_levels[LEVELS] {1, 2, 4}; // 每层时间片长度 void mlfq(void) { int t 0, done 0; while (done pcnt) { int picked -1, lvl LEVELS; for (int i 0; i pcnt; i) { if (procs[i].need 0 || procs[i].arrived t) continue; // 找当前就绪进程中优先级最高的我们用 arrive 顺带标记了初始优先级 if (1 lvl) { picked i; lvl 1; } } if (picked 0) { t; continue; } // 没有就绪进程时间推进 int run procs[picked].need prio_levels[0] ? procs[picked].need : prio_levels[0]; printf([t%2d] %s 运行 %d 单位 (优先层 0)\n, t, procs[picked].name, run); procs[picked].need - run; t run; if (procs[picked].need 0) { printf([t%2d] %s 结束\n, t, procs[picked].name); done; } } }这段简化代码就是让你理解结构多级队列本质是“不同层的进程拿不同长度的时间片”实现时只需要维护prio_levels[]数组层内调度还是轮转。实验包的代码里如果已经有task_struct或pcb结构你把名字替换成它自带的字段其余逻辑可以直接平移。交作业时记得把这个函数命名成实验指导书要求的名字——很多实验评分是脚本自动调用固定函数名你改了名字编译过了但测试脚本调不到分数全丢。3.3 从“编译通过”到“输出正确”的验证方法编译过了只是第一步输出对不对要用多种输入去验。最常见的方法是自己构造三组数据所有进程同时到达、进程间隔到达、某个进程执行时间特别长。第一组测调度算法是否按预期轮转第二组测“到达时间”处理是否正确第三组测时间片轮转在长任务下的公平性。跑输出后不要肉眼看用 diff 和参考输出对拍./sched_demo input1.txt my_out1.txt diff -u ref_out1.txt my_out1.txtref_out1.txt是实验包自带的参考输出或者你自己手算的期望输出。diff 无输出就是通过有和的行就是在告诉你哪里不一致。这一步在交作业前能拦住 80% 的逻辑错误——很多代码“看起来对一 diff 全是错位”。4. 内存管理和同步实验原理、参数与实现的边界4.1 页面置换算法的实验套路FIFO 到 LRU 的不只是“换一个数据结构”操作系统的内存实验通常是给你一个页访问序列让你统计缺页次数。LRU 看起来就是“把最久未用的页换出去”但实现里的坑在“怎么记录最久未用”。很多学生用双向链表每次访问一个页就把它移到链表头缺页时淘汰链表尾。这个做法对但要注意模拟器的访问序列可能长达几万条链表查找是 O(n)跑大用例会明显卡顿评测脚本超时直接判零。常见做法是用“时间戳数组”代替链表#include stdio.h #include string.h #include limits.h #define FRAME_NUM 4 #define PAGE_NUM 10 static int frames[FRAME_NUM]; // 物理块里放的页号 static int last_use[FRAME_NUM]; // 每个物理块最近被访问的时间 static int access_seq[] {1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5}; static int seq_len 12; int lru_page_faults(void) { int faults 0, time 0; memset(frames, -1, sizeof(frames)); memset(last_use, -1, sizeof(last_use)); for (int i 0; i seq_len; i) { int page access_seq[i]; int found 0; for (int j 0; j FRAME_NUM; j) { if (frames[j] page) { found 1; last_use[j] time; // 命中也要更新使用时间 break; } } if (found) continue; int victim 0; for (int j 1; j FRAME_NUM; j) if (last_use[j] last_use[victim]) victim j; // 找最旧 frames[victim] page; last_use[victim] time; faults; } return faults; }逻辑就那两行last_use[j] time在命中时更新last_use[j] last_use[victim]选最旧淘汰。参数FRAME_NUM对应实验指导里的物理块数一般让填 3、4、5 各跑一遍看缺页率变化趋势access_seq是给好的局部性访问串实验包给的序列通常故意让 FIFO 和 LRU 产生不同的缺页数方便对比。如果你把last_use改成fifo_time数组且只在缺页时更新那就是 FIFO 了——两种算法代码相似度极高区别就在“命中时有没有更新时间”。4.2 信号量同步实验为什么你的生产者消费者老死锁同步实验里写得最多的是生产者消费者坑通常在两个地方缓冲区满了生产者还往里写、多个生产者同时操作同一个队列下标。教 Linux 线程用pthread_mutex_t加条件变量但实验包必须你自己补全的条件变量等待逻辑很多人漏了“while 而不是 if”。这里有典型片段#include pthread.h #include stdio.h #define BUFFER_SIZE 8 static int buffer[BUFFER_SIZE]; static int in 0, out 0, cnt 0; static pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t not_full PTHREAD_COND_INITIALIZER; static pthread_cond_t not_empty PTHREAD_COND_INITIALIZER; void *producer(void *arg) { for (int i 0; i 100; i) { pthread_mutex_lock(mtx); while (cnt BUFFER_SIZE) // 必须用 while防止虚假唤醒 pthread_cond_wait(not_full, mtx); buffer[in] i; in (in 1) % BUFFER_SIZE; cnt; pthread_cond_signal(not_empty); pthread_mutex_unlock(mtx); } return NULL; } void *consumer(void *arg) { for (int i 0; i 100; i) { pthread_mutex_lock(mtx); while (cnt 0) pthread_cond_wait(not_empty, mtx); int val buffer[out]; out (out 1) % BUFFER_SIZE; cnt--; pthread_cond_signal(not_full); pthread_mutex_unlock(mtx); } return NULL; }while (cnt BUFFER_SIZE)那句是大多数人翻车的地方如果你写成if多个消费者同时被唤醒时第一个拿走了数据第二三个醒来发现缓冲区已经空了却接着往下执行读到的就是脏数据。这是教科书和实验报告里反复强调的“虚假唤醒 (spurious wakeup)”问题评测脚本会专门构造多消费者并发来测它。编译验证命令gcc -pthread -o pc pc.c ./pc。另外注意in、out是共享变量千万不要图省事写成static然后忘了加锁——实验环境多核跑起来不加锁的 i 都不是原子的能让你看到完全随机的输出。4.3 这些实验的评分逻辑不是“能跑”就给分实验课评分通常看三样程序运行结果、代码结构、实验报告。运行结果看输出对不对代码结构看你是不是硬编码了答案比如直接把缺页次数写成 5 交上去这类作弊老师一眼就看穿实验报告看有没有分析不同参数下的结果差异。所以你在做内存实验时建议把 FRAME_NUM 从 3 到 6 各跑一遍记录缺页次数并画个趋势做调度实验时把时间片从 1 调到 8记录平均周转时间变化。这些数据是报告里最有说服力的部分也是面试时讲项目经历最好的素材。5. 避坑交作业前最容易翻车的 6 个问题5.1 中文注释在 Linux 下乱码现象Windows 上编辑的 .c 文件注释是 GBK 编码Linux 的 gcc 默认按 UTF-8 读编译时报错expected declaration specifiers或注释变成乱码。原因源码文件本身编码与编译器预期不一致gcc 不作编码转换只按字节流读。解决统一用 UTF-8。在 VSCode 右下角点编码选“通过编码保存”为 UTF-8。批量转换用iconv -f GBK -t UTF-8 sched_demo.c sched_demo_utf8.c mv sched_demo_utf8.c sched_demo.c。以后在 Windows 上写代码第一件事就是右下角确认编码。5.2 实验包是 32 位的宿主机缺库编译不过现象make报fatal error: bits/libc-header-start.h: No such file or directory或cannot find -lc。原因Makefile 里写了-m32但 64 位系统没装 32 位库。Ubuntu 22.04 以后默认不带 multilib。解决按本文 2.2 节执行sudo dpkg --add-architecture i386和gcc-multilib安装。装完再make clean make别只 make——之前失败的中间产物可能残留。5.3 自己写的代码把main函数删了现象编译报multiple definition of main或undefined reference to main。原因实验框架自带main.c指导让“补全函数”你却在另一个 .c 文件里又写了一个 main。或者反过来你把框架的 main 误删了。解决先grep -rn int main .看有几个。框架代码只留一个 main其余改成被调用的函数。如果实验要求“独立可执行”那就把框架自带 main 注释掉保留你自己的。5.4 时间片轮转里进程“超时”运行现象输出显示某个进程一次运行了 3 个时间片但时间片是 2。原因循环里先执行再判断need导致多扣了一个时间片的量或者run变量在进程剩余时间小于时间片时算错了。解决检查run的计算是否用了procs[i].need TIMESLICE ? procs[i].need : TIMESLICE这个三元表达式并且t run用的是 run 而不是 TIMESLICE。5.5 pthread 程序编译没加 -pthread现象链接报错undefined reference to pthread_create。原因glibc 2.34 之前 pthread 库需要显式链接现在新版系统的报错位置有时诡异但本质一样。解决GCC 命令加-pthread注意顺序gcc -pthread -o pc pc.c。如果 Makefile 里没有就在CFLAGS或LDFLAGS里补。5.6 脚本评测找不到你的输出文件现象评测跑完你的程序报File not found或output format error。原因实验指导书规定输出到result.txt但你用printf打到了 stdout或文件名大小写不对。解决先看指导书里的输出要求是文件还是标准输出每行字段分隔符是空格还是逗号。如果没写参考实验包自带样例的格式。在代码末尾加一句fprintf(fp, ...)生成文件不要只靠 stdout。6. 进阶用法给自己写一个一键验收脚本让每次改动都立刻被验证做操作系统实验最怕的是“改崩了不知道什么时候改崩的”。等你在第 3 个实验里改了调度函数第 1 个实验的输出被连带影响你根本记不住哪个版本是对的。我给自己带的本科生定了一条规矩每个实验建一个test/目录放一个验收脚本。#!/usr/bin/env bash # text/run_all.sh set -euo pipefail cd $(dirname $0)/.. make clean /dev/null 21 make /dev/null echo 实验1: 调度算法 ./sched_demo test/input1.txt test/my1.txt diff -u test/ref1.txt test/my1.txt echo PASS: 调度实验1 echo 实验2: LRU 缺页统计 ./mem_demo 4 test/ref_page.txt test/my2.txt diff -u test/ref2.txt test/my2.txt echo PASS: 内存实验2 echo 实验3: 生产者消费者 ./pc test/my3.txt diff -u test/ref3.txt test/my3.txt echo PASS: 同步实验3脚本做的事很简单clean 后 make保证构建是干净的每个实验都跑一遍标准输入输出到 my 文件再用 diff 和参考输出对拍任何不一致都有明确 PASS/FAIL。set -euo pipefail让你的脚本遇到第一条错误命令就停下来不会带着半残的二进制文件继续跑。写完脚本之后每次改完代码只执行bash test/run_all.sh出现 FAIL 就顺着 diff 定位。这个习惯的价值在最后一周集中调实验时体现得特别明显——你不用担心改 A 实验碰坏 B 实验脚本会告诉你一切。有了验收脚本还剩一件事把实验报告里的数据补严谨。我的习惯是每个核心实验都跑至少两组参数把输出保存成带时间戳的文件——调度实验跑时间片 2 和 4内存实验跑物理块 3 和 5同步实验跑生产者/消费者数量 1/1 和 3/2。对比这些数据报告里的“结果分析”才有的写也是真正把原理内化的过程。这些习惯累积下来实验课拿高分是顺带的更大的收获是你亲手建过进程、换过页、加过锁以后面试被问到“操作系统里调度器怎么设计”“LRU 怎么实现”时脑子里有图、手上有代码而不是只有教科书上的概念。希望帮到你。本文还有配套的精品资源点击获取