xv6 学习路线:从 C 指针到操作系统源码 / xv6 Learning Roadmap
1. 这条路线解决什么问题
学习 xv6 最大的问题通常不是“资料不够”。
真正的问题是入口太多。
你打开 kernel/proc.c。
马上会看到 struct proc *p。
再往下看,会看到锁、状态机、页表、trapframe、context、sleep、wakeup。
每一个词都像认识。
但连在一起就变成一团。
这说明学习路线没有分层。
操作系统源码不是线性文章。
它更像一座城市。
你不能随便从某条小巷钻进去就指望理解整个城市。
你需要先有地图。
这篇文章就是这张地图。
它默认你现在处在这样的位置:
- C 语言能看懂基本语法。
- 指针、数组、结构体有时候会混。
- 操作系统课学过进程、内存、文件系统。
- 但对内核源码如何实现这些概念不熟。
- 想通过 xv6 建立真正的操作系统理解。
- 后面还想继续学 Linux 0.11。
这条路线的目标不是让你背会 xv6。
它的目标是让你能做到:
- 看见一个内核结构体,知道它描述什么对象。
- 看见一个指针字段,知道它指向哪里、谁拥有它、生命周期如何。
- 看见一次系统调用,能画出用户态到内核态的路径。
- 看见
proc,能解释进程的资源、状态和调度关系。 - 看见页表代码,知道哪些是虚拟地址,哪些是物理地址。
- 看见
sleep和wakeup,能理解为什么需要锁。 - 看见文件系统代码,能区分 fd、file、inode、block。
- 学 Linux 0.11 时,能把 xv6 的小模型迁移过去。
先给出总路线。
Stage 0: 环境和源码地图
|
v
Stage 1: C 指针、结构体和内存对象
|
v
Stage 2: RISC-V 执行模型
|
v
Stage 3: xv6 启动和第一个进程
|
v
Stage 4: trap 和系统调用
|
v
Stage 5: proc、fork、exit、wait
|
v
Stage 6: 调度、上下文切换、sleep/wakeup
|
v
Stage 7: 页表、虚拟内存、copyin/copyout
|
v
Stage 8: 文件、inode、日志、磁盘
|
v
Stage 9: 设备、中断、锁和并发
|
v
Stage 10: Linux 0.11 对照这不是唯一顺序。
但它适合“基础不算牢,但愿意仔细读源码”的学习方式。
1.1 本文基础词小注释
下面这些词在路线图中会反复出现。
先有一个最朴素的理解就够。
源码
|
+-- source code
+-- 程序员写的代码文本
+-- xv6 学习不是只背概念,而是回到源码看实现
路径
|
+-- 一次事情经过哪些文件、函数、结构体
+-- 例如 getpid 从用户程序走到 syscall 再回到用户程序
结构体
|
+-- C 语言里的 struct
+-- 用来描述一个对象有哪些字段
+-- xv6 用 struct proc 描述进程
寄存器
|
+-- CPU 内部的小存储位置
+-- 系统调用参数和返回值会经过寄存器
页表
|
+-- 地址翻译表
+-- 帮 CPU 把虚拟地址翻译成物理地址本专题的路线不是从“定义”开始背。
而是不断问:
这个词表示什么对象?
这个对象在源码哪里?
这个对象什么时候创建、使用、释放?2. 先建立一个学习心法
读 xv6 不要像读小说。
也不要像查字典。
更不要试图第一次就逐行看懂所有代码。
更好的方式是四层阅读。
Layer 1: Object
|
|-- 这个结构体描述什么对象?
|-- 这个对象在系统中真实存在吗?
|-- 它是数组中的槽位,还是动态分配的对象?
v
Layer 2: Relation
|
|-- 它指向谁?
|-- 谁引用它?
|-- 谁负责创建和销毁它?
v
Layer 3: State
|
|-- 它有哪些状态?
|-- 哪些函数会改变状态?
|-- 改状态时需要什么锁?
v
Layer 4: Path
|
|-- 一次真实操作如何经过它?
|-- 系统调用、trap、调度、文件读写各走哪条路径?例如 struct proc。
第一层,Object。
它是内核中的进程记录。
不是用户程序代码本身。
第二层,Relation。
它指向用户页表、trapframe、打开文件、当前目录、父进程。
第三层,State。
它可能是 UNUSED、USED、SLEEPING、RUNNABLE、RUNNING、ZOMBIE。
第四层,Path。
fork()、exit()、wait()、scheduler()、sleep()、wakeup() 都会经过它。
如果你按这四层读,源码会从“很多函数”变成“很多对象关系”。
操作系统就是管理这些对象关系的程序。
3. 学习材料的主次
本专题的主线是本地源码:
E:\Github\xv6-riscv当前文档基于标准 MIT xv6-riscv 结构。
如果本地版本有差异,后续文章要以本地源码为准。
学习材料优先级如下。
Priority 1
|
+-- xv6-riscv source code
|
v
Priority 2
|
+-- xv6 book for RISC-V
|
v
Priority 3
|
+-- this Gains Summary xv6 topic
|
v
Priority 4
|
+-- OS textbook concepts
|
v
Priority 5
|
+-- Linux 0.11 comparison这不是说本专题不重要。
本专题的作用是帮你读源码。
源码才是事实来源。
每篇文章都应该回答:
这个概念在源码哪里?
这个结构体在哪里定义?
这个函数什么时候被调用?
这个字段什么时候改变?
这个机制能用什么实验观察?4. Stage 0:环境和源码地图
第一阶段不是读 proc.c。
第一阶段是知道项目怎么运行、源码怎么组织。
你要先能回答:
- xv6 怎么编译?
- xv6 怎么在 QEMU 里运行?
kernel/、user/、mkfs/分别是什么?- 内核代码和用户程序代码怎么区分?
- 哪些文件是汇编?
- 哪些文件是头文件?
- 哪些文件是生成出来的?
Makefile做了什么?
源码地图如下。
xv6-riscv/
|-- kernel/
| |-- xv6 kernel source
| |-- process, memory, trap, syscall, file system, drivers
|
|-- user/
| |-- user programs
| |-- init, sh, cat, echo, grep, tests
|
|-- mkfs/
| |-- host-side tool for building filesystem image
|
|-- Makefile
| |-- build rules
| |-- QEMU run rules
| |-- GDB debug rules
|
|-- README / LICENSE
| |-- project metadata学习目标:
- 能运行 xv6。
- 能区分 kernel 和 user。
- 能知道系统调用为什么需要用户态 wrapper。
- 能知道
kernel/kernel是最终内核镜像。 - 能知道
fs.img是文件系统镜像。 - 能知道 QEMU 在这里扮演硬件机器。
建议阅读文件:
Makefile
kernel/main.c
user/init.c
user/sh.c建议实验:
make qemu进入 xv6 后运行:
ls
echo hello
cat README然后做一个小改动:
修改 user/echo.c 的输出行为
重新 make qemu
观察用户程序变化这个实验说明:
用户程序和内核是不同部分。
你改 user/echo.c 不等于改内核。
这件事很基础,但非常重要。
5. Stage 1:C 指针、结构体和内存对象
如果 C 指针不稳,读 xv6 会非常痛苦。
但不需要先把整本 C 语言书重学一遍。
应该只补“读 xv6 必须用到的 C”。
第一组:结构体。
struct proc {
struct spinlock lock;
enum procstate state;
pagetable_t pagetable;
struct trapframe *trapframe;
struct context context;
struct file *ofile[NOFILE];
};这段代码里有三种不同的东西。
embedded object
|
+-- struct spinlock lock
+-- struct context context
pointer to object
|
+-- struct trapframe *trapframe
array of pointers
|
+-- struct file *ofile[NOFILE]嵌入对象表示这个结构体里面直接放了一个对象。
指针字段表示这个结构体里面放的是一个地址。
数组的指针表示有多个槽位,每个槽位可以指向一个对象。
第二组:数组和全局表。
xv6 经常用固定大小数组。
proc[NPROC]
ftable.file[NFILE]
bcache.buf[NBUF]
itable.inode[NINODE]这些表是教学内核常见写法。
优点是简单。
缺点是不够弹性。
学习时要关注每个槽位的状态。
global table
|
+-- slot 0
+-- slot 1
+-- slot 2
+-- ...第三组:指针生命周期。
每次看到指针,都问:
Who allocated the object?
Who owns it?
Who can share it?
Who frees it?
What lock protects it?例如 struct file *f。
它可能来自全局文件表。
引用计数决定它什么时候被释放。
进程的 ofile[] 只是持有指针。
关闭文件时,不是简单把 fd 变成空,还要减少 struct file 的引用。
学习目标:
- 能看懂
struct proc里每种字段形式。 - 能区分对象、指针、数组、数组里的指针。
- 能解释
p->state和(*p).state是同一件事。 - 能知道
uint64地址值不一定是可直接解引用指针。 - 能理解为什么内核访问用户地址要用
copyin/copyout。
建议阅读文件:
kernel/proc.h
kernel/file.h
kernel/buf.h
kernel/types.h
kernel/param.h建议实验:
给 proc.c 的 allocproc() 或 fork() 加打印。
观察 pid、state、p 指针地址。
你要看到:
same proc slot may be reused
different processes have different struct proc addresses
state changes over lifecycle6. Stage 2:RISC-V 执行模型
xv6-riscv 运行在 RISC-V 上。
不需要一开始精通 RISC-V。
但必须知道几个概念。
第一个概念:寄存器。
寄存器是 CPU 内部的高速存储位置。
函数参数、返回值、栈指针、返回地址都会用寄存器参与。
RISC-V registers used often
|
+-- a0-a7: arguments and return values
+-- sp: stack pointer
+-- ra: return address
+-- sepc: saved exception program counter
+-- sstatus: supervisor status
+-- satp: page table root register第二个概念:特权级。
xv6 主要关心:
Machine mode
|
v
Supervisor mode
|
v
User mode机器模式最底层。
内核主要在 supervisor mode。
用户程序在 user mode。
系统调用就是用户态通过 ecall 进入内核。
第三个概念:异常和中断。
normal instruction flow
|
+-- syscall
+-- page fault
+-- illegal instruction
+-- timer interrupt
+-- device interrupt
|
v
trap into kernel第四个概念:页表寄存器 satp。
satp 保存当前页表根。
切换页表会改变虚拟地址到物理地址的翻译方式。
这就是为什么用户态和内核态切换很复杂。
学习目标:
- 知道用户态和内核态不是普通函数层级。
- 知道
ecall会触发 trap。 - 知道系统调用号通常通过
a7传递。 - 知道返回值通常通过
a0返回。 - 知道
satp决定当前页表。 - 知道
trampoline.S为什么存在。
建议阅读文件:
kernel/riscv.h
kernel/trampoline.S
kernel/kernelvec.S
kernel/entry.S建议实验:
在系统调用路径上设置断点。
观察 a7 和 a0。
如果暂时不会 GDB,也可以先通过打印验证系统调用号和返回值。
7. Stage 3:启动和第一个进程
启动阶段回答一个非常根本的问题:
内核自己是怎么开始运行的?
C 程序通常从 main() 开始。
但内核不是普通用户程序。
xv6 的启动路径大致是:
QEMU starts RISC-V machine
|
v
entry.S
|
v
start.c
|
v
main.c
|
v
userinit()
|
v
scheduler()
|
v
init process你要重点理解 main.c。
因为它是内核子系统初始化的目录。
main()
|
+-- consoleinit()
+-- printfinit()
+-- kinit()
+-- kvminit()
+-- kvminithart()
+-- procinit()
+-- trapinit()
+-- trapinithart()
+-- plicinit()
+-- plicinithart()
+-- binit()
+-- iinit()
+-- fileinit()
+-- virtio_disk_init()
+-- userinit()
+-- scheduler()学习目标:
- 能解释为什么内核要先初始化内存再创建进程。
- 能解释为什么要初始化 trap。
- 能解释为什么第一个进程不是从磁盘 shell 直接开始。
- 能解释
userinit()如何创建第一个用户进程。
建议阅读文件:
kernel/entry.S
kernel/start.c
kernel/main.c
kernel/proc.c
user/init.c建议实验:
在 main() 的每个初始化函数前后加短打印。
观察启动顺序。
不要打印太多。
启动阶段的打印过多会影响阅读。
只打印子系统名字即可。
8. Stage 4:trap 和系统调用
系统调用是最适合初学者打通 xv6 的路径。
因为它连接了用户程序和内核。
比如 getpid()。
用户程序看起来只是调用一个函数。
但真正路径是:
user code
|
v
user wrapper
|
v
ecall
|
v
trampoline.S
|
v
trap.c:usertrap()
|
v
syscall.c:syscall()
|
v
sysproc.c:sys_getpid()
|
v
proc.c:myproc()
|
v
return through trapframe这里有几个关键词。
trapframe 是保存用户寄存器的结构。
系统调用返回值放在 trapframe->a0。
系统调用号通常在 a7。
syscall.c 里有一个分发表。
它把系统调用号映射到 sys_* 函数。
学习目标:
- 能完整画出一次
getpid()路径。 - 能知道
ecall不是普通函数调用。 - 能知道 trap 进入内核后如何找到当前进程。
- 能知道内核如何把返回值交回用户程序。
- 能知道用户指针参数为什么要小心处理。
建议阅读文件:
user/user.h
user/usys.pl
kernel/syscall.h
kernel/syscall.c
kernel/sysproc.c
kernel/trap.c
kernel/trampoline.S建议实验:
新增一个最简单系统调用。
例如返回固定整数。
user program -> syscall wrapper -> syscall table -> sys_hello -> return这个实验比直接读一堆概念更有效。
它会逼你理解用户态声明、系统调用号、内核分发表、返回路径。
9. Stage 5:proc、fork、exit、wait
到了这里,才真正进入 proc。
proc 不是孤立概念。
它是前面所有内容的汇合点。
struct proc
|
+-- identity: pid, name
+-- lifecycle: state, killed, xstate
+-- memory: pagetable, sz, trapframe, kstack
+-- scheduling: context, chan
+-- family: parent
+-- resources: ofile[], cwd初学者要先理解“程序”和“进程”的区别。
程序是磁盘上的可执行文件或一段代码。
进程是正在被操作系统管理的执行实例。
一个程序可以运行多次,产生多个进程。
一个进程也可以通过 exec() 替换自己的程序内容。
fork() 创建新进程。
exit() 结束当前进程。
wait() 让父进程等待子进程退出。
scheduler() 决定哪个 RUNNABLE 进程运行。
学习目标:
- 能解释
struct proc每个重要字段。 - 能解释
proc[NPROC]是什么。 - 能解释
allocproc()如何找空槽位。 - 能解释
fork()复制了什么,没有复制什么。 - 能解释
exit()为什么留下 ZOMBIE。 - 能解释
wait()为什么负责回收子进程。
建议阅读文件:
kernel/proc.h
kernel/proc.c
kernel/sysproc.c
kernel/vm.c
kernel/file.c建议实验:
写一个用户程序:
parent
|
+-- fork child
|
+-- wait child在内核里打印:
fork pid
exit pid
wait pid
state transition观察父子进程的生命周期。
10. Stage 6:调度、上下文切换、sleep/wakeup
调度是 xv6 最迷人的部分之一。
也是最容易误解的部分之一。
很多人以为调度就是从一个函数跳到另一个函数。
但上下文切换更像这样:
Process A kernel context
|
v
sched()
|
v
swtch(&p->context, &cpu->context)
|
v
Scheduler context
|
v
choose Process B
|
v
swtch(&cpu->context, &p->context)
|
v
Process B kernel context这里的 context 保存的是内核线程恢复所需的寄存器。
它和 trapframe 不一样。
trapframe 面向用户态返回。
context 面向内核态上下文切换。
trapframe
|
+-- saved user registers
+-- used when returning to user mode
context
|
+-- saved kernel callee-saved registers
+-- used by swtch between scheduler and processsleep() 和 wakeup() 解决等待问题。
比如管道没有数据时,读进程不应该一直占着 CPU。
它应该睡眠。
等写进程写入数据后,再唤醒它。
学习目标:
- 能解释 RUNNABLE 和 RUNNING 的区别。
- 能解释 scheduler 为什么每个 CPU 都有。
- 能解释
swtch.S保存和恢复什么。 - 能解释
sleep(chan, lock)为什么需要锁参数。 - 能解释 lost wakeup 是什么。
建议阅读文件:
kernel/proc.c
kernel/swtch.S
kernel/proc.h
kernel/spinlock.c
kernel/pipe.c建议实验:
在 scheduler() 和 swtch() 前后打印 pid。
打印要非常克制。
调度路径频率很高。
过多打印会把系统刷爆。
11. Stage 7:页表、虚拟内存、copyin/copyout
虚拟内存是操作系统学习的一道门槛。
不要一上来背多级页表细节。
先建立一句话模型。
virtual address
|
v
page table
|
v
physical address每个进程有自己的用户页表。
内核也有自己的页表。
这就是为什么同一个虚拟地址,在不同上下文里可能表示不同物理内存。
用户传给内核的指针尤其危险。
例如 read(fd, buf, n) 里的 buf 是用户地址。
内核不能直接信任它。
必须通过 copyout() 把数据复制到用户地址空间。
kernel buffer
|
| copyout checks user pagetable
v
user virtual address学习目标:
- 能区分虚拟地址和物理地址。
- 能解释页表项是什么。
- 能解释
walk()和mappages()大概做什么。 - 能解释
uvmcopy()为什么用于 fork。 - 能解释
uvmfree()为什么用于释放进程地址空间。 - 能解释
copyin/copyout为什么存在。
建议阅读文件:
kernel/vm.c
kernel/kalloc.c
kernel/memlayout.h
kernel/riscv.h
kernel/exec.c
kernel/proc.c建议实验:
在 exec() 或 fork() 后打印进程 pagetable 地址。
再观察不同进程是否有不同页表。
可以进一步让用户程序访问非法地址,观察 trap。
12. Stage 8:文件、inode、日志、磁盘
文件系统要分层学。
不要把 fd、file、inode、block 混成一个“文件”。
fd integer
|
v
proc.ofile[fd]
|
v
struct file
|
v
struct inode
|
v
disk blockfd 是用户程序看到的整数。
struct file 是内核打开文件对象。
inode 描述文件元数据和数据块位置。
block 是磁盘上的固定大小数据块。
日志层 log.c 不是调试日志。
它是文件系统修改的事务机制。
buffer cache bio.c 不是文件内容本身。
它是磁盘块在内存中的缓存。
学习目标:
- 能解释
open()返回的 fd 如何连接到 inode。 - 能解释
read()如何从 fd 走到磁盘块。 - 能解释 pipe 和 inode 文件的区别。
- 能解释 log 层为什么存在。
- 能解释 buffer cache 为什么需要锁。
建议阅读文件:
kernel/sysfile.c
kernel/file.c
kernel/file.h
kernel/fs.c
kernel/fs.h
kernel/log.c
kernel/bio.c
kernel/buf.h
kernel/pipe.c
kernel/virtio_disk.c建议实验:
写一个用户程序:
open file
write text
close
open again
read text在 sys_write()、filewrite()、writei()、bread() 路径加少量打印。
观察层次关系。
13. Stage 9:设备、中断、锁和并发
设备和中断让内核变成真实系统。
没有设备,内核只能自己算。
有了设备,内核必须处理外部事件。
external device
|
v
interrupt controller
|
v
trap into kernel
|
v
driver interrupt handler
|
v
wakeup waiting processUART 负责控制台输入输出。
VirtIO disk 负责磁盘访问。
PLIC 负责外部中断分发。
锁则保护共享内核对象。
shared object
|
+-- proc table
+-- free page list
+-- file table
+-- inode cache
+-- buffer cache
|
v
lock protects invariant学习目标:
- 能解释中断和系统调用都属于 trap,但来源不同。
- 能解释设备驱动为什么会唤醒睡眠进程。
- 能解释 spinlock 和 sleeplock 区别。
- 能解释为什么持锁期间要小心 sleep。
- 能解释为什么 xv6 需要关中断来配合某些锁逻辑。
建议阅读文件:
kernel/console.c
kernel/uart.c
kernel/plic.c
kernel/virtio_disk.c
kernel/spinlock.c
kernel/sleeplock.c
kernel/proc.c建议实验:
从控制台输入字符。
追踪 UART 中断到 console buffer,再到等待输入的进程被唤醒。
这条路径会同时看到设备、中断、锁、sleep/wakeup。
14. Stage 10:从 xv6 过渡到 Linux 0.11
学 Linux 0.11 前,不要把 xv6 忘掉。
它是你的简化地图。
过渡时可以按概念一一对应。
xv6 concept Linux 0.11 concept
----------------------------------------------------------
struct proc -> task_struct
proc[NPROC] -> task table
scheduler() -> schedule()
swtch.S -> switch_to
trap.c -> traps.c
syscall.c -> sys_call_table
vm.c -> paging and memory.c
file.c -> file table logic
fs.c -> inode and filesystem code
bio.c -> buffer cache但也要清楚差异。
xv6-riscv 面向 RISC-V。
Linux 0.11 面向 80386。
xv6 简洁。
Linux 0.11 有更多历史工程痕迹。
xv6 避免了很多复杂优化。
Linux 0.11 更接近真实内核演化。
过渡方法:
先在 xv6 中理解机制
|
v
写出对象关系图
|
v
找到 Linux 0.11 对应结构
|
v
比较字段和调用路径
|
v
总结相同点和不同点例如进程。
先彻底理解 xv6 的 struct proc。
再去读 Linux 0.11 的 task_struct。
你会发现 Linux 0.11 字段更多。
但它仍然要表达:
- 进程身份。
- 调度状态。
- 寄存器上下文。
- 内存信息。
- 文件信息。
- 父子关系。
- 信号和时间信息。
也就是说,复杂系统不是凭空复杂。
它是在同一组基本问题上增加更多约束。
15. 每篇文章应该怎么读
后续每篇专题文章都可以按同一种阅读方式。
第一遍,只看总图。
不要纠结细节。
第二遍,看源码文件列表。
知道这篇会涉及哪些文件。
第三遍,看关键结构体。
把字段抄到纸上。
第四遍,看关键函数。
只看函数名和调用关系。
第五遍,再逐段读源码。
第六遍,做实验。
overview diagram
|
v
source files
|
v
structures
|
v
functions
|
v
line-by-line reading
|
v
experiment如果某一遍卡住,不要硬钻。
退回上一层。
比如看 fork() 卡住,可能不是 fork() 难。
可能是你还没理解:
struct procpagetable_ttrapframefiledupidupRUNNABLE
这时应该补对象关系图,而不是继续死读代码。
16. 每个阶段的合格标准
学习路线要有检查点。
否则容易产生“我好像看过了”的错觉。
Stage 0 合格:
- 能运行 xv6。
- 能找到 kernel 和 user 目录。
- 能解释 QEMU 是什么。
- 能知道
Makefile的大致作用。
Stage 1 合格:
- 能解释
struct proc *p。 - 能解释
p->field。 - 能区分嵌入结构体和指针字段。
- 能区分数组、指针数组、指向数组的指针。
Stage 2 合格:
- 能解释 user mode 和 supervisor mode。
- 能解释
ecall。 - 能知道
a0和a7在系统调用中的作用。 - 能知道
satp和页表有关。
Stage 3 合格:
- 能画出启动路径。
- 能解释
main()初始化顺序。 - 能解释第一个用户进程从哪里来。
Stage 4 合格:
- 能画出
getpid()系统调用路径。 - 能解释
trapframe->a0。 - 能解释
syscall.c分发表。
Stage 5 合格:
- 能解释
struct proc主要字段。 - 能解释
fork/exit/wait的生命周期。 - 能解释 ZOMBIE 为什么存在。
Stage 6 合格:
- 能解释
context和trapframe的区别。 - 能画出
scheduler和进程之间的 swtch。 - 能解释 sleep/wakeup 为什么需要锁。
Stage 7 合格:
- 能解释虚拟地址和物理地址。
- 能解释
copyin/copyout。 - 能解释
uvmcopy和uvmfree。
Stage 8 合格:
- 能画出 fd 到 disk block 的路径。
- 能区分 file、inode、buf。
- 能解释 log 层不是打印日志。
Stage 9 合格:
- 能解释设备中断路径。
- 能解释 spinlock 和 sleeplock。
- 能解释锁保护的是不变量。
Stage 10 合格:
- 能把 xv6 的
struct proc对照到 Linux 0.11 的task_struct。 - 能把 xv6 的 syscall path 对照到 Linux 0.11 的 system call path。
- 能说出架构差异,而不是只说“Linux 更复杂”。
17. 推荐学习节奏
不要一天读太多源码。
操作系统源码需要消化。
推荐节奏:
Day 1
|
+-- read overview
+-- run xv6
+-- identify directories
Day 2-3
|
+-- C pointer and struct for xv6
+-- read proc.h / file.h / buf.h lightly
Day 4-5
|
+-- RISC-V basics
+-- read riscv.h / trampoline.S lightly
Day 6-7
|
+-- boot and main()
+-- trace first process
Week 2
|
+-- syscall path
+-- add simple syscall
Week 3
|
+-- proc, fork, exit, wait
+-- draw lifecycle
Week 4
|
+-- scheduler and swtch
+-- sleep/wakeup
Week 5
|
+-- page table and memory
+-- copyin/copyout
Week 6
|
+-- file system and device
+-- Linux 0.11 bridge这个节奏不是考试复习计划。
它是源码理解计划。
每天真正吃透一小段,比一天翻完几十页更有效。
18. 常见误解
误解一:先把 C 全部学完再读 xv6。
不需要。
你应该补和 xv6 直接相关的 C。
边读边补效果更好。
误解二:先把操作系统教材全学完再读源码。
也不需要。
教材概念要和源码互相印证。
只学教材容易抽象悬空。
只读源码容易陷入细节。
误解三:proc.c 是第一篇应该读的文件。
不一定。
proc.c 是核心,但它依赖很多前置概念。
先读 main.c、系统调用路径、proc.h,再深入 proc.c 会更顺。
误解四:系统调用就是函数调用。
不是。
系统调用跨越用户态和内核态。
它涉及寄存器、trap、权限、页表和返回路径。
误解五:指针就是地址,能直接访问。
在普通 C 程序里这个说法已经不够严谨。
在内核里更危险。
用户虚拟地址不能被内核随便当普通指针用。
误解六:文件描述符就是文件。
不是。
fd 是整数。
它通过进程打开文件表指向 struct file。
struct file 再连接到 inode、pipe 或 device。
误解七:锁只是防止两个函数同时执行。
更准确地说,锁保护共享对象的不变量。
你要问的是“这个锁保护哪些字段的一致性”。
19. 第一轮实践任务
为了让路线落地,第一轮可以做这些任务。
任务一:启动路径笔记。
写出:
entry.S -> start.c -> main.c -> userinit -> scheduler -> init每个节点写一句职责。
任务二:系统调用路径笔记。
选择 getpid()。
画出:
user getpid
-> usys.S
-> ecall
-> trampoline
-> usertrap
-> syscall
-> sys_getpid
-> return任务三:struct proc 字段卡片。
每个字段写:
field name
|
+-- type
+-- meaning
+-- who writes it
+-- who reads it
+-- lock requirement任务四:fork 生命周期图。
画出父进程和子进程如何分开。
任务五:fd 到 inode 图。
选择一次 read()。
画出 fd、file、inode、buf 的关系。
任务六:Linux 0.11 对照表。
先只做 proc -> task_struct。
不要一开始对照所有模块。
20. 本路线的下一篇
下一篇应该是:
02-c-and-pointers-for-xv6.md这篇不是普通 C 语言教程。
它只服务一个目标:
让你读 xv6 时不被指针、结构体、数组、地址、头文件和函数声明卡住。
这条路线最终会把你带到:
能读 xv6
|
v
能解释 xv6
|
v
能改一个小机制
|
v
能用 xv6 理解 Linux 0.11这才是源码学习真正的进展。