〇、全景:页表 + 缺页 = 按需分配
进程看到的是一整片连续的虚拟地址空间,但实际内存是"按需分配"的------大部分地址只是"承诺"了,访问到的那一瞬才真正映射物理页。实现这个机制靠两样东西:
- 多级页表:虚拟地址 → 物理地址的映射,硬件(MMU)逐级翻译;
- 缺页(page fault):访问一个"还没映射"或"权限不够"的地址,MMU 翻译失败、触发缺页异常(#PF),内核补上映射、再重跑那条指令。
#mermaid-svg-oxEU7ijxdMtKIQqI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oxEU7ijxdMtKIQqI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oxEU7ijxdMtKIQqI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oxEU7ijxdMtKIQqI .error-icon{fill:#552222;}#mermaid-svg-oxEU7ijxdMtKIQqI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oxEU7ijxdMtKIQqI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oxEU7ijxdMtKIQqI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oxEU7ijxdMtKIQqI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oxEU7ijxdMtKIQqI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oxEU7ijxdMtKIQqI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oxEU7ijxdMtKIQqI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oxEU7ijxdMtKIQqI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oxEU7ijxdMtKIQqI .marker.cross{stroke:#333333;}#mermaid-svg-oxEU7ijxdMtKIQqI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oxEU7ijxdMtKIQqI p{margin:0;}#mermaid-svg-oxEU7ijxdMtKIQqI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oxEU7ijxdMtKIQqI .cluster-label text{fill:#333;}#mermaid-svg-oxEU7ijxdMtKIQqI .cluster-label span{color:#333;}#mermaid-svg-oxEU7ijxdMtKIQqI .cluster-label span p{background-color:transparent;}#mermaid-svg-oxEU7ijxdMtKIQqI .label text,#mermaid-svg-oxEU7ijxdMtKIQqI span{fill:#333;color:#333;}#mermaid-svg-oxEU7ijxdMtKIQqI .node rect,#mermaid-svg-oxEU7ijxdMtKIQqI .node circle,#mermaid-svg-oxEU7ijxdMtKIQqI .node ellipse,#mermaid-svg-oxEU7ijxdMtKIQqI .node polygon,#mermaid-svg-oxEU7ijxdMtKIQqI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oxEU7ijxdMtKIQqI .rough-node .label text,#mermaid-svg-oxEU7ijxdMtKIQqI .node .label text,#mermaid-svg-oxEU7ijxdMtKIQqI .image-shape .label,#mermaid-svg-oxEU7ijxdMtKIQqI .icon-shape .label{text-anchor:middle;}#mermaid-svg-oxEU7ijxdMtKIQqI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oxEU7ijxdMtKIQqI .rough-node .label,#mermaid-svg-oxEU7ijxdMtKIQqI .node .label,#mermaid-svg-oxEU7ijxdMtKIQqI .image-shape .label,#mermaid-svg-oxEU7ijxdMtKIQqI .icon-shape .label{text-align:center;}#mermaid-svg-oxEU7ijxdMtKIQqI .node.clickable{cursor:pointer;}#mermaid-svg-oxEU7ijxdMtKIQqI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oxEU7ijxdMtKIQqI .arrowheadPath{fill:#333333;}#mermaid-svg-oxEU7ijxdMtKIQqI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oxEU7ijxdMtKIQqI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oxEU7ijxdMtKIQqI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oxEU7ijxdMtKIQqI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oxEU7ijxdMtKIQqI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oxEU7ijxdMtKIQqI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oxEU7ijxdMtKIQqI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oxEU7ijxdMtKIQqI .cluster text{fill:#333;}#mermaid-svg-oxEU7ijxdMtKIQqI .cluster span{color:#333;}#mermaid-svg-oxEU7ijxdMtKIQqI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oxEU7ijxdMtKIQqI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oxEU7ijxdMtKIQqI rect.text{fill:none;stroke-width:0;}#mermaid-svg-oxEU7ijxdMtKIQqI .icon-shape,#mermaid-svg-oxEU7ijxdMtKIQqI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oxEU7ijxdMtKIQqI .icon-shape p,#mermaid-svg-oxEU7ijxdMtKIQqI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oxEU7ijxdMtKIQqI .icon-shape .label rect,#mermaid-svg-oxEU7ijxdMtKIQqI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oxEU7ijxdMtKIQqI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oxEU7ijxdMtKIQqI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oxEU7ijxdMtKIQqI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中
miss
CPU 访问虚拟地址
MMU 逐级翻译页表
页表项不存在
或无权限 → #PF
do_page_fault
arch/x86/mm/fault.c
handle_mm_fault
mm/memory.c
按 pte 状态分流
handle_pte_fault
拿到物理页
直接访问
匿名页
do_anonymous_page 分配零页
文件页
do_fault 从文件读
写保护
do_wp_page 写时复制
一句话主线:虚拟地址靠多级页表(PGD/P4D/PUD/PMD/PTE)映射到物理页;访问一个"页表项不存在/权限不符"的地址时,MMU 触发缺页异常(#PF),内核
do_page_fault→handle_mm_fault→handle_pte_fault,根据 vma 判断是匿名页(do_anonymous_page分配零页)还是文件页(do_fault从文件读),补好页表项后重新执行------这就是"按需分配":进程地址空间大部分是"承诺"了但没实际映射的,访问到才真正分配。
一、5 级页表:虚拟地址切成几段逐级翻译
1.1 层级结构
x86_64 支持 4 级(LA48,48 位虚拟地址) 和 5 级(LA57,57 位虚拟地址) 页表。5 级时,虚拟地址切成 6 段,逐级索引:
虚拟地址(5 级 / LA57):
| PGD | P4D | PUD | PMD | PTE | offset |
| 9b | 9b | 9b | 9b | 9b | 12b |
每一级都是 9 位(= 512 项,正好一页),shift 值定义在 pgtable_64_types.h:
c
// arch/x86/include/asm/pgtable_64_types.h (v6.6, line 62 / 84 / 91)
#define P4D_SHIFT 39
#define PUD_SHIFT 30
#define PMD_SHIFT 21
// PAGE_SHIFT = 12,offset 占低 12 位
- 5 级:
PGDIR_SHIFT = pgdir_shift(= 48,:56)→ 虚拟地址 6 段; - 4 级:
PGDIR_SHIFT = 39(:75),P4D 折叠 成 1 项(MAX_PTRS_PER_P4D = 1,:77),等效 PGD/PUD/PMD/PTE 四级。
翻译过程就是"用每一段当前层的索引查表、拿到下一层表的基址",最后 PTE 指向真正的物理页。顶层表指针存在每个进程的 mm_struct 里(mm_struct.pgd)。
注意这是 MMU 页表(管 CPU 访存:进程虚拟地址 → 物理地址),和 IOMMU 页表(管设备 DMA:设备地址 → 物理地址)是两套独立的页表,别混(见 IOMMU 系列)。
1.2 页表项的"状态"决定缺页行为
每个 PTE 不只是"物理页号",还带一堆状态位(present、write、dirty、accessed...)。这些状态正是 §四 缺页分流判断的依据------比如 present=0 说明"这一页还没映射(或缺页到 swap)",write=0 说明"只读,写它就触发写时复制"。
二、vma:进程地址空间的"登记簿"
页表是"物理映射"的视角,但内核还需要"逻辑归属"的视角------这段地址是代码、堆、栈,还是 mmap 的文件?这就是 vma(virtual memory area):
c
// include/linux/mm_types.h (v6.6, line 565)
struct vm_area_struct {
// VMA covers [vm_start; vm_end) addresses within mm
unsigned long vm_start; // 起始地址
unsigned long vm_end; // 结束地址(开区间)
// ...
const vm_flags_t vm_flags; // 权限/性质:可读可写、匿名/文件...
// ...
unsigned long vm_pgoff; // 在 vm_file 里的页偏移
struct file *vm_file; // 映射的文件(匿名映射则为 NULL)
// ...
};
一个进程的地址空间(mm_struct,mm_types.h:673)由一串 vma 串起来(mm->mmap 链表 / 红黑树)。关键:vm_file 是否为空,决定了这段地址是"匿名"还是"文件映射" ------这正是缺页时走 do_anonymous_page 还是 do_fault 的分水岭(见 §五)。
三、缺页入口:硬件 #PF → do_page_fault
访问非法地址时,CPU 触发 #PF 异常,经 IDT 进到:
c
// arch/x86/mm/fault.c (v6.6, line 1517)
DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)
{
// ...
handle_page_fault(regs, error_code, address); // fault.c:1493
}
handle_page_fault 按 faulting 地址在内核空间还是用户空间分流(fault_in_kernel_space,fault.c:1502),用户地址走 do_user_addr_fault(fault.c:1239)。do_user_addr_fault 的核心逻辑:
- 查 vma :
lock_mm_and_find_vma(fault.c:1385)找到覆盖这个地址的 vma;地址不在任何 vma 里 → 非法访问 → 发SIGSEGV; - 权限检查 :访问类型(读/写/执行)和
vm_flags不符 →SIGSEGV(保护违例); - 调
handle_mm_fault:合法的缺页,交给 mm 子系统补映射; - 补好了 → 返回用户态重跑那条指令;补不了 → 发信号终止进程(
SIGSEGV/SIGBUS,OOM 则触发 OOM killer)。
handle_mm_fault(memory.c:5255)→ __handle_mm_fault(memory.c:5031)逐级往下走页表(PGD→P4D→PUD→PMD,中间的中间表项不存在就现建),直到 PTE 层 ,交给 handle_pte_fault 做最终分流。
四、handle_pte_fault:按 PTE 状态分流
handle_pte_fault(memory.c:4947)是缺页处理的核心分岔口------看 PTE 处于什么状态,决定怎么办:
c
// mm/memory.c (v6.6, line 4947)
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
// ... 定位/建立 PTE ...
if (!vmf->pte)
return do_pte_missing(vmf); // ① PTE 不存在 → 真正的"缺页"
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf); // ② 页被换出到 swap → 换回来
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
return do_numa_page(vmf); // ③ NUMA 迁移标记 → 迁回本节点
// ④ PTE 存在:写访问 + 只读 → 写时复制(COW)
if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
if (!pte_write(entry))
return do_wp_page(vmf);
// ...
}
// ⑤ 其他:只是刷新 accessed/dirty 位,直接返回
}
四种典型分支:
| PTE 状态 | 走哪个 handler | 含义 |
|---|---|---|
pte_none(不存在) |
do_pte_missing |
真正的"第一次访问",按需分配/读入 |
!pte_present |
do_swap_page |
页被 swap 出去了,从 swap 换回 |
pte_protnone |
do_numa_page |
NUMA 自动平衡,按需把页迁到访问它的节点 |
present 但 !pte_write 且写访问 |
do_wp_page |
写时复制(COW) |
其中 ①(do_pte_missing)是最常见的"缺页"本义,也是"按需分配"的核心------往下看。
五、匿名页 vs 文件页:do_pte_missing 的分水岭
do_pte_missing(memory.c:3667)的判断极简,就一个条件------这个 vma 是不是匿名的:
c
// mm/memory.c (v6.6, line 3667)
static vm_fault_t do_pte_missing(struct vm_fault *vmf)
{
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf); // 匿名:分配零页
else
return do_fault(vmf); // 文件:从文件读
}
5.1 匿名页:do_anonymous_page 分配零页
堆、栈、mmap(MAP_ANONYMOUS) 都是匿名映射(vm_file == NULL)。第一次访问时分配一个清零的物理页 (memory.c:4067):
c
// mm/memory.c (v6.6, line 4067)
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
// 读访问:直接用全局的"零页"(所有进程共享),不用真分配
if (!(vmf->flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address), ...));
// ...
goto setpte;
}
// 写访问:真正分配一个清零页
folio = vma_alloc_zeroed_movable_folio(vma, vmf->address);
// ...
}
两个细节:读 匿名页直接用全局共享的零页 (省一次真分配);写才真正从 buddy(上一篇 #2)分配一个清零页。这正是"按需分配"最直观的体现。
5.2 文件页:do_fault 从文件读
mmap(文件) 的 vma 有 vm_file。do_fault(memory.c:4678)按"是否共享 + 是否写"再分三种:
c
// mm/memory.c (v6.6, line 4678,节选)
/* 只读 → 直接从 page cache 读 */
ret = do_read_fault(vmf); // :4550
/* 私有 + 写 → 先读进来再写时复制 */
ret = do_cow_fault(vmf); // :4583
/* 共享 + 写 → 读进来直接写(写穿回文件) */
ret = do_shared_fault(vmf); // :4627
底层都走 __do_fault(memory.c:4181)→ vma->vm_ops->fault()(具体文件系统的回调)把页从文件读进 page cache,再填 PTE。这里体现了"页缓存(page cache)"的角色:文件页缺页,本质是"page cache 里没有这一页,去磁盘读进来"。
六、为什么这样设计(Why 层)
6.1 为什么用多级页表,而不是一张大平铺表
48 位地址空间,按 4KB 页算有 2^36 个页。若用一张平铺表,每个进程要 2^36 × 8 字节 = 512GB 的表------荒谬。多级页表让"稀疏地址空间"只占实际用到的部分:只有被访问的地址路径,才会一层层分配中间页表。这是"按需"思想在页表结构上的体现。
6.2 为什么缺页才分配(按需分配),而不是 mmap 时就分配
因为进程申请的内存,绝大部分申请了根本用不到 (或很久以后才用)。如果 malloc(1GB) 立刻分配 1GB 物理页,既慢又浪费。"承诺时只登记 vma,访问时才分配物理页",把成本推迟到真正用的时候------这是虚拟内存最核心的设计。
6.3 为什么把"匿名/文件"当分水岭,而不是别的方式
因为这是两种来源根本不同 的内存:匿名页没有"来源"(凭空清零),文件页有"来源"(磁盘文件)。缺页时前者"造一个零页"、后者"从文件读进来",处理逻辑完全不同。用 vma->vm_file == NULL 这一个位就能分流,简洁且正确。
6.4 为什么读匿名页用"共享零页"而不分配
大量程序会 malloc 一大块但只读不写(或暂时只读)。这些页内容全是 0,让所有进程共享同一个物理"零页",省下海量内存;一旦有进程要写 ,再通过 COW(do_wp_page)给它单独复制一份。"读共享零页 + 写时复制"是匿名页省内存的关键组合。
七、与已有博客的交叉引用
| 已有博客 | 本篇覆盖的关系 |
|---|---|
| 内存 #2 buddy | 本篇 §5.1 do_anonymous_page 写访问时 vma_alloc_zeroed_movable_folio 就是从 buddy 分配物理页------缺页是 buddy 的"下游消费者" |
| 内存 #1 物理内存初始化 | #1 建好了 struct page(vmemmap)和 buddy,本篇的页表/缺页才能工作 |
| CPU #1 BSP 拉起 | #1 讲 %cr3 切页表(内核页表),本篇讲的是进程级页表 + 缺页 |
| IOMMU 系列 | 本篇是 MMU 页表(CPU 访存);IOMMU 页表(设备 DMA)是另一套,缺页逻辑也不同(见 \[blog-iommu-expansion]) |
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
exc_page_fault / handle_page_fault / do_user_addr_fault |
arch/x86/mm/fault.c:1517 / :1493 / :1239 |
PGDIR_SHIFT / P4D_SHIFT / PUD_SHIFT / PMD_SHIFT |
arch/x86/include/asm/pgtable_64_types.h:56 / :62 / :84 / :91 |
struct vm_area_struct(vm_start/vm_end/vm_flags/vm_file) |
include/linux/mm_types.h:565 |
struct mm_struct(进程地址空间,含 pgd) |
include/linux/mm_types.h:673 |
handle_mm_fault / __handle_mm_fault |
mm/memory.c:5255 / :5031 |
handle_pte_fault(PTE 状态分流) |
mm/memory.c:4947 |
do_pte_missing(匿名/文件分水岭) |
mm/memory.c:3667 |
do_anonymous_page(匿名页) |
mm/memory.c:4067 |
do_fault / do_read_fault / do_cow_fault / do_shared_fault |
mm/memory.c:4678 / :4550 / :4583 / :4627 |
__do_fault(→ vm_ops->fault()) |
mm/memory.c:4181 |
do_wp_page(写时复制) |
mm/memory.c:3338 |
do_numa_page |
mm/memory.c:4739 |