本页目录

CSAPP IV · 虚拟内存与动态内存

对标:CS:APP 第 9 章 | 前置:csapp-02(缓存)、csapp-03(缺页故障) 系统里最精巧的幻觉:每个进程都以为自己独占一整块从 0 开始的连续内存——而物理内存只有几十 GB 且被所有进程瓜分。这个幻觉叫虚拟内存,它同时解决了隔离、共享、超额分配三件事,是现代操作系统的支柱。下半页讲 malloc 底下的真相——堆是怎么管理的,为什么会内存碎片和泄漏。

1. 虚拟内存:地址的一层间接

虚拟地址空间→页表→物理内存映射,两进程同虚拟地址映到不同物理页(隔离)。

图 csapp-04.4虚拟地址空间→页表→物理内存映射,两进程同虚拟地址映到不同物理页(隔离)。

核心思想是计算机科学的万能咒语——加一层间接。程序用虚拟地址,硬件(MMU)+ 操作系统把它翻译成物理地址。这层翻译一举给了三样东西:

2. 分页与页表:翻译怎么做

多级页表翻译:虚拟地址[页号|偏移]→查表→物理页帧,配 TLB 缓存。

图 csapp-04.2多级页表翻译:虚拟地址[页号|偏移]→查表→物理页帧,配 TLB 缓存。

写时复制:fork 后父子共享只读页,写时才复制。

图 csapp-04.3写时复制:fork 后父子共享只读页,写时才复制。

内存按页(通常 4 KB)为单位管理。页表是"虚拟页号 → 物理页帧号"的映射表,每进程一份。虚拟地址 = [虚拟页号 | 页内偏移],翻译时查页表得物理页帧,拼上偏移。

两个工程要点

按需分页的机制(🔗 csapp-03 缺页故障):访问一个未在物理内存的页 → 触发缺页故障 → 内核找空闲页帧、从磁盘调入、更新页表、重试指令——程序全程无感。物理内存满时,用页面置换算法(LRU 近似,🔗 adv-02 在线算法的竞争分析正是分析它)挑一页换出。

写时复制(COW)fork() 不真复制内存,父子共享所有页并标记只读;任一方写时才触发故障、复制那一页。"fork 很快"的秘密就在 COW——这也是为什么 Redis 存快照、Python multiprocessing 能高效 fork。

3. 动态内存:malloc 底下是什么

显式空闲链表 + 边界标记 + 合并。

图 csapp-04.1显式空闲链表 + 边界标记 + 合并。

栈上的局部变量随函数进退自动管理;上的内存要显式 malloc/free——分配器要在一块大内存里,响应任意大小的请求、回收后重用。它是一个精巧的数据结构问题

这就是 [实验 L03]:亲手写一个带首次适配 + 合并 + 边界标记的 malloc——写完你就再也不会把堆当黑盒

4. 内存的两大灾难与工具

这正是两条现代出路的动机:① 工具——valgrind/AddressSanitizer 运行时抓越界与泄漏(必学);② 语言——Rust 用所有权在编译期消灭这类 bug(🔗 rust-01,你会看到本页所有灾难如何被类型系统提前拦下)。理解 C 的内存之痛,才能真正体会 Rust 所有权的价值——这是本站两条线的一个刻意呼应。

5. 练习与要点

例 1(虚拟地址翻译手算) 页大小 4 KB、虚拟地址 0x1234:页内偏移 0x234、虚拟页号 0x1——查页表得物理页帧再拼偏移。手翻一次地址,"分页"从名词变动词

例 2(碎片的产生) 交替 malloc/free 不同大小的块,画出堆的空洞——理解外部碎片(空闲总量够但不连续)为什么让"内存还有很多却分配失败"。这是 [L03] 要对抗的敌人。

例 3(COW 验证) fork 一个占大内存的进程,观察系统内存不翻倍(top 看 RSS)——直到子进程写内存才涨。"fork 便宜"的机制亲眼可见\(\blacksquare\)

▶ 实验 L03(显式空闲链表 malloc)labs/L03-malloc/ —— 首次适配 + 边界标记合并 + 对齐,跑分配器压力测试测碎片率与吞吐。对标 CS:APP malloc lab。


CSAPP 四页到此完成——你已经把一行 C 从字节、缓存、链接、进程到虚拟内存看穿了。下一页进入操作系统 I:进程、线程与调度的原理层。