第6讲 虚拟内存管理II

虚拟内存管理 II:从页表维护到缺页异常

《虚拟内存管理 I》解释了虚拟地址如何通过页表和 MMU 翻译成物理地址。《虚拟内存管理 II》进一步讨论的是:这些页表由谁创建、谁维护,操作系统如何利用页表、虚拟内存区域和缺页异常来构造进程的地址空间。

这一讲的主线可以概括为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CPU 使用虚拟地址

MMU 根据页表翻译

页表由操作系统维护

操作系统自己也运行在虚拟地址中

因此需要直接映射来访问物理内存和页表页

进程地址空间由多个虚拟内存区域组成

mmap 可以新增虚拟内存区域

立即映射会马上分配物理页

延迟映射会等第一次访问时通过缺页异常再分配

写时拷贝、内存去重、内存压缩、大页都建立在这套机制之上

❗ 核心结论: 虚拟内存不是一张静态的地址翻译表,而是操作系统通过页表、虚拟内存区域、缺页异常和一系列优化机制共同维护出来的地址空间抽象。

1. 操作系统自己也使用虚拟地址

一个容易被忽略的问题是:既然普通程序使用虚拟地址,那么操作系统自己访问内存时,使用的是虚拟地址还是物理地址?

答案是:操作系统代码运行时同样使用虚拟地址。

CPU 执行内核代码时,指令中的地址仍然是虚拟地址。只是内核使用的是内核地址空间和内核页表,而用户进程使用的是用户地址空间和用户页表。

在 AArch64 中,可以粗略理解为:

寄存器 作用
TTBR0_EL1 通常指向用户地址空间的页表
TTBR1_EL1 通常指向内核地址空间的页表

于是系统中会有这样的地址空间分布:

1
2
3
4
5
低地址
├── 用户地址空间
├── 无效地址区域
└── 内核地址空间
高地址

这引出一个关键矛盾:

  • 操作系统运行时使用虚拟地址;
  • 但是页表项中保存的是物理地址;
  • 操作系统维护页表时,必须读写页表页;
  • 如果页表项给出的是物理地址,操作系统不能直接把它当作指针使用。

因此,操作系统需要一种简单方法,把物理地址转换成内核可以访问的虚拟地址。这就是直接映射要解决的问题。


❗ 本节核心结论: 操作系统运行时也处在虚拟地址体系中;它不能直接把页表项里的物理地址当作普通指针使用。

2. 直接映射 Direct Mapping:内核访问物理内存的桥

直接映射的核心思想是:内核拿出一段连续的虚拟地址空间,与物理内存建立线性映射关系。

它可以简化成一个公式:

1
内核虚拟地址 = 物理地址 + 固定偏移量

反过来也可以写成:

1
物理地址 = 内核虚拟地址 - 固定偏移量

例如:

1
2
3
物理地址 0x0000_3000
↓ 加上固定偏移 OFFSET
内核虚拟地址 0xFFFF_0000_0000_3000

这样,操作系统只要知道物理地址和固定偏移量,就可以快速得到对应的内核虚拟地址。

直接映射不是给普通应用程序使用的。它主要服务于内核自身,尤其是:

  • 访问任意物理页;
  • 管理物理内存;
  • 读写页表页;
  • 在页表项保存的物理地址和内核可访问的虚拟地址之间转换。

为什么不使用不连续的内核虚拟地址来映射物理内存?不是因为不能,而是因为那会让管理变复杂。

如果使用线性映射:

1
2
3
物理地址 0x0000 → 内核虚拟地址 OFFSET + 0x0000
物理地址 0x1000 → 内核虚拟地址 OFFSET + 0x1000
物理地址 0x2000 → 内核虚拟地址 OFFSET + 0x2000

内核只需要加减偏移量即可完成转换。

如果使用不连续映射:

1
2
3
物理地址 0x0000 → 内核虚拟地址 A
物理地址 0x1000 → 内核虚拟地址 B
物理地址 0x2000 → 内核虚拟地址 C

内核每次都要额外查询一张映射表,代码复杂、效率下降,也会让页表管理更绕。


❗ 本节核心结论: 直接映射的价值在于让内核通过简单的加减偏移访问物理内存,而不是每次都额外查找物理页对应的内核虚拟地址。

3. 进程的虚拟内存段分布

一个进程的虚拟地址空间不是一整块简单内存,而是由多个不同用途的区域组成。

典型进程中常见的区域包括:

区域 作用
.text 保存程序机器指令,可读、可执行
.rodata 保存只读数据,例如字符串字面量
.data 保存已经初始化的全局变量和静态变量
.bss 保存未初始化的全局变量和静态变量,初始值通常为 0
heap 堆,供 mallocnew 等动态分配使用
stack 栈,保存函数调用、局部变量、返回地址等
mmap 区域 mmap 创建的匿名映射或文件映射区域
动态库区域 例如 libc.so 等共享库
vvar / vdso 系统提供的特殊用户态辅助区域

例如:

1
2
3
4
5
6
7
8
int global_init = 10;       // .data
int global_uninit; // .bss
const char *msg = "hello"; // 字符串通常在 .rodata

int main() {
char stack_buf[128]; // stack
char *heap_buf = malloc(4096); // heap
}

程序从源代码到进程地址空间,大致经历:

1
2
3
4
5
6
7
8
9
10
11
12
13
.c / .cpp 源文件

编译

目标文件 .o

链接

ELF 可执行文件

加载

进程虚拟地址空间

ELF 文件记录了程序的代码段、数据段、只读段等信息。加载器和操作系统根据这些信息,为进程建立相应的虚拟内存区域。

这张图强调:进程地址空间是由多个区域组合而成的,不同区域具有不同来源、权限和用途。


❗ 本节核心结论: 进程地址空间不是单一连续内存,而是由代码、数据、堆、栈、动态库和 mmap 区域等多个虚拟内存区域组成。

4. 页表映射管理:操作系统如何建立 va → pa

虚拟地址能否访问,最终取决于页表中有没有有效映射。页表由操作系统创建和维护,MMU 只是按照页表执行翻译。

页表管理的核心操作可以抽象成两个函数:

1
2
void add_mapping(u64 pgtbl, u64 va, u64 pa);
void delete_mapping(u64 pgtbl, u64 va);

它们分别表示:

  • 添加虚拟地址 va 到物理地址 pa 的映射;
  • 删除虚拟地址 va 对应的映射。

在四级页表中,添加映射大致需要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
根据 va 计算 L0 索引

找到或创建 L1 页表

根据 va 计算 L1 索引

找到或创建 L2 页表

根据 va 计算 L2 索引

找到或创建 L3 页表

根据 va 计算 L3 索引

写入 pa 和权限位

可以用下图表示:

在这个过程中,经常会用到类似 get_next_pgtbl_page 的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
u64 get_next_pgtbl_page(u64 *pgtbl, u32 index) {
u64 pgtbl_entry;

pgtbl_entry = pgtbl[index];

if (pgtbl_entry == 0) {
pgtbl_entry = alloc_pgtbl_page();
pgtbl[index] = pgtbl_entry | some_permission;
}

return paddr_to_vaddr(pgtbl_entry);
}

这个函数与直接映射紧密相关,关键在最后一步:

1
return paddr_to_vaddr(pgtbl_entry);

原因是:

  • 页表项里保存的是下一级页表页的物理地址;
  • 但操作系统代码运行时不能直接用物理地址当指针;
  • 所以内核必须把物理地址转换成内核虚拟地址;
  • 这个转换依赖直接映射。

因此:

1
2
3
4
5
6
7
页表项里的物理地址

通过直接映射转换

内核可访问的虚拟地址

操作系统继续读写下一级页表

删除映射时还有一个额外问题:TLB 中可能缓存了旧映射。即使页表项已经删除,如果 TLB 没刷新,CPU 仍可能使用旧的 虚拟页 → 物理页 翻译结果。因此删除或修改映射后,通常需要考虑刷新相关 TLB 项。


❗ 本节核心结论: 页表由操作系统按需创建和修改;页表项保存物理地址,而内核运行时使用虚拟地址,所以页表管理必须借助直接映射完成物理地址到内核虚拟地址的转换。

5. 立即映射 mmap:申请时立刻分配物理页

mmap 是用户程序申请虚拟内存区域的一种方式。例如:

1
2
3
4
char *buf = mmap((void *)0x500000000, 0x2000,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
-1, 0);

这表示程序希望申请一段虚拟地址区域:

  • 起始地址大致为 0x500000000
  • 长度为 0x2000,即 8192 字节;
  • 权限为可读、可写;
  • 匿名映射,不对应具体文件;
  • 私有映射。

如果页面大小是 4KB,那么 0x2000 对应 2 页。

所谓立即映射,是指 mmap 调用时,操作系统马上完成:

  1. 记录这段虚拟地址区域合法;
  2. 分配对应数量的物理页;
  3. 建立虚拟页到物理页的页表映射。

例如:

1
2
虚拟页 0 → 物理页 A
虚拟页 1 → 物理页 B

这样,mmap 返回后,程序第一次访问这段内存时,页表映射已经存在,不需要再因为“没有物理页”而触发缺页处理。

立即映射的优点是第一次访问快,逻辑直接。缺点是可能浪费物理内存。

例如程序申请 1GB 内存,但最后只使用前 4KB。如果采用立即映射,系统可能提前分配大量物理页,其中绝大多数根本没有被使用。


❗ 本节核心结论: 立即映射会在 mmap 时立刻分配物理页并建立页表映射,首次访问快,但可能提前占用大量实际内存。

6. 延迟映射:先记录合法区域,用到再分配

延迟映射的思想是:mmap 时只记录虚拟地址区域合法,不马上分配物理页;等程序第一次真正访问某一页时,再分配物理页并建立映射。

操作系统需要为进程维护虚拟内存区域信息,例如:

1
2
3
4
5
struct vmregion {
u64 start;
u64 end;
u64 perm;
};

一个进程的地址空间可以理解为:

1
2
3
4
struct vmspace {
u64 pgtbl;
list vmregions;
};

也就是说:

1
进程地址空间 = 页表 + 虚拟内存区域列表

延迟映射下,mmap 做的事情主要是:

1
2
3
4
记录 [start, end) 是合法虚拟地址区域
记录权限 perm
暂时不分配物理页
暂时不建立完整物理映射

例如程序申请 16KB 匿名内存,页大小为 4KB:

1
2
3
4
虚拟页 0:合法,但未分配物理页
虚拟页 1:合法,但未分配物理页
虚拟页 2:合法,但未分配物理页
虚拟页 3:合法,但未分配物理页

如果程序第一次只访问 buf[0],通常只会为第 0 页分配一个物理页:

1
2
3
4
虚拟页 0 → 物理页 A
虚拟页 1 → 未分配
虚拟页 2 → 未分配
虚拟页 3 → 未分配

之后如果程序访问 buf[5000],进入第 1 页,系统再为第 1 页分配物理页。

因此,延迟映射最基本的分配粒度是:访问哪一页,分配哪一页。

当然,在某些优化场景中,操作系统可能做预读、预取或透明大页分配。但从基本概念上看,延迟映射不是一次性分配整个申请区域,而是按实际访问逐页补上映射。


❗ 本节核心结论: 延迟映射把“申请虚拟地址区域”和“分配物理内存”分开;通常第一次访问哪一页,就只为哪一页分配物理页。

7. 缺页异常 Page Fault:不是所有缺页都是错误

当程序访问某个虚拟地址时,MMU 查页表发现没有有效映射,就会触发缺页异常。

缺页异常本身不一定意味着程序出错。它只是说明:当前访问无法直接通过页表完成翻译,需要操作系统介入处理。

缺页异常大致分为两类。

7.1 合法缺页

合法缺页指的是:

  • 访问的虚拟地址属于某个合法 vmregion
  • 访问权限也符合该区域的权限;
  • 只是物理页尚未分配,或页表映射尚未建立。

例如:

1
2
char *buf = mmap(...);
buf[0] = 'A';

如果采用延迟映射,buf[0] 第一次访问时可能触发缺页异常。操作系统处理流程是:

1
2
3
4
5
6
7
8
9
10
11
得到 fault_va

检查 fault_va 是否落在某个合法 vmregion 中

检查访问权限是否允许

分配物理页

调用 add_mapping 建立页表映射

返回用户程序继续执行

这类缺页异常是正常机制的一部分。

7.2 非法访问

非法访问指的是:

  • 访问地址不属于任何合法虚拟内存区域;
  • 或者访问方式违反权限,例如向只读区域写入。

例如:

1
2
char *p = NULL;
*p = 'A';

操作系统发现该地址不合法,就不会分配物理页,而是向进程发送异常信号,最终表现为 Segmentation Fault

因此需要区分:

概念 含义
Page Fault 硬件发现页表无法完成翻译,陷入操作系统
Segmentation Fault 操作系统判断访问非法后,终止或通知进程的结果

不是所有 Page Fault 都会导致 Segmentation Fault。延迟映射、写时拷贝、页面换入等机制都会利用 Page Fault 作为正常控制路径。


❗ 本节核心结论: Page Fault 是硬件异常入口,不等于程序错误;Segmentation Fault 是操作系统判断访问非法后的结果。

8. Linux 风格的缺页处理路径

在真实 Linux 中,缺页处理不是简单地“分配一页”就结束。系统需要根据页面类型和访问原因进行分派。

大致路径可以理解为:

1
2
3
4
5
6
7
do_translation_fault

do_page_fault

__do_page_fault

handle_pte_fault

handle_pte_fault 之后可能继续判断:

  • 页表项是否无效;
  • 页面是否在 swap 中;
  • 是否是匿名页;
  • 是否是文件映射;
  • 是否涉及写时拷贝;
  • 是否是权限错误。

常见处理函数包括:

函数 大致作用
do_anonymous_page 处理匿名映射页
do_fault 处理文件映射或共享映射
do_swap_page 处理被换出到磁盘的页面
do_wp_page 处理写保护页,常与写时拷贝相关

学习这一部分时,不必把源码路径机械背诵到每一层。更重要的是理解:缺页异常是一个统一入口,操作系统会根据具体原因选择不同处理方式。


❗ 本节核心结论: Linux 的缺页处理会区分匿名页、文件映射、swap、写保护等多种情况;Page Fault 是多种内存机制共同使用的处理入口。

9. 写时拷贝 Copy-on-Write:不写就共享,一写就复制

写时拷贝简称 COW,是基于页表权限和缺页异常实现的重要优化。

最典型场景是 fork()

假设父进程调用:

1
fork();

如果操作系统立刻复制父进程的全部物理内存,成本会很高。尤其是很多程序在 fork() 后很快调用 exec(),原来的地址空间马上被替换,提前复制就非常浪费。

因此,操作系统使用写时拷贝:

  1. fork() 后父子进程暂时共享同一批物理页;
  2. 父子进程页表都指向相同物理页;
  3. 这些共享页的页表权限被暂时设置为只读;
  4. 如果没有写入,就一直共享;
  5. 如果某一方尝试写入,就触发缺页异常;
  6. 操作系统分配新物理页,复制原页内容;
  7. 修改写入方页表,让它指向新物理页;
  8. 恢复写入方的写权限。

过程可以表示为:

写时拷贝的核心是:

1
不写就共享,一写就复制

它的优点是:

  • 减少 fork() 的时间开销;
  • 节省物理内存;
  • 避免不必要的数据复制;
  • 将复制成本推迟到真正写入时才发生。

❗ 本节核心结论: COW 利用只读权限和缺页异常捕捉写操作,把“立即复制整片内存”优化为“写到哪页才复制哪页”。

10. 内存去重:发现相同页面后合并共享

内存去重可以理解成写时拷贝的反方向应用。

写时拷贝是:

1
2
3
原本共享

写入时复制分开

内存去重是:

1
2
3
4
5
原本分开

发现内容相同

合并共享

例如系统中有两个物理页内容完全一样:

1
2
物理页 A:内容 = 0000...
物理页 B:内容 = 0000...

操作系统可以只保留其中一个页面,让多个虚拟页指向同一个物理页:

1
2
进程 1 虚拟页 → 物理页 A
进程 2 虚拟页 → 物理页 A

然后释放另一个重复物理页。

为了保证语义正确,合并后的共享页通常会被设置为只读。如果某个进程之后写入该页面,就触发写时拷贝:

1
2
3
4
5
6
7
8
9
写入共享页

触发缺页异常

分配新页

复制内容

修改写入方页表

Linux 中典型机制是 KSM,即 Kernel Same-page Merging。

不过,内存去重也有安全隐患。因为页面是否被合并可能影响写入延迟,攻击者有可能通过时间差推测其他进程中是否存在某些特定数据。这类问题属于侧信道风险。


❗ 本节核心结论: 内存去重通过合并内容相同的物理页节省内存,并依靠 COW 保证写入时重新分离;但它可能带来侧信道安全风险。

11. 内存压缩:用 CPU 时间换内存空间

当物理内存紧张时,操作系统不一定立刻把页面换出到磁盘。另一种策略是内存压缩。

传统 swap 的思路是:

1
2
3
4
5
内存页

写入磁盘

再次访问时从磁盘读回

缺点是磁盘 I/O 较慢,也可能增加 SSD 写入压力。

内存压缩的思路是:

1
2
3
4
5
6
7
不常用内存页

压缩

压缩后的数据仍然留在内存

再次访问时解压恢复

例如一个 4KB 页面压缩后可能只占 1KB。这样系统虽然付出了压缩和解压的 CPU 成本,但减少了真实内存占用,也减少了写磁盘的次数。

对比来看:

机制 做法 优点 代价
Swap 把页面写入磁盘 释放内存彻底 磁盘 I/O 慢
内存压缩 把页面压缩后留在内存 比磁盘快,减少 I/O 消耗 CPU 进行压缩/解压

Windows 10 中有内存压缩机制;Linux 中有 zswap,可以把准备换出的页面先压缩放在内存区域中,减少真正写入磁盘的次数。


❗ 本节核心结论: 内存压缩通过压缩不常用页面缓解内存压力,本质上是用 CPU 计算成本换取更少的内存占用和更少的磁盘 I/O。

12. 大页 Huge Page:用更大映射粒度降低 TLB 压力

普通页大小通常是 4KB。大页则可能是:

  • 2MB;
  • 1GB。

大页的主要目标是降低页表和 TLB 压力。

假设程序访问 1GB 连续内存:

如果使用 4KB 页:

1
1GB / 4KB = 262144 个页面

这意味着需要大量页表项,也会占用大量 TLB 项。

如果使用 2MB 大页:

1
1GB / 2MB = 512 个页面

需要管理的页面数量大幅减少。

在四级页表中,普通 4KB 页通常走完整路径:

1
L0 → L1 → L2 → L3 → 4KB 物理页

2MB 大页可以提前停止:

1
L0 → L1 → L2 → 2MB 物理页

1GB 大页可以更早停止:

1
L0 → L1 → 1GB 物理页

为什么 2MB 大页和 1GB 大页会自然出现?

在 4KB 页、每级页表 512 项的设定下:

1
512 × 4KB = 2MB

所以如果跳过 L3,让 L2 页表项直接指向物理内存,就得到 2MB 大页。

继续向上一层:

1
512 × 2MB = 1GB

所以如果跳过 L2 和 L3,让 L1 页表项直接指向物理内存,就得到 1GB 大页。

大页的优缺点如下:

优点 缺点
减少页表项数量 可能浪费物理内存
减少 TLB 项占用 需要更大连续物理内存
提高 TLB 命中率 管理更复杂
减少页表遍历级数 内存碎片问题更明显

一句话概括:大页是用空间换性能。


❗ 本节核心结论: 大页通过扩大单个页表项覆盖的地址范围,减少页表项数量和 TLB 压力,但可能带来内存浪费和管理复杂度。

13. 一个完整例子:OS 运行一个程序

假设有一个程序:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int global_init = 10; // .data
int global_uninit; // .bss
const char *msg = "hello"; // .rodata

int main() {
char stack_buf[128]; // stack
char *heap_buf = malloc(4096); // heap

char *mmap_buf = mmap(NULL, 8192,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);

strcpy(mmap_buf, "Hello mmap");

pid_t pid = fork();

if (pid == 0) {
mmap_buf[0] = 'X';
}

return 0;
}

从操作系统角度看,这个程序经历了以下过程。

13.1 OS 建立直接映射

系统启动后,内核建立直接映射区域,使自己能通过:

1
内核虚拟地址 = 物理地址 + OFFSET

访问物理内存和页表页。

13.2 加载程序并建立虚拟内存区域

加载器根据 ELF 信息建立:

  • .text:程序代码;
  • .rodata:字符串 "hello"
  • .dataglobal_init
  • .bssglobal_uninit
  • heap:供 malloc 使用;
  • stack:供局部变量和函数调用使用;
  • mmap 区域:后续由 mmap 创建。

13.3 OS 维护页表映射

对于需要立即可访问的区域,操作系统分配物理页,并通过 add_mapping 建立:

1
虚拟页 → 物理页

如果某一级页表不存在,就分配新的页表页。因为页表项保存的是物理地址,内核需要通过直接映射把它转成可访问的内核虚拟地址。

13.4 程序调用 mmap

当程序执行:

1
2
mmap(NULL, 8192, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

操作系统可以有两种策略:

  • 立即映射:马上分配 2 个 4KB 物理页并填页表;
  • 延迟映射:只记录这 8192 字节合法,暂时不分配物理页。

13.5 第一次访问触发缺页异常

如果采用延迟映射,程序执行:

1
strcpy(mmap_buf, "Hello mmap");

第一次访问 mmap_buf[0] 时,页表中还没有有效映射,于是触发 Page Fault。操作系统检查该地址合法且可写,于是分配一个物理页,建立映射,然后返回程序继续执行。

如果后续访问第二页,例如 mmap_buf[5000],第二页再触发一次缺页异常,再分配第二个物理页。

13.6 fork 触发写时拷贝机制

程序执行:

1
pid_t pid = fork();

父子进程先共享物理页,页表项被设置为只读。子进程执行:

1
mmap_buf[0] = 'X';

写入只读共享页时触发缺页异常。操作系统识别这是 COW 写异常,于是分配新页、复制内容、修改子进程页表、恢复写权限。

13.7 系统可能进一步做优化

在内存管理过程中,操作系统还可能使用:

  • 内存去重:发现多个页面内容相同后合并共享;
  • 内存压缩:内存紧张时压缩不常用页面,减少磁盘换页;
  • 大页:对大块连续内存使用 2MB 或 1GB 页,降低 TLB 压力。

这个例子把本讲的多个机制串成了一条完整链条。


❗ 本节核心结论: 一个普通程序的运行会同时涉及虚拟内存段分布、页表映射、mmap、缺页异常、COW 以及可能的内存优化机制。

14. 易混点总结

14.1 直接映射不是普通用户程序使用的映射

直接映射主要供内核使用。它帮助内核把物理地址转换成内核虚拟地址,从而访问物理内存和页表页。

14.2 get_next_pgtbl_page 和直接映射相连

因为页表项中保存的是下一级页表的物理地址,而内核要继续访问下一级页表,必须先通过直接映射得到可访问的内核虚拟地址。

14.3 延迟映射不是一次分配整个区域

延迟映射通常按页分配。访问哪一页,就为哪一页分配物理页并建立映射。

14.4 Page Fault 不等于 Segmentation Fault

Page Fault 是硬件触发的异常入口;Segmentation Fault 是操作系统判断访问非法后给进程的结果。

14.5 COW 不是不复制,而是推迟复制

写时拷贝不是永远不复制,而是在写入发生时才复制对应页面。

14.6 大页不是一定更好

大页可以降低 TLB 压力,但如果程序只使用大页中的很小一部分,就可能浪费内存。


❗ 本节核心结论: 本讲最容易混淆的是“物理地址与内核虚拟地址”“Page Fault 与 Segmentation Fault”“延迟映射与立即映射”“共享与写时复制”这几组概念。

15. 复习清单

学完《虚拟内存管理 II》后,应能回答以下问题:

  1. 为什么操作系统自己运行时也使用虚拟地址?
  2. 页表项中保存的是虚拟地址还是物理地址?
  3. 为什么内核不能直接把物理地址当作指针使用?
  4. 直接映射的公式是什么?
  5. 直接映射为什么通常使用连续线性映射?
  6. 一个进程的虚拟地址空间通常有哪些区域?
  7. .text.rodata.data.bss、heap、stack 分别保存什么?
  8. add_mapping 的作用是什么?
  9. 多级页表中,如果下一级页表不存在,操作系统需要做什么?
  10. get_next_pgtbl_page 为什么要调用 paddr_to_vaddr
  11. 删除页表映射后为什么可能需要刷新 TLB?
  12. 立即映射和延迟映射有什么区别?
  13. 延迟映射下,mmap 时操作系统做了什么?
  14. 第一次访问延迟映射区域时,为什么会触发缺页异常?
  15. 合法缺页和非法访问有什么区别?
  16. Page Fault 和 Segmentation Fault 有什么区别?
  17. 写时拷贝如何利用页表权限和缺页异常?
  18. 内存去重和写时拷贝有什么关系?
  19. 内存压缩相对于 swap 的优势是什么?
  20. 2MB 大页和 1GB 大页分别是怎么来的?
  21. 大页为什么能提高 TLB 命中率?
  22. 大页可能带来什么问题?

结论

《虚拟内存管理 II》的核心,是把虚拟内存从“地址翻译机制”推进到“操作系统如何管理地址空间”。

这一讲可以压缩为一句话:

操作系统通过直接映射访问物理内存,通过虚拟内存区域描述进程地址空间,通过页表建立虚拟页到物理页的映射,通过缺页异常按需补上映射,并在此基础上实现写时拷贝、内存去重、内存压缩和大页等优化机制。

真正理解这一讲,关键不是背诵某个函数名,而是形成一个整体直觉:

  • 进程看到的是虚拟地址空间;
  • 页表决定虚拟页是否能翻译到物理页;
  • 操作系统负责维护这些映射;
  • 延迟映射让物理页按需分配;
  • 缺页异常不是单纯的错误,而是操作系统介入内存管理的重要入口;
  • COW、大页、压缩、去重,都是围绕“减少开销、节省内存、提高性能”展开的优化。

因此,虚拟内存不是“假内存”,而是一套由硬件翻译、操作系统策略和异常处理共同实现的精密抽象。