xv6 源码学习专题 / xv6 Source Learning
1. 为什么单独建立 xv6 专题
xv6 是一个教学用 Unix-like 操作系统。
它不像 Linux 那样庞大,也不像普通操作系统教材那样只讲抽象概念。
它的价值在于:每个核心概念几乎都能回到一段可以读完的 C 代码。
对现在的学习目标来说,xv6 很适合放在“系统与高性能”下面单独成专题。
原因有三点。
第一,xv6 不是普通操作系统概念笔记。
它应该以源码为主线,而不是按教材章节松散排列。
第二,你现在的关键难点不是“知道进程是什么”这么简单。
真正的难点是把 C 指针、结构体、RISC-V 寄存器、页表、trap、调度器这些东西接起来。
第三,后面要学 Linux 0.11。
xv6 可以先建立干净的小模型,再把这些模型迁移到 Linux 0.11 里更复杂的实现。
这个专题的学习路线应该是:
C 语言和指针基础
|
v
RISC-V 执行模型
|
v
xv6 启动和源码目录
|
v
内核核心机制
|
+--> 进程与 proc
+--> 页表与虚拟内存
+--> trap 与系统调用
+--> 调度与上下文切换
+--> sleep / wakeup / exit / wait
+--> 文件系统与设备
|
v
Linux 0.11 对照学习这个专题要坚持一个原则:每个操作系统概念都必须落到源码文件、结构体字段、函数调用路径和内存关系上。
如果一篇文章只解释了概念,却没有解释 xv6 代码怎么实现,就不算完成。
如果一篇文章只逐行翻译代码,却没有解释操作系统为什么要这样设计,也不算完成。
1.1 本文基础词小注释
先把几个会反复出现的词放在这里。
不用一次背下来。
读到后文时,可以随时回来看。
kernel
|
+-- 内核
+-- 操作系统最核心的那部分程序
+-- 负责管理进程、内存、文件和设备
user program
|
+-- 用户程序
+-- 普通程序,例如 shell、cat、echo
+-- 不能随便访问硬件和内核数据
trap
|
+-- CPU 转入内核处理特殊事件
+-- 例如系统调用、中断、非法内存访问
+-- 不是普通 C 函数调用
syscall
|
+-- 系统调用
+-- 用户程序请求内核帮忙做事
+-- 例如 read、write、fork、exec
proc
|
+-- process 的缩写
+-- xv6 中常指 struct proc
+-- 是内核保存进程信息的结构体
page table
|
+-- 页表
+-- 虚拟地址到物理地址的翻译表
+-- 让每个进程拥有自己的地址空间最粗略地说:
用户程序想做敏感事情
|
v
syscall
|
v
trap into kernel
|
v
kernel uses proc/page table/file/device data1.2 xv6 可以先拆成哪些模块
如果一开始按源码文件读,xv6 会显得很散。
今天看 proc.c。
明天看 vm.c。
后天又跳到 trap.c。
这些文件都很重要,但初学时更适合先按“模块”看。
模块就是一组共同解决某类问题的代码。
比如“内存管理模块”回答:内存从哪里来,页表怎么建,虚拟地址怎么变成物理地址。
“进程管理模块”回答:进程怎么创建,怎么退出,怎么调度,怎么睡眠和唤醒。
“硬件接口模块”回答:内核怎样和 UART、磁盘、中断控制器这些硬件或虚拟硬件交流。
先给一张模块总图。
+====================================================================+
| xv6 kernel modules |
+====================================================================+
| |
| +----------------------+ +------------------------------+ |
| | Boot / Init | -----> | Process Management | |
| | 启动与初始化 | | 进程管理 | |
| | | | | |
| | entry.S | | proc.c / proc.h | |
| | start.c | | swtch.S | |
| | main.c | | trapframe / context | |
| +----------------------+ +------------------------------+ |
| | | |
| v v |
| +----------------------+ +------------------------------+ |
| | Memory Management | <----> | Trap / Syscall / Interrupt | |
| | 内存管理 | | 陷入、系统调用、中断 | |
| | | | | |
| | kalloc.c | | trap.c | |
| | vm.c | | trampoline.S | |
| | memlayout.h | | syscall.c | |
| | riscv.h | | sysproc.c / sysfile.c | |
| +----------------------+ +------------------------------+ |
| | | |
| v v |
| +----------------------+ +------------------------------+ |
| | File / FS / Log | <----> | Device / Driver / Hardware | |
| | 文件、文件系统、日志 | | 设备、驱动、硬件接口 | |
| | | | | |
| | file.c / pipe.c | | console.c | |
| | fs.c / log.c | | uart.c | |
| | bio.c / buf.h | | plic.c | |
| | virtio_disk.c | | virtio_disk.c | |
| +----------------------+ +------------------------------+ |
| |
| +--------------------------------------------------------------+ |
| | Shared Foundation / 公共基础设施 | |
| | defs.h, types.h, param.h, spinlock.c, sleeplock.c, printf.c | |
| +--------------------------------------------------------------+ |
| |
+====================================================================+这张图里有一个细节:virtio_disk.c 同时出现在文件系统链路和设备链路里。
这是正常的。
因为同一个源码文件可以站在两个视角里看。
从文件系统视角看,它是“磁盘块读写的最后一站”。
从设备视角看,它是“VirtIO 磁盘驱动”。
所以模块划分不是把文件硬切开。
模块划分是为了回答问题。
下面按模块说明。
问题类型
|
+-- 系统怎么启动? -> Boot / Init
+-- 进程怎么存在和切换? -> Process Management
+-- 地址和内存怎么管理? -> Memory Management
+-- 用户怎么进入内核? -> Trap / Syscall / Interrupt
+-- 文件名怎么变成磁盘内容? -> File / FS / Log
+-- 内核怎么碰硬件? -> Device / Driver / Hardware
+-- 全局工具和锁在哪里? -> Shared Foundation1.2.1 Boot / Init:启动与初始化模块
这个模块回答一个最朴素的问题:内核第一口气是怎么喘起来的。
它主要涉及:
kernel/entry.S
kernel/start.c
kernel/main.c
kernel/kernel.ldentry.S 是最早执行的内核代码之一。
它负责准备栈,然后跳到 C 世界。
start.c 处理更早期的 RISC-V 特权级和定时器设置。
main.c 像一个初始化清单,依次初始化内存、进程表、trap、设备、文件系统等。
可以这样理解:
CPU starts
|
v
assembly entry prepares stack
|
v
C initialization code runs
|
v
kernel services become available1.2.2 Process Management:进程管理模块
这个模块回答:进程是什么,进程怎么活,怎么死,怎么被 CPU 选中。
主要文件:
kernel/proc.h
kernel/proc.c
kernel/swtch.S核心对象是 struct proc。
它不是 CPU。
它也不是一段单纯的代码。
它是内核保存“一个进程所有关键信息”的结构体。
比如:
struct proc
|
+-- state 当前状态
+-- pid 进程编号
+-- pagetable 用户页表
+-- trapframe 用户寄存器保存区
+-- context 内核上下文切换保存区
+-- ofile[] 打开的文件
+-- cwd 当前工作目录进程管理模块内部又可以拆成几条线:
creation line: fork / allocproc / userinit
exit line: exit / wait / reparent
sleep line: sleep / wakeup / kill
switch line: scheduler / sched / yield / swtch如果后面你看到 proc,先不要害怕。
它大部分时候就是“当前内核正在讨论的那个进程记录”。
1.2.3 Memory Management:内存管理模块
这个模块回答:内核和进程的内存从哪里来,地址怎么翻译。
主要文件:
kernel/kalloc.c
kernel/vm.c
kernel/memlayout.h
kernel/riscv.h
kernel/defs.h这里有两层问题。
第一层是物理页分配。
kalloc.c 管理一页一页的物理内存。
它像一个简单的内存仓库。
free physical pages
|
v
kalloc()
|
v
one 4096-byte page第二层是页表。
vm.c 管理虚拟地址到物理地址的映射。
用户程序看到的是虚拟地址。
CPU 真正访问硬件内存时需要物理地址。
页表就是中间翻译表。
virtual address
|
v
page table
|
v
physical address初学时不要急着背 Sv39 的每一位。
先记住:页表让每个进程拥有自己的地址空间,也让内核能控制哪些地址能访问、哪些地址不能访问。
1.2.4 Trap / Syscall / Interrupt:陷入、系统调用、中断模块
这个模块回答:CPU 为什么会从用户程序跳进内核。
主要文件:
kernel/trap.c
kernel/trampoline.S
kernel/syscall.c
kernel/sysproc.c
kernel/sysfile.c
user/usys.pl这里有三个词很容易混。
trap
|
+-- CPU 进入内核处理特殊事件的总称
|
+-- syscall: 用户程序主动请求内核服务
+-- interrupt: 外部设备或定时器打断当前执行
+-- exception: 程序出错或触发异常所以 syscall 是 trap 的一种。
interrupt 也是 trap 的一种。
后文如果说 trap,不一定只是在说系统调用。
一次系统调用大致是:
user code
|
v
ecall
|
v
trampoline.S saves user registers
|
v
trap.c usertrap()
|
v
syscall.c dispatches by syscall number
|
v
sysproc.c or sysfile.c does real work这个模块是用户态和内核态之间的门。
理解它之后,read()、write()、fork() 就不再只是函数名,而是一条跨权限边界的路径。
1.2.5 File / FS / Log:文件、文件系统和日志模块
这个模块回答:一个文件名怎样变成文件内容,崩溃时为什么不容易把文件系统弄坏。
主要文件:
kernel/file.h
kernel/file.c
kernel/fs.h
kernel/fs.c
kernel/log.c
kernel/bio.c
kernel/buf.h
kernel/pipe.c这里可以先分三层:
fd layer
|
+-- 用户程序看到的小整数 fd
|
v
file layer
|
+-- struct file,记录打开文件状态
|
v
backend layer
|
+-- inode file
+-- pipe
+-- device普通文件还会继续往下走:
file descriptor
|
v
struct file
|
v
inode
|
v
log transaction
|
v
buffer cache
|
v
disk block文件系统模块最适合用“链路”来学。
不要一开始就陷在 bmap() 的所有细节里。
先知道每层负责什么,再回头看函数。
1.2.6 Device / Driver / Hardware:设备、驱动和硬件接口模块
这个模块回答:内核怎样和外部世界说话。
主要文件:
kernel/console.c
kernel/uart.c
kernel/plic.c
kernel/virtio_disk.c
kernel/timer.c设备驱动可以看作翻译官。
内核其他部分说:“我要输出一个字符。”
UART 驱动负责把它变成对串口寄存器的操作。
文件系统说:“我要读一个磁盘块。”
VirtIO 磁盘驱动负责把它变成虚拟磁盘设备能理解的请求。
kernel request
|
v
driver code
|
v
MMIO registers / device queues
|
v
hardware or virtual hardware中断也属于这个世界。
设备不是总让内核主动轮询。
很多时候设备会说:“我好了。”
这个“我好了”的通知就是中断。
1.2.7 Shared Foundation:公共基础设施模块
这个模块不直接代表某个操作系统概念,但到处都会被用到。
主要文件:
kernel/types.h
kernel/param.h
kernel/defs.h
kernel/spinlock.c
kernel/sleeplock.c
kernel/printf.c
kernel/string.ctypes.h 放基础类型。
param.h 放系统规模参数,比如最大进程数。
defs.h 放跨文件函数声明。
锁相关文件给其他模块提供同步工具。
printf.c 和 string.c 提供内核内部常用工具函数。
这些文件像地基。
你平时不一定先读它们。
但当你看到某个函数声明、某个锁、某个类型不知道从哪来,就常常要回到这里。
1.3 后续文章按模块怎么读
后面的 01 到 18 不是孤立文章。
它们可以按模块成组阅读。
如果你喜欢先看“大块结构”,可以先按下面这张导读图走。
xv6 topic reading guide
|
+-- A. 入门准备
| |
| +-- 01-learning-roadmap.md
| +-- 02-c-and-pointers-for-xv6.md
| +-- 03-riscv-basics-for-xv6.md
| +-- 04-how-to-read-xv6-source.md
|
+-- B. 启动与内核入口
| |
| +-- 05-boot-and-kernel-entry.md
|
+-- C. 内存管理
| |
| +-- 06-memory-layout-and-page-table.md
|
+-- D. trap、系统调用和边界切换
| |
| +-- 07-trap-interrupt-and-system-call.md
| +-- 15-user-programs-and-shell.md
|
+-- E. 进程管理与调度
| |
| +-- 08-process-and-proc-structure.md
| +-- 09-context-switch-and-scheduler.md
| +-- 10-sleep-wakeup-and-wait-exit.md
| +-- 11-locks-and-concurrency.md
|
+-- F. 文件、文件系统和设备
| |
| +-- 12-file-descriptor-and-inode.md
| +-- 13-file-system-and-log.md
| +-- 14-device-driver-console-uart-disk.md
|
+-- G. 调试、迁移和实践
|
+-- 16-debugging-xv6-with-gdb.md
+-- 17-xv6-to-linux-0-11-bridge.md
+-- 18-summary-and-practice-projects.md更细一点,可以这样理解每一组。
1.3.1 A 组:入门准备
对应文章:
01-learning-roadmap.md
02-c-and-pointers-for-xv6.md
03-riscv-basics-for-xv6.md
04-how-to-read-xv6-source.md这一组不急着讲某个内核机制。
它先解决“能不能读懂 xv6”的问题。
01 是路线图。
02 专门补 C 语言、指针、结构体和数组这些读源码会反复碰到的东西。
03 补 RISC-V 的基本执行模型,比如寄存器、特权级、ecall、页表相关寄存器。
04 讲怎么进入源码目录,怎么按文件和调用路径读。
读完这一组,至少要能回答:
struct 是什么?
指针字段大概指向哪里?
RISC-V 为什么有用户态和内核态?
xv6 源码应该从哪些文件开始看?1.3.2 B 组:启动与内核入口
对应文章:
05-boot-and-kernel-entry.md这一组讲内核怎样从“还没运行起来”走到 main() 初始化。
它连接汇编入口、C 初始化函数、内核链接脚本和多核启动。
读完这一组,至少要能回答:
xv6 为什么不是从普通 main() 直接开始?
entry.S 做了什么?
start.c 和 main.c 分别负责什么?
第一个用户进程是怎么被准备出来的?1.3.3 C 组:内存管理
对应文章:
06-memory-layout-and-page-table.md这一组讲物理页、虚拟地址、物理地址、页表、内核地址布局。
它是后面理解进程、trap、exec 的基础。
读完这一组,至少要能回答:
kalloc() 分配的是什么?
虚拟地址和物理地址有什么区别?
页表为什么是每个进程自己的?
trampoline 为什么要映射在特殊位置?1.3.4 D 组:trap、系统调用和边界切换
对应文章:
07-trap-interrupt-and-system-call.md
15-user-programs-and-shell.md这一组讲用户态和内核态之间的门。
07 从内核角度讲 trap、系统调用和中断。
15 从用户程序和 shell 角度讲 fork/exec/wait 如何触发内核服务。
这两篇建议合在一起看。
因为系统调用不是只属于内核,也不是只属于用户程序。
它是一条跨边界路径。
user/sh.c
|
v
fork / exec / wait
|
v
usys.S
|
v
ecall
|
v
trap.c
|
v
syscall.c读完这一组,至少要能回答:
trap 是什么?
syscall 为什么是 trap 的一种?
用户程序为什么不能直接调用内核函数?
shell 执行一条命令为什么要 fork 再 exec?1.3.5 E 组:进程管理与调度
对应文章:
08-process-and-proc-structure.md
09-context-switch-and-scheduler.md
10-sleep-wakeup-and-wait-exit.md
11-locks-and-concurrency.md这一组是 xv6 的核心之一。
08 先讲 struct proc。
09 讲进程如何被调度,CPU 如何从一个执行路径切到另一个执行路径。
10 讲进程不运行时怎么办,比如睡眠、唤醒、退出和父进程回收。
11 讲为什么多核和共享数据必须用锁保护。
这一组可以用一张状态图串起来:
UNUSED
|
v
USED / embryo-like allocated slot
|
v
RUNNABLE <---- wakeup
|
v
RUNNING
|
+-- yield -----> RUNNABLE
|
+-- sleep -----> SLEEPING
|
+-- exit ------> ZOMBIE读完这一组,至少要能回答:
struct proc 保存了什么?
RUNNABLE 和 RUNNING 有什么区别?
swtch.S 切换的到底是什么?
sleep 为什么必须配合锁?
ZOMBIE 为什么不是“真正活着”的进程?1.3.6 F 组:文件、文件系统和设备
对应文章:
12-file-descriptor-and-inode.md
13-file-system-and-log.md
14-device-driver-console-uart-disk.md这一组回答一个日常问题:为什么 read() 和 write() 可以同时用于文件、管道、终端和磁盘相关操作。
12 讲文件描述符、struct file、inode、pipe、device 的层次。
13 讲文件系统如何把名字映射到 inode,再映射到磁盘块,并用日志保护一致性。
14 讲 console、UART、PLIC、VirtIO 磁盘这些设备驱动。
这三篇合起来是一条 I/O 链:
fd number
|
v
struct file
|
+-- pipe backend
|
+-- device backend ---> console / uart
|
+-- inode backend
|
v
fs.c
|
v
log.c
|
v
bio.c
|
v
virtio_disk.c读完这一组,至少要能回答:
fd 为什么只是一个整数?
struct file 和 inode 有什么区别?
目录为什么也是一种文件?
日志为什么能降低崩溃后的损坏风险?
驱动为什么经常和中断、sleep/wakeup 联系在一起?1.3.7 G 组:调试、迁移和实践
对应文章:
16-debugging-xv6-with-gdb.md
17-xv6-to-linux-0-11-bridge.md
18-summary-and-practice-projects.md这一组把“读懂”推进到“验证”和“迁移”。
16 教你用 QEMU/GDB 看真实运行路径。
17 把 xv6 的模型迁移到 Linux 0.11。
18 用实践项目把知识变成能动手修改的能力。
读完这一组,至少要能回答:
如何在 usertrap 或 fork 下断点?
如何观察当前进程的 struct proc?
Linux 0.11 的 task_struct 和 xv6 的 struct proc 怎么对照?
我能做哪些小实验确认自己真的懂了?如果只想快速建立全局感,可以先读:
00 -> 01 -> 04 -> 08 -> 09 -> 12 -> 16如果想按完整顺序稳扎稳打,就从 00 一直读到 18。
如果某个词卡住,比如 trap、chan、inode、context,优先回到对应文章的 本文基础词小注释。
2. 学习对象和前置假设
这个专题默认读者具备最基础的 C 语言经验。
但不假设读者已经熟练掌握复杂指针。
也不假设读者真正理解操作系统内核。
因此每篇文章都要把以下内容讲清楚:
- 这个机制解决什么实际问题。
- 这个机制在 xv6 哪些文件中实现。
- 关键结构体有哪些字段。
- 指针字段指向哪里。
- 函数之间怎么调用。
- CPU 在用户态和内核态之间怎样切换。
- 内存中的对象在哪里。
- 常见误解是什么。
- 可以做什么小实验验证理解。
一个典型解释应该长这样:
概念问题
|
v
源码位置
|
v
关键结构体
|
v
字段含义
|
v
函数调用路径
|
v
状态变化
|
v
调试实验例如讲 struct proc 时,不能只说“进程控制块”。
必须解释:
struct proc是一个 C 结构体。- 它在
kernel/proc.h中定义。 - xv6 用
proc[NPROC]这个全局数组保存所有进程槽位。 - 每个槽位不是一个正在运行的 CPU,而是一个内核维护的进程记录。
state表示这个槽位当前处于什么生命周期阶段。pagetable指向用户页表。trapframe指向保存用户寄存器的位置。context保存内核线程切换时需要恢复的寄存器。parent是指向父进程struct proc的指针。ofile[]是当前进程打开的文件表。cwd指向当前工作目录 inode。
这样才是真的“以 xv6 学操作系统”。
3. 专题目录规划
建议把专题放在:
02-systems-and-performance/
05-xv6/第一阶段文档结构如下:
05-xv6/
|-- 00-overview.md
| |-- 专题总览、内核架构图、源码文件职责图
|
|-- 01-learning-roadmap.md
| |-- 从 C 指针薄弱到能读 xv6 的学习路线
|
|-- 02-c-and-pointers-for-xv6.md
| |-- struct、指针、数组、函数指针、头文件、内存布局
|
|-- 03-riscv-basics-for-xv6.md
| |-- RISC-V 寄存器、特权级、中断、页表、汇编入口
|
|-- 04-how-to-read-xv6-source.md
| |-- Makefile、目录、GDB、QEMU、阅读顺序
|
|-- 05-boot-and-kernel-entry.md
| |-- entry.S、start.c、main.c、kernel.ld
|
|-- 06-memory-layout-and-page-table.md
| |-- kalloc、vm、memlayout、trampoline、satp
|
|-- 07-trap-interrupt-and-system-call.md
| |-- trap、syscall、trampoline、sysproc、sysfile
|
|-- 08-process-and-proc-structure.md
| |-- proc.h、proc.c、struct proc、fork、exit、wait
|
|-- 09-context-switch-and-scheduler.md
| |-- scheduler、swtch.S、context、RUNNABLE/RUNNING
|
|-- 10-sleep-wakeup-and-wait-exit.md
| |-- sleep、wakeup、wait、exit、kill、lost wakeup
|
|-- 11-locks-and-concurrency.md
| |-- spinlock、sleeplock、中断关闭、死锁
|
|-- 12-file-descriptor-and-inode.md
| |-- file、inode、fd、pipe、sysfile
|
|-- 13-file-system-and-log.md
| |-- fs、bio、log、buffer cache、transaction
|
|-- 14-device-driver-console-uart-disk.md
| |-- console、uart、plic、virtio_disk
|
|-- 15-user-programs-and-shell.md
| |-- user 程序、init、sh、系统调用用户态入口
|
|-- 16-debugging-xv6-with-gdb.md
| |-- 断点、单步、观察 proc、页表和 trapframe
|
|-- 17-xv6-to-linux-0-11-bridge.md
| |-- 从 xv6 迁移到 Linux 0.11 的概念对照
|
|-- 18-summary-and-practice-projects.md
| |-- 实验任务、源码改造、小项目这个顺序故意把 proc 放在第 8 篇。
不是因为进程不重要,而是因为进程太重要。
如果没有先理解启动、地址空间、trap 和系统调用,proc 会变成一堆字段名。
等前面铺好之后,proc 就会自然变成所有机制的汇合点。
4. xv6 内核总架构图
xv6 内核可以从“用户程序发起一次系统调用”这条主线理解。
用户程序运行在用户态。
内核运行在内核态。
二者之间通过 trap 进入内核。
内核内部再根据系统调用号、进程状态、文件描述符、页表、设备中断等信息完成工作。
下面是第一张总图。
+====================================================================+
| User Space |
+====================================================================+
| |
| user/init.c user/sh.c user/*.c |
| | | | |
| +----------------+--------------+ |
| | |
| v |
| user/user.h declarations |
| | |
| v |
| user/usys.pl generates usys.S |
| | |
| v |
| ecall: enter kernel by syscall |
| |
+======================= trap boundary ==============================+
|
v
+====================================================================+
| Kernel Space |
+====================================================================+
| |
| Trap and syscall path |
| +------------------+ +------------------+ |
| | trampoline.S | --> | trap.c | |
| | user/kernel jump | | usertrap/kernel | |
| +------------------+ +------------------+ |
| | |
| v |
| +------------------+ |
| | syscall.c | |
| | dispatch table | |
| +------------------+ |
| / \ |
| v v |
| +----------------+ +----------------+ |
| | sysproc.c | | sysfile.c | |
| | process syscalls| | file syscalls | |
| +----------------+ +----------------+ |
| |
| Process and scheduling |
| +------------------+ +------------------+ |
| | proc.h | <-> | proc.c | |
| | struct proc/cpu | | fork/exit/wait | |
| +------------------+ | scheduler/sleep | |
| | +------------------+ |
| | | |
| | v |
| | +------------------+ |
| | | swtch.S | |
| | | context switch | |
| | +------------------+ |
| | |
| v |
| Memory management |
| +------------------+ +------------------+ |
| | memlayout.h | | vm.c | |
| | address map | | page tables | |
| +------------------+ +------------------+ |
| | |
| v |
| +------------------+ |
| | kalloc.c | |
| | physical pages | |
| +------------------+ |
| |
| File and pipe layer |
| +------------------+ +------------------+ |
| | file.h | <-> | file.c | |
| | file/inode defs | | file table ops | |
| +------------------+ +------------------+ |
| | | |
| v v |
| +------------------+ +------------------+ |
| | pipe.c | | fs.c | |
| | pipe read/write | | inode/path/fs | |
| +------------------+ +------------------+ |
| | |
| v |
| +------------------+ |
| | log.c | |
| | fs transaction | |
| +------------------+ |
| | |
| v |
| +------------------+ |
| | bio.c | |
| | buffer cache | |
| +------------------+ |
| |
| Device and interrupt layer |
| +------------------+ +------------------+ |
| | console.c | <-> | uart.c | |
| | console input | | serial device | |
| +------------------+ +------------------+ |
| | |
| v |
| +------------------+ |
| | plic.c | |
| | interrupt ctrl | |
| +------------------+ |
| | |
| v |
| +------------------+ |
| | virtio_disk.c | |
| | disk driver | |
| +------------------+ |
| |
| Shared infrastructure |
| +----------+ +----------+ +----------+ +----------+ +----------+ |
| | defs.h | | types.h | | param.h | | riscv.h | | printf.c | |
| +----------+ +----------+ +----------+ +----------+ +----------+ |
| +----------+ +----------+ +----------+ +----------+ +----------+ |
| | string.c | | spinlock | | sleeplock| | kernel.ld| | entry.S | |
| +----------+ +----------+ +----------+ +----------+ +----------+ |
| |
+====================================================================+这张图先不要背。
先记住四个核心方向。
第一,用户程序不会直接调用内核函数。
它通过 ecall 进入 trap 路径,再由 syscall.c 分发。
第二,进程不是单个函数。
进程是 struct proc 加上一组资源:页表、内核栈、trapframe、打开文件、当前目录、调度状态。
第三,文件系统不是直接读写磁盘。
文件操作会经过 file 层、inode 层、log 层、buffer cache 层,最后才到 virtio 磁盘。
第四,设备中断不是普通函数调用。
设备触发中断,trap 入口接住中断,内核再调用对应驱动处理。
5. 从硬件到内核的启动架构
xv6-riscv 运行在 QEMU 模拟的 RISC-V virt 机器上。
启动路径不是从 main() 直接开始。
它先经过汇编入口,再进入 C 代码。
QEMU virt machine
|
v
RISC-V reset / machine mode
|
v
kernel/entry.S
|
|-- set stack for each hart
|-- jump to start()
v
kernel/start.c
|
|-- configure machine mode state
|-- delegate interrupts/exceptions
|-- switch toward supervisor mode
v
kernel/main.c
|
|-- initialize console
|-- initialize printf
|-- initialize physical allocator
|-- initialize page table
|-- initialize traps
|-- initialize plic
|-- initialize buffer cache
|-- initialize inode table
|-- initialize file table
|-- initialize disk
|-- create first user process
v
kernel/proc.c: scheduler()
|
v
first user process: user/init.c这里的 hart 是 RISC-V 对硬件线程的称呼。
可以先粗略理解成“一个能独立执行指令的 CPU 执行单元”。
xv6 支持多个 hart。
每个 hart 都需要自己的内核栈和 CPU 状态。
这就是为什么后面会看到 struct cpu 和 struct proc 同时存在。
struct cpu
|
+-- describes one executing CPU/hart
+-- has current process pointer
+-- has scheduler context
struct proc
|
+-- describes one process
+-- may run on one CPU at a time
+-- may also be sleeping or runnable一个初学者容易混淆的点是:
CPU 是正在执行的硬件资源。
进程是内核管理的执行对象。
一个 CPU 可以先运行进程 A,再切换到进程 B。
一个进程也可以暂时不运行,只停留在 RUNNABLE 或 SLEEPING 状态。
6. 进程子系统总图
进程是这个专题的中心。
xv6 里和进程有关的内容散落在多个文件里。
但核心结构在 kernel/proc.h。
核心实现集中在 kernel/proc.c。
+-----------------------------+
| kernel/proc.h |
| |
| enum procstate |
| struct context |
| struct cpu |
| struct trapframe |
| struct proc |
+--------------+--------------+
|
v
+-----------------------------+
| kernel/proc.c |
| |
| procinit() |
| allocproc() |
| userinit() |
| fork() |
| exit() |
| wait() |
| scheduler() |
| sched() |
| yield() |
| sleep() |
| wakeup() |
| kill() |
+--------------+--------------+
|
+---------------------------+
| |
v v
+-----------------------------+ +-----------------------------+
| kernel/swtch.S | | kernel/trap.c |
| | | |
| save old context | | usertrap() |
| restore new context | | kerneltrap() |
| return on new kernel stack | | usertrapret() |
+-----------------------------+ +-----------------------------+
| |
v v
+-----------------------------+ +-----------------------------+
| kernel/vm.c | | kernel/sysproc.c |
| | | |
| proc page table | | fork/exit/wait/kill/sleep |
| copy user memory | | expose process syscalls |
+-----------------------------+ +-----------------------------+先建立一句话模型:
进程 = 用户地址空间 + 内核记录 + 调度状态 + 资源表再展开一点:
struct proc
|
+-- lock: protects fields inside proc
|
+-- state: process lifecycle state
|
+-- pagetable: user virtual address space
|
+-- kstack: kernel stack address
|
+-- trapframe: saved user registers for trap return
|
+-- context: saved kernel registers for scheduler switch
|
+-- chan: sleep channel
|
+-- killed: whether process should be killed
|
+-- xstate: exit status
|
+-- pid: process id
|
+-- parent: parent process pointer
|
+-- sz: size of user memory
|
+-- ofile[]: open files
|
+-- cwd: current working directory inode
|
+-- name[]: debug name这里每个指针都要认真看。
parent 不是父进程编号,而是直接指向另一个 struct proc。
pagetable 不是一块普通数组,而是指向页表根节点。
trapframe 指向一页特殊内存,用来保存用户态寄存器。
ofile[] 里面不是磁盘文件本身,而是指向 struct file 的指针。
cwd 不是字符串路径,而是指向当前目录的 inode。
这些指针串起来后,进程才有“运行环境”。
7. 虚拟内存和页表总图
xv6 通过页表让每个进程拥有自己的用户地址空间。
页表的核心代码在 kernel/vm.c。
物理页分配在 kernel/kalloc.c。
地址常量在 kernel/memlayout.h。
RISC-V 页表位和寄存器操作在 kernel/riscv.h。
User virtual address
|
v
process pagetable
|
|-- maps user text/data/heap/stack
|-- maps trapframe
|-- maps trampoline
v
physical page
|
v
RAM managed by kalloc.c一个进程的用户地址空间大致像这样:
high virtual address
+-------------------------------+ TRAMPOLINE
| trampoline code |
+-------------------------------+ TRAPFRAME
| trapframe page |
+-------------------------------+
| user stack |
+-------------------------------+
| guard page |
+-------------------------------+
| heap grows upward |
| ... |
+-------------------------------+
| user data |
+-------------------------------+
| user text |
+-------------------------------+ 0
low virtual addresstrampoline 是一段特殊代码。
它帮助 CPU 在用户态和内核态之间切换。
trapframe 是一块特殊内存。
它保存用户寄存器,使内核处理完系统调用或中断后可以回到原来的用户程序。
对于 C 指针不熟的读者,要注意:
页表里的地址不是 C 指针那种“拿来就能解引用”的普通变量。
页表项描述的是虚拟地址到物理地址的映射关系。
内核需要按照硬件规定的格式填写这些页表项。
然后 RISC-V 的 MMU 才会在执行访存指令时自动做地址翻译。
8. trap、系统调用和中断总图
trap 是一个总称。
它表示 CPU 从正常执行流跳入内核处理特殊事件。
这些特殊事件包括:
- 用户程序执行系统调用。
- 用户程序访问非法地址。
- 设备产生中断。
- 定时器中断要求内核重新调度。
xv6 的 trap 路径主要在:
kernel/trampoline.Skernel/trap.ckernel/syscall.ckernel/sysproc.ckernel/sysfile.c
系统调用路径可以画成:
user program
|
| calls read(), write(), fork(), exec(), ...
v
user/usys.S wrapper
|
| puts syscall number in a7
| executes ecall
v
kernel/trampoline.S
|
| switches register context enough to enter kernel
v
kernel/trap.c:usertrap()
|
| detects syscall trap
v
kernel/syscall.c:syscall()
|
| reads syscall number
| dispatches by table
v
+----------------------+----------------------+
| sysproc.c | sysfile.c |
| process syscalls | file syscalls |
+----------------------+----------------------+
|
v
return value placed in trapframe->a0
|
v
kernel/trap.c:usertrapret()
|
v
kernel/trampoline.S:userret
|
v
back to user program这条路径非常适合初学者反复调试。
因为它连接了用户态、汇编、trapframe、系统调用分发表、内核函数和返回值。
如果能把 getpid() 或 write() 的路径跑通,很多操作系统概念会突然变清楚。
9. 文件系统总图
xv6 的文件系统不是一个单独文件。
它分成几层。
每一层解决一个问题。
system call layer
|
|-- sys_open
|-- sys_read
|-- sys_write
|-- sys_close
v
file descriptor layer
|
|-- per-process fd table: proc.ofile[]
|-- global file table: file.c
v
file abstraction
|
|-- FD_INODE
|-- FD_PIPE
|-- FD_DEVICE
v
inode layer
|
|-- path lookup
|-- inode cache
|-- readi/writei
v
logging layer
|
|-- begin_op
|-- log_write
|-- end_op
v
buffer cache
|
|-- bread
|-- bwrite
|-- brelse
v
virtio disk driver对应文件是:
sysfile.c
|
v
file.c + file.h
|
+--> pipe.c
|
v
fs.c + fs.h
|
v
log.c
|
v
bio.c + buf.h
|
v
virtio_disk.c + virtio.h初学者读文件系统时,最容易被“文件”这个词骗到。
在 xv6 里,文件描述符、struct file、inode、目录项、磁盘块不是一个东西。
它们是不同层次的对象。
一个进程手里的 fd 是整数。
fd 通过 proc.ofile[] 找到 struct file。
struct file 可能指向 inode,也可能指向 pipe,也可能表示设备。
inode 描述磁盘文件的元数据和数据块位置。
buffer cache 缓存磁盘块。
log 保证文件系统修改具备简单的崩溃恢复能力。
10. 锁和并发总图
xv6 是多核内核。
所以很多内核对象必须用锁保护。
主要锁实现是:
kernel/spinlock.ckernel/spinlock.hkernel/sleeplock.ckernel/sleeplock.h
spinlock 可以理解成短时间忙等的锁。
拿不到锁时,CPU 会循环等待。
sleeplock 可以理解成允许睡眠的锁。
拿不到锁时,进程可以睡眠,让 CPU 去运行别的进程。
short critical section
|
v
spinlock
|
|-- protects proc table fields
|-- protects allocator free list
|-- protects buffer cache metadata
|-- disables interrupt nesting rules where needed
longer operation that may block
|
v
sleeplock
|
|-- protects inode content
|-- allows disk I/O while not spinning forever并发学习不要一开始追求“高级无锁”。
先理解三个问题:
第一,谁共享这个数据。
第二,什么时候可能同时访问。
第三,锁保护的是哪个不变量。
例如 struct proc 的 state 字段就不是普通变量。
它决定调度器能不能运行这个进程。
如果多个 CPU 同时修改它,就可能出现一个进程被重复运行、丢失唤醒、或者永远睡眠。
11. xv6 kernel 文件职责总览
下面的表按标准 MIT xv6-riscv 的 kernel/ 目录组织。
后续如果本地 E:\Github\xv6-riscv 有额外文件,应在本表后追加“本地差异”小节。
11.1 启动、链接和特权级切换
entry.S
|
+-- earliest kernel assembly entry
+-- sets up stack for each hart
+-- jumps into start()entry.S 是内核最早执行的代码之一。
它是汇编文件,不是 C 文件。
原因是此时 C 运行环境还没有完全准备好。
它主要负责给每个 hart 设置栈,然后进入 C 函数。
start.c
|
+-- machine-mode setup
+-- configures delegation and interrupts
+-- prepares transition to supervisor modestart.c 处理 RISC-V 机器模式相关初始化。
它让后续内核主要运行在 supervisor mode。
这里会接触到很多 CSR。
CSR 是 Control and Status Register,也就是控制和状态寄存器。
这些寄存器不是普通内存变量,而是 CPU 内部的特殊寄存器。
main.c
|
+-- kernel initialization sequence
+-- calls consoleinit, kinit, kvminit, trapinit, plicinit, binit, iinit
+-- creates first user process
+-- enters schedulermain.c 是读 xv6 的重要入口。
它把各个子系统的初始化顺序串起来。
读 main() 时,不要急着深入每个函数。
先把初始化顺序抄成一张图。
kernel.ld
|
+-- linker script
+-- controls kernel memory layout
+-- defines where text, rodata, data, bss are placedkernel.ld 告诉链接器怎么摆放内核。
这对理解内核地址、trampoline、符号边界很重要。
它不是 C 代码,但决定 C 代码最终在内存里的位置。
11.2 RISC-V、地址和基础类型
riscv.h
|
+-- RISC-V constants and inline assembly helpers
+-- reads/writes CSR registers
+-- defines page table flags
+-- provides interrupt enable/disable helpersriscv.h 是 xv6 和 RISC-V 硬件对话的地方。
初学者可以先把它看成“硬件操作工具箱”。
例如读写 satp、sstatus、sepc 这些寄存器,都会在这里看到封装。
memlayout.h
|
+-- physical and virtual address layout
+-- UART, VIRTIO, PLIC addresses
+-- KERNBASE, PHYSTOP, TRAMPOLINE, TRAPFRAMEmemlayout.h 是地址地图。
读内存管理前必须熟悉它。
它告诉你哪些地址代表设备,哪些地址代表内核,哪些地址代表特殊 trampoline 页。
types.h
|
+-- fixed-width integer aliases
+-- uint, ushort, uchar, uint64types.h 简化基础类型书写。
uint64 在 xv6-riscv 中非常常见,因为 RISC-V 地址和寄存器通常按 64 位处理。
param.h
|
+-- global size constants
+-- NPROC, NOFILE, NFILE, NINODE, NBUF, MAXPATHparam.h 定义系统规模参数。
例如 NPROC 决定最多有多少进程槽位。
这类常量让 xv6 保持简单:很多结构直接使用固定大小数组。
defs.h
|
+-- cross-file function declarations
+-- lets C files call functions implemented elsewheredefs.h 是内核内部函数声明总表。
C 编译器需要先知道函数原型,才能在一个 .c 文件里调用另一个 .c 文件实现的函数。
这对 C 基础薄弱的读者很重要。
头文件不是“复制代码”,而是在编译阶段提供声明。
11.3 进程和调度
proc.h
|
+-- enum procstate
+-- struct context
+-- struct cpu
+-- struct trapframe
+-- struct procproc.h 是进程专题最重要的头文件。
它定义了进程长什么样。
后续所有进程行为都围绕这里的结构体字段展开。
proc.c
|
+-- process table
+-- process allocation
+-- first user process
+-- fork/exit/wait
+-- scheduler/yield/sched
+-- sleep/wakeup/kill
+-- either_copyin/either_copyoutproc.c 是进程生命周期实现。
它不是只负责调度。
它负责从创建、运行、睡眠、唤醒、退出到回收的整条路径。
swtch.S
|
+-- low-level context switch
+-- saves callee-saved registers
+-- restores another context
+-- returns on a different kernel execution pathswtch.S 是很多人第一次觉得 xv6 “神奇”的地方。
它不是普通函数调用。
它保存当前上下文,再恢复另一个上下文。
返回时可能已经不是原来的进程。
11.4 trap 和系统调用
trampoline.S
|
+-- code mapped at same virtual address in user and kernel page tables
+-- handles uservec and userret
+-- bridges user mode and kernel modetrampoline.S 是用户态和内核态切换的桥。
它必须映射在特殊地址。
因为切换页表前后,CPU 还需要继续执行这段代码。
trap.c
|
+-- trap initialization
+-- usertrap
+-- kerneltrap
+-- device interrupt dispatch
+-- return to user modetrap.c 是异常、中断、系统调用进入内核后的总入口。
它决定当前 trap 是来自用户态还是内核态。
也决定是否要调用系统调用分发器,或者处理设备中断。
syscall.h
|
+-- syscall number definitionssyscall.h 给系统调用编号。
用户态和内核态都要对这些编号有一致理解。
syscall.c
|
+-- syscall dispatch table
+-- argument fetching helpers
+-- calls sys_* functionssyscall.c 是系统调用分发器。
它根据用户程序放入寄存器的系统调用号,调用对应内核函数。
sysproc.c
|
+-- process-related syscall implementations
+-- fork, exit, wait, kill, getpid, sbrk, sleep, uptimesysproc.c 处理和进程相关的系统调用。
它通常会进一步调用 proc.c 中的核心函数。
sysfile.c
|
+-- file-related syscall implementations
+-- open, read, write, close, link, unlink, mkdir, mknod, chdir, execsysfile.c 处理文件系统相关系统调用。
它把用户态传来的路径、fd、buffer 参数转换成内核里的 file、inode、buffer 操作。
11.5 内存管理
kalloc.c
|
+-- physical page allocator
+-- maintains free list of 4096-byte pages
+-- provides kalloc and kfreekalloc.c 管理物理页。
它不是通用 malloc。
它按页分配和释放物理内存。
vm.c
|
+-- kernel page table
+-- user page table
+-- map/unmap
+-- copyin/copyout
+-- uvmcreate, uvmcopy, uvmfreevm.c 是虚拟内存核心。
它让内核能建立页表、复制进程地址空间、从用户地址复制数据、释放用户内存。
读 vm.c 时要不断画图。
因为很多参数名字都是地址,但它们不是同一种地址。
elf.h
|
+-- ELF executable file format structures
+-- used by exec.c to load user programself.h 定义 ELF 文件格式。
ELF 是 Unix-like 系统常见的可执行文件格式。
exec.c 会根据这些结构把用户程序加载到新地址空间。
exec.c
|
+-- implements exec
+-- loads ELF program
+-- builds new user stack
+-- replaces current process imageexec.c 实现 exec()。
exec() 不创建新进程。
它把当前进程的用户程序镜像换成另一个程序。
这点非常重要。
fork() 是复制进程。
exec() 是替换当前进程的用户地址空间。
11.6 文件、目录、管道和文件系统
file.h
|
+-- struct file
+-- struct inode
+-- device switch declarationsfile.h 定义文件层和 inode 层的核心结构。
这里的 struct file 不是磁盘上的文件。
它是内核打开文件表中的对象。
file.c
|
+-- global file table
+-- filealloc, filedup, fileclose
+-- fileread, filewrite, filestatfile.c 管理打开文件对象。
它连接进程 fd 和底层 inode、pipe、device。
fcntl.h
|
+-- open flags
+-- O_RDONLY, O_WRONLY, O_RDWR, O_CREATE, O_TRUNCfcntl.h 定义打开文件时使用的标志。
这些标志会被 open() 系统调用解释。
stat.h
|
+-- struct stat
+-- file metadata returned to user programsstat.h 定义文件状态信息。
用户程序调用 stat() 时会得到这个结构。
fs.h
|
+-- on-disk filesystem layout
+-- superblock, dinode, dirent
+-- block size and inode macrosfs.h 描述磁盘上的文件系统格式。
这里的结构关系到磁盘块怎么解释。
它和 file.h 里的内存对象不是一回事。
fs.c
|
+-- inode cache
+-- path lookup
+-- directory operations
+-- readi/writei
+-- block mappingfs.c 是文件系统核心。
它把路径名、目录、inode、数据块连接起来。
log.c
|
+-- simple filesystem logging
+-- groups disk writes into transactions
+-- provides crash recovery boundarylog.c 实现文件系统日志。
它不是打印日志。
它是崩溃恢复用的 write-ahead log。
bio.c
|
+-- buffer cache
+-- caches disk blocks in memory
+-- coordinates block-level reads and writesbio.c 管理 buffer cache。
读磁盘块不是每次都直接访问磁盘。
内核会先查缓存。
buf.h
|
+-- struct buf
+-- buffer cache block metadatabuf.h 定义缓存块结构。
它描述一个磁盘块在内存中的缓存状态。
pipe.c
|
+-- pipe allocation
+-- pipe read/write
+-- pipe close
+-- sleep/wakeup coordination for pipe bufferpipe.c 实现管道。
管道不是磁盘文件。
它是内核中的一段缓冲区,用来让进程之间传输字节流。
11.7 设备、中断和驱动
console.c
|
+-- console input editing
+-- console read/write
+-- connects user-visible console to uartconsole.c 处理控制台。
它负责把键盘输入、控制台输出和用户程序的读写连接起来。
uart.c
|
+-- UART device driver
+-- low-level serial input/output
+-- interrupt handling for console deviceuart.c 是串口驱动。
在 QEMU 环境中,终端输入输出会经过 UART。
plic.c
|
+-- platform-level interrupt controller setup
+-- claims and completes external interruptsplic.c 管理外部中断控制器。
设备中断需要经过 PLIC 分发给 CPU。
virtio.h
|
+-- VirtIO MMIO constants and data structuresvirtio.h 定义 VirtIO 磁盘设备相关结构和常量。
VirtIO 是虚拟化环境中常见的设备接口。
virtio_disk.c
|
+-- VirtIO disk driver
+-- submits disk read/write requests
+-- handles disk interruptsvirtio_disk.c 是磁盘驱动。
文件系统最终的块读写会走到这里。
11.8 锁、字符串和打印
spinlock.h
|
+-- struct spinlock
+-- spinlock function declarationsspinlock.h 定义自旋锁结构。
很多内核结构都嵌入一个 struct spinlock。
spinlock.c
|
+-- initlock
+-- acquire
+-- release
+-- holding
+-- interrupt push_off/pop_off coordinationspinlock.c 实现自旋锁。
这里同时涉及原子操作和中断开关。
不能把它简单理解成用户态互斥锁。
sleeplock.h
|
+-- struct sleeplock
+-- sleeplock declarationssleeplock.h 定义睡眠锁结构。
它用于可能等待较久的内核对象。
sleeplock.c
|
+-- initsleeplock
+-- acquiresleep
+-- releasesleep
+-- holdingsleepsleeplock.c 实现睡眠锁。
它会和 sleep() / wakeup() 机制发生关系。
printf.c
|
+-- kernel printf
+-- panic
+-- formatted output for debuggingprintf.c 是内核调试输出的重要工具。
它不能直接等同于用户态 libc 的 printf。
xv6 内核没有完整 C 标准库。
string.c
|
+-- minimal string and memory helpers
+-- memmove, memset, memcmp, safestrcpy, strlenstring.c 提供内核需要的少量字符串和内存操作。
这些函数虽然简单,但读源码时经常出现。
12. 代码职责总图:从文件到机制
下面把内核文件按机制重新分组。
这比单纯按文件名排序更适合学习。
+---------------------+----------------------------------------------+
| Mechanism | Main files |
+---------------------+----------------------------------------------+
| Boot and link | entry.S, start.c, main.c, kernel.ld |
| RISC-V interface | riscv.h, memlayout.h |
| Basic declarations | types.h, param.h, defs.h |
| Process | proc.h, proc.c, swtch.S |
| Trap/syscall | trampoline.S, trap.c, syscall.c, syscall.h |
| Process syscalls | sysproc.c |
| File syscalls | sysfile.c |
| Virtual memory | vm.c, kalloc.c, elf.h, exec.c |
| File abstraction | file.h, file.c, fcntl.h, stat.h |
| Filesystem | fs.h, fs.c, log.c, bio.c, buf.h |
| Pipe | pipe.c |
| Devices | console.c, uart.c, plic.c, virtio_disk.c |
| Device constants | virtio.h |
| Locks | spinlock.h, spinlock.c, sleeplock.h |
| | sleeplock.c |
| Utility | printf.c, string.c |
+---------------------+----------------------------------------------+再从调用关系看一次:
main.c
|
+--> console.c / uart.c / printf.c
|
+--> kalloc.c
|
+--> vm.c
|
+--> proc.c
|
+--> trap.c / plic.c
|
+--> bio.c / fs.c / log.c / file.c / virtio_disk.c
|
+--> userinit()
|
v
scheduler()系统调用路径:
trampoline.S
|
v
trap.c
|
v
syscall.c
|
+--> sysproc.c
| |
| +--> proc.c
| +--> vm.c
|
+--> sysfile.c
|
+--> file.c
+--> fs.c
+--> pipe.c
+--> exec.c文件系统路径:
sysfile.c
|
v
file.c
|
+--> pipe.c
|
v
fs.c
|
v
log.c
|
v
bio.c
|
v
virtio_disk.c调度路径:
trap timer interrupt
|
v
trap.c
|
v
yield()
|
v
sched()
|
v
swtch.S
|
v
scheduler()
|
v
choose RUNNABLE process
|
v
swtch.S
|
v
process kernel context resumes13. 第一轮学习建议
第一轮不要试图一次读懂所有文件。
建议按“能跑通一条路径”为目标。
第一条路径:启动到第一个用户进程。
entry.S -> start.c -> main.c -> proc.c:userinit -> scheduler -> init第二条路径:一次简单系统调用。
user getpid()
-> usys.S
-> ecall
-> trampoline.S
-> trap.c
-> syscall.c
-> sysproc.c
-> proc.c
-> return to user第三条路径:一次 fork()。
sys_fork
-> fork
-> allocproc
-> uvmcopy
-> filedup
-> idup
-> child RUNNABLE第四条路径:一次 read()。
sys_read
-> fileread
-> either pipe or inode
-> readi
-> bread
-> virtio_disk这四条路径跑通后,xv6 的整体骨架就出来了。
然后再逐篇深入每个机制。
14. 对 C 指针薄弱读者的提醒
读 xv6 时,最常见的困难不是语法,而是“对象关系”。
尤其是这些写法:
struct proc *p;
struct file *f;
struct inode *ip;
pagetable_t pagetable;
char *dst;
uint64 srcva;每次看到指针,都要问四个问题。
pointer variable
|
+-- points to what type?
|
+-- points to one object or an array?
|
+-- who owns the pointed object?
|
+-- how long does that object live?比如:
struct proc *p 通常指向进程表中的一个槽位。
struct file *f 通常指向全局文件表中的一个打开文件对象。
struct inode *ip 通常指向 inode cache 里的一个 inode。
pagetable_t 是页表根节点地址,不是普通业务对象。
char *dst 可能是内核地址,也可能只是函数参数名,需要看上下文。
uint64 srcva 里的 va 通常表示 virtual address。
它是一个数值形式的虚拟地址,不一定能被内核直接当普通指针解引用。
这个区别非常关键。
C pointer
|
+-- compiler treats it as an address of typed object
|
+-- may be dereferenced if valid in current address space
virtual address value
|
+-- may belong to user page table
|
+-- kernel must use copyin/copyout to access safely这就是为什么 xv6 有 copyin() 和 copyout()。
内核不能随便相信用户传进来的地址。
用户地址属于用户页表。
内核需要检查映射,并按规则复制数据。
15. 和 Linux 0.11 的衔接
学 xv6 的最终目的不是只会 xv6。
它还要成为 Linux 0.11 的预备地图。
可以这样对应:
xv6 Linux 0.11
------------------------------------------------------------
struct proc -> task_struct
scheduler() -> schedule()
swtch.S -> switch_to / context switch macros
trap.c -> traps.c / system_call.s
syscall.c -> sys_call_table
vm.c -> memory.c / paging logic
file.c + fs.c -> file_table / inode_table / fs layer
bio.c -> buffer.c
exec.c -> exec.c
pipe.c -> pipe.c两者差异也要提前知道。
xv6-riscv 基于 RISC-V。
Linux 0.11 基于 80386。
xv6 的教学代码更清爽。
Linux 0.11 更接近真实早期 Linux。
xv6 很适合先形成概念骨架。
Linux 0.11 适合再看真实系统如何在 x86、段页机制、历史约束和工程复杂性中落地。
16. 本专题的写作要求
后续每篇 xv6 文章必须遵守本仓库 contract.md。
另外,xv6 专题额外要求:
- 每篇实质文章不少于 500 有效行。
- 每篇至少包含一张 ASCII 架构图。
- 每篇必须列出本篇涉及的源码文件。
- 每篇必须解释关键 C 指针和结构体字段。
- 每篇必须有“初学者常见误解”小节。
- 每篇必须有一个可以在 xv6 中验证的小实验。
- 每篇必须区分概念解释和源码事实。
- 如果没有读到本地
E:\Github\xv6-riscv,必须声明基于标准 MIT xv6-riscv。
一篇合格的 xv6 文章应该像这样组织:
1. 这篇文章解决什么问题
2. 用普通语言解释直觉
3. 列出涉及的源码文件
4. 画出结构图或调用图
5. 解释关键结构体
6. 逐段读源码
7. 解释 C 指针和内存关系
8. 解释操作系统概念
9. 总结常见误解
10. 设计一个小实验17. 推荐第一篇深挖文章
第一篇真正深挖可以写:
08-process-and-proc-structure.md原因是你现在明确提到 proc 和进程不熟。
但它应该在阅读路线中引用前置文章。
这篇文章可以作为“边补边读”的重点案例。
建议结构:
为什么操作系统需要进程
|
v
进程不是程序
|
v
proc.h 中的结构体
|
v
proc.c 中的生命周期函数
|
v
fork / exit / wait / scheduler
|
v
trapframe 和 context 的区别
|
v
指针字段逐个解释
|
v
GDB 观察 proc[NPROC]这篇文章写好后,你会第一次真正看见:
程序
|
v
进程
|
v
地址空间
|
v
CPU 上下文
|
v
内核调度这也是从“期末考试通过”走向“能读操作系统源码”的关键跨越。
18. 参考入口
本专题当前基于标准 MIT xv6-riscv 源码结构。
后续应以本地仓库为准:
E:\Github\xv6-riscv建议参考:
- MIT PDOS xv6-riscv source repository.
- xv6: a simple, Unix-like teaching operating system.
- xv6 book for RISC-V.
- 本仓库
02-systems-and-performance/01-operating-systems中已有操作系统基础材料。
下一步最适合补齐的是 01-learning-roadmap.md。
它应该把“C 指针薄弱、OS 基础较浅”的实际状态转化成可执行学习路线。