Skip to content

虚拟内存:地址为何不是物理位置 ​

malloc 返回的地址属于进程虚拟地址空间。CPU 访问它时,需要根据当前地址空间的映射和权限找到物理内存。虚拟内存同时承担隔离、地址布局、共享与按需装入等作用,不能只把它理解为“内存不够时使用磁盘”。

一次翻译的数字例子 ​

假设页大小 4 KiB,页内偏移占 12 位。虚拟地址 0x12345 拆成 VPN=0x12、offset=0x345。若页表把 VPN 0x12 映射到物理页号 PPN 0xab,物理地址就是 0xab345。页内偏移保留,改变的是页号。

页表项不仅有页号,还包含有效性、读写执行权限、用户/内核访问权限,以及硬件或软件使用的状态位。多级页表把庞大而稀疏的页号空间分层组织,仅为需要的区域分配下一级表,代价是一次未缓存的翻译可能读取多个表项。

TLB 缓存近期地址翻译。TLB hit 时直接取得映射;TLB miss 时需要查页表,但映射完全可能有效,所以不会自动导致 page fault。缺页或权限异常则意味着硬件不能直接完成访问,需要操作系统判断下一步。

flowchart TD
  A[虚拟地址] --> B{TLB 命中?}
  B -- 是 --> D[取得映射并检查权限]
  B -- 否 --> C[遍历页表]
  C --> D
  D --> E{映射与权限允许?}
  E -- 是 --> F[访问物理地址对应缓存层次]
  E -- 否 --> G[陷入内核]
  G --> H{可修复的缺页?}
  H -- 是 --> I[补页或调整映射 后重试]
  H -- 否 --> J[报告非法访问]
查看流程图文本
flowchart TD
  A[虚拟地址] --> B{TLB 命中?}
  B -- 是 --> D[取得映射并检查权限]
  B -- 否 --> C[遍历页表]
  C --> D
  D --> E{映射与权限允许?}
  E -- 是 --> F[访问物理地址对应缓存层次]
  E -- 否 --> G[陷入内核]
  G --> H{可修复的缺页?}
  H -- 是 --> I[补页或调整映射 后重试]
  H -- 否 --> J[报告非法访问]

图是概念顺序;真实 CPU 可能并行或交叠部分地址翻译与缓存访问,具体依微架构而异。

三种容易混淆的“miss” ​

事件缺的是什么常见处理者
数据 cache miss当前缓存层里的数据行缓存控制逻辑,到下一级寻找
TLB miss缓存里的翻译结果硬件页表遍历或软件机制,依架构
page fault当前页表状态无法直接完成访问操作系统异常处理

即使所有页面都在内存里,也可能频繁 cache miss。即使 TLB miss,也可能遍历到有效页表后立即继续。反过来,缺页也未必需要磁盘 I/O:匿名零页的首次写入、写时复制和延迟分配都可能由内存操作完成。

fork 与写时复制 ​

父进程 fork 后,父子最初可以共同引用物理页,同时把可写映射暂时设成只读并标记 COW。读取不需要复制;当一方写入时,异常处理器判断它是合法的 COW 写入,分配并复制页、更新页表和引用计数,再重新执行原写入。

原本的只读代码页不能被误判为 COW,否则非法写入会获得写权限。释放也需要按物理页引用计数判断:删除某一个页表映射,不一定意味着该物理页已无人引用。这些问题会在 MIT xv6 的 COW 实验中落实到真实内核代码;CSAPP 先建立应用程序视角。

举例:父进程有 100 个 4 KiB 可写页,孩子只修改其中 2 页。在理想化模型中,数据复制可以只涉及 8 KiB,而非立即复制 400 KiB。页表、元数据和实现开销未包括在这个简单估算里。

mmap、文件与共享 ​

文件映射把文件的一段内容与虚拟地址区间建立关系。读取相应地址可能触发按需加载。共享映射允许进程通过相同底层对象观察变化;私有映射提供私有修改语义,通常与 COW 相关。是否落盘、什么时候持久化、其他进程何时看到更新,取决于映射方式、同步操作与操作系统语义,不能把“写入地址”直接等同于“已经可靠保存到磁盘”。

文件描述符、文件内容缓存、进程页表是不同层次的对象。关闭 fd 并不等价于撤销已有 mmap;解除映射则会使那段虚拟地址不再具有原有访问权限。把生命周期逐层画清楚,比背 API 名字更可靠。

malloc 在哪一层 ​

应用调用 malloc 请求字节,用户态分配器在已拥有的内存区域中寻找块;必要时再向系统请求更多区域。一次小额 malloc 不一定发生系统调用,也不一定立即获得独占物理页。free 常常只是把块交还给分配器复用,进程 RSS 不必立即降低。

反过来,C 的对象生命周期在 free 时已结束,即使字节仍留在原地址,继续访问也是错误。偶尔读到旧数据不构成“这块内存还能用”的证据。ASan 可帮助发现许多此类错误,但对自建 arena 内部的小块边界,它未必理解你的分配语义;因此下一章仍需自己的堆检查器。

两个观察练习 ​

在 Linux 中可查看自己进程的 /proc/<pid>/maps,辨认程序代码、堆、库和栈的映射;路径与布局依运行环境变化。再写一个申请较大内存、每页写一个字节的程序,区分虚拟空间保留与实际触页。记录页大小、申请量与测量方法,不要预先宣称会观察到某个固定 RSS 数值。

更小的纸面练习:页大小 4096,顺序访问 1 MiB 数组且每次读取 8 字节,需要跨越多少页?理想情况下是 256 页;如果数组起点不页对齐,可能覆盖 257 页。访问次数为 131072,并不等于翻译次数或缺页次数,因为缓存和页面状态会复用。

自测:两个进程里的相同虚拟地址一定对应相同物理页吗?

不一定。翻译依赖进程当前地址空间;相同虚拟地址可映射不同物理页。共享内存则可让不同虚拟地址映射同一物理页。

自测:free 后程序没崩溃,能否证明没有错误?

不能。对象生命周期已经结束,分配器可能尚未复用或归还该区域。未定义行为不保证立即崩溃;检查生命周期与所有权才是依据。

参考入口:CS:APP3e 学生资源。下一章:动态内存分配。