跳转至

内存进阶 — 按需分页、内存映射文件、写入时复制

这一章讲什么: 到上一章为止,页面故障还是「杀应用」的死刑判决; 本章把它改造成一个机会——「这页还没分配?好,现在分配,应用你继续跑」。 三个进阶全是这一个思路:按需分页(用到才给帧)、内存映射文件(用到才从文件读)、 写入时复制(用到才真复制)。它们今天还服务在每一个正经操作系统里。

1. 先看现象:坏消息变成好时机

主线例子:

  按需分页:应用调 DemandPages(地址范围)「我要用这段」→ OS 只记账
      → 应用一访问 → 页面故障 → HandlePageFault 检查「在声明范围内?P=0?」
      → 分配一帧、建映射 → 返回应用,应用毫无察觉
      → sbrk/malloc 从此按需生长,不再预占大块
  映射文件:fd = open("memmap"); MapFile(fd, 大小) → 一段虚拟地址
      → 读它 = 缺页 → OS 从文件的簇读一页进帧 → 像读内存一样读文件
  COW:第二次启动 rpn → LoadApp 发现 app_loads 里有模板
      → 新 PML4 浅拷贝模板的应用区(新页表、同一批物理帧、writable=0)
      → 两个 rpn 共用代码页;谁写自己的全局变量 → 写故障 → 复制那一页再写
  memstat:启动后 / 开终端后 / 跑应用后,内存占用一屏可见

2. 顶层全景

  27.1 按需分页:P=0 检查;应用声明范围(dpaging_begin/end);DemandPages 系统调用;
       sbrk 架在 DemandPages 上
  27.2 映射文件:FileMapping{fd, vaddr 范围};地址从空间末端往前长;
       缺页时读文件簇进帧;退出清理 file_maps_(漏了就出错)
  27.3 memstat:BitmapMemoryManager::Stat();三次运行对比
  27.4 COW:app_loads 模板表;LoadApp = 新 PML4 + 浅拷贝应用区;
       SetupPageMaps 加 supervisor 写位(加载期 memcpy 只读页的特许);
       写故障 → 复制帧、改映射 writable=1;释放时只放 writable=1 的页

  图说:同一颗「缺页」火种,点三盏灯。

3. 核心原理

3.1 按需分页:异常处理程序里的分配器

按需分页 = 起初不分配帧,首次访问页面时才分配——它恰好利用页面故障这个 「通常不希望发生的 CPU 异常」1。 实现小得惊人:异常处理程序里调 HandlePageFault()——检查错误码 P 位 (P=1 的故障是权限问题,不该按缺页处理),再检查地址落在应用声明的 dpaging 范围(dpaging_begin_/end_)内,是就 AllocateFrame + 建映射,返回; 「按需分页听起来很困难,实际操作起来却令人大吃一惊」2。 范围必须应用事先声明(DemandPages 系统调用扩界)——不声明就能缺页分配的话, 恶意应用读写空闲地址会把物理内存耗尽3。 sbrk 顺势重写:第一次调 DemandPages 划一页,之后在同页内挪动指针即可4。

3.2 内存映射文件:文件是按需读进内存的

内存映射文件 = 把文件映射进虚拟地址,像读写内存一样读写文件5。 实现同样骑在缺页上:MapFile(fd, len) 记一条 FileMapping(fd → 虚拟地址范围), 地址从虚拟空间末端往前长(往上涨会撞应用区)6; 应用一访问 → 缺页 → OS 从文件对应位置读数据进帧、映射上7。 代价要认账:整个文件被一次性读进内存对纯搜索是浪费——但映射机制本身要求 「文件数据在虚拟地址里连续」,而 FAT 的簇链物理上不一定连续, 所以先得搬进内存再映射;「这种方法对于实现内存映射文件是必不可少的」8。 应用退出要清 file_maps_——「最初忘了写这个,出现了一个错误」,小账也要记9。

3.3 memstat:把内存变成数字

BitmapMemoryManager::Stat() 数出「用了多少帧 / 共多少帧」, memstat 命令直接打印;三次运行(启动后 / 开终端后 / 跑应用后)对比, 内存的去向第一次可见10。

3.4 写入时复制:同一份代码,人人有份,写时才分家

同一个应用跑 N 次,机器语言和只读数据完全相同——共享同一批物理帧就能省内存11。 机制三步12:

  1. 模板:首次加载应用时,把它的页表结构(入口、LOAD 段映射)注册进 app_loads(以文件目录项为键);
  2. 浅拷贝:第二次启动,新建 PML4(复制内核前半),把模板的应用区页表浅拷贝进来—— 新页表、但 PT 指向同一批物理帧;帧的 writable 全设 0;
  3. 写时复制:谁写自己的页 → 写故障(writable=0)→ 异常处理程序复制该页到新帧、 改映射指向新帧并置 writable=1 → 返回,写入指令重放成功。

两个工程细节透着老练:加载期 memcpy LOAD 段要写只读页, SetupPageMaps 加了一个 supervisor 写位(内核特权可写用户只读页)作特许13; 释放页表时只释放 writable=1 的页——共享帧(writable=0)还在被别人指着14。

3.5 写入时复制的完整走读(27.4 节补完)

AppLoadInfo:模板里存什么。 第一次加载某应用时,OS 建一条 AppLoadInfo 存三样: 应用地址空间的末尾 vaddr_end、入口点地址 entry、以及整套页表结构的指针 pml4—— 这份页表从此就是这个应用的「模板」15。模板以应用的文件(目录项)为键,登记进 app_loads 映射;主函数在第一个终端任务启动前就把它初始化好16。

LoadApp 的三段式。 第二次启动同一个应用时,LoadApp() 走三段17:

  1. SetupPML4():新建一张 PML4 表(4 KiB 物理帧),把内核区(下标 0–255)从当前 页表复制过去——所有应用共享同一套内核映射;此时应用区(下标 256 起)是空的18;
  2. 查 app_loads:找到了,就把模板的应用区页表浅拷贝进新 PML4(新页表、同一批物理帧), 返回这条加载信息;没找到,走完整的「读文件 + LoadELF + 注册模板」老流程, 唯一区别是 LoadELF 里把 LOAD 段的页全部置为 writable=0(只读)19;
  3. 模板里的页全是只读,加载期 memcpy LOAD 段怎么写进去的? 答案是 SetupPageMaps 加了一个 supervisor(监督者)写位:内核特权可以写用户只读页,写完应用也看不见20。

写故障的分支。 应用在运行中写自己的全局变量 → CPU 发现页只读 → 页面故障 → 异常处理程序认出「这是 COW 页」→ 分配新帧、把旧页内容拷过去、让应用自己的页表指向 新帧并置 writable=1 → 返回,故障指令重放成功21。此后这个应用改的是自己的副本, 别的应用手里的共享页纹丝不动。图 27.8 画的就是全局变量区从「共享只读」到「各写各的」 的分裂过程22。

清理的讲究。 应用退出释放页表时,只释放 writable=1 的页——共享帧(writable=0) 还被别的应用指着,放了就 double-free23。作者自己也没忘记账:「如果几乎整个页面都被 均匀地重写,最终就会复制每一页,这可能会违背直觉且效率低下」——全写场景 COW 反而更亏, 启动期只有部分区域会被写,这才划算24。

4. 作者的判断与证据

判断:异常是机制,不是事故。 本章的题眼:「此前,一旦出现异常,应用就被迫终止; 有了按需分页,即使发生页面故障,应用也能在分配完物理内存后继续执行」—— OS 设计的通用套路:把坏消息拦下来,换成一件该做的事。

判断:COW 值得它的复杂度。 作者自己算过账:如果启动期几乎每页都要写, COW 反而更慢;但「写入多发生在部分区域」,所以划算——没有盲目崇拜机制。

证据: memstat 三次对比;COW 后多开 rpn 内存不涨;映射文件里 grep 直接搜映射区; 以及一条反向证据——作者明说全写场景 COW 更亏,所以它是「按场景选的优化」,不是无条件信仰25。 COW 后多开 rpn 内存不涨;映射文件里 grep 直接搜映射区。

5. 边界与局限

  • 按需分页范围靠应用自觉声明,没有 overcommit 策略与回换(swap);
  • 映射文件整文件进内存,不是真·零拷贝,大文件不友好;
  • COW 模板以目录项为键,同名不同内容的文件会错配(教学简化);
  • 页表/帧的引用计数没有,释放靠「writable 标志」这个约定——能跑,但脆。

6. 可带走的

本章主线例子,一行写完: 缺页拦截 → 按需分页(DemandPages 声明范围 + 现场分帧 + sbrk 复用)→ MapFile(地址末端往前长,缺页读文件)→ app_loads 模板 + 浅拷贝 + 写时复制 → memstat 看效果。

  1. 缺页可以是分配时机:先检查 P 位与声明范围,再给帧;
  2. 范围必须应用先声明——无限缺页分配 = 内存耗尽漏洞;
  3. 映射文件 = FileMapping + 缺页读文件;地址从末端往前规划;
  4. memstat 这类「把内部状态变成命令」的小工具,是系统成熟的标志;
  5. COW = 页表浅拷贝(共享帧、全只读) + 写故障复制那一页;
  6. supervisor 写位解决「加载期要写只读页」;释放只放 writable 的页;
  7. 机制的账要会算:COW 不是免费午餐,全写场景反而更慢。

7. 原文地图

主题 原书章 原文位置
按需分页定义与思路 27.1 按需分页(osbook_day27a) text/170-ch27-01-27-1-osbook-day27a.txt:4(搜「请求或要求」) · text/170-ch27-01-27-1-osbook-day27a.txt:9(搜「利用了页面故障」)
HandlePageFault 与 P 位检查 27.1 按需分页(osbook_day27a) text/170-ch27-01-27-1-osbook-day27a.txt:37(搜「HandlePageFault」) · text/170-ch27-01-27-1-osbook-day27a.txt:66(搜「第0位为1」)
声明范围与 DemandPages 27.1 按需分页(osbook_day27a) text/170-ch27-01-27-1-osbook-day27a.txt:83(搜「有效的虚拟地址范围」) · text/170-ch27-01-27-1-osbook-day27a.txt:94(搜「DemandPages」)
sbrk 架在 DemandPages 上 27.1 按需分页(osbook_day27a) text/170-ch27-01-27-1-osbook-day27a.txt:132(搜「sbrk()」)
映射文件定义与整文件读入 27.2 内存映射文件(osbook_day27b) text/171-ch27-02-27-2-osbook-day27b.txt:16(搜「映射到内存」) · text/171-ch27-02-27-2-osbook-day27b.txt:56(搜「浪费」)
FileMapping 与末端规划 27.2 内存映射文件(osbook_day27b) text/171-ch27-02-27-2-osbook-day27b.txt:62(搜「FileMapping」) · text/171-ch27-02-27-2-osbook-day27b.txt:83(搜「末端」) · text/171-ch27-02-27-2-osbook-day27b.txt:95(搜「忘了写」)
memstat 27.3 测量内存使用情况(osbook_day27c) text/172-ch27-03-27-3-osbook-day27c.txt:25(搜「free」) · text/172-ch27-03-27-3-osbook-day27c.txt:31(搜「Stat()」)
COW 动机与定义 27.4 写入时复制(osbook_day27d) text/173-ch27-04-27-4-osbook-day27d.txt:19(搜「机器语言和只读数据」) · text/173-ch27-04-27-4-osbook-day27d.txt:56(搜「只读变量」) · text/173-ch27-04-27-4-osbook-day27d.txt:59(搜「违背直觉」)
app_loads 与浅拷贝 27.4 写入时复制(osbook_day27d) text/173-ch27-04-27-4-osbook-day27d.txt:80(搜「app_loads」) · text/173-ch27-04-27-4-osbook-day27d.txt:109(搜「CopyPageMaps」)
supervisor 写位与释放约定 27.4 写入时复制(osbook_day27d) text/173-ch27-04-27-4-osbook-day27d.txt:146(搜「允许监督写入」) · text/173-ch27-04-27-4-osbook-day27d.txt:212(搜「writable=1时才释放」)

  1. 出处:「27.1 按需分页」(text/170-ch27-01-27-1-osbook-day27a.txt:4,搜「请求或要求」;:7,搜「首次访问页面时才为该页面分配帧」;:9,搜「利用了页面故障」)。 ↩

  2. 出处:「27.1 按需分页」(text/170-ch27-01-27-1-osbook-day27a.txt:37,搜「HandlePageFault」;:53,搜「令人大吃一惊」;:65,搜「第0位为1」)。 ↩

  3. 出处:「27.1 按需分页」(text/170-ch27-01-27-1-osbook-day27a.txt:83,搜「有效的虚拟地址范围」;:87,搜「耗尽物理内存」)。 ↩

  4. 出处:「27.1 按需分页」(text/170-ch27-01-27-1-osbook-day27a.txt:94,搜「DemandPages」;:132,搜「sbrk()」;:141,搜「4KiB」)。 ↩

  5. 出处:「27.2 内存映射文件」(text/171-ch27-02-27-2-osbook-day27b.txt:16,搜「映射到内存」;:30,搜「普通内存一样读写文件」)。 ↩

  6. 出处:「27.2 内存映射文件」(text/171-ch27-02-27-2-osbook-day27b.txt:83,搜「末端」);:86,搜「初始值」;:93,搜「向前端延伸」)。 ↩

  7. 出处:「27.2 内存映射文件」(text/171-ch27-02-27-2-osbook-day27b.txt:39,搜「虚拟地址空间」;:44,搜「映射到应用的虚拟地址空间」)。 ↩

  8. 出处:「27.2 内存映射文件」(text/171-ch27-02-27-2-osbook-day27b.txt:56,搜「浪费」;:59,搜「连续的方式」;:62,搜「必不可少」)。 ↩

  9. 出处:「27.2 内存映射文件」(text/171-ch27-02-27-2-osbook-day27b.txt:95,搜「忘了写」;:97,搜「MapFile」)。 ↩

  10. 出处:「27.3 测量内存使用情况」(text/172-ch27-03-27-3-osbook-day27c.txt:25,搜「free」;:31,搜「Stat()」;:45,搜「三次运行」)。 ↩

  11. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:19,搜「机器语言和只读数据」;:24,搜「共享相同的物理内存」)。 ↩

  12. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:80,搜「app_loads」;:40,搜「CopyPageMaps」;:42,搜「浅层复制」;:64,搜「同一个物理帧」)。 ↩

  13. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:146,搜「允许监督写入」;:128,搜「必须禁用写入」)。 ↩

  14. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:212,搜「writable=1时才释放」;:65,搜「页面故障」——写故障复制页的说明)。 ↩

  15. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:74,搜「AppLoadInfo」;:7,搜「vaddr_end」)。 ↩

  16. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:80,搜「app_loads」;:83,搜「初始化」)。 ↩

  17. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:95,搜「LoadApp」;:104,搜「it != app_loads」)。 ↩

  18. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:100,搜「SetupPML4」;:101,搜「0至255」;:103,搜「256」)。 ↩

  19. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:109,搜「CopyPageMaps」;:127,搜「必须禁用写入」;:122,搜「模板」)。 ↩

  20. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:146,搜「允许监督写入」;:140,搜「难道不会出错吗」)。 ↩

  21. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:170,搜「causal_addr」;:65,搜「页面故障」;:183,搜「再次导致页面故障」)。 ↩

  22. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:54,搜「图27.8」;:56,搜「只读变量」)。 ↩

  23. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:211,搜「writable=1」;:224,搜「特徴を利用して」——日文残页,释放约定的说明)。 ↩

  24. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:59,搜「违背直觉」;:61,搜「更有效」)。 ↩

  25. 出处:「27.4 写入时复制」(text/173-ch27-04-27-4-osbook-day27d.txt:61,搜「重写的区域只是一部分」)。 ↩