恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
操作系统实验进阶:从OSTEP模拟器到xv6内核动手改代码
首页
资讯中心
/
操作系统实验进阶:从OSTEP模拟器到xv6内核动手改代码
操作系统实验进阶:从OSTEP模拟器到xv6内核动手改代码
发布时间:2026/10/17 16:50:28
简介操作系统实验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 完成于 3P2 完成于 5P3 完成于 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已经把调试链路做好了值得花一小时学会后面所有内核实验都会用它收尾。这三条习惯本质上是把“写代码”这件事变成了“写代码加设计验证”。操作系统实验的评分是自动的但工作中没有自动化的裁判自己当裁判的能力就是从这种“先猜后验”的日常训练里长出来的。希望你做完这套实验之后记住的不是那几个算法而是这一点。希望帮到你。本文还有配套的精品资源点击获取