ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux 6.6内核内存管理深度解析(三):页表与缺页处理 — 虚拟地址怎么按需映射到物理页

Linux 6.6内核内存管理深度解析(三):页表与缺页处理 — 虚拟地址怎么按需映射到物理页

〇、全景:页表 + 缺页 = 按需分配

进程看到的是一整片连续的虚拟地址空间,但实际内存是"按需分配"的——大部分地址只是"承诺"了,访问到的那一瞬才真正映射物理页。实现这个机制靠两样东西:

  • 多级页表:虚拟地址 → 物理地址的映射,硬件(MMU)逐级翻译;
  • 缺页(page fault):访问一个"还没映射"或"权限不够"的地址,MMU 翻译失败、触发缺页异常(#PF),内核补上映射、再重跑那条指令。

命中

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:

// arch/x86/include/asm/pgtable_64_types.h (v6.6, line 62 / 84 / 91)#defineP4D_SHIFT39#definePUD_SHIFT30#definePMD_SHIFT21// 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):

// include/linux/mm_types.h (v6.6, line 565)structvm_area_struct{// VMA covers [vm_start; vm_end) addresses within mmunsignedlongvm_start;// 起始地址unsignedlongvm_end;// 结束地址(开区间)// …constvm_flags_tvm_flags;// 权限/性质:可读可写、匿名/文件…// …unsignedlongvm_pgoff;// 在 vm_file 里的页偏移structfile*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 进到:

// 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的核心逻辑:

  1. 查 vma:lock_mm_and_find_vma(fault.c:1385)找到覆盖这个地址的 vma;地址不在任何 vma 里 → 非法访问 → 发SIGSEGV;
  2. 权限检查:访问类型(读/写/执行)和vm_flags不符 →SIGSEGV(保护违例);
  3. 调handle_mm_fault:合法的缺页,交给 mm 子系统补映射;
  4. 补好了 → 返回用户态重跑那条指令;补不了 → 发信号终止进程(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 处于什么状态,决定怎么办:

// mm/memory.c (v6.6, line 4947)staticvm_fault_thandle_pte_fault(structvm_fault*vmf){// … 定位/建立 PTE …if(!vmf->pte)returndo_pte_missing(vmf);// ① PTE 不存在 → 真正的"缺页"if(!pte_present(vmf->orig_pte))returndo_swap_page(vmf);// ② 页被换出到 swap → 换回来if(pte_protnone(vmf->orig_pte)&&vma_is_accessible(vmf->vma))returndo_numa_page(vmf);// ③ NUMA 迁移标记 → 迁回本节点// ④ PTE 存在:写访问 + 只读 → 写时复制(COW)if(vmf->flags&(FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)){if(!pte_write(entry))returndo_wp_page(vmf);// …}// ⑤ 其他:只是刷新 accessed/dirty 位,直接返回}

四种典型分支:

PTE 状态走哪个 handler含义
pte_none(不存在)do_pte_missing真正的"第一次访问",按需分配/读入
!pte_presentdo_swap_page页被 swap 出去了,从 swap 换回
pte_protnonedo_numa_pageNUMA 自动平衡,按需把页迁到访问它的节点
present 但!pte_write且写访问do_wp_page写时复制(COW)

其中 ①(do_pte_missing)是最常见的"缺页"本义,也是"按需分配"的核心——往下看。


五、匿名页 vs 文件页:do_pte_missing的分水岭

do_pte_missing(memory.c:3667)的判断极简,就一个条件——这个 vma 是不是匿名的:

// mm/memory.c (v6.6, line 3667)staticvm_fault_tdo_pte_missing(structvm_fault*vmf){if(vma_is_anonymous(vmf->vma))returndo_anonymous_page(vmf);// 匿名:分配零页elsereturndo_fault(vmf);// 文件:从文件读}

5.1 匿名页:do_anonymous_page分配零页

堆、栈、mmap(MAP_ANONYMOUS)都是匿名映射(vm_file == NULL)。第一次访问时分配一个清零的物理页(memory.c:4067):

// mm/memory.c (v6.6, line 4067)staticvm_fault_tdo_anonymous_page(structvm_fault*vmf){// 读访问:直接用全局的"零页"(所有进程共享),不用真分配if(!(vmf->flags&FAULT_FLAG_WRITE)&&!mm_forbids_zeropage(vma->vm_mm)){entry=pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),…));// …gotosetpte;}// 写访问:真正分配一个清零页folio=vma_alloc_zeroed_movable_folio(vma,vmf->address);// …}

两个细节:读匿名页直接用全局共享的零页(省一次真分配);写才真正从 buddy(上一篇 #2)分配一个清零页。这正是"按需分配"最直观的体现。

5.2 文件页:do_fault从文件读

mmap(文件)的 vma 有vm_file。do_fault(memory.c:4678)按"是否共享 + 是否写"再分三种:

// 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.1do_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_faultarch/x86/mm/fault.c:1517/:1493/:1239
PGDIR_SHIFT/P4D_SHIFT/PUD_SHIFT/PMD_SHIFTarch/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_faultmm/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_faultmm/memory.c:4678/:4550/:4583/:4627
__do_fault(→vm_ops->fault())mm/memory.c:4181
do_wp_page(写时复制)mm/memory.c:3338
do_numa_pagemm/memory.c:4739
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进