Skip to content

按需分页:为什么要等访问到了才给内存

你写程序 malloc 一块内存,或者 mmap 映射一段地址,你以为内核当场就把物理内存给你备好了——其实没有。内核只是在本子上记了一笔"这块虚拟地址归你",一个字节的物理内存都没动。等你哪天真去读写这块地址了,内核这才"哦你要用了",临时给你配一页物理内存。这种"先用后给、用到哪给到哪"的做法叫按需分页(demand paging),现代操作系统几乎都这么干。它听着像偷懒,其实是内存管理最聪明的一招——这一篇咱们就把它讲透:为什么会"缺页"、缺页时内核干什么、这种懒办法到底图什么。

先垫底:虚拟内存和页表

要理解按需分页,得先有个底子:程序用的是虚拟地址,而内存条上真正存数据的是物理地址,两者不是一回事,中间靠页表翻译。CPU 拿到程序发出的一个虚拟地址,会去查页表:这个虚拟页对应哪个物理页?查到了就把虚拟地址翻成物理地址,去内存条上读写。

这个"查页表"的机制,让每个程序都能以为自己独占了整整一大片连续的地址空间(64 位下大得像用不完),根本不用管物理内存到底有多少、被谁分了——它只管用虚拟地址,翻译的活儿内核和硬件替它干。这是虚拟内存带给程序的自由。

关键:一个虚拟页,在页表里有两种状态

页表里,一个虚拟页的条目(页表项)可以是两种状态之一:

  • 已映射:这个虚拟页对应着一个实打实的物理页,条目里记着物理页号和权限(可读、可写等)。程序访问它,页表查得到,顺利翻译,读写完成。
  • 未映射:这个虚拟页没有对应的物理页——要么从没分配过,要么曾经分配过后来被回收了。条目要么是空的,要么被标记成"无效"。程序访问它,页表查不到合法的翻译。

按需分页的玄机,全在"未映射"这个状态上。

访问到未映射的页,会发生什么:缺页

程序不知道、也不关心某个虚拟页现在是"已映射"还是"未映射"——它只管发地址。当它访问的那个虚拟页恰好处在"未映射"状态时,CPU 查页表查不到合法翻译,没法继续——这时它不会傻站着,而是陷入内核:触发一个异常,叫缺页异常(page fault,也叫缺页中断)。这个"fault"不是程序写错了(至少现在还不能下定论),它的字面意思就是"缺了一页"——你要用的这页,页表里现在没有。

陷进内核后,控制权交给内核的缺页处理程序(page fault handler)。它拿到出事的虚拟地址和一些线索(是读还是写、是用户态还是内核态触发的),开始判断:这个地址该不该有页?

缺页处理程序干什么:该补就补一页

内核怎么知道一个虚拟地址"该不该有页"?它有一本账本(下一篇要讲的 VMA 就是这本账),记着"哪些虚拟地址范围是合法分配给这个程序的、各自是什么属性"。缺页处理程序拿出事的地址查这本账:

  • 如果这个地址落在某个合法范围里(比如程序 mmap 申请过、或是它的栈、它的堆),说明这是一次"合理的缺页"——程序要用一块它有权用、但还没真给物理内存的地址。处理程序于是从物理内存里分配一页,在页表里建好映射(把这个虚拟页指到刚分配的物理页),然后让 CPU 回到出事的那条指令重试。这次再访问,页表查得到了,读写顺利完成——程序浑然不觉,还以为自己第一次访问就成功了。
  • 如果这个地址不落在任何合法范围里(比如一个野指针、或越界访问),说明这压根不是"该有的缺页",而是非法访问。处理程序不给页,而是给程序发一个段错误信号(通常终止程序)。

这就是按需分页的完整闭环:程序访问 → 缺页 → 内核判断合法 → 分配补页 → 重试成功。页不是预先备好的,是缺了才补的。

这种懒办法图什么

你可能要问:何必这么折腾,程序 mmap 的时候一次性把物理页全配好不就行了?非要等它访问、缺页、陷内核、再补——绕这么大圈。原因在于,一次性配齐有三个躲不开的坏处,而按需分页把它们都解了。

第一,省物理内存。 程序经常是"申请得多、用得少"——malloc 一个大数组,可能只用头几个元素;mmap 映射一个大文件,可能只读开头几页。一次性配齐,意味着把那些程序压根不会碰的地址也占上了物理页,纯属浪费。按需分页只给"真访问过"的页配物理内存,没用到的永远不占——一台物理内存有限的机器,能撑起远超它容量的"承诺"。

第二,程序能用上比物理内存大得多的地址空间。 因为不是所有虚拟页都同时占着物理页,程序尽可以拥有一个庞大的虚拟地址空间(64 位下理论上有 16 EB),实际只在用的那一小撮落在物理内存里。这在"一次性配齐"模式下根本不可能——物理内存才多大,配齐就满了。

第三,启动快、开新映射便宜。 程序刚跑起来,内核不用急着把它用到的所有地址都映射好;fork 一个新进程、mmap 一块新区域,都只是"记一笔",几乎不花时间,真正的分配推迟到第一次访问。这让创建进程、映射内存这些操作都很轻。

合起来,按需分页用"缺了再补"的小代价(每次 fault 的开销),换来了"按实际用量占用物理内存"的大收益——这是它能成为现代操作系统标配的根本。

代价:每一次首次访问都付一次 fault

天下没白吃的午餐。按需分页的代价是:每个虚拟页的第一次访问,都免不了一次缺页——陷内核、查账、分配、建映射、再返回,这套流程比一次普通的内存访问慢得多(普通访问是硬件查页表、纳秒级;一次 fault 要陷内核跑一堆代码、微秒级)。好在它只发生在每个页的"第一次",之后这页就映射好了,再访问就是普通访问。所以整体上,这点开销换来内存的大幅节省,划算。

还有一层更要紧的事:缺页处理程序判断"该不该有页",直接攥着程序的安全——一个不该被访问的地址(比如野指针)也会触发缺页,处理程序查账发现不合法,就该拒绝、甚至终止程序。这本"该不该有页"的账,就是下一篇要讲的 VMA。下一篇咱们就看 Cinux 怎么给地址空间配一本叫 VMA 的区域账本,用它回答"这个地址该不该有页",又怎么把这本账用在 mmap 的懒分配上。

035_multi_terminal-45-gf25de18 · f25de18 · 2026-08-04