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 源码 / How To Read xv6 Source ​

1. 为什么需要专门讲“怎么读” ​

xv6 的源码不大。

但“不大”不等于“容易读”。

操作系统源码和普通应用源码最大的区别是:控制流经常不是普通函数调用。

你会遇到:

  • 启动阶段从汇编进入 C。
  • 用户态通过 ecall 进入内核。
  • 中断在任意时刻打断执行。
  • 调度器通过 swtch.S 切换上下文。
  • 页表决定同一个地址在不同上下文的含义。
  • 文件系统通过多层对象间接访问磁盘块。

如果你按普通 C 项目方式从一个 main() 一路点进去,很快就会断。

所以读 xv6 要有方法。

这篇文章的目标是建立一套稳定阅读流程。

text
read repository map
  |
  v
run xv6
  |
  v
find one execution path
  |
  v
identify involved files
  |
  v
draw structures and pointers
  |
  v
read key functions
  |
  v
verify by experiment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这套流程会贯穿整个专题。

每次学一个机制,都按照这个方法走。

1.1 本文基础词小注释 ​

这篇讲“怎么读源码”,会频繁提到这些词。

text
目录
  |
  +-- folder
  +-- 用来组织源码文件
  +-- kernel、user、mkfs 是 xv6 的关键目录

Makefile
  |
  +-- 构建脚本
  +-- 告诉 make 如何编译内核、用户程序和文件系统镜像

QEMU
  |
  +-- 虚拟机/模拟器
  +-- xv6-riscv 通常跑在 QEMU 模拟的 RISC-V 机器里

GDB
  |
  +-- 调试器
  +-- 可以打断点、单步执行、看变量和寄存器

调用路径
  |
  +-- 一次操作经过哪些函数
  +-- 例如 getpid 经过 user wrapper、trap、syscall、sys_getpid
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

读源码时不要只问“这一行是什么意思”。

先问:

text
我现在在哪个目录?
这个文件属于内核还是用户程序?
这段代码位于哪条执行路径上?
1
2
3

2. 项目目录先看成三层 ​

标准 xv6-riscv 大致可以分成三层。

text
xv6-riscv/
|
+-- kernel/
|   |
|   +-- operating system kernel
|   +-- runs in supervisor mode
|   +-- manages processes, memory, files, devices
|
+-- user/
|   |
|   +-- user programs
|   +-- runs in user mode
|   +-- calls kernel through syscalls
|
+-- mkfs/
    |
    +-- host-side filesystem image builder
    +-- runs on your development machine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

再加上几个顶层文件:

text
Makefile
  |
  +-- build kernel
  +-- build user programs
  +-- build filesystem image
  +-- run QEMU
  +-- support GDB

README / LICENSE
  |
  +-- project notes and license
1
2
3
4
5
6
7
8
9
10
11

先不要急着读每个文件。

先把这三层分清楚。

kernel/ 是内核。

user/ 是运行在 xv6 里的用户程序。

mkfs/ 是在宿主机上运行的工具,用来制作 xv6 的文件系统镜像。

这三个层次的执行环境不同。

text
host machine
  |
  +-- compiler
  +-- make
  +-- mkfs tool
  +-- qemu
       |
       v
     xv6 virtual machine
       |
       +-- kernel code
       +-- user programs
1
2
3
4
5
6
7
8
9
10
11
12

初学者常把 mkfs 和 xv6 内核混在一起。

mkfs 不是 xv6 内核的一部分。

它是在你的开发环境里运行,用来生成 fs.img。

3. 内核源码应该按机制分组 ​

kernel/ 目录里文件很多。

按文件名顺序读通常不合适。

应该按机制分组。

text
+---------------------+----------------------------------------------+
| Group               | Files                                        |
+---------------------+----------------------------------------------+
| Boot                | entry.S, start.c, main.c, kernel.ld          |
| Hardware interface  | riscv.h, memlayout.h                         |
| Process             | proc.h, proc.c, swtch.S                      |
| Trap and syscall    | trampoline.S, kernelvec.S, trap.c            |
| Syscall dispatch    | syscall.h, syscall.c, sysproc.c, sysfile.c   |
| Memory              | kalloc.c, vm.c, exec.c, elf.h                |
| 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     |
| Locks               | spinlock.h, spinlock.c, sleeplock.h          |
| Utilities           | defs.h, types.h, param.h, printf.c, string.c |
+---------------------+----------------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这张表不是为了背。

它的作用是帮助你定位。

例如你看到 read() 系统调用。

你就知道它可能跨越:

text
sysfile.c
  |
  v
file.c
  |
  v
fs.c or pipe.c
  |
  v
bio.c
  |
  v
virtio_disk.c
1
2
3
4
5
6
7
8
9
10
11
12
13

看到 fork()。

你就知道它可能跨越:

text
sysproc.c
  |
  v
proc.c
  |
  +-- vm.c
  +-- file.c
  +-- fs.c
1
2
3
4
5
6
7
8

读源码时,先问“这属于哪组机制”。

再问“主文件在哪里”。

4. 用户程序和内核代码的边界 ​

xv6 里有用户程序。

例如:

text
user/init.c
user/sh.c
user/cat.c
user/echo.c
user/grep.c
1
2
3
4
5

这些程序运行在用户态。

它们不能直接访问内核内部函数。

用户程序需要通过系统调用请求内核服务。

边界图:

text
user program
  |
  | calls read(), write(), fork(), exec()
  v
user syscall wrapper
  |
  | ecall
  v
kernel trap path
  |
  v
syscall dispatch
  |
  v
kernel implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

用户态头文件:

text
user/user.h
  |
  +-- user-visible function declarations
  +-- includes syscall wrappers and libc-like helpers
1
2
3
4

内核态头文件:

text
kernel/defs.h
  |
  +-- kernel internal function declarations
1
2
3

不要把这两个混起来。

用户程序包含的是用户态接口。

内核文件包含的是内核内部接口。

系统调用正是它们之间的桥。

5. Makefile 要读什么 ​

初学者容易跳过 Makefile。

但 xv6 的 Makefile 很重要。

它告诉你:

  • 哪些文件会编译进内核。
  • 哪些文件是用户程序。
  • 用户系统调用汇编如何生成。
  • 文件系统镜像如何生成。
  • QEMU 如何启动。
  • GDB 如何连接。

读 Makefile 不需要精通 Make。

先找这些信息。

text
kernel object list
  |
  +-- which kernel .c/.S files are compiled

user program list
  |
  +-- which user programs are built

usys generation
  |
  +-- user/usys.pl -> user/usys.S

fs image rule
  |
  +-- mkfs creates fs.img

qemu rule
  |
  +-- runs kernel and disk image

gdb rule
  |
  +-- starts QEMU with debug options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

构建关系可以画成:

text
kernel/*.c + kernel/*.S
  |
  v
kernel object files
  |
  v
kernel/kernel

user/*.c + user/usys.S
  |
  v
user programs
  |
  v
mkfs
  |
  v
fs.img

kernel/kernel + fs.img
  |
  v
QEMU runs xv6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这张图能帮助你理解:

内核镜像和文件系统镜像是两个东西。

用户程序不是直接“放进内核”。

它们被编译后放入文件系统镜像。

xv6 启动后,从文件系统中执行用户程序。

6. 第一次运行 xv6 时看什么 ​

第一次运行 xv6,不要只看到 shell 出来就结束。

你应该观察三件事。

第一,启动输出。

它说明内核已经初始化并进入用户空间。

第二,shell。

它说明第一个用户进程已经启动,并且可以执行用户程序。

第三,简单命令。

例如:

text
ls
echo hello
cat README
1
2
3

这些命令会走文件系统和系统调用。

运行结构:

text
QEMU
  |
  v
xv6 kernel boots
  |
  v
init process starts
  |
  v
shell starts
  |
  v
user commands call syscalls
1
2
3
4
5
6
7
8
9
10
11
12
13

第一次运行的学习目标不是马上调试。

而是确认:

  • 内核能启动。
  • 用户进程能运行。
  • 系统调用能工作。
  • 文件系统镜像中有用户程序。

这是一切源码阅读的基础。

7. 如何选择第一条阅读路径 ​

读源码最怕没有路径。

建议第一条路径选 getpid()。

因为它最短。

text
user program calls getpid
  |
  v
user syscall wrapper
  |
  v
ecall
  |
  v
trap
  |
  v
syscall dispatch
  |
  v
sys_getpid
  |
  v
myproc
  |
  v
return pid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

涉及文件:

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

为什么选 getpid()?

因为它不涉及复杂文件系统,不涉及大量数据复制。

它只需要找到当前进程并返回 pid。

但它完整经过系统调用边界。

第二条路径可以选 fork()。

它开始接触进程复制。

第三条路径可以选 read() 或 write()。

它开始接触 fd、file、inode、pipe、console。

推荐顺序:

text
getpid
  |
  v
fork
  |
  v
exit/wait
  |
  v
write to console
  |
  v
read from file
1
2
3
4
5
6
7
8
9
10
11
12
13

这比一开始读 scheduler() 更友好。

8. 如何读一个结构体 ​

读 xv6 不能只追函数。

结构体更重要。

每个核心机制都有核心结构体。

text
process
  |
  +-- struct proc
  +-- struct cpu
  +-- struct context
  +-- struct trapframe

file
  |
  +-- struct file
  +-- struct inode
  +-- struct pipe

filesystem
  |
  +-- struct superblock
  +-- struct dinode
  +-- struct dirent
  +-- struct buf

lock
  |
  +-- struct spinlock
  +-- struct sleeplock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

读结构体用固定模板。

text
structure name
  |
  +-- what object does it represent?
  |
  +-- where is it allocated?
  |
  +-- who owns it?
  |
  +-- what fields are pointers?
  |
  +-- what fields are state?
  |
  +-- what lock protects it?
  |
  +-- what functions create/change/destroy it?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

例如 struct proc:

text
represents: process
allocated in: proc[NPROC]
owned by: process subsystem
protected by: p->lock
state field: state
memory fields: pagetable, trapframe, kstack, sz
resource fields: ofile[], cwd
lifecycle functions: allocproc, fork, exit, wait, freeproc
1
2
3
4
5
6
7
8

如果一开始就逐行看 proc.c,很容易迷路。

先把 struct proc 读成对象地图,会顺很多。

9. 如何读一个函数 ​

读函数也要有固定模板。

不要只看每一行做什么。

先看这个函数在机制里扮演什么角色。

text
function
  |
  +-- who calls it?
  |
  +-- when is it called?
  |
  +-- what object does it operate on?
  |
  +-- what locks must be held?
  |
  +-- what state does it change?
  |
  +-- what errors can happen?
  |
  +-- what does it return?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

以 fork() 为例。

text
fork()
  |
  +-- called by sys_fork()
  |
  +-- allocates a new proc
  |
  +-- copies user memory
  |
  +-- copies trapframe
  |
  +-- duplicates open files
  |
  +-- duplicates cwd
  |
  +-- sets child runnable
  |
  +-- returns child pid to parent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这比直接从第一行读到最后一行更清楚。

再看 exit()。

text
exit()
  |
  +-- closes files
  +-- releases cwd
  +-- reparents children
  +-- records exit status
  +-- becomes ZOMBIE
  +-- wakes parent
  +-- calls sched
1
2
3
4
5
6
7
8
9

函数阅读要结合状态图。

text
state before
  |
  v
function body
  |
  v
state after
1
2
3
4
5
6
7

操作系统代码的重点常常不是计算结果。

而是状态变化。

10. 如何画调用路径 ​

每学一个机制,都画调用路径。

调用路径不要一开始画得过细。

先画主干。

例如系统调用:

text
user wrapper
  |
  v
ecall
  |
  v
usertrap
  |
  v
syscall
  |
  v
sys_* implementation
1
2
3
4
5
6
7
8
9
10
11
12
13

再补文件:

text
user/usys.S
  |
  v
kernel/trampoline.S
  |
  v
kernel/trap.c
  |
  v
kernel/syscall.c
  |
  v
kernel/sysproc.c or kernel/sysfile.c
1
2
3
4
5
6
7
8
9
10
11
12
13

最后补结构体:

text
trapframe
  |
  +-- a7 syscall number
  +-- a0 return value
1
2
3
4

这就是三层图。

text
mechanism path
  |
  v
file path
  |
  v
data structure path
1
2
3
4
5
6
7

如果只画函数名,容易忽略数据。

如果只画结构体,容易忽略控制流。

两者要结合。

11. 如何使用搜索 ​

读源码离不开搜索。

推荐用 rg。

常见搜索策略:

text
find definition
  |
  +-- rg "struct proc"
  +-- rg "enum procstate"
  +-- rg "void scheduler"

find callers
  |
  +-- rg "fork\\("
  +-- rg "wakeup\\("
  +-- rg "copyout\\("

find constants
  |
  +-- rg "NPROC"
  +-- rg "TRAMPOLINE"
  +-- rg "SYS_getpid"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

搜索时要注意函数名可能出现在声明、定义、调用、注释中。

所以看结果要分类。

text
search result
  |
  +-- declaration
  +-- definition
  +-- caller
  +-- comment
  +-- macro
1
2
3
4
5
6
7

例如搜索 fork。

你会看到:

  • 用户态声明。
  • 系统调用号。
  • 系统调用 wrapper。
  • sys_fork()。
  • fork()。
  • 用户程序调用。

不要被结果数量吓到。

把它们按层次摆开。

text
user layer
  |
  +-- user.h
  +-- usys.S
  +-- user programs

syscall layer
  |
  +-- syscall.h
  +-- syscall.c
  +-- sysproc.c

kernel core
  |
  +-- proc.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

12. 如何用打印调试 ​

打印是最简单的观察手段。

但在内核里,打印也要小心。

因为有些路径频率很高。

例如:

  • 调度器。
  • 时钟中断。
  • 文件读写。
  • 锁获取释放。

随便打印可能让输出爆炸。

推荐原则:

text
print only at lifecycle boundary
  |
  +-- process created
  +-- process exits
  +-- syscall enters
  +-- file opens/closes
  +-- page table created/freed
1
2
3
4
5
6
7

打印内容要短。

例如:

text
fork: parent=2 child=3
exit: pid=3 status=0
wait: parent=2 reaped=3
syscall: pid=2 num=11
1
2
3
4

不要一开始打印整个结构体。

先打印关键字段。

text
object address
  |
  +-- p pointer

identity
  |
  +-- pid

state
  |
  +-- state

relation
  |
  +-- parent pid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

打印要带阶段名。

否则多进程输出混在一起时看不懂。

13. 如何用 GDB ​

GDB 是读 xv6 的重要工具。

但初学者不要一开始追求复杂命令。

先会这几件事:

  • 设置断点。
  • 单步。
  • 查看变量。
  • 查看结构体字段。
  • 查看寄存器。
  • 继续运行。

GDB 阅读目标:

text
break at function
  |
  v
observe current process
  |
  v
inspect fields
  |
  v
step to next function
  |
  v
compare with diagram
1
2
3
4
5
6
7
8
9
10
11
12
13

适合断点:

text
main
usertrap
syscall
sys_getpid
fork
allocproc
exit
wait
scheduler
swtch
copyin
copyout
1
2
3
4
5
6
7
8
9
10
11
12

系统调用路径调试:

text
break syscall
run user command
inspect syscall number
inspect current proc
continue
1
2
3
4
5

进程路径调试:

text
break fork
break allocproc
break exit
break wait
1
2
3
4

页表路径调试:

text
break uvmcopy
break mappages
break walk
break copyout
1
2
3
4

GDB 不只是为了找 bug。

它是为了验证你的脑内模型。

14. 如何记录学习笔记 ​

每次读源码都应该有固定笔记格式。

不要只摘抄代码。

建议模板:

text
Topic:

Question:

Source files:

Core structures:

Call path:

State changes:

Pointer relations:

Locks:

Experiment:

What I misunderstood:

Summary:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

例如读 fork():

text
Topic:
  fork creates child process

Question:
  What is copied from parent to child?

Source files:
  sysproc.c, proc.c, vm.c, file.c, fs.c

Core structures:
  struct proc, trapframe, pagetable, struct file, inode

Call path:
  sys_fork -> fork -> allocproc -> uvmcopy -> filedup -> idup

State changes:
  UNUSED -> USED -> RUNNABLE

Pointer relations:
  child parent points to parent
  child ofile entries point to same file objects with increased ref

Locks:
  proc locks around state changes

Experiment:
  print parent pid and child pid
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

这种笔记会直接变成后续文章素材。

15. 阅读顺序建议 ​

第一次完整学习 xv6,建议不要按文件名排序。

按路径排序。

第一组:全局地图。

text
Makefile
kernel/main.c
kernel/defs.h
kernel/param.h
kernel/types.h
1
2
3
4
5

第二组:第一个用户进程。

text
kernel/proc.h
kernel/proc.c
user/init.c
user/sh.c
1
2
3
4

第三组:系统调用。

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
kernel/proc.h
kernel/proc.c
kernel/swtch.S
1
2
3

第五组:虚拟内存。

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

第六组:文件系统。

text
kernel/file.h
kernel/file.c
kernel/sysfile.c
kernel/fs.h
kernel/fs.c
kernel/log.c
kernel/bio.c
kernel/virtio_disk.c
1
2
3
4
5
6
7
8

第七组:设备和并发。

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

这个顺序的好处是:

从最短路径开始。

逐步增加对象关系。

不会一上来被文件系统或调度细节压垮。

16. 不同机制的推荐入口函数 ​

每个机制都有推荐入口。

不要随便打开最大文件。

text
Boot
  |
  +-- main()

Syscall
  |
  +-- syscall()
  +-- usertrap()

Process
  |
  +-- allocproc()
  +-- fork()
  +-- exit()
  +-- wait()

Scheduling
  |
  +-- scheduler()
  +-- sched()
  +-- yield()

Memory
  |
  +-- kalloc()
  +-- mappages()
  +-- walk()
  +-- uvmcopy()
  +-- copyin()
  +-- copyout()

File
  |
  +-- sys_open()
  +-- sys_read()
  +-- filealloc()
  +-- fileread()

Filesystem
  |
  +-- namei()
  +-- readi()
  +-- writei()
  +-- bread()

Device
  |
  +-- devintr()
  +-- consoleintr()
  +-- virtio_disk_rw()
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
46
47
48
49
50

这些入口函数就像城市地铁站。

从它们出发,容易连接到周围机制。

17. 读源码时如何处理“不懂” ​

不懂很正常。

关键是判断“不懂属于哪一层”。

text
I do not understand this line
  |
  +-- C syntax problem?
  |
  +-- data structure problem?
  |
  +-- hardware/RISC-V problem?
  |
  +-- OS concept problem?
  |
  +-- concurrency problem?
  |
  +-- build/tool problem?
1
2
3
4
5
6
7
8
9
10
11
12
13

例如看到:

c
p->trapframe->a0 = p->pid;
1

如果不懂,可能有三层问题。

第一层 C 语法:

p->trapframe->a0 是连续通过指针访问字段。

第二层结构体:

p 是 struct proc *。

trapframe 是 struct trapframe *。

a0 是保存的用户寄存器字段。

第三层系统调用语义:

返回值要放在用户寄存器 a0 中。

所以要写进 trapframe。

把不懂的问题分层后,就知道该补哪里。

18. 如何避免“看懂了但不会用” ​

看懂源码注释不等于理解。

你需要做小实验。

每个机制至少做一个实验。

text
Boot
  |
  +-- print init sequence

Syscall
  |
  +-- add simple syscall

Process
  |
  +-- trace fork/exit/wait

Scheduling
  |
  +-- trace RUNNABLE/RUNNING changes

Memory
  |
  +-- print pagetable address
  +-- trigger invalid memory access

File
  |
  +-- trace open/read/write path

Pipe
  |
  +-- trace sleep/wakeup

Device
  |
  +-- trace console input interrupt
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

实验不一定复杂。

核心是验证关系。

例如:

text
Hypothesis:
  fork creates child process with copied trapframe.

Experiment:
  print parent and child trapframe addresses.

Expected:
  child has separate trapframe object.
1
2
3
4
5
6
7
8

这比只说“fork 复制进程”更扎实。

19. 如何写自己的源码注释 ​

建议不要直接在源码里大面积写注释。

容易污染代码。

更好的方式是在知识库文章里写。

如果确实要在本地 xv6 源码中临时注释,建议只写短注释。

并且用实验分支。

笔记里可以更详细。

注释格式可以这样:

text
Function:
  allocproc

Role:
  Find an UNUSED proc slot and prepare kernel resources.

Key state transition:
  UNUSED -> USED

Important pointers:
  p->trapframe
  p->pagetable
  p->context
1
2
3
4
5
6
7
8
9
10
11
12
13

这种方式比逐行中文翻译更有效。

逐行翻译容易让你以为自己懂了。

但真正重要的是角色、状态和对象关系。

20. 如何把 xv6 文章和源码同步 ​

本知识库里的 xv6 文章应该遵守一条规则:

文章必须服务源码阅读。

每篇文章至少包含:

  • 本篇问题。
  • 涉及源码文件。
  • ASCII 架构图。
  • 关键结构体。
  • 关键函数路径。
  • C 指针或地址边界说明。
  • 常见误解。
  • 小实验。

文章和源码关系:

text
xv6 source code
  |
  v
extract mechanisms
  |
  v
draw object/call/state diagrams
  |
  v
write article
  |
  v
experiment verifies article
1
2
3
4
5
6
7
8
9
10
11
12
13

不要写成:

text
concept article
  |
  v
random source snippets
1
2
3
4

而应该写成:

text
source path
  |
  v
concept extracted from source
1
2
3
4

这才是这个专题的风格。

21. 读 proc.c 前的准备清单 ​

很多人想直接读 proc.c。

可以。

但先检查:

text
Before proc.c
  |
  +-- know struct proc fields
  +-- know p->field syntax
  +-- know process states
  +-- know what pagetable means
  +-- know what trapframe means
  +-- know what context means
  +-- know basic syscall path
  +-- know scheduler exists per CPU
1
2
3
4
5
6
7
8
9
10

如果这些不清楚,读 proc.c 会变成硬啃。

proc.c 可以分段读。

text
initialization
  |
  +-- procinit
  +-- userinit

allocation
  |
  +-- allocproc
  +-- freeproc

lifecycle
  |
  +-- fork
  +-- exit
  +-- wait

scheduling
  |
  +-- scheduler
  +-- sched
  +-- yield

sleep/wakeup
  |
  +-- sleep
  +-- wakeup
  +-- kill

helpers
  |
  +-- mycpu
  +-- myproc
  +-- either_copyin/out
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

这样读比从第一行滑到最后一行更清楚。

22. 读 vm.c 前的准备清单 ​

虚拟内存也不能硬读。

先准备:

text
Before vm.c
  |
  +-- know virtual address vs physical address
  +-- know page size
  +-- know PTE flags
  +-- know pagetable_t is pointer-like
  +-- know satp selects page table
  +-- know user address cannot be trusted
1
2
3
4
5
6
7
8

vm.c 分段读:

text
kernel page table setup
  |
  +-- kvmmake
  +-- kvminit
  +-- kvminithart

page table walking
  |
  +-- walk
  +-- walkaddr

mapping
  |
  +-- mappages
  +-- uvmunmap

user memory lifecycle
  |
  +-- uvmcreate
  +-- uvmfirst
  +-- uvmalloc
  +-- uvmdealloc
  +-- uvmcopy
  +-- uvmfree

user/kernel copy
  |
  +-- copyin
  +-- copyout
  +-- copyinstr
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

关键问题:

text
Which page table?
Which virtual address?
Which physical address?
Which permission bits?
Who owns the page?
1
2
3
4
5

23. 读文件系统前的准备清单 ​

文件系统要慢慢来。

先区分层次。

text
fd
  |
  v
struct file
  |
  v
struct inode
  |
  v
disk block
  |
  v
virtio disk
1
2
3
4
5
6
7
8
9
10
11
12
13

准备清单:

text
Before filesystem
  |
  +-- know proc.ofile[]
  +-- know struct file
  +-- know inode is not file descriptor
  +-- know buffer cache
  +-- know log is filesystem transaction log
  +-- know disk block abstraction
1
2
3
4
5
6
7
8

推荐阅读顺序:

text
sysfile.c
  |
  v
file.c
  |
  v
fs.h
  |
  v
fs.c
  |
  v
bio.c
  |
  v
log.c
  |
  v
virtio_disk.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

先读 read() 路径。

再读 write() 路径。

再读 open() 路径。

不要一开始读所有 inode 操作。

24. 初学者常见误解 ​

误解一:源码应该按文件顺序读。

不应该。

应该按机制路径读。

误解二:main.c 看完就等于知道启动。

不够。

还要知道 entry.S、start.c、kernel.ld。

误解三:用户程序和内核函数可以直接互调。

不可以。

它们通过系统调用边界连接。

误解四:能运行 xv6 就等于环境理解完成。

不是。

还要知道内核镜像、用户程序、文件系统镜像之间的关系。

误解五:GDB 只在程序出 bug 时用。

不是。

GDB 是验证源码理解的工具。

误解六:打印越多越好。

不是。

内核高频路径打印太多会干扰系统。

误解七:读源码就是逐行翻译。

不是。

读源码要提取对象、状态、路径和不变量。

25. 本篇实践任务 ​

任务一:画出目录地图。

text
xv6-riscv
  |
  +-- kernel
  +-- user
  +-- mkfs
  +-- Makefile
1
2
3
4
5
6

每个目录写一句职责。

任务二:画出构建地图。

text
kernel source -> kernel image
user source -> user programs -> fs.img
kernel image + fs.img -> QEMU
1
2
3

任务三:追踪 getpid()。

列出它经过的文件。

不要先逐行读。

先画路径。

任务四:阅读 struct proc。

按字段分类:

text
identity
state
memory
scheduling
resources
family relation
1
2
3
4
5
6

任务五:选择一个函数写阅读卡片。

推荐 fork()。

模板:

text
Function:
Caller:
Objects:
State changes:
Pointer relations:
Locks:
Return value:
1
2
3
4
5
6
7

任务六:做一个最小打印实验。

例如在 sys_getpid() 打印当前 pid。

观察用户程序调用和内核输出之间的关系。

26. 本篇总结 ​

读 xv6 要有方法。

核心不是一次读完所有代码。

核心是不断建立这四张图:

text
directory map
  |
  v
build map
  |
  v
call path map
  |
  v
object relation map
1
2
3
4
5
6
7
8
9
10

每个机制都要问:

  • 源码在哪些文件。
  • 入口函数是什么。
  • 核心结构体是什么。
  • 指针关系是什么。
  • 状态怎么变化。
  • 什么锁保护状态。
  • 如何实验验证。

下一篇进入:

text
05-boot-and-kernel-entry.md
1

那篇会沿着 entry.S -> start.c -> main.c -> userinit -> scheduler 这条路径,仔细解释 xv6 内核如何从硬件启动走到第一个用户进程。

最后更新于:

Pager
上一篇4. 读 xv6 必备的 RISC-V 基础 / RISC-V Basics For xv6
下一篇6. xv6 启动与内核入口 / xv6 Boot And Kernel Entry

持续记录,持续成长

Copyright © Tidenflow