资讯动态

南大OSLabs实操指南:基于xv6的四个内核Lab与踩坑记录

发布时间:2026/9/2 4:22:26 来源:尧图企业网站定制
简介面向操作系统课程学习者与实验开发者的南京大学操作系统实验完整框架包覆盖实模式/保护模式Hello World、系统调用printf实现、进程切换与Fork/Sleep/Exit、信号量同步等核心实验环节适合高校学生对照课程要求进行内核编程实践。包体共107个文件包含31个C源文件、43个头文件、15个Makefile构建脚本、8个汇编文件及7个Perl辅助脚本另有Git忽略与说明文档整体约65KB目录按lab1至lab4分模块组织便于按阶段查阅。已有1879人浏览学习。借助这套框架可快速搭建QEMUUbuntu实验环境理解Bootloader启动流程、中断与系统调用实现、进程调度与同步机制所有源码与配置文件齐全可直接在此基础上修改调试节省从零编写底层代码的时间。 OSLabs这套实验我在带学生和自己复刷的时候反复折腾过好几轮今天把完整的踩坑记录和实操路径整理出来。如果你正准备刷南京大学的操作系统实验或者想找个能真正动手写内核的入门项目这篇文章应该能帮你省下大量绕路时间。先说这个项目是什么。OSLabs对应的是南京大学计算机系的操作系统课程实验整体基于xv6这个教学操作系统内核展开。xv6是MIT开发的类Unix教学内核代码量控制在几千行级别用RISC-V指令集专门为课堂设计能跑通完整的进程管理、虚拟内存、文件系统。南大这套实验把它做成了四个递进的Lab每个Lab都会往内核里加东西从进程管理到虚拟内存再到写时复制一路做到用户态线程库。它不是一个“看着视频敲代码”的课而是真的要求你读懂内核源码、改内核、跑通测试最终理解操作系统到底在干什么。很多初学者问的第一个问题是为什么不直接学Linux内核非要拿xv6练手。答案很现实Linux内核几千万行代码新手连从哪下手都不知道。xv6把操作系统最核心的概念浓缩在不到一万行代码里你能在几周内通读全部源码并且亲手改它、跑它、调试它。南大招办和课程团队把这个实验公开出来之后基本成了国内高校操作系统实验的天花板之一也是很多考研党复试前突击内核实战的首选资料。1. 项目整体思路与实验设计1.1 为什么选xv6而不是别的内核先说说南大为什么选xv6。操作系统实验要落地教学内核得满足几个条件一是代码量要少到学生能在学期内读完二是功能要完整到能展示操作系统核心机制三是有成熟的模拟器支持让学生不需要真实硬件就能跑内核。xv6完美踩中这三个点。代码量大约六千行C语言加少量汇编一个学期通读完全可行。功能上覆盖了进程调度、虚拟内存、文件系统、中断异常、系统调用等全部核心子系统麻雀虽小五脏俱全。运行环境用QEMU模拟RISC-V机器不需要买开发板笔记本上就能跑调试手段也比真机丰富得多。另外一个被很多人忽略的点是xv6的源码风格非常接近真实内核。它没有为了教学把代码过度简化而是保留了真实内核的关键结构比如进程控制块、页表层级、inode缓存这些概念你在xv6里搞懂了看Linux内核的时候会发现底层逻辑是相通的。1.2 四个Lab的递进逻辑南大OSLabs的四个Lab设计有一条清晰的递进线从“会用”到“能改”再到“能造”每一步都踩在上一步的基础上。第一个Lab是入门热身核心是理解xv6的进程模型和系统调用机制。任务量不大但需要你真正读代码、跑通内核、学会调试工具。第二个Lab开始上强度要求实现内核线程和用户态线程库需要你理解线程切换的本质在用户态模拟内核调度器。第三个Lab进入内存管理实现页表、缺页异常和mmap这一步是很多人的分水岭虚拟内存的概念从“课本上的图”变成“自己写的代码”。第四个Lab做写时复制Copy-on-Write属于虚拟内存和进程管理的综合实战要处理的问题非常接近真实内核面临的问题。这四步走完你对操作系统核心机制的理解深度会完全不一样。前三个Lab对应的是“书上怎么说的”第四个Lab对应的是“实际工程里怎么做的”。2. 实验环境搭建与工具链2.1 开发环境配置Windows/WSL2/macOS/Linux环境搭建是第一道坎很多人在这一步卡了一两天。先说结论最省心的方案是装一个原生Linux虚拟机或双系统其次是Windows下用WSL2macOS也能跑但有些兼容性小坑。我用WSL2比较多原因是一个终端搞定编译和调试不需要来回切虚拟机窗口。如果你在Windows上建议走这条路径# 在WSL2的Ubuntu 22.04里安装依赖 sudo apt update sudo apt install build-essential gdb qemu-system-misc \ gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu \ git make python3 # 克隆南大实验代码仓库 git clone git://gitee.com/nju-operating-system-lab/oslab.git cd oslabmacOS用户需要注意QEMU的安装方式和Linux不同brew install qemu brew install riscv-tools这里有个容易踩的坑xv6教学内核需要特定版本的QEMU支持RISC-V架构新版本通常没问题但个别发行版预装的QEMU可能缺少RISC-V支持。如果输入qemu-system-riscv64报command not found需要单独安装qemu-system-misc或qemu-riscv相关包。2.2 QEMU调试环境与GDB联动环境装好只是开始真正提升效率的是调试环境。这是整个实验里我认为最值得提前花时间配置的一步。xv6的Makefile里内置了调试目标在实验目录下执行make qemu-gdb这会启动QEMU但暂停CPU等待GDB连接。再开一个终端gdb-multiarch然后在GDB里连接QEMUset architecture riscv64 target remote localhost:26000连接成功之后你就可以像调试普通程序一样调试内核了。比如在内核的进程切换函数里设断点单步跟踪switch机制看寄存器上下文怎么保存和恢复的。这比在代码里加printf然后重新编译要高效太多强烈建议提前学会。另一个提升效率的工具是vim/VS Code的远程开发插件用WSL2的话直接在Windows上用VS Code Remote连进去编辑体验和本地开发没区别。3. 四个Lab的核心细节与实操拆解3.1 Lab1进程与系统调用这个Lab的目标是让你搞清楚一个进程从创建到退出的完整生命周期。任务要求新增一个系统调用同时在用户态实现一个应用程序来验证。先读源码重点是kernel/proc.c里的allocproc()和freeproc()以及kernel/syscall.c里的系统调用分发逻辑。你需要理解内核是怎么通过trap机制从用户态陷入内核态的syscall()函数怎么根据系统调用号找到对应的处理函数。实操时有个关键细节在proc.c里新分配的进程拿到了一个内核栈内核栈的trapframe字段保存了用户态寄存器的完整快照。你新增的系统调用需要正确使用argint/argaddr这些参数读取函数否则从用户态传进来的参数会拿不到。我见过大量同学在这个Lab踩同一个坑在用户程序里直接调用了系统调用但忘了在user/usys.pl或对应的汇编入口里生成跳转代码导致链接时找不到函数。xv6用的系统调用入口是通过脚本生成的新增系统调用必须同步修改生成脚本这个步骤很容易漏。这个Lab有一个“陷阱”设计涉及内核态与用户态切换时的上下文保存你需要仔细看kernel/trap.c的usertrap和kernel/trapasm.S里的汇编代码。理解了这段后面所有Lab的难度会降低三分之一。3.2 Lab2内核线程与用户态线程库进入Lab2事情开始变得有意思。任务要求实现两套线程机制内核态的内核线程以及用户态线程库。内核线程相对好理解本质上是给进程加一个上下文切换接口。你需要实现thread_create和thread_join核心是掌握进程调度的切换函数swtch。这里的关键是理解每个线程要有独立的栈和独立的上下文结构切换的时候把当前CPU寄存器的状态保存到旧线程的上下文然后从新线程的上下文恢复。用户态线程库则是完全另一套逻辑。它不依赖内核而是在用户态用ucontext库或自写汇编实现上下文切换。这个Lab的难点在于你需要模拟一个“调度器”跑在某个线程的栈上负责切换各个用户态线程的执行流。我在做这个Lab时花了最多时间理解的东西是用户态线程切换时栈怎么切换。每个用户态线程要分配一块独立的栈空间线程切换的本质就是换栈加换指令流通过setcontext/getcontext或自写的汇编代码完成。实操建议写context_switch的核心汇编时先把寄存器的保存和恢复逻辑画出来对照RISC-V的ABI规范看清楚哪些寄存器是caller-saved哪些是callee-saved。我当时漏掉了ra寄存器的保存导致线程切换后返回地址错乱整个程序崩溃调了一整个晚上才找到。3.3 Lab3虚拟内存与mmapLab3是这套实验真正的分水岭。之前的Lab你还在“操作系统外围”打转这一步直接钻进最核心的虚拟内存机制内部。任务要求实现mmap和munmap系统调用让用户程序能把文件映射到进程的地址空间。这需要你理解xv6的页表机制、缺页异常处理流程、以及文件系统的交互。先理清概念每个进程有一个独立的页表walkaddr函数负责把虚拟地址翻译成物理地址。mmap要做的事情是在进程的虚拟地址空间里找一块空闲区域建立虚拟地址到文件页面之间的映射关系但真正的物理页分配可以推迟到访问时再触发。这个“延迟分配”是理解虚拟内存的关键。访问一个尚未映射物理内存的页面时CPU会触发缺页异常内核在异常处理流程里分配物理页、把文件内容读进来、更新页表然后重新执行触发异常的指令。整个过程对用户程序完全透明用户只感觉到“程序能跑”不知道背后发生了这么多事。实操层面有几个难点。第一页表结构是三级页表用PTE标志位控制读写权限。你要在trap.c里处理scause等于13load page fault和15store page fault的异常。第二和Lab1、Lab2不同Lab3的测试会做大量边界检查。比如mmap的地址对齐、长度参数必须按页对齐、映射重叠时怎么办这些细节都会成为测试用例的扣分点。第三内存回收时机。munmap需要判断页面是否被修改过dirty bit如果修改过需要回写文件否则直接释放物理页。xv6的PTE里有D标志位实现回写时要读这个位这个细节被我忽略过导致文件内容不对。3.4 Lab4写时复制Copy-on-WriteLab4作为压轴实验难度直接拉满。核心目标是实现fork时的写时复制优化父进程被fork时不再复制全部物理内存而是让父子进程共享同一份物理页并且把页表标记为只读。当其中一方尝试写入时触发缺页异常内核在异常处理中复制物理页再重新映射并恢复写权限。这个机制为什么重要因为真实操作系统里的fork基本都是用写时复制实现的。如果每次fork都把全部内存复制一遍fork大程序的开销会非常夸张。写时复制让fork几乎瞬间完成只有真正写入时才产生复制成本。实现时最核心的逻辑在缺页异常处理函数里。Lab3你已经处理过缺页异常Lab4的缺页处理复杂得多你要判断触发异常的虚拟地址对应的物理页是不是共享页如果是就分配新页、复制内容、更新页表、设置写权限然后返回用户态重新执行指令。调试这个Lab最头疼的问题是“共享页面的引用计数”。一个物理页可能被多个进程的页表同时引用全都不写时谁也不能释放一旦有进程写入触发复制原来那个页的引用计数要减一。如果引用计数管理出了bug要么内存泄漏要么一个进程释放了另一个进程还在用的页出现极其诡异的内存破坏问题。我在做这个实验时用了一个技巧在物理页结构体里加一个引用计数字段然后在进程释放页表时递归遍历并递减计数。排查引用计数bug时写一个小的调试函数打印每块物理页的引用次数肉眼对比看是否和预期一致。4. 常见问题与排错经验速查4.1 环境与编译类问题这个类别的问题很多时候和代码无关纯粹是环境没配好但你会觉得莫名其妙。现象可能原因解决方案make qemu提示找不到qemu-system-riscv64QEMU未安装或缺少RISC-V支持安装qemu-system-miscLinux发行版不同包名不同编译报错找不到riscv64-linux-gnu-gcc交叉编译工具链未安装apt install gcc-riscv64-linux-gnumake grade全部失败但自己测试正常测试脚本期望的输出格式不匹配检查是否启用了make qemu后的默认输出重新执行make clean代码改动后测试结果没变化编译缓存或未重新编译执行make clean make强制重新编译这里特别提一下make clean的重要性。xv6的构建系统有时不会正确跟踪所有依赖关系改了一个头文件但其他文件没触发重编译导致你测试的还是旧版本内核。遇到“明明改了代码但行为没变”的诡异情况先无脑make clean make。4.2 运行与逻辑类问题运行时的崩溃和逻辑问题才是实验的主战场。内核panic提示“page fault”大概率是你的页表设置有问题或者访问了未映射的内存。用GDB看触发异常的地址然后检查该地址在页表里有没有对应映射以及权限位是否正确。测试用例超时xv6的测试脚本对每个用例有运行时间限制。如果超时通常是你的实现陷入了死循环或者是某个锁没有释放导致调度卡死。僵尸进程不消失和wait系统调用的实现有关系。检查你在proc.c里回收进程的逻辑确认zombie状态下的进程真的被父进程回收了而不是停留在进程表里占着位置。空闲内存越来越少这是经典问题。多半是kfree调用次数少于kalloc也就是物理页泄漏了。Lab3和Lab4里最容易出现这类问题尤其是munmap和写时复制的页面释放逻辑。写一个内存统计函数跑几轮测试后对比可用内存数量很容易定位是哪一部分泄漏了。4.3 调试工具使用技巧调试这块建议花半小时专门练一下GDB连接QEMU的流程。具体操作前面已经写了这里补充几个调试系统内核时特别有用的命令# 查看当前进程的进程控制块 p *myproc() # 查看页表项 p *walkaddr(myproc()-pagetable, 0x1000) # 查看寄存器上下文 info registers # 在内核切换点设置条件断点 b swtch if myproc()-pid 2还有一个非常实用的技巧在代码里加临时printf输出关键变量值比GDB在用户态配合QEMU串口输出更直观。xv6的printf会直接打印到QEMU的终端里而且它会自动加锁不会多核并发输出乱序。5. 一些个人建议与扩展方向整套实验刷下来我的一个总体感受是它不是在教你“背操作系统概念”而是在逼你“像工程师一样解决问题”。每个Lab给出的代码骨架足够你跑通基础功能但真正拿满分需要你自己在边界条件、并发安全、内存管理这些细节上做文章。如果你刷完四个Lab还有余力建议深入做两件事。一是去读xv6原版MIT 6.S081的课程代码和作业南大有不少实验题就是从那里演化来的两边对照着看能加深理解。二是尝试给xkernel南大自己开发的xv6变体添加新功能比如实现一个简单的信号量、加一个优先级调度算法甚至试着移植一个简单的用户态程序。另外一个容易被忽略的环节是写实验报告。做实验时随手记录你改动过的文件、关键函数、遇到的问题和解决思路。这些记录不仅能帮你通过验收还是面试时非常好的项目素材。我在面试候选人时最怕听到“操作系统实验我做了但忘了细节”而能清晰讲出“我在写时复制里怎么处理引用计数”的候选人给了我很深的印象。最后如果你在实验中途卡在某个点上很久我的建议是放下代码重新去读xv6源码里对应子系统的那部分。很多看起来逻辑不对的地方其实是你没理解xv6原有的设计意图。读源码花的时间往往比瞎试代码省得多。祝实验顺利。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价