地址空间与分页¶
程序使用地址访问代码、栈和堆,但物理内存由许多进程共享,位置还会随运行变化。虚拟地址空间把“程序看到的地址”与“当前存放数据的物理页框”分开。
为什么直接使用物理地址不够¶
若程序中的地址就是物理地址,会遇到:
- 两个程序可能使用同一地址范围而冲突。
- 程序装入位置改变后,代码中的地址要重定位。
- 一个程序可以读写另一个程序或内核的内存。
- 物理内存不足时,很难只保留当前需要的部分。
虚拟内存通过每个进程独立的映射解决这些问题。相同虚拟地址在不同进程中可以指向不同物理页。
地址空间的直观布局¶
典型进程地址空间包含:
这只是概念布局。地址随机化、体系结构、可执行格式和运行时会改变实际位置。空洞并不等于占用同等大小的物理内存;虚拟空间可以稀疏。
历史路径:重定位、分段与分页¶
基址与界限¶
进程产生逻辑地址 \(x\),硬件检查 \(0\le x<limit\),再加基址:
这提供简单重定位和连续区域保护,但每个进程需要一大块连续物理内存,容易产生外部碎片,也不方便让代码、堆和栈独立增长。
分段¶
分段把地址空间按代码、数据、栈等逻辑区域拆分,每段有独立基址、界限和权限。它符合程序结构,也方便共享只读代码。
问题是段大小可变,物理内存分配仍会产生外部碎片;段增长还可能需要搬迁。现代通用系统多以分页作为主机制,但“区域具有不同语义和权限”的思想仍存在于虚拟内存区域中。
分页¶
分页把虚拟空间切成固定大小的虚拟页,把物理内存切成同样大小的页框。虚拟页可以映射到任意页框,无需物理连续。
若页大小为 \(P=2^p\) 字节,虚拟地址可分为:
地址翻译只替换页号,页内偏移保持不变。
最小地址翻译例子¶
假设页大小为 4096 字节,虚拟地址为 13,000:
若页表把虚拟页 3 映射到物理页框 20,则:
程序只使用 13,000,硬件和操作系统共同维护映射到 82,632 的关系。
页表项包含什么¶
页表项不仅保存物理页框号,还常包含:
- present/valid:映射当前是否有效。
- readable、writable、executable:访问权限。
- user/supervisor:用户态能否访问。
- accessed/reference:近期是否访问过。
- dirty:映射后是否写过。
- cache policy、global、copy-on-write 等架构或系统相关状态。
权限按页实施,使代码页可读可执行但不可写,数据页可读写但不可执行。W^X 思想避免同一页同时可写又可执行,减少代码注入风险,但 JIT 等场景需要受控切换或双映射。
内部碎片¶
固定页消除了进程分配的外部碎片,却可能在最后一页浪费空间。若对象大小随机分布且不考虑其他因素,平均内部浪费常粗略估为半页:
这只是直觉模型。实际映射共享、文件尾页、分配器和大页都会改变浪费情况。
按需分页¶
建立虚拟区域时不必立即分配所有物理页。首次访问尚未驻留的合法页面会触发缺页异常,内核再分配零页、读取文件或从交换区换入。
优点:
- 程序启动更快,只装入真正访问的页面。
- 稀疏大地址空间无需占用同等物理内存。
- 内存可在活跃工作集之间动态分配。
代价是首次访问延迟不可预测;内存紧张时还可能发生回收和磁盘 I/O。
共享页面¶
不同进程的虚拟页可映射到同一物理页:
- 多个进程共享只读库代码。
- 父子进程在
fork后写时复制。 - 进程通过共享内存通信。
- 文件页同时被页缓存和进程映射引用。
共享提高内存效率,但写权限和一致性必须明确。共享可写页上的普通数据访问需要同步,和同一进程多线程共享内存一样。
写时复制¶
fork 后父子进程最初共享物理页,页表将其标记为只读和写时复制。任一方写入时:
- 触发写保护缺页。
- 内核确认这是合法 COW 页面。
- 分配新页框并复制原内容。
- 更新写入方页表为可写新页。
- 重试写指令。
若双方都不写,复制永不发生。代价是首次写入出现缺页和复制延迟;大进程 fork 还需复制或共享页表结构,并非完全免费。
mmap 与文件映射¶
文件映射把文件的一段映射进虚拟地址空间,应用通过普通加载/存储访问。首次访问可能缺页并从文件读取,写入可能进入页缓存,最终按映射类型和刷写策略持久化。
MAP_PRIVATE 的写入通常采用写时复制,不回写原文件;MAP_SHARED 则建立共享文件映射,但“内存已经修改”不等于“数据已经持久化到设备”,还需理解同步与刷盘语义。
大页¶
增大页大小会减少页表项数量,并提高 TLB 单项覆盖范围:
代价包括更大内部碎片、更高缺页搬运成本、内存整理需求和更粗的保护粒度。大页适合大而连续的工作集,不一定适合大量小映射。
地址随机化与保护¶
地址空间布局随机化改变栈、堆、库和可执行映像位置,使攻击者难以预测目标地址。它增加利用难度,却不是内存安全:信息泄漏可能暴露布局,越界写的根因仍存在。
页级不可执行、只读和用户/内核权限提供基础保护。更细对象边界通常需要语言、编译器、运行时或额外硬件机制配合。
与分配器的区别¶
应用调用 malloc 获得的是进程虚拟空间中的一段可用区域。用户态分配器可能先从已有 arena 切块,未必每次都发系统调用;只有需要扩展或归还大块虚拟区域时,才通过 brk 或 mmap 等接口联系内核。
内核物理页分配器管理页框,slab 类分配器再从页中提供小型内核对象。不同层都叫“内存分配”,但对象、碎片和同步问题不同。
自测¶
- 两个进程能否使用相同虚拟地址保存不同数据?
- 分页消除了哪类碎片,又引入哪类碎片?
fork的写时复制为什么要先把共享页设为只读?- 大页怎样改善 TLB 覆盖,又有什么代价?
mmap写入成功是否表示数据已经落到非易失设备?
参考思路
每个地址空间的页表不同,所以可以。分页避免可变连续分配的外部碎片,但有页内碎片。只读写保护让首次写入可被硬件捕获。大页减少翻译项,代价是碎片和搬运粒度。内存修改与持久化是不同阶段。