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 源码学习专题 / xv6 Source Learning ​

1. 为什么单独建立 xv6 专题 ​

xv6 是一个教学用 Unix-like 操作系统。

它不像 Linux 那样庞大,也不像普通操作系统教材那样只讲抽象概念。

它的价值在于:每个核心概念几乎都能回到一段可以读完的 C 代码。

对现在的学习目标来说,xv6 很适合放在“系统与高性能”下面单独成专题。

原因有三点。

第一,xv6 不是普通操作系统概念笔记。

它应该以源码为主线,而不是按教材章节松散排列。

第二,你现在的关键难点不是“知道进程是什么”这么简单。

真正的难点是把 C 指针、结构体、RISC-V 寄存器、页表、trap、调度器这些东西接起来。

第三,后面要学 Linux 0.11。

xv6 可以先建立干净的小模型,再把这些模型迁移到 Linux 0.11 里更复杂的实现。

这个专题的学习路线应该是:

text
C 语言和指针基础
  |
  v
RISC-V 执行模型
  |
  v
xv6 启动和源码目录
  |
  v
内核核心机制
  |
  +--> 进程与 proc
  +--> 页表与虚拟内存
  +--> trap 与系统调用
  +--> 调度与上下文切换
  +--> sleep / wakeup / exit / wait
  +--> 文件系统与设备
  |
  v
Linux 0.11 对照学习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这个专题要坚持一个原则:每个操作系统概念都必须落到源码文件、结构体字段、函数调用路径和内存关系上。

如果一篇文章只解释了概念,却没有解释 xv6 代码怎么实现,就不算完成。

如果一篇文章只逐行翻译代码,却没有解释操作系统为什么要这样设计,也不算完成。

1.1 本文基础词小注释 ​

先把几个会反复出现的词放在这里。

不用一次背下来。

读到后文时,可以随时回来看。

text
kernel
  |
  +-- 内核
  +-- 操作系统最核心的那部分程序
  +-- 负责管理进程、内存、文件和设备

user program
  |
  +-- 用户程序
  +-- 普通程序,例如 shell、cat、echo
  +-- 不能随便访问硬件和内核数据

trap
  |
  +-- CPU 转入内核处理特殊事件
  +-- 例如系统调用、中断、非法内存访问
  +-- 不是普通 C 函数调用

syscall
  |
  +-- 系统调用
  +-- 用户程序请求内核帮忙做事
  +-- 例如 read、write、fork、exec

proc
  |
  +-- process 的缩写
  +-- xv6 中常指 struct proc
  +-- 是内核保存进程信息的结构体

page table
  |
  +-- 页表
  +-- 虚拟地址到物理地址的翻译表
  +-- 让每个进程拥有自己的地址空间
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

最粗略地说:

text
用户程序想做敏感事情
  |
  v
syscall
  |
  v
trap into kernel
  |
  v
kernel uses proc/page table/file/device data
1
2
3
4
5
6
7
8
9
10

1.2 xv6 可以先拆成哪些模块 ​

如果一开始按源码文件读,xv6 会显得很散。

今天看 proc.c。

明天看 vm.c。

后天又跳到 trap.c。

这些文件都很重要,但初学时更适合先按“模块”看。

模块就是一组共同解决某类问题的代码。

比如“内存管理模块”回答:内存从哪里来,页表怎么建,虚拟地址怎么变成物理地址。

“进程管理模块”回答:进程怎么创建,怎么退出,怎么调度,怎么睡眠和唤醒。

“硬件接口模块”回答:内核怎样和 UART、磁盘、中断控制器这些硬件或虚拟硬件交流。

先给一张模块总图。

text
+====================================================================+
|                         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   |   |
|  +--------------------------------------------------------------+   |
|                                                                    |
+====================================================================+
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

这张图里有一个细节:virtio_disk.c 同时出现在文件系统链路和设备链路里。

这是正常的。

因为同一个源码文件可以站在两个视角里看。

从文件系统视角看,它是“磁盘块读写的最后一站”。

从设备视角看,它是“VirtIO 磁盘驱动”。

所以模块划分不是把文件硬切开。

模块划分是为了回答问题。

下面按模块说明。

text
问题类型
  |
  +-- 系统怎么启动?             -> Boot / Init
  +-- 进程怎么存在和切换?        -> Process Management
  +-- 地址和内存怎么管理?        -> Memory Management
  +-- 用户怎么进入内核?          -> Trap / Syscall / Interrupt
  +-- 文件名怎么变成磁盘内容?    -> File / FS / Log
  +-- 内核怎么碰硬件?            -> Device / Driver / Hardware
  +-- 全局工具和锁在哪里?        -> Shared Foundation
1
2
3
4
5
6
7
8
9

1.2.1 Boot / Init:启动与初始化模块 ​

这个模块回答一个最朴素的问题:内核第一口气是怎么喘起来的。

它主要涉及:

text
kernel/entry.S
kernel/start.c
kernel/main.c
kernel/kernel.ld
1
2
3
4

entry.S 是最早执行的内核代码之一。

它负责准备栈,然后跳到 C 世界。

start.c 处理更早期的 RISC-V 特权级和定时器设置。

main.c 像一个初始化清单,依次初始化内存、进程表、trap、设备、文件系统等。

可以这样理解:

text
CPU starts
  |
  v
assembly entry prepares stack
  |
  v
C initialization code runs
  |
  v
kernel services become available
1
2
3
4
5
6
7
8
9
10

1.2.2 Process Management:进程管理模块 ​

这个模块回答:进程是什么,进程怎么活,怎么死,怎么被 CPU 选中。

主要文件:

text
kernel/proc.h
kernel/proc.c
kernel/swtch.S
1
2
3

核心对象是 struct proc。

它不是 CPU。

它也不是一段单纯的代码。

它是内核保存“一个进程所有关键信息”的结构体。

比如:

text
struct proc
  |
  +-- state       当前状态
  +-- pid         进程编号
  +-- pagetable   用户页表
  +-- trapframe   用户寄存器保存区
  +-- context     内核上下文切换保存区
  +-- ofile[]     打开的文件
  +-- cwd         当前工作目录
1
2
3
4
5
6
7
8
9

进程管理模块内部又可以拆成几条线:

text
creation line: fork / allocproc / userinit
exit line:     exit / wait / reparent
sleep line:    sleep / wakeup / kill
switch line:   scheduler / sched / yield / swtch
1
2
3
4

如果后面你看到 proc,先不要害怕。

它大部分时候就是“当前内核正在讨论的那个进程记录”。

1.2.3 Memory Management:内存管理模块 ​

这个模块回答:内核和进程的内存从哪里来,地址怎么翻译。

主要文件:

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

这里有两层问题。

第一层是物理页分配。

kalloc.c 管理一页一页的物理内存。

它像一个简单的内存仓库。

text
free physical pages
  |
  v
kalloc()
  |
  v
one 4096-byte page
1
2
3
4
5
6
7

第二层是页表。

vm.c 管理虚拟地址到物理地址的映射。

用户程序看到的是虚拟地址。

CPU 真正访问硬件内存时需要物理地址。

页表就是中间翻译表。

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

初学时不要急着背 Sv39 的每一位。

先记住:页表让每个进程拥有自己的地址空间,也让内核能控制哪些地址能访问、哪些地址不能访问。

1.2.4 Trap / Syscall / Interrupt:陷入、系统调用、中断模块 ​

这个模块回答:CPU 为什么会从用户程序跳进内核。

主要文件:

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

这里有三个词很容易混。

text
trap
  |
  +-- CPU 进入内核处理特殊事件的总称
      |
      +-- syscall: 用户程序主动请求内核服务
      +-- interrupt: 外部设备或定时器打断当前执行
      +-- exception: 程序出错或触发异常
1
2
3
4
5
6
7

所以 syscall 是 trap 的一种。

interrupt 也是 trap 的一种。

后文如果说 trap,不一定只是在说系统调用。

一次系统调用大致是:

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

这个模块是用户态和内核态之间的门。

理解它之后,read()、write()、fork() 就不再只是函数名,而是一条跨权限边界的路径。

1.2.5 File / FS / Log:文件、文件系统和日志模块 ​

这个模块回答:一个文件名怎样变成文件内容,崩溃时为什么不容易把文件系统弄坏。

主要文件:

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

这里可以先分三层:

text
fd layer
  |
  +-- 用户程序看到的小整数 fd
  |
  v
file layer
  |
  +-- struct file,记录打开文件状态
  |
  v
backend layer
  |
  +-- inode file
  +-- pipe
  +-- device
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

普通文件还会继续往下走:

text
file descriptor
  |
  v
struct file
  |
  v
inode
  |
  v
log transaction
  |
  v
buffer cache
  |
  v
disk block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

文件系统模块最适合用“链路”来学。

不要一开始就陷在 bmap() 的所有细节里。

先知道每层负责什么,再回头看函数。

1.2.6 Device / Driver / Hardware:设备、驱动和硬件接口模块 ​

这个模块回答:内核怎样和外部世界说话。

主要文件:

text
kernel/console.c
kernel/uart.c
kernel/plic.c
kernel/virtio_disk.c
kernel/timer.c
1
2
3
4
5

设备驱动可以看作翻译官。

内核其他部分说:“我要输出一个字符。”

UART 驱动负责把它变成对串口寄存器的操作。

文件系统说:“我要读一个磁盘块。”

VirtIO 磁盘驱动负责把它变成虚拟磁盘设备能理解的请求。

text
kernel request
      |
      v
driver code
      |
      v
MMIO registers / device queues
      |
      v
hardware or virtual hardware
1
2
3
4
5
6
7
8
9
10

中断也属于这个世界。

设备不是总让内核主动轮询。

很多时候设备会说:“我好了。”

这个“我好了”的通知就是中断。

1.2.7 Shared Foundation:公共基础设施模块 ​

这个模块不直接代表某个操作系统概念,但到处都会被用到。

主要文件:

text
kernel/types.h
kernel/param.h
kernel/defs.h
kernel/spinlock.c
kernel/sleeplock.c
kernel/printf.c
kernel/string.c
1
2
3
4
5
6
7

types.h 放基础类型。

param.h 放系统规模参数,比如最大进程数。

defs.h 放跨文件函数声明。

锁相关文件给其他模块提供同步工具。

printf.c 和 string.c 提供内核内部常用工具函数。

这些文件像地基。

你平时不一定先读它们。

但当你看到某个函数声明、某个锁、某个类型不知道从哪来,就常常要回到这里。

1.3 后续文章按模块怎么读 ​

后面的 01 到 18 不是孤立文章。

它们可以按模块成组阅读。

如果你喜欢先看“大块结构”,可以先按下面这张导读图走。

text
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
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

更细一点,可以这样理解每一组。

1.3.1 A 组:入门准备 ​

对应文章:

text
01-learning-roadmap.md
02-c-and-pointers-for-xv6.md
03-riscv-basics-for-xv6.md
04-how-to-read-xv6-source.md
1
2
3
4

这一组不急着讲某个内核机制。

它先解决“能不能读懂 xv6”的问题。

01 是路线图。

02 专门补 C 语言、指针、结构体和数组这些读源码会反复碰到的东西。

03 补 RISC-V 的基本执行模型,比如寄存器、特权级、ecall、页表相关寄存器。

04 讲怎么进入源码目录,怎么按文件和调用路径读。

读完这一组,至少要能回答:

text
struct 是什么?
指针字段大概指向哪里?
RISC-V 为什么有用户态和内核态?
xv6 源码应该从哪些文件开始看?
1
2
3
4

1.3.2 B 组:启动与内核入口 ​

对应文章:

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

这一组讲内核怎样从“还没运行起来”走到 main() 初始化。

它连接汇编入口、C 初始化函数、内核链接脚本和多核启动。

读完这一组,至少要能回答:

text
xv6 为什么不是从普通 main() 直接开始?
entry.S 做了什么?
start.c 和 main.c 分别负责什么?
第一个用户进程是怎么被准备出来的?
1
2
3
4

1.3.3 C 组:内存管理 ​

对应文章:

text
06-memory-layout-and-page-table.md
1

这一组讲物理页、虚拟地址、物理地址、页表、内核地址布局。

它是后面理解进程、trap、exec 的基础。

读完这一组,至少要能回答:

text
kalloc() 分配的是什么?
虚拟地址和物理地址有什么区别?
页表为什么是每个进程自己的?
trampoline 为什么要映射在特殊位置?
1
2
3
4

1.3.4 D 组:trap、系统调用和边界切换 ​

对应文章:

text
07-trap-interrupt-and-system-call.md
15-user-programs-and-shell.md
1
2

这一组讲用户态和内核态之间的门。

07 从内核角度讲 trap、系统调用和中断。

15 从用户程序和 shell 角度讲 fork/exec/wait 如何触发内核服务。

这两篇建议合在一起看。

因为系统调用不是只属于内核,也不是只属于用户程序。

它是一条跨边界路径。

text
user/sh.c
  |
  v
fork / exec / wait
  |
  v
usys.S
  |
  v
ecall
  |
  v
trap.c
  |
  v
syscall.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

读完这一组,至少要能回答:

text
trap 是什么?
syscall 为什么是 trap 的一种?
用户程序为什么不能直接调用内核函数?
shell 执行一条命令为什么要 fork 再 exec?
1
2
3
4

1.3.5 E 组:进程管理与调度 ​

对应文章:

text
08-process-and-proc-structure.md
09-context-switch-and-scheduler.md
10-sleep-wakeup-and-wait-exit.md
11-locks-and-concurrency.md
1
2
3
4

这一组是 xv6 的核心之一。

08 先讲 struct proc。

09 讲进程如何被调度,CPU 如何从一个执行路径切到另一个执行路径。

10 讲进程不运行时怎么办,比如睡眠、唤醒、退出和父进程回收。

11 讲为什么多核和共享数据必须用锁保护。

这一组可以用一张状态图串起来:

text
UNUSED
  |
  v
USED / embryo-like allocated slot
  |
  v
RUNNABLE <---- wakeup
  |
  v
RUNNING
  |
  +-- yield -----> RUNNABLE
  |
  +-- sleep -----> SLEEPING
  |
  +-- exit ------> ZOMBIE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

读完这一组,至少要能回答:

text
struct proc 保存了什么?
RUNNABLE 和 RUNNING 有什么区别?
swtch.S 切换的到底是什么?
sleep 为什么必须配合锁?
ZOMBIE 为什么不是“真正活着”的进程?
1
2
3
4
5

1.3.6 F 组:文件、文件系统和设备 ​

对应文章:

text
12-file-descriptor-and-inode.md
13-file-system-and-log.md
14-device-driver-console-uart-disk.md
1
2
3

这一组回答一个日常问题:为什么 read() 和 write() 可以同时用于文件、管道、终端和磁盘相关操作。

12 讲文件描述符、struct file、inode、pipe、device 的层次。

13 讲文件系统如何把名字映射到 inode,再映射到磁盘块,并用日志保护一致性。

14 讲 console、UART、PLIC、VirtIO 磁盘这些设备驱动。

这三篇合起来是一条 I/O 链:

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

读完这一组,至少要能回答:

text
fd 为什么只是一个整数?
struct file 和 inode 有什么区别?
目录为什么也是一种文件?
日志为什么能降低崩溃后的损坏风险?
驱动为什么经常和中断、sleep/wakeup 联系在一起?
1
2
3
4
5

1.3.7 G 组:调试、迁移和实践 ​

对应文章:

text
16-debugging-xv6-with-gdb.md
17-xv6-to-linux-0-11-bridge.md
18-summary-and-practice-projects.md
1
2
3

这一组把“读懂”推进到“验证”和“迁移”。

16 教你用 QEMU/GDB 看真实运行路径。

17 把 xv6 的模型迁移到 Linux 0.11。

18 用实践项目把知识变成能动手修改的能力。

读完这一组,至少要能回答:

text
如何在 usertrap 或 fork 下断点?
如何观察当前进程的 struct proc?
Linux 0.11 的 task_struct 和 xv6 的 struct proc 怎么对照?
我能做哪些小实验确认自己真的懂了?
1
2
3
4

如果只想快速建立全局感,可以先读:

text
00 -> 01 -> 04 -> 08 -> 09 -> 12 -> 16
1

如果想按完整顺序稳扎稳打,就从 00 一直读到 18。

如果某个词卡住,比如 trap、chan、inode、context,优先回到对应文章的 本文基础词小注释。

2. 学习对象和前置假设 ​

这个专题默认读者具备最基础的 C 语言经验。

但不假设读者已经熟练掌握复杂指针。

也不假设读者真正理解操作系统内核。

因此每篇文章都要把以下内容讲清楚:

  • 这个机制解决什么实际问题。
  • 这个机制在 xv6 哪些文件中实现。
  • 关键结构体有哪些字段。
  • 指针字段指向哪里。
  • 函数之间怎么调用。
  • CPU 在用户态和内核态之间怎样切换。
  • 内存中的对象在哪里。
  • 常见误解是什么。
  • 可以做什么小实验验证理解。

一个典型解释应该长这样:

text
概念问题
  |
  v
源码位置
  |
  v
关键结构体
  |
  v
字段含义
  |
  v
函数调用路径
  |
  v
状态变化
  |
  v
调试实验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

例如讲 struct proc 时,不能只说“进程控制块”。

必须解释:

  • struct proc 是一个 C 结构体。
  • 它在 kernel/proc.h 中定义。
  • xv6 用 proc[NPROC] 这个全局数组保存所有进程槽位。
  • 每个槽位不是一个正在运行的 CPU,而是一个内核维护的进程记录。
  • state 表示这个槽位当前处于什么生命周期阶段。
  • pagetable 指向用户页表。
  • trapframe 指向保存用户寄存器的位置。
  • context 保存内核线程切换时需要恢复的寄存器。
  • parent 是指向父进程 struct proc 的指针。
  • ofile[] 是当前进程打开的文件表。
  • cwd 指向当前工作目录 inode。

这样才是真的“以 xv6 学操作系统”。

3. 专题目录规划 ​

建议把专题放在:

text
02-systems-and-performance/
  05-xv6/
1
2

第一阶段文档结构如下:

text
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
|   |-- 实验任务、源码改造、小项目
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
51
52
53
54
55
56
57

这个顺序故意把 proc 放在第 8 篇。

不是因为进程不重要,而是因为进程太重要。

如果没有先理解启动、地址空间、trap 和系统调用,proc 会变成一堆字段名。

等前面铺好之后,proc 就会自然变成所有机制的汇合点。

4. xv6 内核总架构图 ​

xv6 内核可以从“用户程序发起一次系统调用”这条主线理解。

用户程序运行在用户态。

内核运行在内核态。

二者之间通过 trap 进入内核。

内核内部再根据系统调用号、进程状态、文件描述符、页表、设备中断等信息完成工作。

下面是第一张总图。

text
+====================================================================+
|                            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  |   |
|  +----------+ +----------+ +----------+ +----------+ +----------+   |
|                                                                    |
+====================================================================+
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

这张图先不要背。

先记住四个核心方向。

第一,用户程序不会直接调用内核函数。

它通过 ecall 进入 trap 路径,再由 syscall.c 分发。

第二,进程不是单个函数。

进程是 struct proc 加上一组资源:页表、内核栈、trapframe、打开文件、当前目录、调度状态。

第三,文件系统不是直接读写磁盘。

文件操作会经过 file 层、inode 层、log 层、buffer cache 层,最后才到 virtio 磁盘。

第四,设备中断不是普通函数调用。

设备触发中断,trap 入口接住中断,内核再调用对应驱动处理。

5. 从硬件到内核的启动架构 ​

xv6-riscv 运行在 QEMU 模拟的 RISC-V virt 机器上。

启动路径不是从 main() 直接开始。

它先经过汇编入口,再进入 C 代码。

text
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
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

这里的 hart 是 RISC-V 对硬件线程的称呼。

可以先粗略理解成“一个能独立执行指令的 CPU 执行单元”。

xv6 支持多个 hart。

每个 hart 都需要自己的内核栈和 CPU 状态。

这就是为什么后面会看到 struct cpu 和 struct proc 同时存在。

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

一个初学者容易混淆的点是:

CPU 是正在执行的硬件资源。

进程是内核管理的执行对象。

一个 CPU 可以先运行进程 A,再切换到进程 B。

一个进程也可以暂时不运行,只停留在 RUNNABLE 或 SLEEPING 状态。

6. 进程子系统总图 ​

进程是这个专题的中心。

xv6 里和进程有关的内容散落在多个文件里。

但核心结构在 kernel/proc.h。

核心实现集中在 kernel/proc.c。

text
+-----------------------------+
| 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     |
+-----------------------------+   +-----------------------------+
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

先建立一句话模型:

text
进程 = 用户地址空间 + 内核记录 + 调度状态 + 资源表
1

再展开一点:

text
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
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

这里每个指针都要认真看。

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。

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

一个进程的用户地址空间大致像这样:

text
high virtual address

+-------------------------------+  TRAMPOLINE
| trampoline code               |
+-------------------------------+  TRAPFRAME
| trapframe page                |
+-------------------------------+
| user stack                    |
+-------------------------------+
| guard page                    |
+-------------------------------+
| heap grows upward             |
| ...                           |
+-------------------------------+
| user data                     |
+-------------------------------+
| user text                     |
+-------------------------------+  0

low virtual address
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

trampoline 是一段特殊代码。

它帮助 CPU 在用户态和内核态之间切换。

trapframe 是一块特殊内存。

它保存用户寄存器,使内核处理完系统调用或中断后可以回到原来的用户程序。

对于 C 指针不熟的读者,要注意:

页表里的地址不是 C 指针那种“拿来就能解引用”的普通变量。

页表项描述的是虚拟地址到物理地址的映射关系。

内核需要按照硬件规定的格式填写这些页表项。

然后 RISC-V 的 MMU 才会在执行访存指令时自动做地址翻译。

8. trap、系统调用和中断总图 ​

trap 是一个总称。

它表示 CPU 从正常执行流跳入内核处理特殊事件。

这些特殊事件包括:

  • 用户程序执行系统调用。
  • 用户程序访问非法地址。
  • 设备产生中断。
  • 定时器中断要求内核重新调度。

xv6 的 trap 路径主要在:

  • kernel/trampoline.S
  • kernel/trap.c
  • kernel/syscall.c
  • kernel/sysproc.c
  • kernel/sysfile.c

系统调用路径可以画成:

text
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
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

这条路径非常适合初学者反复调试。

因为它连接了用户态、汇编、trapframe、系统调用分发表、内核函数和返回值。

如果能把 getpid() 或 write() 的路径跑通,很多操作系统概念会突然变清楚。

9. 文件系统总图 ​

xv6 的文件系统不是一个单独文件。

它分成几层。

每一层解决一个问题。

text
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
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

对应文件是:

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

初学者读文件系统时,最容易被“文件”这个词骗到。

在 xv6 里,文件描述符、struct file、inode、目录项、磁盘块不是一个东西。

它们是不同层次的对象。

一个进程手里的 fd 是整数。

fd 通过 proc.ofile[] 找到 struct file。

struct file 可能指向 inode,也可能指向 pipe,也可能表示设备。

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

buffer cache 缓存磁盘块。

log 保证文件系统修改具备简单的崩溃恢复能力。

10. 锁和并发总图 ​

xv6 是多核内核。

所以很多内核对象必须用锁保护。

主要锁实现是:

  • kernel/spinlock.c
  • kernel/spinlock.h
  • kernel/sleeplock.c
  • kernel/sleeplock.h

spinlock 可以理解成短时间忙等的锁。

拿不到锁时,CPU 会循环等待。

sleeplock 可以理解成允许睡眠的锁。

拿不到锁时,进程可以睡眠,让 CPU 去运行别的进程。

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

并发学习不要一开始追求“高级无锁”。

先理解三个问题:

第一,谁共享这个数据。

第二,什么时候可能同时访问。

第三,锁保护的是哪个不变量。

例如 struct proc 的 state 字段就不是普通变量。

它决定调度器能不能运行这个进程。

如果多个 CPU 同时修改它,就可能出现一个进程被重复运行、丢失唤醒、或者永远睡眠。

11. xv6 kernel 文件职责总览 ​

下面的表按标准 MIT xv6-riscv 的 kernel/ 目录组织。

后续如果本地 E:\Github\xv6-riscv 有额外文件,应在本表后追加“本地差异”小节。

11.1 启动、链接和特权级切换 ​

text
entry.S
  |
  +-- earliest kernel assembly entry
  +-- sets up stack for each hart
  +-- jumps into start()
1
2
3
4
5

entry.S 是内核最早执行的代码之一。

它是汇编文件,不是 C 文件。

原因是此时 C 运行环境还没有完全准备好。

它主要负责给每个 hart 设置栈,然后进入 C 函数。

text
start.c
  |
  +-- machine-mode setup
  +-- configures delegation and interrupts
  +-- prepares transition to supervisor mode
1
2
3
4
5

start.c 处理 RISC-V 机器模式相关初始化。

它让后续内核主要运行在 supervisor mode。

这里会接触到很多 CSR。

CSR 是 Control and Status Register,也就是控制和状态寄存器。

这些寄存器不是普通内存变量,而是 CPU 内部的特殊寄存器。

text
main.c
  |
  +-- kernel initialization sequence
  +-- calls consoleinit, kinit, kvminit, trapinit, plicinit, binit, iinit
  +-- creates first user process
  +-- enters scheduler
1
2
3
4
5
6

main.c 是读 xv6 的重要入口。

它把各个子系统的初始化顺序串起来。

读 main() 时,不要急着深入每个函数。

先把初始化顺序抄成一张图。

text
kernel.ld
  |
  +-- linker script
  +-- controls kernel memory layout
  +-- defines where text, rodata, data, bss are placed
1
2
3
4
5

kernel.ld 告诉链接器怎么摆放内核。

这对理解内核地址、trampoline、符号边界很重要。

它不是 C 代码,但决定 C 代码最终在内存里的位置。

11.2 RISC-V、地址和基础类型 ​

text
riscv.h
  |
  +-- RISC-V constants and inline assembly helpers
  +-- reads/writes CSR registers
  +-- defines page table flags
  +-- provides interrupt enable/disable helpers
1
2
3
4
5
6

riscv.h 是 xv6 和 RISC-V 硬件对话的地方。

初学者可以先把它看成“硬件操作工具箱”。

例如读写 satp、sstatus、sepc 这些寄存器,都会在这里看到封装。

text
memlayout.h
  |
  +-- physical and virtual address layout
  +-- UART, VIRTIO, PLIC addresses
  +-- KERNBASE, PHYSTOP, TRAMPOLINE, TRAPFRAME
1
2
3
4
5

memlayout.h 是地址地图。

读内存管理前必须熟悉它。

它告诉你哪些地址代表设备,哪些地址代表内核,哪些地址代表特殊 trampoline 页。

text
types.h
  |
  +-- fixed-width integer aliases
  +-- uint, ushort, uchar, uint64
1
2
3
4

types.h 简化基础类型书写。

uint64 在 xv6-riscv 中非常常见,因为 RISC-V 地址和寄存器通常按 64 位处理。

text
param.h
  |
  +-- global size constants
  +-- NPROC, NOFILE, NFILE, NINODE, NBUF, MAXPATH
1
2
3
4

param.h 定义系统规模参数。

例如 NPROC 决定最多有多少进程槽位。

这类常量让 xv6 保持简单:很多结构直接使用固定大小数组。

text
defs.h
  |
  +-- cross-file function declarations
  +-- lets C files call functions implemented elsewhere
1
2
3
4

defs.h 是内核内部函数声明总表。

C 编译器需要先知道函数原型,才能在一个 .c 文件里调用另一个 .c 文件实现的函数。

这对 C 基础薄弱的读者很重要。

头文件不是“复制代码”,而是在编译阶段提供声明。

11.3 进程和调度 ​

text
proc.h
  |
  +-- enum procstate
  +-- struct context
  +-- struct cpu
  +-- struct trapframe
  +-- struct proc
1
2
3
4
5
6
7

proc.h 是进程专题最重要的头文件。

它定义了进程长什么样。

后续所有进程行为都围绕这里的结构体字段展开。

text
proc.c
  |
  +-- process table
  +-- process allocation
  +-- first user process
  +-- fork/exit/wait
  +-- scheduler/yield/sched
  +-- sleep/wakeup/kill
  +-- either_copyin/either_copyout
1
2
3
4
5
6
7
8
9

proc.c 是进程生命周期实现。

它不是只负责调度。

它负责从创建、运行、睡眠、唤醒、退出到回收的整条路径。

text
swtch.S
  |
  +-- low-level context switch
  +-- saves callee-saved registers
  +-- restores another context
  +-- returns on a different kernel execution path
1
2
3
4
5
6

swtch.S 是很多人第一次觉得 xv6 “神奇”的地方。

它不是普通函数调用。

它保存当前上下文,再恢复另一个上下文。

返回时可能已经不是原来的进程。

11.4 trap 和系统调用 ​

text
trampoline.S
  |
  +-- code mapped at same virtual address in user and kernel page tables
  +-- handles uservec and userret
  +-- bridges user mode and kernel mode
1
2
3
4
5

trampoline.S 是用户态和内核态切换的桥。

它必须映射在特殊地址。

因为切换页表前后,CPU 还需要继续执行这段代码。

text
trap.c
  |
  +-- trap initialization
  +-- usertrap
  +-- kerneltrap
  +-- device interrupt dispatch
  +-- return to user mode
1
2
3
4
5
6
7

trap.c 是异常、中断、系统调用进入内核后的总入口。

它决定当前 trap 是来自用户态还是内核态。

也决定是否要调用系统调用分发器,或者处理设备中断。

text
syscall.h
  |
  +-- syscall number definitions
1
2
3

syscall.h 给系统调用编号。

用户态和内核态都要对这些编号有一致理解。

text
syscall.c
  |
  +-- syscall dispatch table
  +-- argument fetching helpers
  +-- calls sys_* functions
1
2
3
4
5

syscall.c 是系统调用分发器。

它根据用户程序放入寄存器的系统调用号,调用对应内核函数。

text
sysproc.c
  |
  +-- process-related syscall implementations
  +-- fork, exit, wait, kill, getpid, sbrk, sleep, uptime
1
2
3
4

sysproc.c 处理和进程相关的系统调用。

它通常会进一步调用 proc.c 中的核心函数。

text
sysfile.c
  |
  +-- file-related syscall implementations
  +-- open, read, write, close, link, unlink, mkdir, mknod, chdir, exec
1
2
3
4

sysfile.c 处理文件系统相关系统调用。

它把用户态传来的路径、fd、buffer 参数转换成内核里的 file、inode、buffer 操作。

11.5 内存管理 ​

text
kalloc.c
  |
  +-- physical page allocator
  +-- maintains free list of 4096-byte pages
  +-- provides kalloc and kfree
1
2
3
4
5

kalloc.c 管理物理页。

它不是通用 malloc。

它按页分配和释放物理内存。

text
vm.c
  |
  +-- kernel page table
  +-- user page table
  +-- map/unmap
  +-- copyin/copyout
  +-- uvmcreate, uvmcopy, uvmfree
1
2
3
4
5
6
7

vm.c 是虚拟内存核心。

它让内核能建立页表、复制进程地址空间、从用户地址复制数据、释放用户内存。

读 vm.c 时要不断画图。

因为很多参数名字都是地址,但它们不是同一种地址。

text
elf.h
  |
  +-- ELF executable file format structures
  +-- used by exec.c to load user programs
1
2
3
4

elf.h 定义 ELF 文件格式。

ELF 是 Unix-like 系统常见的可执行文件格式。

exec.c 会根据这些结构把用户程序加载到新地址空间。

text
exec.c
  |
  +-- implements exec
  +-- loads ELF program
  +-- builds new user stack
  +-- replaces current process image
1
2
3
4
5
6

exec.c 实现 exec()。

exec() 不创建新进程。

它把当前进程的用户程序镜像换成另一个程序。

这点非常重要。

fork() 是复制进程。

exec() 是替换当前进程的用户地址空间。

11.6 文件、目录、管道和文件系统 ​

text
file.h
  |
  +-- struct file
  +-- struct inode
  +-- device switch declarations
1
2
3
4
5

file.h 定义文件层和 inode 层的核心结构。

这里的 struct file 不是磁盘上的文件。

它是内核打开文件表中的对象。

text
file.c
  |
  +-- global file table
  +-- filealloc, filedup, fileclose
  +-- fileread, filewrite, filestat
1
2
3
4
5

file.c 管理打开文件对象。

它连接进程 fd 和底层 inode、pipe、device。

text
fcntl.h
  |
  +-- open flags
  +-- O_RDONLY, O_WRONLY, O_RDWR, O_CREATE, O_TRUNC
1
2
3
4

fcntl.h 定义打开文件时使用的标志。

这些标志会被 open() 系统调用解释。

text
stat.h
  |
  +-- struct stat
  +-- file metadata returned to user programs
1
2
3
4

stat.h 定义文件状态信息。

用户程序调用 stat() 时会得到这个结构。

text
fs.h
  |
  +-- on-disk filesystem layout
  +-- superblock, dinode, dirent
  +-- block size and inode macros
1
2
3
4
5

fs.h 描述磁盘上的文件系统格式。

这里的结构关系到磁盘块怎么解释。

它和 file.h 里的内存对象不是一回事。

text
fs.c
  |
  +-- inode cache
  +-- path lookup
  +-- directory operations
  +-- readi/writei
  +-- block mapping
1
2
3
4
5
6
7

fs.c 是文件系统核心。

它把路径名、目录、inode、数据块连接起来。

text
log.c
  |
  +-- simple filesystem logging
  +-- groups disk writes into transactions
  +-- provides crash recovery boundary
1
2
3
4
5

log.c 实现文件系统日志。

它不是打印日志。

它是崩溃恢复用的 write-ahead log。

text
bio.c
  |
  +-- buffer cache
  +-- caches disk blocks in memory
  +-- coordinates block-level reads and writes
1
2
3
4
5

bio.c 管理 buffer cache。

读磁盘块不是每次都直接访问磁盘。

内核会先查缓存。

text
buf.h
  |
  +-- struct buf
  +-- buffer cache block metadata
1
2
3
4

buf.h 定义缓存块结构。

它描述一个磁盘块在内存中的缓存状态。

text
pipe.c
  |
  +-- pipe allocation
  +-- pipe read/write
  +-- pipe close
  +-- sleep/wakeup coordination for pipe buffer
1
2
3
4
5
6

pipe.c 实现管道。

管道不是磁盘文件。

它是内核中的一段缓冲区,用来让进程之间传输字节流。

11.7 设备、中断和驱动 ​

text
console.c
  |
  +-- console input editing
  +-- console read/write
  +-- connects user-visible console to uart
1
2
3
4
5

console.c 处理控制台。

它负责把键盘输入、控制台输出和用户程序的读写连接起来。

text
uart.c
  |
  +-- UART device driver
  +-- low-level serial input/output
  +-- interrupt handling for console device
1
2
3
4
5

uart.c 是串口驱动。

在 QEMU 环境中,终端输入输出会经过 UART。

text
plic.c
  |
  +-- platform-level interrupt controller setup
  +-- claims and completes external interrupts
1
2
3
4

plic.c 管理外部中断控制器。

设备中断需要经过 PLIC 分发给 CPU。

text
virtio.h
  |
  +-- VirtIO MMIO constants and data structures
1
2
3

virtio.h 定义 VirtIO 磁盘设备相关结构和常量。

VirtIO 是虚拟化环境中常见的设备接口。

text
virtio_disk.c
  |
  +-- VirtIO disk driver
  +-- submits disk read/write requests
  +-- handles disk interrupts
1
2
3
4
5

virtio_disk.c 是磁盘驱动。

文件系统最终的块读写会走到这里。

11.8 锁、字符串和打印 ​

text
spinlock.h
  |
  +-- struct spinlock
  +-- spinlock function declarations
1
2
3
4

spinlock.h 定义自旋锁结构。

很多内核结构都嵌入一个 struct spinlock。

text
spinlock.c
  |
  +-- initlock
  +-- acquire
  +-- release
  +-- holding
  +-- interrupt push_off/pop_off coordination
1
2
3
4
5
6
7

spinlock.c 实现自旋锁。

这里同时涉及原子操作和中断开关。

不能把它简单理解成用户态互斥锁。

text
sleeplock.h
  |
  +-- struct sleeplock
  +-- sleeplock declarations
1
2
3
4

sleeplock.h 定义睡眠锁结构。

它用于可能等待较久的内核对象。

text
sleeplock.c
  |
  +-- initsleeplock
  +-- acquiresleep
  +-- releasesleep
  +-- holdingsleep
1
2
3
4
5
6

sleeplock.c 实现睡眠锁。

它会和 sleep() / wakeup() 机制发生关系。

text
printf.c
  |
  +-- kernel printf
  +-- panic
  +-- formatted output for debugging
1
2
3
4
5

printf.c 是内核调试输出的重要工具。

它不能直接等同于用户态 libc 的 printf。

xv6 内核没有完整 C 标准库。

text
string.c
  |
  +-- minimal string and memory helpers
  +-- memmove, memset, memcmp, safestrcpy, strlen
1
2
3
4

string.c 提供内核需要的少量字符串和内存操作。

这些函数虽然简单,但读源码时经常出现。

12. 代码职责总图:从文件到机制 ​

下面把内核文件按机制重新分组。

这比单纯按文件名排序更适合学习。

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

再从调用关系看一次:

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

系统调用路径:

text
trampoline.S
  |
  v
trap.c
  |
  v
syscall.c
  |
  +--> sysproc.c
  |      |
  |      +--> proc.c
  |      +--> vm.c
  |
  +--> sysfile.c
         |
         +--> file.c
         +--> fs.c
         +--> pipe.c
         +--> exec.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

文件系统路径:

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

调度路径:

text
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 resumes
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

13. 第一轮学习建议 ​

第一轮不要试图一次读懂所有文件。

建议按“能跑通一条路径”为目标。

第一条路径:启动到第一个用户进程。

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

第二条路径:一次简单系统调用。

text
user getpid()
  -> usys.S
  -> ecall
  -> trampoline.S
  -> trap.c
  -> syscall.c
  -> sysproc.c
  -> proc.c
  -> return to user
1
2
3
4
5
6
7
8
9

第三条路径:一次 fork()。

text
sys_fork
  -> fork
  -> allocproc
  -> uvmcopy
  -> filedup
  -> idup
  -> child RUNNABLE
1
2
3
4
5
6
7

第四条路径:一次 read()。

text
sys_read
  -> fileread
  -> either pipe or inode
  -> readi
  -> bread
  -> virtio_disk
1
2
3
4
5
6

这四条路径跑通后,xv6 的整体骨架就出来了。

然后再逐篇深入每个机制。

14. 对 C 指针薄弱读者的提醒 ​

读 xv6 时,最常见的困难不是语法,而是“对象关系”。

尤其是这些写法:

c
struct proc *p;
struct file *f;
struct inode *ip;
pagetable_t pagetable;
char *dst;
uint64 srcva;
1
2
3
4
5
6

每次看到指针,都要问四个问题。

text
pointer variable
  |
  +-- points to what type?
  |
  +-- points to one object or an array?
  |
  +-- who owns the pointed object?
  |
  +-- how long does that object live?
1
2
3
4
5
6
7
8
9

比如:

struct proc *p 通常指向进程表中的一个槽位。

struct file *f 通常指向全局文件表中的一个打开文件对象。

struct inode *ip 通常指向 inode cache 里的一个 inode。

pagetable_t 是页表根节点地址,不是普通业务对象。

char *dst 可能是内核地址,也可能只是函数参数名,需要看上下文。

uint64 srcva 里的 va 通常表示 virtual address。

它是一个数值形式的虚拟地址,不一定能被内核直接当普通指针解引用。

这个区别非常关键。

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

这就是为什么 xv6 有 copyin() 和 copyout()。

内核不能随便相信用户传进来的地址。

用户地址属于用户页表。

内核需要检查映射,并按规则复制数据。

15. 和 Linux 0.11 的衔接 ​

学 xv6 的最终目的不是只会 xv6。

它还要成为 Linux 0.11 的预备地图。

可以这样对应:

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

两者差异也要提前知道。

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 文章应该像这样组织:

text
1. 这篇文章解决什么问题
2. 用普通语言解释直觉
3. 列出涉及的源码文件
4. 画出结构图或调用图
5. 解释关键结构体
6. 逐段读源码
7. 解释 C 指针和内存关系
8. 解释操作系统概念
9. 总结常见误解
10. 设计一个小实验
1
2
3
4
5
6
7
8
9
10

17. 推荐第一篇深挖文章 ​

第一篇真正深挖可以写:

text
08-process-and-proc-structure.md
1

原因是你现在明确提到 proc 和进程不熟。

但它应该在阅读路线中引用前置文章。

这篇文章可以作为“边补边读”的重点案例。

建议结构:

text
为什么操作系统需要进程
  |
  v
进程不是程序
  |
  v
proc.h 中的结构体
  |
  v
proc.c 中的生命周期函数
  |
  v
fork / exit / wait / scheduler
  |
  v
trapframe 和 context 的区别
  |
  v
指针字段逐个解释
  |
  v
GDB 观察 proc[NPROC]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这篇文章写好后,你会第一次真正看见:

text
程序
  |
  v
进程
  |
  v
地址空间
  |
  v
CPU 上下文
  |
  v
内核调度
1
2
3
4
5
6
7
8
9
10
11
12
13

这也是从“期末考试通过”走向“能读操作系统源码”的关键跨越。

18. 参考入口 ​

本专题当前基于标准 MIT xv6-riscv 源码结构。

后续应以本地仓库为准:

text
E:\Github\xv6-riscv
1

建议参考:

  • 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 基础较浅”的实际状态转化成可执行学习路线。

最后更新于:

Pager
上一篇← 系统与高性能 / Systems & Performance
下一篇2. xv6 学习路线:从 C 指针到操作系统源码 / xv6 Learning Roadmap

持续记录,持续成长

Copyright © Tidenflow