简介Xv6是麻省理工学院面向操作系统课程设计的类Unix教学内核这套第五修订版源码包面向计算机专业学生、自学开发者与系统研究者用于从代码层面理解内核的进程调度、虚拟内存、文件系统与中断处理等核心机制。资源共85个文件主体为44个C源文件、19个头文件与8个汇编文件辅以少量Perl脚本、Makefile及配置文档压缩包仅78KB便于快速下载与反复对照阅读目前已有1460人浏览学习。通过分析这套代码可以清楚看到fork、exec、wait等系统调用的实现路径追踪页表映射、管道通信、inode文件管理和自旋锁的同步逻辑也能从汇编入口到C主函数逐步还原操作系统启动全过程。对准备深入操作系统课程实验、完成MIT6.828类项目或从事底层开发的人来说这份体积小巧但结构完整的源码提供了极佳的解剖样本尤其有助于理解多道程序设计、页面替换与并发控制等经典主题。1. xv6-rev5.tar.gz一个能读懂、能跑起来、能改着玩的操作系统源码包xv6-rev5.tar.gz 这个压缩包是我见过对“操作系统到底是怎么工作的”这个问题最好的回答之一。它不是动辄几千万行的生产内核而是一个几万行就能编译、启动、跑命令的教学内核源码包解开后从开机第一行汇编到 shell 里的 fork 调用都能对着代码一行行说清。拿到这个包的人通常有两种一种想把抽象概念落到代码另一种想写自己的内核却不知道从哪下手。前者能在这里找到进程、内存、文件系统的具体实现后者能把它当试验田。相比读 Linux 源码读 xv6-rev5 不会半路迷路它小到能完整读一遍又大到能装下课程设计题。建议用一下午先跑起来一周读通主干再用一个月改出自己的内核。这篇内容就把这条路每一步怎么走、坑在哪一次说清。2. 解压与通读源码包的目录结构和每个文件的角色tar.gz 这个后缀已经很直白先用 tar 打包再用 gzip 压缩。解压本身只需要一条命令真正的难点在于解压之后怎么不迷路。好在这个包足够小几十个文件摊在眼前按职责分清楚后面每一步都能顺着结构走。2.1 用 tar 解压并确认包结构# 解压x 解包z 用 gzip 解压v 显示过程f 指定归档文件名 tar xzvf xv6-rev5.tar.gz # 进入解出来的源码目录目录名通常是 xv6-rev5 cd xv6-rev5 # 列出全部文件重点看 Makefile、*.c、*.h、*.S 的分布 ls -la # 如果只想先看压缩包里有什么不解压也可以列出清单 tar tzvf ../xv6-rev5.tar.gz | head -30四个参数里x 是解包z 是解 gzipv 是显示解压过程f 是后面必须跟文件名。有人会漏掉 z 写成 tar xvf在旧版 tar 上会直接报文件格式错误现代 tar 会自动识别 gzip但为了在所有环境上都可靠我习惯把 z 带上。ls -la 之后你会看到文件总数少得惊人这正是教学内核和工业内核最直观的区别。看完清单再用一行命令感受一下代码量级# 统计 C 源文件数量 find . -maxdepth 1 -name *.c | wc -l # 统计 C 和头文件的总行数 find . -maxdepth 1 \( -name *.c -o -name *.h \) | xargs wc -l | tail -1这个数字落在几万行量级意味着它可以被完整读完。对一份教学源码包来说几万行是最佳体量再大就超出人力通读的上限再小又覆盖不了进程、内存、文件系统这些核心主题。2.2 源码包的三大模块内核、用户程序和 mkfs解压出来的文件按职责可以分三类。这个分类方法比死记文件名有用得多因为后续你改任何地方都需要先判断它在哪个模块里。第一类是内核源码。启动相关的汇编文件负责进入保护模式、加载内核、跳转到 C 入口main.c 里是初始化顺序进程相关代码在 proc 和 sched 相关文件里虚拟内存在 vm 相关文件里文件系统在 fs 和 buffer 相关文件里。头文件里放着关键结构定义比如进程结构体、地址翻译宏、系统调用编号。第二类是用户态程序。sh.c 是 shell还有 ls、cat、echo 这一组常用命令。它们不是内核的一部分但会被打包进文件系统镜像最终在 xv6 的 shell 里运行。之所以把内核和用户程序放在同一个源码包里是因为你能从一次 fork 调用出发一路追到用户态函数、中断入口、syscall 分发、内核实现这条完整路径是读大型内核时很难得到的体验。第三类是构建工具和脚本。mkfs 相关源码是运行在宿主机上的工具作用是把编译好的用户程序整理成文件系统镜像Makefile 则把编译、链接、打包整个过程串联起来。拿到包之后先按这个分类在脑子里画一张图哪些文件跑在硬件上哪些文件跑在虚拟机里哪些文件跑在宿主机上。这张图画清楚后面的编译和调试点就都有了定位基准。2.3 建议的阅读顺序从初始化到主路径读源码的顺序和编译顺序不一样。编译从引导块开始最后生成文件系统镜像读代码我建议从 Makefile 和 main.c 入手先知道系统有哪些模块、按什么顺序初始化再回头抠具体算法。不然一上来就钻进内存管理很快就不知道当前代码在启动流程的哪一步。# 看 main.c 里各子系统的初始化顺序 grep -n init\|setup main.c | head -30 # 看进程结构体长什么样 grep -n struct proc proc.h | head -5 # 看系统调用编号表 grep -n SYS_ syscall.h | head -30grep 是读这份源码最好的工具。main.c 里的初始化函数列表几乎就是内核启动的目录控制台、内存、中断、文件系统、进程。第一遍读的时候建议只走主路径main.c 怎么创建第一个进程调度器怎么把它切进来shell 启动之后怎么等待键盘输入。主路径打通之后再往分支走fork、exec、打开文件、读写文件。这样走完一遍你对整个源码包的掌控感会比按文件顺序从头读强很多。3. 编译 xv6-rev5工具链、make 参数和构建产物编译这一步是大多数人的第一道坎。xv6-rev5 写于 32 位 x86 时代而现在的开发环境基本是 64 位 Linux所以多数编译错误不是因为源码有问题而是因为宿主机上少了 32 位支持。把工具链补齐编译本身很快。3.1 先检查工具链gcc、make、qemu 缺一不可# 检查编译器和构建工具 gcc --version | head -1 make --version | head -1 # 检查运行所需的系统模拟器 qemu-system-i386 --version # 用一个小程序验证 32 位编译是否可用 echo int main(){return 0;} | gcc -m32 -x c - -o /tmp/xv6_probe echo 32-bit OKxv6-rev5 是为 32 位 x86 设计的。现代发行版默认工具链是 64 位但 gcc 加上 -m32 就能生成 32 位代码前提是系统装了 32 位版本的库和头文件。所以最关键的检查不是 gcc 有没有而是最后那条 32-bit OK 有没有出现。如果这条失败后面编译必然出错。缺工具的情况下Debian/Ubuntu 系一般这样装sudo apt-get update sudo apt-get install build-essential qemu-system-x86 gcc-multilib libc6-dev-i386gcc-multilib 和 libc6-dev-i386 是专门为交叉编译 32 位程序准备的。少了它们编译可能在预处理阶段就报头文件错误也可能在链接阶段报找不到库后面排查都会绕远路。3.2 make 的完整构建流程工具链齐了之后构建通常就是两条命令make clean # 清掉上次的产物和依赖信息避免脏状态 make # 按 Makefile 中定义的顺序构建整个系统make 会依次做几件事汇编生成引导块编译内核源码并链接成 kernel编译所有用户程序最后用 mkfs 工具把所有用户程序打包成文件系统镜像 fs.img。任何一个环节失败make 都会停在对应位置这时看终端里最后几行多半就是问题所在。建议看一眼 Makefile 里到底传了哪些编译参数这比瞎猜编译行为靠谱grep -n CFLAGS\|LDFLAGS\|QEMU Makefile | head -20几个参数值得记住-m32 强制生成 32 位代码少了它整个内核会按 64 位编译跑都跑不起来-ggdb 生成 gdb 可读的调试信息没有它后面用 gdb 断点调试就无从谈起-fno-builtin 告诉编译器不要自作主张调用 C 库内建函数内核里很多自定义实现需要保留原样-fno-pic 避免生成位置无关代码因为内核代码被链接到固定地址。编译完成一次之后目录里会多出一批 .d 文件。它们不是垃圾是 make 自动生成的依赖文件记录每个源文件包含哪些头文件这样你改了头文件make 才知道哪些源文件需要重编。3.3 构建产物与它们在启动流程中的位置编译成功后目录里会多出几个关键文件。我第一次把这三个文件和启动过程对上号时整个系统是怎么从零站起来的才真正串起来。| 产物 | 是什么 | 在启动流程里的位置 | | bootblock | 512 字节的引导程序 | BIOS 把它读到内存 0x7c00再由它加载内核 | | kernel | 链接后的内核可执行文件 | bootblock 把它读进内存跳转到入口 | | fs.img | 文件系统镜像 | 内核挂载它作为根文件系统加载用户程序 |可以用 ls -l 看一下 bootblock 的大小。如果超过 512 字节链接阶段会直接报错因为引导扇区的大小是硬件规定没有讨价还价的余地。kernel 的大小没有这个限制它在引导之后由 ELF 头里的信息加载。fs.img 里打包了 sh、ls、cat 等用户程序内核完成初始化后执行的第一个用户进程就从 fs.img 里加载没有这个镜像shell 根本起不来。注意区分 bootblock 和 kernel 的加载方式是看启动日志的关键。前者是裸二进制被 BIOS 直接读后者是 ELF 格式被 bootblock 解析报错位置完全不同。3.4 编译告警不等于错误新版 gcc 的处理策略新版 gcc 编译这份老源码时通常会有不少告警。很多人看到告警就慌其实可以分两种情况处理。一种告警是函数隐式声明。老代码写作年份早头文件包含没有那么严谨新版 gcc 默认把隐式声明当成 error 处理这种会直接中断构建需要补头文件或者按排查章节里的方法处理。另一种是变量未使用、类型转换建议之类的提示一般不影响运行。我的习惯是先别急着往 CFLAGS 里加 -Wno- 系列参数那等于关掉安全网。正确做法是先看告警发生在哪个文件、哪一行确认它是不是当前 Makefile 参数导致的最后再决定是修源码还是调参数。为了快速跑通临时调整参数可以想长期在上面做实验还是应该把源码修干净。4. 运行与验证用 QEMU 把 xv6-rev5 真正跑起来编译通过只是第一步真正在终端里看到 shell 提示符才叫跑通。QEMU 是这套源码最常见的运行载体因为它既能模拟整台机器又支持远程调试这对内核实验来说基本是刚需。4.1 一行命令启动make qemu 做了什么make qemu这个 target 在 Makefile 里通常被定义成先确认 kernel 和 fs.img 是最新的再调用 QEMU 加载它们。所以它既是构建验证也是运行验证。启动成功后终端或图形窗口里会出现 xv6 的 shell 提示符一般是 $ 或 sh从这一刻起你就在一台真实的 32 位虚拟机里跑着一个完整的操作系统内核了。但如果你在纯服务器环境或者通过远程终端连接可能会遇到黑屏或完全没有输出。这不是内核没跑而是图形界面和串口通路的问题。遇到这种情况不要纠结 make qemu直接用下面的手动命令。4.2 手动启动 QEMU 时的关键参数手动启动比 make qemu 更可控尤其在排查启动问题时每个参数都知道是干嘛的才能判断问题出在哪一层qemu-system-i386 \ -m 128 \ -kernel kernel \ -drive filefs.img,index0,formatraw \ -nographic \ -smp 1参数逐个说-m 128 表示给虚拟机 128MB 内存这个量跑完整测试都足够-kernel kernel 直接把 ELF 内核加载进内存相当于跳过了磁盘引导阶段-drive 把 fs.img 挂成第一块 IDE 硬盘内核里的磁盘驱动会访问它-nographic 把串口重定向到当前终端让内核输出直接打在屏幕上-smp 1 强制单核xv6-rev5 的锁设计比较朴素单核跑能避免很多输出乱序。我见过有人在这步反复翻车原因是直接用 make qemu 在图形环境里等窗口结果窗口弹不出来又不知道加参数。手动命令把这些因素全部拿掉剩下的就只有真实执行路径。4.3 在 xv6 的 shell 里做冒烟测试内核启动后会执行 init最终进入 shell。现在可以在 xv6 里跑几条命令验证核心子系统是否正常# 查看文件系统里的内容 ls # 测试系统调用和输出路径 echo hello # 如果有说明文件走一遍打开读出的 IO 链路 cat README # 专门测试进程创建和回收 forktestls 能列出 fs.img 里的用户程序看到 sh、ls、cat 这些文件说明文件系统挂载成功。echo 走的是 write 系统调用cat 走的是 open、read、write 整条 IO 路径。forktest 是包里自带的小测试程序会以较高频次创建和回收进程输出类似 fork test passed说明进程调度核心没崩。更完整的验证是 usertests它把文件、管道、内存、退出码等几十项测试跑一遍顺利通过需要几分钟。我习惯把 usertests 当作改动内核后的回归测试它比任何手工测试都更能说明问题。4.4 结束模拟会话的正确姿势QEMU 的退出热键是 Ctrl-A X这和平时关虚拟机的方式完全不一样。第一次跑的人几乎都会先按 Ctrl-C然后疑惑为什么没反应。原因很简单QEMU 会把 Ctrl-C 当成 guest 终端的信号送进 xv6而 xv6 的 shell 收到这个信号并不会退出模拟器。提示退出模拟器用 Ctrl-A 松开后再按 X如果终端模拟器占用了这个组合键就另开一个终端结束 QEMU 进程。5. 编译和运行 xv6-rev5 的常见问题排查这一章的每一条都是我从各种环境里实打实踩过的坑。这些坑的共同点是错误信息看起来吓人实际原因都很集中并且全部指向同一个方向宿主机环境和这份 2000 年代源码之间的代沟。5.1 报错缺 bits/libc-header-start.h32 位系统库没装现象编译进行到预处理阶段报出 fatal error: bits/libc-header-start.h: No such file or directory后面往往还跟着一串头文件依赖错误。原因gcc 加了 -m32 之后会去寻找 32 位版本的系统头文件。而默认的 Linux 发行版只安装了 64 位头文件于是第一个被包含的系统头文件就找不到了。这个错误和源码本身无关属于典型的宿主环境配置缺失。解决sudo apt-get install libc6-dev-i386 gcc-multilib make clean make装完依赖后重新构建。要 clean 一次再编因为上次失败留下的依赖信息可能记录不完整路径不清干净容易产生误导性的重编行为。5.2 链接过程找不到 crt1.o 或 libgccmultilib 缺失现象编译阶段全部通过在最后链接时出现 ld: cannot find crt1.o或者 ld: cannot find -lgcc最后以 collect2: error: ld returned 1 exit status 收场。原因预处理和编译都过了说明头文件已经找到但链接器还需要 32 位版本的启动对象文件和 libgcc 库。这些文件属于 multilib 支持的一部分只装了普通 gcc 而没装 gcc-multilib就会卡在这一步。解决安装 gcc-multilib。如果已经装了还报同样错误检查链接器的搜索路径里是否存在 32 位库目录比如 /usr/lib32 或 /usr/lib/i386-linux-gnu某些精简系统还需要额外的 32 位 gcc 运行库包。这个问题比 5.1 更隐蔽因为编译全程都正常只在最后一步爆掉。5.3 QEMU 启动后黑屏无输出串口通路没接上现象make qemu 之后弹出一个黑窗口或者终端一片空白没有引导信息没有 shell 提示符看起来像死机。原因xv6 的早期调试输出走串口COM1默认的 QEMU 图形模式不一定把串口内容显示出来。如果跑在没有图形界面的服务器上窗口可能根本不出现。其实内核已经在执行只是你看不到它在说话。解决用手动启动命令加 -nographic 参数或者用 -serial mon:stdio 把串口重定向到标准输入输出。看到引导日志后再判断下一层问题。这一条解决掉大概能排除掉八成“启动失败”的假象。5.4 Ctrl-C 没有反应QEMU 的退出键不是它现象在 QEMU 里想退出系统按 Ctrl-C 没反应甚至让 xv6 的 shell 出现异常输出。原因QEMU 作为模拟器需要把一部分键盘输入转发给 guest。Ctrl-C 在 guest 里是中断信号会被 xv6 当作普通输入或信号处理并不会退出模拟器。所以这个行为不是系统卡死而是操作方式不对。解决用 Ctrl-A 松开再按 X 退出。如果终端模拟器拦截了这个组合键另开一个终端 kill QEMU 进程。不要反复按 Ctrl-C它只会让 guest 里的 shell 行为越来越乱。5.5 隐式函数声明直接报 error新版 gcc 更严格现象编译报错 implicit declaration of function xxx而这份源码在老编译器时代这只是一个 warning。原因新版 gcc 默认把隐式函数声明当作错误处理。xv6-rev5 里有部分函数依赖跨文件声明有些源文件没有完整包含对应头文件恰好触发这条规则。这类报错在源码构建中频率不低属于老代码与新编译器的典型冲突。解决最干净的修法是定位这个函数真实声明在哪个头文件把它 include 到当前 C 文件里。如果只是临时跑通不打算维护可以在 Makefile 的 CFLAGS 上追加 -Wno-implicit-function-declaration。但我不推荐一上来就全局关告警因为隐式声明往往意味着类型不匹配这种 bug 在底层系统里极难排查。6. 从跑通到改代码用 gdb 调试内核和最小改造练习跑通和读懂之间还差一个趁手的调试手段。很多初学者只看代码不看运行状态遇到问题只能到处加打印效率很低。xv6 的 Makefile 通常自带 qemu-gdb 目标这正是我推荐你最后掌握的工具。6.1 用 qemu-gdb 断点调试调度器先在一个终端运行make qemu-gdb然后另开一个终端启动 gdbgdb kernel (gdb) target remote localhost:26000 (gdb) b scheduler (gdb) ctarget remote 让 gdb 通过 26000 端口连接 QEMU 内置的 gdbserver。断点打在调度函数上以后每次内核进行进程切换都会停住。这时你能看到当前进程结构体的字段、寄存器状态和完整调用栈比 printk 大法直观得多。我第一次用这个组合时才真正理解进程控制块里的状态字段在切换时是怎么变化的。6.2 值得做的第一个改造给 shell 加一个新命令如果已经跑通了上面所有步骤可以做一个小改造来验证整条链路在 shell 源码的命令表里注册一个新命令让它执行一个用户程序。改完重新 make再启动 QEMU在 shell 里敲这条新命令能正常输出你就完整走过了“改代码、编译、打包、运行”的闭环。这个改造看起来小但它涉及用户程序编译进文件系统镜像的完整流程后续做系统调用扩展、调度器改造都以这条链路为基础。直接上手改调度器容易迷失在小节里先通过这个练习建立信心更稳妥。我有一次改调度器只跑了 ls 就以为没问题结果 usertests 里子进程退出码全乱查到最后才发现自己把时间片改成负数调度算法在极端参数下直接翻车。从那以后我定了一条规矩改完任何内核逻辑先跑完整回归再收工。希望你也能在这个源码包里找到“原来操作系统是这么工作”的实在感希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?