2. Linux 进程地址空间布局:用户空间与内核空间划分、虚拟内存区域(VMA)结构、页表层级与页表项(PTE)

在深入写时复制(COW)优化之前,必须透彻理解进程地址空间的底层布局。本章将拆解三个核心层次:用户/内核空间划分、虚拟内存区域(VMA)管理、以及页表层级与页表项(PTE)的硬件级实现。这些概念是 fork 时 COW 机制能够高效运作的基石。

2.1 用户空间与内核空间划分

Linux 采用虚拟内存管理,每个进程拥有独立的 4GB(32位)或 128TB(64位)虚拟地址空间。地址空间被明确划分为用户空间和内核空间,其分界线由 PAGE_OFFSET 宏定义。

架构 用户空间范围 内核空间范围 分界线
x86_32 (3:1) 0x00000000 – 0xBFFFFFFF 0xC0000000 – 0xFFFFFFFF 0xC0000000
x86_32 (2:2) 0x00000000 – 0x7FFFFFFF 0x80000000 – 0xFFFFFFFF 0x80000000
x86_64 0x0000000000000000 – 0x00007FFFFFFFFFFF 0xFFFF800000000000 – 0xFFFFFFFFFFFFFFFF 0xFFFF800000000000
关键区别:用户空间进程不可直接访问内核空间地址,但内核可以通过 copy_to_user/copy_from_user 等安全函数访问用户空间。fork 时 COW 页面的保护属性切换,正是利用这种权限隔离来实现的。
Linux 进程地址空间布局(x86_64) 用户空间 (User Space) 0x0000000000000000 – 0x00007FFFFFFFFFFF 代码段 | 数据段 | 堆 | 栈 | 内存映射文件 | 共享库 每个进程独立,通过 CR3 切换页表 PAGE_OFFSET = 0xFFFF800000000000 内核空间 (Kernel Space) 0xFFFF800000000000 – 0xFFFFFFFFFFFFFFFF 内核代码 | 内核数据 | 模块 | 直接映射区 (ZONE_NORMAL) 所有进程共享同一内核页表,高权限执行

2.2 虚拟内存区域(VMA)结构

进程地址空间并非连续整块,而是由多个 虚拟内存区域(VMA) 组成的链表/红黑树。每个 VMA 描述一段连续的虚拟地址区间及其属性。内核使用 struct vm_area_struct 管理这些区域。

2.2.1 VMA 核心字段

字段 类型 说明
vm_start unsigned long VMA 起始虚拟地址
vm_end unsigned long VMA 结束虚拟地址
vm_flags unsigned long 访问权限标志 (读/写/执行/共享/私有)
vm_file struct file * 映射的文件对象 (文件映射时非 NULL)
vm_ops const struct vm_operations_struct * VMA 操作函数表 (open/close/fault)
vm_next struct vm_area_struct * 链表下一个 VMA
vm_rb struct rb_node 红黑树节点 (用于快速查找)
COW 关联:VMA 的 vm_flags 中包含 VM_SHARED 和 VM_MAYWRITE 标志。fork 后,父子进程的私有匿名 VMA 会被标记为只读,触发写时复制。文件映射 VMA 若为私有映射,同样适用 COW。

2.2.2 VMA 类型分类

  • 匿名 VMA:无文件后端,用于堆、栈、mmap(MAP_ANONYMOUS)。COW 时分配新物理页。
  • 文件映射 VMA:有 vm_file,用于可执行文件、共享库、文件 mmap。COW 时复制物理页,但文件页缓存可被复用。
  • 共享映射 VMA:VM_SHARED 标志置位,fork 时不 COW,父子进程共享同一物理页。
  • 栈 VMA:特殊匿名 VMA,通常带有 VM_GROWSDOWN 标志,自动向下扩展。
进程 VMA 链表与红黑树结构 VMA 1: 代码段 0x400000-0x401000 r-xp (私有) 文件: /bin/app VMA 2: 数据段 0x601000-0x602000 rw-p (私有) 文件: /bin/app VMA 3: 堆 0x602000-0x603000 rw-p (私有) 匿名 VMA 4: 栈 0x7fff0000-... rw-p (私有) 匿名, GROWSDOWN 红黑树组织 (快速查找) 根节点 VMA 2 左子 VMA 1 右子 VMA 3 右子 VMA 4

2.3 页表层级与页表项(PTE)

页表是 CPU 将虚拟地址转换为物理地址的硬件数据结构。x86_64 架构使用 4 级页表(PGD → PUD → PMD → PTE),每级页表包含 512 个表项。ARM64 支持 3 级或 4 级可配置。

2.3.1 四级页表地址转换流程

  1. PGD (Page Global Directory):由 CR3 寄存器指向,索引虚拟地址的 bit 47:39。
  2. PUD (Page Upper Directory):由 PGD 表项指向,索引 bit 38:30。
  3. PMD (Page Middle Directory):由 PUD 表项指向,索引 bit 29:21。
  4. PTE (Page Table Entry):由 PMD 表项指向,索引 bit 20:12,最终得到物理页帧号(PFN)。
  5. 页内偏移:bit 11:0 直接作为物理页内偏移。
x86_64 四级页表地址转换 虚拟地址 (48位) [47:39] PGD 索引 | [38:30] PUD 索引 | [29:21] PMD 索引 | [20:12] PTE 索引 | [11:0] 页内偏移 PGD CR3 → 表项 PUD PGD项 → 表项 PMD PUD项 → 表项 PTE PMD项 → PFN 物理页帧 (4KB) PFN + 页内偏移 PTE 结构 (64位) [63:52] 保留 | [51:12] PFN (物理页帧号) | [11:9] 忽略 | [8] G | [7] PS | [6] D | [5] A | [4] PCD | [3] PWT | [2] U/S | [1] R/W | [0] P

2.3.2 PTE 关键标志位

位 标志 说明 COW 关联
0 P (Present) 页面是否在物理内存中 COW 缺页时需置位
1 R/W (Read/Write) 0=只读, 1=可读写 COW 核心:fork 后清 0,写时触发缺页
2 U/S (User/Supervisor) 0=内核态, 1=用户态 用户空间页必须置 1
5 A (Accessed) 页面是否被访问过 页面回收算法使用
6 D (Dirty) 页面是否被写过 COW 复制后需清除
COW 触发条件:当 PTE 的 R/W 位为 0 且 U/S 位为 1 时,用户态写操作会触发 page fault。内核缺页处理程序检查 VMA 的 vm_flags 是否包含 VM_WRITE,若 VMA 可写但 PTE 只读,则判定为 COW 场景,执行物理页复制并更新 PTE。

2.3.3 页表层级与 COW 性能

  • 大页(Huge Pages):使用 PMD 级映射(2MB)或 PUD 级映射(1GB),减少页表层级和 TLB 缺失。fork 时 COW 复制大页代价更高,但 TLB 覆盖范围更大。
  • 透明大页(THP):内核自动将匿名页合并为 2MB 大页。fork 后 COW 会拆解大页为 4KB 基础页,增加缺页次数。
  • 页表锁:多核 CPU 并发访问页表时,使用 mmap_lock 和 PTE lock 保护。fork 时需持有写锁,影响并发性能。
性能调校要点:
  • 监控 /proc/meminfo 中的 AnonHugePages 和 PageTables 指标。
  • 对于频繁 fork 的服务器,考虑禁用 THP:echo never > /sys/kernel/mm/transparent_hugepage/enabled。
  • 使用 perf stat -e page-faults 测量 COW 触发的缺页次数。

2.4 本章小结

进程地址空间的用户/内核划分提供了权限隔离基础;VMA 结构以链表+红黑树形式管理地址区间,其 vm_flags 决定了 COW 策略;四级页表通过 PTE 的 R/W 位和 Present 位控制访问权限,是 COW 缺页异常的硬件触发点。理解这三层结构,是后续分析 fork 时 VMA 复制、PTE 权限修改、以及缺页处理流程的前提。