news 2026/9/7 19:11:27

Linux 内存惰性分配:从虚拟地址到物理页的深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 内存惰性分配:从虚拟地址到物理页的深度解析

2026-09-04 · 操作系统 · 内存管理
标签:Linux内存管理MMUPage Faultmmap

引言

在 Linux 系统中,当你调用malloc(8192)申请 8KB 内存时,操作系统真的立刻从内存条上划出 8KB 给你了吗?

答案是否定的。

Linux 采用惰性分配(Lazy Allocation,也称 Demand Paging 按需分页)策略:mallocmmap只是"预定"了一段虚拟地址空间,真正的物理内存分配,要等到程序第一次读写这片地址时才会发生。这一机制是现代操作系统内存管理的核心支柱之一。


一、完整 C 语言伪代码示例

以下伪代码完整展示了从申请内存到触发缺页、再到物理页分配的完整生命周期:

#include<stdio.h>#include<stdlib.h>intmain(void){/* =========================================================== 阶段 1: 申请虚拟地址空间 malloc 底层通过 brk/mmap 向内核申请 8KB(2 个页) 内核只记录 "0x7f0000 ~ 0x7f1fff 这 8KB 属于该进程" 但此时没有分配任何物理内存! =========================================================== */char*heap=malloc(8192);printf("虚拟地址: %p\n",heap);// 例如 0x7f0000printf("物理内存占用: 0 KB\n");// 真的 0 KB/* =========================================================== 阶段 2: 第一次写入,触发缺页异常 (Page Fault) CPU 将虚拟地址 0x7f0000 送给 MMU 翻译 MMU 查页表发现 "Present 位 = 0"(未映射) 触发缺页异常,CPU 陷入内核态 =========================================================== */heap[0]='A';/* =========================================================== 阶段 3: 内核处理缺页异常(对用户态完全透明) 1) 内核检查: 0x7f0000 是否在已申请的 vma 范围内?是。 2) 内核从伙伴系统分配一个 4KB 物理页(如 PFN = 0x3a) 3) 内核更新页表: 虚拟页 0x7f0 -> 物理页 0x3a, 权限 R/W 4) 内核将物理页清零,返回用户态,重新执行 heap[0] = 'A' =========================================================== *//* =========================================================== 阶段 4: 第二次写入,页表命中 (Page Table Hit) 虚拟地址 0x7f0064 和 0x7f0000 同属一个页 MMU 查页表命中,直接翻译为物理地址,无异常开销 =========================================================== */heap[100]='B';/* =========================================================== 阶段 5: 访问第二页,再次缺页 虚拟地址 0x7f1000 属于页 2,页表中仍未映射 再次触发缺页异常,内核再分配一个物理页 =========================================================== */heap[4096]='C';/* =========================================================== 最终状态: - 申请了 8KB 虚拟地址 - 实际占用了 8KB 物理内存 - 如果这 8KB 从未被访问,物理内存占用永远是 0 =========================================================== */return0;}

关键洞察:如果程序申请了 1GB 内存但只读了其中 1MB,那么物理内存实际占用只有 1MB。这就是惰性分配的力量。


二、动图演示:惰性分配的完整生命周期

下面这张动图以三栏视角(虚拟地址空间 / 页表 / 物理内存)完整演示了上面代码的七个关键时刻:

逐阶段解读:

阶段操作虚拟地址空间页表物理内存
malloc(8192)两页标记"已预留"两项均为P=0未映射占用 0 KB
首次写heap[0]MMU 发现P=0→ 触发#PF 缺页异常
内核do_page_fault建立映射0x7f0 → 0x3a伙伴系统分配 4KB(清零)
重新执行写入指令页1 已映射P=1, R/W'A'落入物理页,占用 4 KB
heap[100]同页访问页表命中,直通占用不变,纳秒级完成
heap[4096]页2 未映射再次P=0→ 再次 #PF
内核再分配一页页2 已映射0x7f1 → 0x15占用 8 KB

内核处理缺页异常的核心逻辑(伪代码):

voiddo_page_fault(unsignedlongaddress,intwrite){// address = 0x7f0000(触发缺页的虚拟地址)structvm_area_struct*vma=find_vma(address);// 1. 检查该虚拟地址是否在已申请的 vma 范围内if(!vma||address<vma->vm_start)send_sigsegv();// 非法访问 -> 段错误 (SIGSEGV)// 2. 从伙伴系统分配一个物理页(4KB)structpage*pg=alloc_page(GFP_KERNEL);unsignedlongpfn=page_to_pfn(pg);// 例如 PFN = 0x3a// 3. 建立页表映射:VPN 0x7f0 -> PFN 0x3aset_pte_at(mm,address,pte,pfn_pte(pfn,PAGE_RW));}

异常处理完毕后,CPU返回到触发异常的那条指令重新执行——这次 MMU 翻译成功,对用户态来说整个过程完全透明,仿佛什么都没有发生过。

全景流程图:申请 → 首次访问 → 缺页处理 → 后续命中

阶段四:后续访问(全程快路径)

阶段三:内核缺页处理 do_page_fault

阶段二:首次访问(按需掏钱)

阶段一:申请内存(只记账,不掏钱)

读访问

第一次写入

之后再写

非法地址 / 越权

合法

COW 场景:PTE 存在但只读

用户态 malloc(大内存)

glibc 调用 brk / mmap 匿名映射

内核创建 VMA(vm_area_struct)
仅记录虚拟地址区间 + 权限
✅ 分配虚拟地址 ❌ 不分配物理页 ❌ 不填充 PTE

malloc 返回有效虚拟地址

进程首次操作虚拟地址?

Minor Fault
映射共享 zero page(全零页)
多进程共用,不占私有物理页

MMU 查页表:PTE 无效(P=0)
触发缺页异常 #PF,陷入内核态

find_vma 校验
地址在 VMA 内 && 权限允许写?

发送 SIGSEGV 段错误
进程崩溃

伙伴系统 alloc_page
分配 4KB 物理页并清零

填充 PTE:VPN ↔ PFN 建立映射(P=1, R/W)

返回用户态
重新执行触发异常的指令

第二次及后续读写同一页

MMU 页表命中(P=1)
直接翻译物理地址,纳秒级,无缺页

💡 写时复制(fork)
初始父子共享物理页并标记只读
写入时触发缺页 → do_wp_page 拷贝新页


三、动图演示:MMU 地址翻译的两条路径

每次内存访问,MMU 都要走一次"查页表"。结果只有两种——命中(快路径)缺页(慢路径)

  • 快路径(页表命中):页表项P=1,MMU 纯硬件完成翻译,纳秒级,内核完全无感知。这是 99.9% 的情况。
  • 慢路径(缺页异常):页表项P=0,MMU 触发 #PF 异常(x86 中断向量 14),CPU 保存现场、陷入内核态,走do_page_fault分配物理页并更新页表,然后返回用户态重新执行同一条指令。每页只发生一次,之后都走快路径。

四、核心概念详解

名词通俗解释技术细节
虚拟地址(Virtual Address)进程"以为"自己在用的地址每个进程有独立的虚拟地址空间(64 位系统上 128TB),彼此隔离。进程 A 的0x7f0000和进程 B 的0x7f0000完全是两码事
物理地址(Physical Address)内存条上真实的电路地址DRAM 芯片的硬件地址,范围由实际安装的内存容量决定(如 32GB)
MMUCPU 里的"地址翻译官"Memory Management Unit,硬件电路,负责把虚拟地址翻译成物理地址。翻译失败时向 CPU 报告缺页异常
页表(Page Table)虚拟地址到物理地址的"对照表"每个进程一张页表,存于物理内存。页表项(PTE)记录物理页号(PFN)、读写权限、Present 位等。x86-64 通常采用 4 级页表(PGD/PUD/PMD/PTE)
VPN虚拟页编号Virtual Page Number,虚拟地址的高位部分。如0x7f0000按 4KB 分页,VPN =0x7f0
PFN物理页框编号Page Frame Number,物理页的高位地址。如物理地址0x3a000对应的 PFN =0x3a
mmap向内核"预定"一段虚拟地址mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)是分配匿名内存的核心系统调用。它只创建 vma,不分配物理页
vma内核记录的虚拟地址区间vm_area_struct,描述进程地址空间中的一段连续区域,记录起止地址、权限、映射文件等。缺页处理时内核先查 vma 确认地址合法性
缺页异常(Page Fault)MMU"找不到路"时触发的 CPU 异常MMU 发现页表项 Present 位为 0 时触发(x86 #PF,中断向量 14)。CPU 保存现场、切换内核态、执行do_page_fault()。惰性分配的核心触发点
伙伴系统(Buddy System)内核管理物理页分配的数据结构物理内存划分为 4KB 页框,空闲页框由伙伴系统管理。分配时找合适的连续页框,释放时合并相邻空闲块减少碎片。alloc_page()就是从这里取页
brk / mmap用户态申请堆内存的两条路径小块内存(< 128KB 默认阈值)用brk扩展堆顶;大块用mmap匿名映射。两者最终都走惰性分配路线

五、惰性分配的本质

一句话总结:操作系统在malloc/mmap只记账、不掏钱(不分配物理内存),等到程序真正读写那片地址时,才按需掏钱(分配物理页并建立映射)。

为什么这么做?

  • 节省物理内存:程序经常申请大内存但只用一小部分(如稀疏数组、Java 预分配的大堆)。惰性分配确保"没用过的一分钱不花"。
  • 加速内存申请mmap只需修改内核数据结构(创建 vma),无需遍历物理内存找空闲页、清零页等重操作。
  • 提高 fork 效率fork子进程时拷贝页表但标记只读,父子共享物理页,真正写入时才复制(Copy-on-Write),这也是惰性分配思想的延伸。

代价是什么?

代价说明
第一次访问有延迟缺页异常处理涉及内核路径、物理页分配、页表更新,约需几微秒到几十微秒(相比直接访问的纳秒级)
可能触发 OOM申请了 10GB 虚拟内存但物理内存只有 8GB,如果程序真的逐页访问满 10GB,就会触发OOM Killer

动图演示:申请 ≠ 占用

下面这张动图模拟了malloc(1GB)后逐步写入的过程——VSZ(虚拟地址空间)一开始就占满 1024MB,但 RSS(实际物理占用)只随真实触碰的页面阶梯增长

这也是为什么top/ps里经常看到进程 VSZ 巨大但 RSS 很小——灰色区域就是"申请了但从未触碰、物理内存零消耗"的部分


六、总结

阶段发生了什么物理内存占用
malloc(8192)内核创建 vma,记录虚拟地址区间0 KB
第一次写入heap[0]缺页异常 → 内核分配 4KB 物理页 → 建立映射4 KB
写入heap[100]页表命中,直接访问,无内核介入4 KB
第一次写入heap[4096]缺页异常 → 内核再分配 4KB 物理页8 KB

核心记忆点(口诀):
申请堆 = 只拿虚拟地址;第一次写入 = 才分配物理内存;没被访问过的堆虚拟地址,全程零物理占用。
快路径走 MMU,慢路径进内核;每页只慢一次,之后全是直通。


参考资料

  1. Page Fault Handler — Linux Kernel Internals — 从硬件异常到页表映射的完整 fault path,含 x86 错误码与各类 fault 的 handler 分派表
  2. Life of a malloc — Linux Kernel Internals — malloc 在内核侧的完整旅程:per-CPU page list、zone free list、伙伴系统
  3. Linux Page Faults, mmap, and userfaultfd — Shayon Mukherjee — 从 VM 快照恢复的实际问题出发讲透缺页与惰性填充
  4. 深入理解 Linux 内存管理核心机制:从分页、页表到大页内存实战 — 博客园 — 中文优质长文,分页/页表/缺页中断到 HugePages
  5. linux mmap 底层实现 — CSDN — mmap 系统调用六步拆解:建 vma → 首次访问缺页 → 分配物理页建映射
  6. Why malloc Isn’t ‘Using Up’ Memory on Linux — LinuxVox — "malloc 为什么不占内存"的通俗解释
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:06:35

Node.js卸载残留全解析:Windows/macOS/Linux彻底清理指南

我在工作中经常遇到同事或网友跟我抱怨&#xff1a;Node.js 装了又卸、卸了又装&#xff0c;折腾了半天&#xff0c;命令行里输入node -v还是能蹦出版本号&#xff1b;或者更诡异的是&#xff0c;明明从控制面板删掉了&#xff0c;再装新版本时却提示“已存在”或者各种权限冲突…

作者头像 李华
网站建设 2026/9/7 19:05:22

Word侧边页码设置全攻略:文本框+域代码实现竖排页码

做排版的人大概率都遇到过这个需求&#xff1a;正文已经排得差不多了&#xff0c;客户或主编突然提了一句“页码不要放下面&#xff0c;放到页面侧边&#xff0c;而且要竖排”。第一次听到这个需求的时候&#xff0c;我也愣了几秒&#xff0c;因为常规的页脚页码谁都会做&#…

作者头像 李华
网站建设 2026/9/7 19:04:59

中国高分辨率土壤信息网格数据实操指南:1km栅格与16项属性解析

拿到这份“中国高分辨率国家土壤信息网格基本属性数据集&#xff08;2010–2018年&#xff09;”的时候&#xff0c;我的第一反应是&#xff1a;终于有一套能直接用、不用自己吭哧吭哧去翻土壤普查报告的数字土壤底图了。1km栅格、16项精细属性、TIFF格式&#xff0c;这几个关键…

作者头像 李华
网站建设 2026/9/7 19:04:09

数据结构队列:从排队打饭到消息中间件的核心逻辑

数据结构&#xff1a;队列&#xff0c;从排队打饭到消息中间件的核心逻辑队列这东西&#xff0c;说简单是真简单&#xff0c;一句话就能讲完&#xff1a;先进先出。但你要是只把它当成一个“排队”概念&#xff0c;那就亏大了。我这些年看过的代码里&#xff0c;凡是涉及到系统…

作者头像 李华