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 Memory Layout And Page Tables ​

1. 这篇文章解决什么问题 ​

虚拟内存是 xv6 学习里第一道真正的硬门槛。

很多人读 proc.c、trap.c、exec.c 卡住,根源其实在页表。

你会看到这些名字:

  • pagetable_t
  • pte_t
  • walk
  • mappages
  • uvmalloc
  • uvmcopy
  • copyin
  • copyout
  • TRAMPOLINE
  • TRAPFRAME
  • satp

如果不知道它们之间的关系,代码就会像咒语。

这篇文章要建立一张完整地图:

text
address
  |
  v
virtual address vs physical address
  |
  v
page and page table
  |
  v
kernel memory layout
  |
  v
physical page allocator
  |
  v
kernel page table
  |
  v
user process page table
  |
  v
copyin / copyout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

学完后,你应该能解释:

  • 为什么用户地址不能直接当内核指针用。
  • 为什么每个进程有自己的页表。
  • 为什么 fork() 要调用 uvmcopy()。
  • 为什么 exec() 要重建用户地址空间。
  • 为什么 TRAMPOLINE 要映射在特殊高地址。
  • 为什么 trapframe 和页表关系很紧。
  • kalloc() 分配的是物理页,不是普通 C 对象。

1.1 本文基础词小注释 ​

内存篇最容易卡在词上。

下面这些词先用朴素版本理解。

text
内存
  |
  +-- memory
  +-- 程序运行时存放代码、数据、栈、页表的地方

页
  |
  +-- page
  +-- 操作系统管理内存的固定大小块
  +-- xv6 里通常一页是 4096 字节

虚拟地址
  |
  +-- virtual address
  +-- 程序看到的地址
  +-- 需要页表翻译

物理地址
  |
  +-- physical address
  +-- 真实 RAM 或设备地址

页表
  |
  +-- page table
  +-- 虚拟地址到物理地址的翻译表

PTE
  |
  +-- Page Table Entry
  +-- 页表项,一条具体映射记录
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

2. 先分清三种“地址” ​

读 xv6 内存代码时,最重要的是分清地址。

至少有三类。

text
C pointer
  |
  +-- a typed address used by C code in current address space

virtual address
  |
  +-- address used by CPU before translation
  +-- meaning depends on current page table

physical address
  |
  +-- address of actual RAM or memory-mapped device
1
2
3
4
5
6
7
8
9
10
11
12

普通 C 代码里,指针通常看起来就是地址。

但内核里不能这么简单。

例如系统调用参数中的用户 buffer。

用户程序传入:

c
read(fd, buf, n);
1

buf 对用户程序来说是 char *。

进入内核后,它是用户虚拟地址。

内核不能直接写:

c
*buf = value;
1

因为这个地址属于用户页表。

内核需要通过 copyout()。

text
kernel data
  |
  v
copyout(pagetable, user_va, kernel_src, len)
  |
  v
user memory
1
2
3
4
5
6
7

这就是本篇的核心边界。

地址不是孤立数字。

地址要和页表一起理解。

3. 页:内存管理的基本单位 ​

xv6 按页管理很多内存。

一页通常是 4096 字节。

也就是 PGSIZE。

text
physical memory

+---------+---------+---------+---------+---------+
| page 0  | page 1  | page 2  | page 3  | page 4  |
+---------+---------+---------+---------+---------+
   4KB       4KB       4KB       4KB       4KB
1
2
3
4
5
6

为什么用页?

因为硬件页表按页映射。

操作系统也可以按页分配、回收、保护内存。

页表不是把每个字节都记录一遍。

它记录的是虚拟页到物理页的映射。

text
virtual page number
  |
  v
page table entry
  |
  v
physical page number + permission flags
1
2
3
4
5
6
7

权限标志包括:

text
PTE_V: valid
PTE_R: readable
PTE_W: writable
PTE_X: executable
PTE_U: user accessible
1
2
3
4
5

如果没有 PTE_V,页表项无效。

如果没有 PTE_U,用户态不能访问。

如果没有 PTE_W,写入会失败。

这些位让页表不仅负责地址翻译,也负责保护。

4. RISC-V Sv39 页表的直觉模型 ​

xv6-riscv 使用 RISC-V 的页表机制。

常见是 Sv39 模式。

你不需要一开始记完所有位。

先建立三级页表模型。

text
virtual address
  |
  +-- VPN[2]
  +-- VPN[1]
  +-- VPN[0]
  +-- page offset
1
2
3
4
5
6

三级查找:

text
root page table
  |
  | index VPN[2]
  v
level-1 page table
  |
  | index VPN[1]
  v
level-0 page table
  |
  | index VPN[0]
  v
PTE points to physical page
  |
  +-- offset selects byte inside page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

图:

text
VA
 |
 +-- VPN2 ----> root page table entry
                  |
                  v
               next page table
                  |
 +-- VPN1 --------+
                  |
                  v
               next page table
                  |
 +-- VPN0 --------+
                  |
                  v
               leaf PTE -> physical page
                              |
 +-- offset ------------------+
                              v
                         final physical address
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

walk() 的作用就是走这棵页表树。

如果中间页表不存在,在某些场景下它还可以分配页表页。

mappages() 则负责把一段虚拟地址映射到物理地址。

5. satp:当前页表的根 ​

页表存在内存里。

CPU 怎么知道当前用哪张页表?

答案是 satp。

satp 是 RISC-V 的 supervisor address translation and protection 寄存器。

可以理解成当前页表根寄存器。

text
satp
  |
  v
root page table physical address
  |
  v
hardware page table walk
1
2
3
4
5
6
7

切换 satp 会改变地址翻译。

这就是上下文切换和用户/内核切换复杂的原因之一。

text
Process A satp -> page table A
Process B satp -> page table B
Kernel satp    -> kernel page table
1
2
3

同一个虚拟地址,在不同页表下可以映射到不同物理页。

text
VA 0x1000 under process A -> PA A
VA 0x1000 under process B -> PA B
1
2

这提供进程隔离。

用户程序以为自己拥有连续地址空间。

内核和硬件通过页表把它映射到真实物理内存。

6. memlayout.h:xv6 的地址地图 ​

memlayout.h 是读内存管理前必须看的文件。

它定义了内核认为重要的地址。

这些地址包括:

  • 设备地址。
  • 物理内存起点。
  • 内核基址。
  • 最大物理内存。
  • trampoline 地址。
  • trapframe 地址。

可以先把它理解成一张地图。

text
high virtual address

+-------------------------------+ TRAMPOLINE
| trampoline code               |
+-------------------------------+ TRAPFRAME
| per-process trapframe page    |
+-------------------------------+
| user stack / heap / data/text |
| ...                           |
+-------------------------------+ 0

low virtual address
1
2
3
4
5
6
7
8
9
10
11
12

内核侧还有设备和物理内存映射。

text
physical / device address space

+-------------------------------+
| PLIC                          |
+-------------------------------+
| VIRTIO disk MMIO              |
+-------------------------------+
| UART                          |
+-------------------------------+
| kernel text/data              |
+-------------------------------+
| free physical memory          |
+-------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13

设备地址不是普通 RAM。

它们是内存映射 I/O。

CPU 访问这些地址,实际上是在读写设备寄存器。

这就是为什么 memlayout.h 同时和内存、设备相关。

7. kalloc.c:物理页分配器 ​

kalloc.c 管理物理页。

它提供:

text
kinit()
kalloc()
kfree()
1
2
3

kinit() 初始化空闲页链表。

kalloc() 分配一页。

kfree() 释放一页。

它不是通用 malloc()。

它每次处理一整页。

text
free physical pages

+--------+    +--------+    +--------+
| page A | -> | page B | -> | page C |
+--------+    +--------+    +--------+
1
2
3
4
5

一个很有意思的技巧是:

空闲页本身被当作链表节点使用。

也就是说,一页空闲时,内核可以在这页里放一个 next 指针。

text
free page memory
+--------------------------+
| struct run { next }      |
| unused bytes             |
+--------------------------+
1
2
3
4
5

这说明:

对象的含义取决于管理者。

一页被分配后,可能变成页表页、用户内存页、trapframe 页。

一页空闲时,可以变成 allocator 的链表节点。

8. vm.c:虚拟内存核心 ​

vm.c 是 xv6 虚拟内存核心。

它可以按功能分组。

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

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

mapping
  |
  +-- mappages
  +-- uvmunmap

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

copy between kernel and user
  |
  +-- 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

不要从第一行硬读。

先按这五组理解。

walk() 是基础。

mappages() 依赖它。

uvmalloc()、uvmcopy() 等用户内存操作也依赖页表映射。

copyin/copyout 又依赖用户页表查找。

调用关系可以粗略画成:

text
walk
  |
  +-- mappages
  |     |
  |     +-- kernel page table setup
  |     +-- user page table setup
  |
  +-- walkaddr
        |
        +-- copyin
        +-- copyout
        +-- copyinstr
1
2
3
4
5
6
7
8
9
10
11
12

9. 内核页表 ​

内核也需要页表。

内核页表负责让内核访问:

  • 内核代码。
  • 内核数据。
  • 物理内存。
  • 设备寄存器。
  • trampoline。
text
kernel virtual address
  |
  v
kernel page table
  |
  +-- kernel text: read + execute
  +-- kernel data: read + write
  +-- physical memory direct map
  +-- devices
  +-- trampoline
1
2
3
4
5
6
7
8
9
10

权限很重要。

内核代码段通常可执行,不应该随便可写。

设备映射需要读写。

trampoline 需要执行。

简化图:

text
+------------------+------------------+------------------+
| virtual region   | physical target  | purpose          |
+------------------+------------------+------------------+
| KERNBASE         | kernel loaded PA | kernel text/data |
| UART0            | UART device      | console I/O      |
| VIRTIO0          | disk device      | block I/O        |
| PLIC             | interrupt ctrl   | device interrupt |
| TRAMPOLINE       | trampoline code  | trap transition  |
+------------------+------------------+------------------+
1
2
3
4
5
6
7
8
9

内核页表初始化完成后,每个 hart 通过 kvminithart() 启用。

启用意味着写 satp 并刷新相关地址翻译缓存。

10. 用户进程页表 ​

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

这让每个进程有独立地址空间。

典型用户地址空间:

text
high address

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

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

这里最特殊的是顶部两页:

TRAMPOLINE 映射 trampoline 代码。

TRAPFRAME 映射当前进程的 trapframe。

trampoline 需要在用户页表中可见。

因为从用户态 trap 到内核、从内核返回用户态时,页表切换过程中仍要执行这段代码。

trapframe 需要在切换过程中保存/恢复用户寄存器。

text
struct proc
  |
  +-- pagetable
  |
  +-- trapframe
        |
        v
user page table maps TRAPFRAME to this physical page
1
2
3
4
5
6
7
8

这就是 proc、trap、页表三者交汇的地方。

11. walk():走页表 ​

walk() 的作用是找到某个虚拟地址对应的 PTE。

PTE 是 page table entry。

也就是页表项。

简化过程:

text
walk(pagetable, va, alloc)
  |
  v
use VPN[2] to find level 2 entry
  |
  v
use VPN[1] to find level 1 entry
  |
  v
use VPN[0] to find leaf entry
  |
  v
return pointer to PTE
1
2
3
4
5
6
7
8
9
10
11
12
13

如果中间页表不存在:

text
alloc == 0
  |
  +-- return 0

alloc == 1
  |
  +-- allocate a page table page
  +-- install it
  +-- continue walking
1
2
3
4
5
6
7
8
9

这里要注意:

walk() 返回的是 PTE 的地址。

也就是指向页表项的指针。

不是直接返回最终物理地址。

如果要得到可访问的物理地址,还要解析 PTE。

这就是为什么 walk() 和 walkaddr() 是不同函数。

12. mappages():建立映射 ​

mappages() 把一段虚拟地址映射到物理地址。

它会逐页建立映射。

text
virtual range
  |
  +-- page 0 -> physical page 0
  +-- page 1 -> physical page 1
  +-- page 2 -> physical page 2
1
2
3
4
5

伪流程:

text
for each page in range
  |
  v
pte = walk(pagetable, va, alloc=1)
  |
  v
fill pte with physical address and permission
1
2
3
4
5
6
7

映射包含两部分:

text
PTE
  |
  +-- physical page number
  +-- permission flags
1
2
3
4

权限决定谁能访问、如何访问。

例如用户代码页可能是:

text
PTE_R | PTE_X | PTE_U | PTE_V
1

用户数据页可能是:

text
PTE_R | PTE_W | PTE_U | PTE_V
1

内核页通常不带 PTE_U。

这样用户态不能直接访问内核内存。

13. uvmalloc() 和 uvmdealloc():用户内存增长和收缩 ​

用户进程的地址空间不是固定不变。

sbrk() 可以增长用户内存。

内核会通过 uvmalloc() 分配并映射新页。

text
old size
  |
  v
allocate physical pages
  |
  v
map new virtual pages
  |
  v
new size
1
2
3
4
5
6
7
8
9
10

图:

text
before

+---------+---------+
| text    | data    |
+---------+---------+

after uvmalloc

+---------+---------+---------+---------+
| text    | data    | heap    | heap    |
+---------+---------+---------+---------+
1
2
3
4
5
6
7
8
9
10
11

uvmdealloc() 则负责缩小地址空间。

它会取消映射并释放对应物理页。

这里要注意:

虚拟地址空间变大,不等于物理内存凭空出现。

必须有物理页支撑映射。

这就是 uvmalloc() 会调用 kalloc()。

14. uvmcopy():fork 时复制用户地址空间 ​

fork() 需要创建子进程。

子进程看起来拥有和父进程一样的用户内存内容。

xv6 的简单做法是复制。

text
parent pagetable
  |
  v
for each mapped user page
  |
  +-- allocate child physical page
  +-- copy bytes from parent page
  +-- map child virtual page
  v
child pagetable
1
2
3
4
5
6
7
8
9
10

图:

text
parent VA 0x0 -> parent PA A
                 |
                 | copy content
                 v
child  VA 0x0 -> child  PA B
1
2
3
4
5

父子进程虚拟地址可以相同。

但物理页不同。

这就是进程隔离。

text
same virtual address
  |
  +-- parent maps to physical page A
  |
  +-- child maps to physical page B
1
2
3
4
5

现代系统常用写时复制优化。

xv6 基础版本更直接。

它真的复制页面内容。

这更适合教学。

15. uvmfree():释放用户地址空间 ​

进程退出或 exec 替换地址空间时,需要释放旧用户内存。

uvmfree() 负责释放用户页表和映射的物理页。

生命周期图:

text
process created
  |
  v
user pagetable created
  |
  v
pages allocated and mapped
  |
  v
process exits or exec replaces image
  |
  v
uvmfree
  |
  v
physical pages returned to allocator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

释放内存时要小心两类东西:

text
leaf physical pages
  |
  +-- actual user memory

page table pages
  |
  +-- pages used to store page table entries
1
2
3
4
5
6
7

页表本身也占物理页。

所以释放页表不只是释放用户数据页。

还要释放页表结构页。

16. copyin():从用户地址复制到内核 ​

copyin() 用于从用户地址空间复制数据到内核缓冲区。

例如系统调用参数是用户指针时。

text
user memory
  |
  | copyin
  v
kernel buffer
1
2
3
4
5

函数参数大致含义:

text
copyin(pagetable, dst, srcva, len)
  |
  +-- pagetable: user page table
  +-- dst: kernel destination pointer
  +-- srcva: user virtual address
  +-- len: bytes to copy
1
2
3
4
5
6

重点:

srcva 不是内核指针。

它是用户虚拟地址。

copyin() 需要通过页表查找每个虚拟页对应的物理地址。

如果用户地址非法,复制失败。

这保护内核不被用户传入坏地址破坏。

17. copyout():从内核复制到用户地址 ​

copyout() 方向相反。

text
kernel buffer
  |
  | copyout
  v
user memory
1
2
3
4
5

函数参数:

text
copyout(pagetable, dstva, src, len)
  |
  +-- pagetable: user page table
  +-- dstva: user virtual destination address
  +-- src: kernel source pointer
  +-- len: bytes to copy
1
2
3
4
5
6

典型场景:

read() 从文件读到内核缓冲,再复制到用户 buffer。

stat() 把内核构造的文件状态复制到用户传入的地址。

图:

text
disk / file / pipe
  |
  v
kernel data
  |
  v
copyout
  |
  v
user buffer
1
2
3
4
5
6
7
8
9
10

这条边界是安全边界。

内核不能随便信任用户地址。

用户不能随便访问内核地址。

copyout() 就是跨边界搬运数据。

18. copyinstr():复制用户字符串 ​

字符串比普通字节数组多一个问题:

要找到结尾。

C 字符串以 \0 结束。

copyinstr() 从用户地址复制字符串到内核。

它需要:

  • 逐页检查用户地址。
  • 复制字节。
  • 遇到 \0 停止。
  • 避免超过最大长度。
text
user string
  |
  v
copyinstr
  |
  +-- check page mapping
  +-- copy byte by byte
  +-- stop at '\0'
  v
kernel string buffer
1
2
3
4
5
6
7
8
9
10

典型用途:

系统调用传入路径名。

例如:

text
open("file", ...)
exec("sh", ...)
chdir("/")
1
2
3

这些路径字符串来自用户空间。

内核必须安全复制后再使用。

19. exec() 和地址空间替换 ​

exec() 是理解地址空间的关键。

fork() 创建新进程。

exec() 不创建新进程。

它替换当前进程的用户地址空间。

text
before exec

proc pid = 3
  |
  +-- old pagetable
  +-- old user text/data/stack

after exec

same proc pid = 3
  |
  +-- new pagetable
  +-- new program text/data/stack
1
2
3
4
5
6
7
8
9
10
11
12
13

exec() 会读取 ELF 文件。

ELF 是可执行文件格式。

它告诉内核程序各段应该加载到哪些虚拟地址。

简化流程:

text
read ELF header
  |
  v
create new page table
  |
  v
load program segments
  |
  v
allocate user stack
  |
  v
commit new pagetable to process
  |
  v
free old pagetable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这就是为什么 exec.c 和 vm.c 关系紧密。

exec() 是用户地址空间重建。

20. TRAMPOLINE 和 TRAPFRAME ​

TRAMPOLINE 和 TRAPFRAME 是 xv6 地址空间中最特殊的两个位置。

text
top of user virtual address space

+-------------------------------+ TRAMPOLINE
| trampoline code               |
+-------------------------------+ TRAPFRAME
| current process trapframe     |
+-------------------------------+
| user stack / heap / data/text |
+-------------------------------+
1
2
3
4
5
6
7
8
9

TRAMPOLINE 映射一段汇编代码。

它负责用户态和内核态切换。

TRAPFRAME 映射当前进程保存用户寄存器的那页。

为什么放在高地址?

为了不和普通用户程序的 text、data、heap、stack 混在一起。

为什么 trampoline 要同时在用户页表和内核页表可见?

因为切换页表时,CPU 仍然要继续执行这段代码。

text
execute trampoline
  |
  v
switch satp
  |
  v
continue executing trampoline at same virtual address
1
2
3
4
5
6
7

这是 xv6 trap 设计的核心。

21. 页表和 proc 的关系 ​

进程结构体里有页表字段。

简化:

text
struct proc
  |
  +-- pagetable
  +-- sz
  +-- trapframe
  +-- kstack
1
2
3
4
5
6

关系:

text
proc
  |
  +-- pagetable -------> user page table
  |
  +-- sz --------------> size of user memory
  |
  +-- trapframe -------> physical page mapped at TRAPFRAME
  |
  +-- kstack ----------> kernel stack virtual address
1
2
3
4
5
6
7
8
9

一个进程不仅是 pid。

它包含一个地址空间。

fork() 操作 pagetable。

exec() 替换 pagetable。

exit() 释放 pagetable。

copyin/copyout 使用 pagetable。

trap 返回用户态也依赖 pagetable。

所以 pagetable 是 proc 的核心字段之一。

22. 内存相关源码职责图 ​

text
memlayout.h
  |
  +-- address constants
  +-- devices
  +-- KERNBASE / PHYSTOP
  +-- TRAMPOLINE / TRAPFRAME

riscv.h
  |
  +-- page table flags
  +-- satp helpers
  +-- PTE macros

kalloc.c
  |
  +-- physical page allocator
  +-- kalloc / kfree

vm.c
  |
  +-- page table walk/map/unmap
  +-- kernel page table
  +-- user page table lifecycle
  +-- copyin/copyout/copyinstr

exec.c
  |
  +-- builds new user address space from ELF

proc.c
  |
  +-- allocates process page table
  +-- copies/free page table during fork/exit

trampoline.S
  |
  +-- depends on TRAMPOLINE mapping
  +-- switches between user and kernel page tables
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

内存不是单独模块。

它连接启动、进程、trap、系统调用、exec、文件系统。

23. 常见误解 ​

误解一:虚拟地址就是物理地址。

不是。

虚拟地址要经过页表翻译。

误解二:pagetable_t 不是指针。

它表面没有星号,但底层是指针类型。

一定要追 typedef。

误解三:用户指针可以直接解引用。

不能。

用户地址需要通过 copyin/copyout。

误解四:kalloc() 类似普通 malloc。

不准确。

kalloc() 按页分配物理内存。

误解五:页表只负责映射,不负责权限。

不对。

PTE flags 同时表达访问权限。

误解六:fork() 只是复制 struct proc。

不是。

它还要复制用户地址空间、trapframe、文件引用等。

误解七:exec() 创建新进程。

不是。

exec() 替换当前进程的用户程序镜像。

误解八:trampoline 是普通函数。

不是。

它是特殊映射的汇编桥接代码。

24. 小实验:观察进程页表 ​

实验目标:

观察不同进程有不同页表。

可以在 fork() 或 userinit() 中打印:

text
pid
pagetable address
sz
trapframe address
1
2
3
4

示意输出:

text
userinit: pid=1 pagetable=0x... sz=...
fork: parent=1 pgtbl=0x...
fork: child=2  pgtbl=0x...
1
2
3

观察问题:

  • 父子进程页表地址是否不同?
  • 虚拟地址是否可以相同?
  • trapframe 地址是否不同?
  • sz 代表什么?

不要只看数值。

要把输出放回对象图。

text
parent proc -> parent pagetable
child proc  -> child pagetable
1
2

25. 小实验:触发非法地址访问 ​

实验目标:

观察用户程序访问非法地址如何进入 trap。

可以写一个用户程序,故意访问明显非法地址。

预期:

text
user invalid access
  |
  v
page fault trap
  |
  v
kernel detects error
  |
  v
process killed
1
2
3
4
5
6
7
8
9
10

观察点:

  • scause。
  • stval。
  • 当前进程 pid。

这个实验连接:

text
user address
  |
  v
page table permission
  |
  v
hardware trap
  |
  v
kernel handling
1
2
3
4
5
6
7
8
9
10

它能帮助你理解页表不只是“地址转换表”。

它也是保护机制。

26. 本篇总结 ​

xv6 虚拟内存的核心可以压缩成一张图:

text
physical memory pages
  |
  v
kalloc / kfree
  |
  v
page tables
  |
  +-- kernel page table
  |
  +-- per-process user page table
          |
          +-- text/data/heap/stack
          +-- trapframe
          +-- trampoline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

读内存代码时,要持续问:

  • 这是虚拟地址还是物理地址?
  • 使用哪张页表?
  • 这个页从哪里分配?
  • 这个映射权限是什么?
  • 用户态能不能访问?
  • 什么时候释放?

下一篇进入:

text
07-trap-interrupt-and-system-call.md
1

页表是 trap 的前置。

因为用户态和内核态切换时,寄存器、页表、trampoline、trapframe 都会一起出现。

最后更新于:

Pager
上一篇6. xv6 启动与内核入口 / xv6 Boot And Kernel Entry
下一篇8. xv6 trap、中断与系统调用 / xv6 Traps Interrupts And System Calls

持续记录,持续成长

Copyright © Tidenflow