Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 系统与高性能 / Systems & Performance

xv6 源码学习 / xv6 Source Learning

1. xv6 源码学习专题 / xv6 Source Learning

2. xv6 学习路线:从 C 指针到操作系统源码 / xv6 Learning Roadmap

3. 读 xv6 必备的 C 语言和指针基础 / C And Pointers For xv6

4. 读 xv6 必备的 RISC-V 基础 / RISC-V Basics For xv6

5. 如何阅读 xv6 源码 / How To Read xv6 Source

6. xv6 启动与内核入口 / xv6 Boot And Kernel Entry

7. xv6 内存布局与页表 / xv6 Memory Layout And Page Tables

8. xv6 trap、中断与系统调用 / xv6 Traps Interrupts And System Calls

9. xv6 进程与 struct proc / xv6 Process And struct proc

10. xv6 上下文切换与调度器 / xv6 Context Switch And Scheduler

11. xv6 sleep/wakeup、wait 与 exit / xv6 Sleep Wakeup Wait And Exit

12. xv6 锁与并发 / xv6 Locks And Concurrency

13. xv6 文件描述符与 inode / xv6 File Descriptor And Inode

14. xv6 文件系统与日志 / xv6 File System And Log

15. xv6 设备驱动、console、UART 与磁盘 / xv6 Device Driver Console UART And Disk

16. xv6 用户程序与 shell / xv6 User Programs And Shell

17. 用 QEMU/GDB 调试 xv6 / Debugging xv6 With QEMU And GDB

18. 从 xv6 过渡到 Linux 0.11 / Bridge From xv6 To Linux 0.11

19. xv6 专题总结与实践项目 / xv6 Summary And Practice Projects

本页目录

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 的小模型迁移过去。

先给出总路线。

text
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

这不是唯一顺序。

但它适合“基础不算牢,但愿意仔细读源码”的学习方式。

1.1 本文基础词小注释 ​

下面这些词在路线图中会反复出现。

先有一个最朴素的理解就够。

text
源码
  |
  +-- source code
  +-- 程序员写的代码文本
  +-- xv6 学习不是只背概念,而是回到源码看实现

路径
  |
  +-- 一次事情经过哪些文件、函数、结构体
  +-- 例如 getpid 从用户程序走到 syscall 再回到用户程序

结构体
  |
  +-- C 语言里的 struct
  +-- 用来描述一个对象有哪些字段
  +-- xv6 用 struct proc 描述进程

寄存器
  |
  +-- CPU 内部的小存储位置
  +-- 系统调用参数和返回值会经过寄存器

页表
  |
  +-- 地址翻译表
  +-- 帮 CPU 把虚拟地址翻译成物理地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

本专题的路线不是从“定义”开始背。

而是不断问:

text
这个词表示什么对象?
这个对象在源码哪里?
这个对象什么时候创建、使用、释放?
1
2
3

2. 先建立一个学习心法 ​

读 xv6 不要像读小说。

也不要像查字典。

更不要试图第一次就逐行看懂所有代码。

更好的方式是四层阅读。

text
Layer 1: Object
  |
  |-- 这个结构体描述什么对象?
  |-- 这个对象在系统中真实存在吗?
  |-- 它是数组中的槽位,还是动态分配的对象?
  v
Layer 2: Relation
  |
  |-- 它指向谁?
  |-- 谁引用它?
  |-- 谁负责创建和销毁它?
  v
Layer 3: State
  |
  |-- 它有哪些状态?
  |-- 哪些函数会改变状态?
  |-- 改状态时需要什么锁?
  v
Layer 4: Path
  |
  |-- 一次真实操作如何经过它?
  |-- 系统调用、trap、调度、文件读写各走哪条路径?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

例如 struct proc。

第一层,Object。

它是内核中的进程记录。

不是用户程序代码本身。

第二层,Relation。

它指向用户页表、trapframe、打开文件、当前目录、父进程。

第三层,State。

它可能是 UNUSED、USED、SLEEPING、RUNNABLE、RUNNING、ZOMBIE。

第四层,Path。

fork()、exit()、wait()、scheduler()、sleep()、wakeup() 都会经过它。

如果你按这四层读,源码会从“很多函数”变成“很多对象关系”。

操作系统就是管理这些对象关系的程序。

3. 学习材料的主次 ​

本专题的主线是本地源码:

text
E:\Github\xv6-riscv
1

当前文档基于标准 MIT xv6-riscv 结构。

如果本地版本有差异,后续文章要以本地源码为准。

学习材料优先级如下。

text
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这不是说本专题不重要。

本专题的作用是帮你读源码。

源码才是事实来源。

每篇文章都应该回答:

text
这个概念在源码哪里?
这个结构体在哪里定义?
这个函数什么时候被调用?
这个字段什么时候改变?
这个机制能用什么实验观察?
1
2
3
4
5

4. Stage 0:环境和源码地图 ​

第一阶段不是读 proc.c。

第一阶段是知道项目怎么运行、源码怎么组织。

你要先能回答:

  • xv6 怎么编译?
  • xv6 怎么在 QEMU 里运行?
  • kernel/、user/、mkfs/ 分别是什么?
  • 内核代码和用户程序代码怎么区分?
  • 哪些文件是汇编?
  • 哪些文件是头文件?
  • 哪些文件是生成出来的?
  • Makefile 做了什么?

源码地图如下。

text
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

学习目标:

  • 能运行 xv6。
  • 能区分 kernel 和 user。
  • 能知道系统调用为什么需要用户态 wrapper。
  • 能知道 kernel/kernel 是最终内核镜像。
  • 能知道 fs.img 是文件系统镜像。
  • 能知道 QEMU 在这里扮演硬件机器。

建议阅读文件:

text
Makefile
kernel/main.c
user/init.c
user/sh.c
1
2
3
4

建议实验:

text
make qemu
1

进入 xv6 后运行:

text
ls
echo hello
cat README
1
2
3

然后做一个小改动:

text
修改 user/echo.c 的输出行为
重新 make qemu
观察用户程序变化
1
2
3

这个实验说明:

用户程序和内核是不同部分。

你改 user/echo.c 不等于改内核。

这件事很基础,但非常重要。

5. Stage 1:C 指针、结构体和内存对象 ​

如果 C 指针不稳,读 xv6 会非常痛苦。

但不需要先把整本 C 语言书重学一遍。

应该只补“读 xv6 必须用到的 C”。

第一组:结构体。

c
struct proc {
  struct spinlock lock;
  enum procstate state;
  pagetable_t pagetable;
  struct trapframe *trapframe;
  struct context context;
  struct file *ofile[NOFILE];
};
1
2
3
4
5
6
7
8

这段代码里有三种不同的东西。

text
embedded object
  |
  +-- struct spinlock lock
  +-- struct context context

pointer to object
  |
  +-- struct trapframe *trapframe

array of pointers
  |
  +-- struct file *ofile[NOFILE]
1
2
3
4
5
6
7
8
9
10
11
12

嵌入对象表示这个结构体里面直接放了一个对象。

指针字段表示这个结构体里面放的是一个地址。

数组的指针表示有多个槽位,每个槽位可以指向一个对象。

第二组:数组和全局表。

xv6 经常用固定大小数组。

text
proc[NPROC]
ftable.file[NFILE]
bcache.buf[NBUF]
itable.inode[NINODE]
1
2
3
4

这些表是教学内核常见写法。

优点是简单。

缺点是不够弹性。

学习时要关注每个槽位的状态。

text
global table
  |
  +-- slot 0
  +-- slot 1
  +-- slot 2
  +-- ...
1
2
3
4
5
6

第三组:指针生命周期。

每次看到指针,都问:

text
Who allocated the object?
Who owns it?
Who can share it?
Who frees it?
What lock protects it?
1
2
3
4
5

例如 struct file *f。

它可能来自全局文件表。

引用计数决定它什么时候被释放。

进程的 ofile[] 只是持有指针。

关闭文件时,不是简单把 fd 变成空,还要减少 struct file 的引用。

学习目标:

  • 能看懂 struct proc 里每种字段形式。
  • 能区分对象、指针、数组、数组里的指针。
  • 能解释 p->state 和 (*p).state 是同一件事。
  • 能知道 uint64 地址值不一定是可直接解引用指针。
  • 能理解为什么内核访问用户地址要用 copyin/copyout。

建议阅读文件:

text
kernel/proc.h
kernel/file.h
kernel/buf.h
kernel/types.h
kernel/param.h
1
2
3
4
5

建议实验:

给 proc.c 的 allocproc() 或 fork() 加打印。

观察 pid、state、p 指针地址。

你要看到:

text
same proc slot may be reused
different processes have different struct proc addresses
state changes over lifecycle
1
2
3

6. Stage 2:RISC-V 执行模型 ​

xv6-riscv 运行在 RISC-V 上。

不需要一开始精通 RISC-V。

但必须知道几个概念。

第一个概念:寄存器。

寄存器是 CPU 内部的高速存储位置。

函数参数、返回值、栈指针、返回地址都会用寄存器参与。

text
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
1
2
3
4
5
6
7
8

第二个概念:特权级。

xv6 主要关心:

text
Machine mode
  |
  v
Supervisor mode
  |
  v
User mode
1
2
3
4
5
6
7

机器模式最底层。

内核主要在 supervisor mode。

用户程序在 user mode。

系统调用就是用户态通过 ecall 进入内核。

第三个概念:异常和中断。

text
normal instruction flow
  |
  +-- syscall
  +-- page fault
  +-- illegal instruction
  +-- timer interrupt
  +-- device interrupt
  |
  v
trap into kernel
1
2
3
4
5
6
7
8
9
10

第四个概念:页表寄存器 satp。

satp 保存当前页表根。

切换页表会改变虚拟地址到物理地址的翻译方式。

这就是为什么用户态和内核态切换很复杂。

学习目标:

  • 知道用户态和内核态不是普通函数层级。
  • 知道 ecall 会触发 trap。
  • 知道系统调用号通常通过 a7 传递。
  • 知道返回值通常通过 a0 返回。
  • 知道 satp 决定当前页表。
  • 知道 trampoline.S 为什么存在。

建议阅读文件:

text
kernel/riscv.h
kernel/trampoline.S
kernel/kernelvec.S
kernel/entry.S
1
2
3
4

建议实验:

在系统调用路径上设置断点。

观察 a7 和 a0。

如果暂时不会 GDB,也可以先通过打印验证系统调用号和返回值。

7. Stage 3:启动和第一个进程 ​

启动阶段回答一个非常根本的问题:

内核自己是怎么开始运行的?

C 程序通常从 main() 开始。

但内核不是普通用户程序。

xv6 的启动路径大致是:

text
QEMU starts RISC-V machine
  |
  v
entry.S
  |
  v
start.c
  |
  v
main.c
  |
  v
userinit()
  |
  v
scheduler()
  |
  v
init process
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

你要重点理解 main.c。

因为它是内核子系统初始化的目录。

text
main()
  |
  +-- consoleinit()
  +-- printfinit()
  +-- kinit()
  +-- kvminit()
  +-- kvminithart()
  +-- procinit()
  +-- trapinit()
  +-- trapinithart()
  +-- plicinit()
  +-- plicinithart()
  +-- binit()
  +-- iinit()
  +-- fileinit()
  +-- virtio_disk_init()
  +-- userinit()
  +-- scheduler()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

学习目标:

  • 能解释为什么内核要先初始化内存再创建进程。
  • 能解释为什么要初始化 trap。
  • 能解释为什么第一个进程不是从磁盘 shell 直接开始。
  • 能解释 userinit() 如何创建第一个用户进程。

建议阅读文件:

text
kernel/entry.S
kernel/start.c
kernel/main.c
kernel/proc.c
user/init.c
1
2
3
4
5

建议实验:

在 main() 的每个初始化函数前后加短打印。

观察启动顺序。

不要打印太多。

启动阶段的打印过多会影响阅读。

只打印子系统名字即可。

8. Stage 4:trap 和系统调用 ​

系统调用是最适合初学者打通 xv6 的路径。

因为它连接了用户程序和内核。

比如 getpid()。

用户程序看起来只是调用一个函数。

但真正路径是:

text
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这里有几个关键词。

trapframe 是保存用户寄存器的结构。

系统调用返回值放在 trapframe->a0。

系统调用号通常在 a7。

syscall.c 里有一个分发表。

它把系统调用号映射到 sys_* 函数。

学习目标:

  • 能完整画出一次 getpid() 路径。
  • 能知道 ecall 不是普通函数调用。
  • 能知道 trap 进入内核后如何找到当前进程。
  • 能知道内核如何把返回值交回用户程序。
  • 能知道用户指针参数为什么要小心处理。

建议阅读文件:

text
user/user.h
user/usys.pl
kernel/syscall.h
kernel/syscall.c
kernel/sysproc.c
kernel/trap.c
kernel/trampoline.S
1
2
3
4
5
6
7

建议实验:

新增一个最简单系统调用。

例如返回固定整数。

text
user program -> syscall wrapper -> syscall table -> sys_hello -> return
1

这个实验比直接读一堆概念更有效。

它会逼你理解用户态声明、系统调用号、内核分发表、返回路径。

9. Stage 5:proc、fork、exit、wait ​

到了这里,才真正进入 proc。

proc 不是孤立概念。

它是前面所有内容的汇合点。

text
struct proc
  |
  +-- identity: pid, name
  +-- lifecycle: state, killed, xstate
  +-- memory: pagetable, sz, trapframe, kstack
  +-- scheduling: context, chan
  +-- family: parent
  +-- resources: ofile[], cwd
1
2
3
4
5
6
7
8

初学者要先理解“程序”和“进程”的区别。

程序是磁盘上的可执行文件或一段代码。

进程是正在被操作系统管理的执行实例。

一个程序可以运行多次,产生多个进程。

一个进程也可以通过 exec() 替换自己的程序内容。

fork() 创建新进程。

exit() 结束当前进程。

wait() 让父进程等待子进程退出。

scheduler() 决定哪个 RUNNABLE 进程运行。

学习目标:

  • 能解释 struct proc 每个重要字段。
  • 能解释 proc[NPROC] 是什么。
  • 能解释 allocproc() 如何找空槽位。
  • 能解释 fork() 复制了什么,没有复制什么。
  • 能解释 exit() 为什么留下 ZOMBIE。
  • 能解释 wait() 为什么负责回收子进程。

建议阅读文件:

text
kernel/proc.h
kernel/proc.c
kernel/sysproc.c
kernel/vm.c
kernel/file.c
1
2
3
4
5

建议实验:

写一个用户程序:

text
parent
  |
  +-- fork child
  |
  +-- wait child
1
2
3
4
5

在内核里打印:

text
fork pid
exit pid
wait pid
state transition
1
2
3
4

观察父子进程的生命周期。

10. Stage 6:调度、上下文切换、sleep/wakeup ​

调度是 xv6 最迷人的部分之一。

也是最容易误解的部分之一。

很多人以为调度就是从一个函数跳到另一个函数。

但上下文切换更像这样:

text
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这里的 context 保存的是内核线程恢复所需的寄存器。

它和 trapframe 不一样。

trapframe 面向用户态返回。

context 面向内核态上下文切换。

text
trapframe
  |
  +-- saved user registers
  +-- used when returning to user mode

context
  |
  +-- saved kernel callee-saved registers
  +-- used by swtch between scheduler and process
1
2
3
4
5
6
7
8
9

sleep() 和 wakeup() 解决等待问题。

比如管道没有数据时,读进程不应该一直占着 CPU。

它应该睡眠。

等写进程写入数据后,再唤醒它。

学习目标:

  • 能解释 RUNNABLE 和 RUNNING 的区别。
  • 能解释 scheduler 为什么每个 CPU 都有。
  • 能解释 swtch.S 保存和恢复什么。
  • 能解释 sleep(chan, lock) 为什么需要锁参数。
  • 能解释 lost wakeup 是什么。

建议阅读文件:

text
kernel/proc.c
kernel/swtch.S
kernel/proc.h
kernel/spinlock.c
kernel/pipe.c
1
2
3
4
5

建议实验:

在 scheduler() 和 swtch() 前后打印 pid。

打印要非常克制。

调度路径频率很高。

过多打印会把系统刷爆。

11. Stage 7:页表、虚拟内存、copyin/copyout ​

虚拟内存是操作系统学习的一道门槛。

不要一上来背多级页表细节。

先建立一句话模型。

text
virtual address
  |
  v
page table
  |
  v
physical address
1
2
3
4
5
6
7

每个进程有自己的用户页表。

内核也有自己的页表。

这就是为什么同一个虚拟地址,在不同上下文里可能表示不同物理内存。

用户传给内核的指针尤其危险。

例如 read(fd, buf, n) 里的 buf 是用户地址。

内核不能直接信任它。

必须通过 copyout() 把数据复制到用户地址空间。

text
kernel buffer
  |
  | copyout checks user pagetable
  v
user virtual address
1
2
3
4
5

学习目标:

  • 能区分虚拟地址和物理地址。
  • 能解释页表项是什么。
  • 能解释 walk() 和 mappages() 大概做什么。
  • 能解释 uvmcopy() 为什么用于 fork。
  • 能解释 uvmfree() 为什么用于释放进程地址空间。
  • 能解释 copyin/copyout 为什么存在。

建议阅读文件:

text
kernel/vm.c
kernel/kalloc.c
kernel/memlayout.h
kernel/riscv.h
kernel/exec.c
kernel/proc.c
1
2
3
4
5
6

建议实验:

在 exec() 或 fork() 后打印进程 pagetable 地址。

再观察不同进程是否有不同页表。

可以进一步让用户程序访问非法地址,观察 trap。

12. Stage 8:文件、inode、日志、磁盘 ​

文件系统要分层学。

不要把 fd、file、inode、block 混成一个“文件”。

text
fd integer
  |
  v
proc.ofile[fd]
  |
  v
struct file
  |
  v
struct inode
  |
  v
disk block
1
2
3
4
5
6
7
8
9
10
11
12
13

fd 是用户程序看到的整数。

struct file 是内核打开文件对象。

inode 描述文件元数据和数据块位置。

block 是磁盘上的固定大小数据块。

日志层 log.c 不是调试日志。

它是文件系统修改的事务机制。

buffer cache bio.c 不是文件内容本身。

它是磁盘块在内存中的缓存。

学习目标:

  • 能解释 open() 返回的 fd 如何连接到 inode。
  • 能解释 read() 如何从 fd 走到磁盘块。
  • 能解释 pipe 和 inode 文件的区别。
  • 能解释 log 层为什么存在。
  • 能解释 buffer cache 为什么需要锁。

建议阅读文件:

text
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
1
2
3
4
5
6
7
8
9
10

建议实验:

写一个用户程序:

text
open file
write text
close
open again
read text
1
2
3
4
5

在 sys_write()、filewrite()、writei()、bread() 路径加少量打印。

观察层次关系。

13. Stage 9:设备、中断、锁和并发 ​

设备和中断让内核变成真实系统。

没有设备,内核只能自己算。

有了设备,内核必须处理外部事件。

text
external device
  |
  v
interrupt controller
  |
  v
trap into kernel
  |
  v
driver interrupt handler
  |
  v
wakeup waiting process
1
2
3
4
5
6
7
8
9
10
11
12
13

UART 负责控制台输入输出。

VirtIO disk 负责磁盘访问。

PLIC 负责外部中断分发。

锁则保护共享内核对象。

text
shared object
  |
  +-- proc table
  +-- free page list
  +-- file table
  +-- inode cache
  +-- buffer cache
  |
  v
lock protects invariant
1
2
3
4
5
6
7
8
9
10

学习目标:

  • 能解释中断和系统调用都属于 trap,但来源不同。
  • 能解释设备驱动为什么会唤醒睡眠进程。
  • 能解释 spinlock 和 sleeplock 区别。
  • 能解释为什么持锁期间要小心 sleep。
  • 能解释为什么 xv6 需要关中断来配合某些锁逻辑。

建议阅读文件:

text
kernel/console.c
kernel/uart.c
kernel/plic.c
kernel/virtio_disk.c
kernel/spinlock.c
kernel/sleeplock.c
kernel/proc.c
1
2
3
4
5
6
7

建议实验:

从控制台输入字符。

追踪 UART 中断到 console buffer,再到等待输入的进程被唤醒。

这条路径会同时看到设备、中断、锁、sleep/wakeup。

14. Stage 10:从 xv6 过渡到 Linux 0.11 ​

学 Linux 0.11 前,不要把 xv6 忘掉。

它是你的简化地图。

过渡时可以按概念一一对应。

text
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
1
2
3
4
5
6
7
8
9
10
11
12

但也要清楚差异。

xv6-riscv 面向 RISC-V。

Linux 0.11 面向 80386。

xv6 简洁。

Linux 0.11 有更多历史工程痕迹。

xv6 避免了很多复杂优化。

Linux 0.11 更接近真实内核演化。

过渡方法:

text
先在 xv6 中理解机制
  |
  v
写出对象关系图
  |
  v
找到 Linux 0.11 对应结构
  |
  v
比较字段和调用路径
  |
  v
总结相同点和不同点
1
2
3
4
5
6
7
8
9
10
11
12
13

例如进程。

先彻底理解 xv6 的 struct proc。

再去读 Linux 0.11 的 task_struct。

你会发现 Linux 0.11 字段更多。

但它仍然要表达:

  • 进程身份。
  • 调度状态。
  • 寄存器上下文。
  • 内存信息。
  • 文件信息。
  • 父子关系。
  • 信号和时间信息。

也就是说,复杂系统不是凭空复杂。

它是在同一组基本问题上增加更多约束。

15. 每篇文章应该怎么读 ​

后续每篇专题文章都可以按同一种阅读方式。

第一遍,只看总图。

不要纠结细节。

第二遍,看源码文件列表。

知道这篇会涉及哪些文件。

第三遍,看关键结构体。

把字段抄到纸上。

第四遍,看关键函数。

只看函数名和调用关系。

第五遍,再逐段读源码。

第六遍,做实验。

text
overview diagram
  |
  v
source files
  |
  v
structures
  |
  v
functions
  |
  v
line-by-line reading
  |
  v
experiment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果某一遍卡住,不要硬钻。

退回上一层。

比如看 fork() 卡住,可能不是 fork() 难。

可能是你还没理解:

  • struct proc
  • pagetable_t
  • trapframe
  • filedup
  • idup
  • RUNNABLE

这时应该补对象关系图,而不是继续死读代码。

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. 推荐学习节奏 ​

不要一天读太多源码。

操作系统源码需要消化。

推荐节奏:

text
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

这个节奏不是考试复习计划。

它是源码理解计划。

每天真正吃透一小段,比一天翻完几十页更有效。

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. 第一轮实践任务 ​

为了让路线落地,第一轮可以做这些任务。

任务一:启动路径笔记。

写出:

text
entry.S -> start.c -> main.c -> userinit -> scheduler -> init
1

每个节点写一句职责。

任务二:系统调用路径笔记。

选择 getpid()。

画出:

text
user getpid
  -> usys.S
  -> ecall
  -> trampoline
  -> usertrap
  -> syscall
  -> sys_getpid
  -> return
1
2
3
4
5
6
7
8

任务三:struct proc 字段卡片。

每个字段写:

text
field name
  |
  +-- type
  +-- meaning
  +-- who writes it
  +-- who reads it
  +-- lock requirement
1
2
3
4
5
6
7

任务四:fork 生命周期图。

画出父进程和子进程如何分开。

任务五:fd 到 inode 图。

选择一次 read()。

画出 fd、file、inode、buf 的关系。

任务六:Linux 0.11 对照表。

先只做 proc -> task_struct。

不要一开始对照所有模块。

20. 本路线的下一篇 ​

下一篇应该是:

text
02-c-and-pointers-for-xv6.md
1

这篇不是普通 C 语言教程。

它只服务一个目标:

让你读 xv6 时不被指针、结构体、数组、地址、头文件和函数声明卡住。

这条路线最终会把你带到:

text
能读 xv6
  |
  v
能解释 xv6
  |
  v
能改一个小机制
  |
  v
能用 xv6 理解 Linux 0.11
1
2
3
4
5
6
7
8
9
10

这才是源码学习真正的进展。

最后更新于:

Pager
上一篇1. xv6 源码学习专题 / xv6 Source Learning
下一篇3. 读 xv6 必备的 C 语言和指针基础 / C And Pointers For xv6

持续记录,持续成长

Copyright © Tidenflow