简介:操作系统实验HNU_ostep资源包面向高校操作系统课程与自学人群,以OSTEP教材为理论基础,涵盖进程管理、内存管理、文件系统、I/O系统等关键实验场景。压缩包共2000个文件、合计42.51MB,其中包含910个C源文件、760个头文件、61个Python脚本、55个Markdown文档,以及shell/cmake/make等构建辅助文件,代码与文档结构清晰,便于按模块查阅。ostep_code目录提供可直接修改运行的实验源代码,说明文档明确实验环境及提交要求,附带的教材解读和讲义则补充了底层原理,帮助理解调度算法、页面置换、文件分配等实现细节。已有164人学习下载,适合需要从理论跨越到实践的本科生、研究生及准备系统软件方向的开发者。通过动手调整调度策略、内存分配或文件系统代码,读者能直观观察不同设计对性能的影响,从而提升系统级调试、分析与设计能力。
1. 先搞清楚它在做什么:一门把《操作系统导论》课后题变成动手实验的课
很多人听到“操作系统实验”,第一反应是“又要从零写一个操作系统”。但拿到这套实验项目之后你会发现,它的主线不是从 bootloader 开始造轮子,而是沿着《操作系统导论(OSTEP)》这本书的章节,把每一章末尾的作业变成可以运行、可以评分、可以对照答案的实验。实验材料分两类:一类是 Python 写的模拟器,用来复现调度算法、内存分配、地址翻译、并发调度这些“看不见的内核行为”;另一类是 xv6 教学操作系统,用来真正动手改内核代码。它能解决的核心问题,是“上课听懂了但不会用”的断层。这套实验项目适合正在跟这门课的学生、想自学 OSTEP 的开发者,以及准备被拉去当助教的人。
2. 模拟器实验怎么做:三类经典实验的复现路径与参数调整
模拟器实验是整套 ostep 操作系统实验里门槛最低、但信息量最大的一类。说它门槛低,是因为你只需要一个 Python 解释器,没有任何内核环境依赖;说它信息量大,是因为调度、内存、并发这三块最难讲清楚的概念,都能被拆成一行命令和一个可对比的输出。这一章按“为什么做、怎么做、怎么算会了”的顺序,把三类模拟器实验的完整路径走一遍。
2.1 为什么先跑模拟器:把黑匣子变成可观察的命令行
OSTEP 每一章末尾的作业分成两类:模拟器和真实代码。模拟器是一个几百行的 Python 脚本,它内部实现了一个迷你调度器、一个内存分配器、一张页表或一个线程调度器,然后通过命令行参数让你改变策略、负载和随机种子,观察不同情况下系统行为如何变化。选型理由很实在:直接改内核,编译一次要几十秒,跑起来还要处理 qemu 环境,出错后还不容易定位;模拟器则是零依赖、秒级出结果、输出完全确定,适合把一个概念反复“折腾”。
关键认知:模拟器实验不是让你“跑出输出然后粘贴”。它的设计和评分方式,默认你会在运行之前先自己推导结果,再用-c参数打开参考答案对自己。这个“先猜后验”的节奏,才是这类实验真正的训练目标。很多同学把模拟器当计算器用,跑完就交,结果到了 xv6 改内核的部分,连“预期行为是什么”都描述不出来。
2.2 调度模拟器:scheduler.py 的最小运行与必调参数
调度实验对应 OSTEP 里 CPU 虚拟化那一章,核心是搞清楚不同调度策略下,进程的周转时间、响应时间和完成时间分别怎么变化。常见做法是用scheduler.py这个模拟器,最小命令是这样:
# 先不开 -c,自己推导完成时间和周转时间 python3 scheduler.py -p FIFO -l 3,2,1 -s 100 # 打开 -c 对答案,看每个进程的实际完成时刻 python3 scheduler.py -p MLFQ -l 3,2,1 -s 100 -c第一行命令用 FIFO 策略处理三个进程,负载分别是 3、2、1 个时间单位,随机种子是 100;第二行换成 MLFQ 策略,并打开-c让模拟器打印计算过程。逻辑上,-p决定用哪种调度策略,-l给出每个进程需要的 CPU 时间,-s是随机种子,它决定进程的到达顺序和可能的优先级初值。-c是“计算并显示答案”的开关,不开它,模拟器只输出进程列表和最终统计,逼你自己算。
调度模拟器里最值得调的参数是这几个:
| 参数 | 作用 | 建议实验 |
|---|---|---|
-p | 指定调度策略:FIFO、SJF、RR、MLFQ | 同一组负载下换策略,对比完成时间 |
-l | 设置每个进程的负载,多个值用逗号分隔 | 让负载分布从“均匀”改成“两极分化” |
-s | 随机种子,决定到达序列 | 固定种子才能复现和对比输出 |
-q | 时间片长度,RR 和 MLFQ 专用 | 从 10 改成 1,观察响应时间变化 |
-j | 进程数量,配合随机负载生成 | 和-l二选一用 |
我一般会做的第一个实验不是调策略,而是把同一个负载分别用 FIFO、SJF、RR 跑一遍,先不开-c,把每个进程的完成时间写在纸上,再打开-c对比。对不上的地方,九成不是算错,而是把“周转时间 = 完成时间 - 到达时间”和“响应时间 = 第一次被调度时间 - 到达时间”这两个定义记混了。
2.3 内存与并发模拟器:malloc.py、paging.py、threads.py 的用法对比
内存部分有三个模拟器覆盖不同场景:用户态内存分配、地址翻译、页表相关逻辑。malloc.py模拟的是堆内存分配策略,核心参数是-p指定分配策略、-A指定分配和释放序列:
# BEST 适配:先分配 100 字节,再释放前 50 字节,观察碎片情况 python3 malloc.py -p BEST -A +100,-50 # 地址翻译模拟:地址空间 64KB,物理内存 32KB,页大小 4KB python3 paging.py -a 64 -p 32 -P 4 -c第二条命令里,-a是地址空间大小,-p是物理内存大小,-P是页大小,单位都是 KB。paging.py会生成一组虚拟地址,让你手动完成页号、偏移量的拆分和页表查询,-c打印翻译结果。这里最容易忽略的是:页大小-P一变,页表项数量呈反比变化。实验时应固定地址空间、只改页大小,观察页表膨胀和碎片的变化趋势。
并发模拟器threads.py对应并发那一章,参数少但语义容易误解:-t是线程数,-l是每个线程循环次数,-i是中断频率。中断频率越大(数值越小),线程越容易被切换,竞态条件复现概率越高。很多人以为-l越大越容易出问题,实际上关键在-i,把-i从 100 改成 1,几乎每次跑都能看到中间状态被切走。
2.4 怎么确认自己真的会了:先猜答案,再开 -c 对答案
这个项目里模拟器实验的提交物,一般不是运行输出,而是你的一份推导记录:对每个用例,先写明你预测的完成时间、分配到的内存块位置或页表项内容,再贴命令和-c后的真实输出。两者一致的地方不用多看,不一致的地方才是得分点。所以实际操作时,我会把每个实验拆成三步:第一步,闭卷推导,用书里的公式手算;第二步,把推导结果写成注释放在命令上方;第三步,跑命令对答案。
# 预测:FIFO 下 P1 完成于 3,P2 完成于 5,P3 完成于 6 python3 scheduler.py -p FIFO -l 3,2,1 -s 100 -c如果预测和实际不一致,回书里找到对应公式,而不是调参数“凑出”正确答案。这一步养成的习惯,到 xv6 实验时价值极大。
提示:模拟器输出里的“答案”是参考答案,不是实验现象。提交时不要只粘输出,作业要求里一般明确写了要附推导过程,哪怕没写,附上也能在批改时少很多争议。
3. xv6 实验怎么做:从读内核源码到实现系统调用的完整链路
模拟器解决的是“看懂概念”,xv6 部分解决的是“会改代码”。xv6 是一套专门为教学设计的操作系统,代码量不大,但进程、调度、内存、文件系统都有,是目前高校操作系统实验里最常见的载体。一套 ostep 实验项目里的 xv6 环节,通常由几个递进的任务组成:加一个系统调用、改调度策略、处理页表相关问题。这一章按“认路、动手、验证”三个环节展开。
3.1 先认识 xv6 的代码结构:只看最关键的 4 个目录
拿到 xv6 源码先别从头读到尾。按实验要动的范围,有 4 个位置必须认清楚:
| 目录/文件 | 内容 | 实验相关点 |
|---|---|---|
kernel/ | 内核核心代码:proc.c、syscall.c、trap.c、vm.c等 | 系统调用实现、调度器、页表都在这里 |
user/ | 用户态程序,包括 shell,以及你自己写的测试程序 | 测试程序要放这里,并在 Makefile 注册 |
Makefile | 构建脚本,包含 qemu 启动参数和 UPROGS 列表 | 新增用户程序必须改它 |
mkfs/或相关工具 | 制作文件系统镜像 | 改内核启动参数时偶尔需要 |
阅读顺序建议是:先读kernel/proc.h里的struct proc,知道一个进程有哪些字段;再读kernel/proc.c里的allocproc()和scheduler(),知道进程如何创建、如何被调度;最后读kernel/syscall.c的分发表,知道用户态请求如何到达内核。按实验点读,而不是按目录顺序通读,是这套 xv6 实验里最省时间的方法。
3.2 系统调用实验:从用户态到内核态的完整链路
实验的入门任务通常是“新增一个系统调用”。以最常见的任务“获取当前进程的父进程 PID”为例,需要动 5 处代码,链路从用户态到内核态逐一打通:
// 1. kernel/syscall.h:分配系统调用号(在现有最大号之后顺延) #define SYS_getppid 23 // 2. kernel/syscall.c:声明并注册分发函数 extern uint64 sys_getppid(void); [SYS_getppid] sys_getppid, // 3. kernel/sysproc.c:实现内核函数本体 uint64 sys_getppid(void) { struct proc *p = myproc(); if (p->parent == 0) return -1; return p->parent->pid; }// 4. user/usys.S:添加用户态入口(汇编跳板) SYSCALL(getppid) // 5. user/user.h:声明用户态可见的函数 int getppid(void);这 5 处分别对应:系统调用号的全局约定、内核分发表、内核实现函数、用户态汇编入口、用户态头文件声明。漏掉第 4 处,用户程序能编译但链接不到入口;漏掉第 2 处,内核收到调用号会找不到分发函数;漏掉第 1 处,整个表项都会错位。检查顺序就按这 5 处从上到下 grep,基本能定位 90% 的问题。
注意:系统调用号的分配必须从当前最大值往后顺延,不要使用已有编号;函数名约定是
sys_前缀,大小写不能错。
3.3 调度与页表实验:改一个函数和改一张表的不同难度
系统调用做完,下一个任务通常是改调度器。xv6 默认的scheduler()是一个最简单的轮转:遍历进程表,找到第一个RUNNABLE的进程就切换过去。改成优先级调度或彩票调度,核心改动集中在一个函数:
// kernel/proc.c 中 scheduler() 的骨架(示意) for (;;) { // 关中断,释放内核锁,开始选择下一个进程 for (p = proc; p < &proc[NPROC]; p++) { if (p->state == RUNNABLE) { // 你的选择逻辑: // 默认是“遇到第一个就选” // 做作业时通常改成“遍历完所有 RUNNABLE,按优先级字段挑一个” } } }改动面积不大,但连带动作很多:在struct proc里加优先级字段、在fork()或allocproc()里初始化、在yield()的时机上判断是否抢占。这个任务最多的翻车点不在调度逻辑本身,而在“加了字段但没初始化”,导致新进程的优先级是内存里残留的垃圾值,调度行为随机出现。
页表类实验则完全不是一个量级,它要求你改vm.c里的页表创建和映射逻辑,如果做写时复制还要改trap.c的缺页处理,属于横跨内存管理和陷阱处理的综合题。做这类任务前,把walk()和mappages()这两个函数读透,比急着改代码有用得多。这类实验在 ostep 操作系统实验项目里通常被放在最后,作为区分“会写代码”和“理解系统”的分水岭。
3.4 实验验证:qemu 运行、测试程序与 gdb 断点
xv6 实验跑在 qemu 模拟器上,验证路径有三条。第一条是启动系统并手动操作:
make qemu # 编译并启动 xv6在 xv6 的 shell 里运行你写的测试程序,观察输出是否符合预期。第二条是自动评分,课程实验如果配置了评分脚本,通常执行make grade,它会一次性跑完所有用例并逐项打分。第三条是内核调试,启动时用make qemu-gdb,会拉起一个 gdb 等待远程连接,可以在内核函数上打断点、查变量,适合定位“一跑就 panic”这类问题。
手动验证时要注意:测试程序得放在user/目录,并且在Makefile的UPROGS列表里加一行,否则编译出的镜像里没有这个程序。很多同学改完代码在 xv6 的 shell 里说“找不到命令”,根因就是漏了这一步。
4. 实验避坑:5 个让代码“看起来对了但就是拿不到分”的典型问题
这套实验项目里,最常见的一类挫败感是:代码逻辑看着没问题,输出也“差不多”,但评分就是不过。这一章把模拟器和 xv6 两侧的高频问题各挑几个,按“现象、原因、解决”写清楚。
4.1 模拟器输出对不上与推导全错:这两个入门坑最隐蔽
现象一:自己跑scheduler.py的输出,和别人贴出来的答案完全对不上,连进程到达顺序都不一样,以为是实验包装错了。原因:随机种子-s不一致,或两个环境下默认 Python 版本不同导致随机序列不同。老版本的模拟器作业说明里到处是-s 0、-s 1这类种子,但并没有强制要求所有对比都固定同一个种子。解决:统一用python3执行,并且对比前确认双方命令里的-s完全一致,否则没有任何可比性。这是模拟器实验里最浪费时间的一个坑,十有八九是参数没对齐。
现象二:推导时觉得自己算得没错,一开-c发现完成时间全部错位。原因:把进程“到达时间”默认成了 0,但模拟器生成随机负载时,不同进程的到达时间可能不同;另外“周转时间 = 完成 - 到达”和“响应时间 = 首次调度 - 到达”这两个公式混用,也会导致整列数据对不上。解决:先看模拟器打印的进程列表里有没有到达时间字段,有的话把到达时间标在草稿上再算;-c输出里每一列都有公式提示,对着差异列反推是哪个公式错了。
4.2 xv6 的编译、入口与返回值:这三个翻车现场是扣分重灾区
现象三:照着文档加了 5 处代码,make报undefined reference to sys_xxx。原因:往往是syscall.h的编号加了,但syscall.c里的函数声明拼写不一致,或者extern漏了;还有人是把实现写进了syscall.c,但分发表里注册的函数名和实现函数名大小写不一致——这类问题编译器会给一个看起来吓人、实际原因很简单的链接错误。解决:按前面 3.2 的 5 个位置逐一 grep,先查syscall.c的声明,再查syscall.h的编号,最后查实现文件里的函数名大小写。这三处一致,链接一定过。
现象四:用户态程序调用新系统调用后,返回值是 -1,或者直接触发 panic。原因:xv6 的系统调用返回值约定是a0寄存器里的值就是用户态拿到的值,默认“成功返回 0,失败返回 -1”。很多实现把这条约定搞反,或者内核函数里访问了空指针,比如拿p->parent时没判断父进程是否存在,就解引用。解决:先在sysproc.c里写一个固定返回值,比如return 0;,跑通链路后再加真实逻辑;涉及指针访问前先做空值判断,尤其是进程关系相关的字段,宁可返回 -1 也不要 panic。
现象五:手动跑测试程序一切正常,make grade却挂在同一个用例上。原因:评分脚本对标准输出格式敏感。你在调试时往printf里打的额外信息,手动运行看不出来,但评分脚本会把它和期望输出做逐字符比较,多一点换行或少一个空格都算失败。解决:调试信息不要走printf,改用条件编译或专门的环境变量开关控制;提交前把调试输出全部清掉,确认输出与预期完全一致再交。
注意:评分脚本比较的是“行为”而不是“意图”。代码里把算法写对了,但输出格式不对,依然会扣分。这类问题在 xv6 实验中比算法错误本身更常见。
5. 把实验做成能力:三个进阶验证习惯
做完这一整套 ostep 操作系统实验,收获最大的往往不是某个算法的实现,而是三套“怎么验证自己写对了”的习惯。这三套习惯可以直接带走,用在你以后任何涉及系统编程的工作里。
第一个习惯:先写预测,再运行。模拟器实验里是手算完成时间,xv6 实验里是在注释里写“这行改动应该让哪个测试用例从 fail 变 pass”。预测错了不要紧,预测和结果不一致的位置,就是你理解有偏差的位置,比闷头调代码有价值得多。第二个习惯:改完一处,跑全量回归。很多人做 xv6 实验只跑自己新增的用例,结果改了scheduler()之后,原来已经通过的系统调用测试挂了。每次改动后用make grade把全部用例跑一遍,能在提交前就把副作用找出来,这比手动一个个敲命令可靠得多。
第三个习惯:用调试器代替 printf。页表类和陷阱类实验,printf 大法会让人疯掉——你根本不知道哪一行触发了缺页中断。我自己做页表相关任务时,就是在trap.c里加了一堆打印,最后靠 gdb 的 watch 命令监视某个变量在被谁改写,才定位到问题。xv6 的make qemu-gdb已经把调试链路做好了,值得花一小时学会,后面所有内核实验都会用它收尾。这三条习惯,本质上是把“写代码”这件事变成了“写代码加设计验证”。操作系统实验的评分是自动的,但工作中没有自动化的裁判,自己当裁判的能力,就是从这种“先猜后验”的日常训练里长出来的。希望你做完这套实验之后,记住的不是那几个算法,而是这一点。希望帮到你。
本文还有配套的精品资源,点击获取