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

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

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

  • 多级页表:虚拟地址 → 物理地址的映射,硬件(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 的核心逻辑:

  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 处于什么状态,决定怎么办:

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
相关推荐
研发小能1 小时前
DevOps 流水线怎么选?自研、开源拼装还是企业级平台
运维·开源·devops·嘉为蓝鲸
ouynagda1 小时前
iMX6ull Linux 嵌入式驱动开发笔记(LCD / ADXL345 / SPI 子系统)
linux·arm开发·驱动开发·笔记·嵌入式硬件
小诗懂技术1 小时前
linux驱动中断,相关中断的方式与适用场景
linux·arm开发·驱动开发·算法
xiaoqiMikko1 小时前
Linux 实战(九·完):重定向、sudo、通配符的坑 + 报错速查表 —— 三台真机实测
linux·centos
迪康Defender1 小时前
终端安全行为分析:审计日志如何转化为业务报表?落地实践与避坑指南
运维·经验分享·安全·信息可视化·自动化·安全威胁分析·运维开发
H.莓飛1 小时前
【C++】模板初阶
linux·开发语言·c++·visual studio
十五年专注C++开发1 小时前
基于Asio2实现http服务器的实践总结(二)
服务器·c++·http·asio2
kmomo..1 小时前
Linux SPI 驱动开发学习笔记:从框架到实战
linux·驱动开发·学习
Lzg_na1 小时前
基于 inode 的文件系统详解
linux