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 页面的保护属性切换,正是利用这种权限隔离来实现的。
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标志,自动向下扩展。
2.3 页表层级与页表项(PTE)
页表是 CPU 将虚拟地址转换为物理地址的硬件数据结构。x86_64 架构使用 4 级页表(PGD → PUD → PMD → PTE),每级页表包含 512 个表项。ARM64 支持 3 级或 4 级可配置。
2.3.1 四级页表地址转换流程
- PGD (Page Global Directory):由 CR3 寄存器指向,索引虚拟地址的 bit 47:39。
- PUD (Page Upper Directory):由 PGD 表项指向,索引 bit 38:30。
- PMD (Page Middle Directory):由 PUD 表项指向,索引 bit 29:21。
- PTE (Page Table Entry):由 PMD 表项指向,索引 bit 20:12,最终得到物理页帧号(PFN)。
- 页内偏移:bit 11:0 直接作为物理页内偏移。
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 权限修改、以及缺页处理流程的前提。