第9章:缺页异常处理
基于 Linux 6.12.38 源码
9.1 缺页异常概述
9.1.1 什么是缺页异常
缺页异常 (Page Fault) 是当程序访问的虚拟页面不在物理内存中时触发的异常。
触发场景:
- 按需加载: 程序访问的代码/数据页首次被访问
- 写时复制 (COW): fork 后父子进程共享页面,一方尝试写入
- 匿名页面: malloc/mmap 分配的内存首次访问
- 文件映射: mmap 映射的文件页首次访问
- 保护违例: 尝试写入只读页面
9.1.2 x86_64 缺页异常
异常向量: #PF (异常 14)
错误码格式:
1 2 3 4 5 6 7 8 9
| ┌────┬──────────┬─────────┬──────────┐ │ 位 │ 名称 │ 值 │ 描述 │ ├────┼──────────┼─────────┼──────────┤ │ 0 │ P │ 0/1 │ 0=页不存在, 1=页存在但保护违例 │ │ 1 │ W │ 0/1 │ 0=读操作, 1=写操作 │ │ 2 │ U │ 0/1 │ 0=监督模式, 1=用户模式 │ │ 3 │ R │ 0/1 │ 0=普通访问, 1=保留位访问 │ │ 4 │ I │ 0/1 │ 0=取数据, 1=取指令 │ └────┴──────────┴─────────┴──────────┘
|
错误码宏定义:
位置: arch/x86/include/asm/traps.h
1 2 3 4 5 6 7 8
| #define X86_PF_WRITE (1UL << 0) #define X86_PF_PRESENT (1UL << 1) #define X86_PF_USER (1UL << 2) #define X86_PF_RESERVED (1UL << 3) #define X86_PF_INSTR (1UL << 4) #define X86_PF_PK (1UL << 5) #define X86_PF_SGX (1UL << 15)
|
9.2 缺页异常处理流程
9.2.1 处理流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| 用户/内核访问无效地址 │ ▼ ┌──────────────────────────────────────┐ │ CPU 触发 #PF 异常 │ │ - 错误码推入栈 │ │ - 故障地址存入 CR2 │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ do_page_fault(regs, error_code, │ │ address) │ │ │ │ 1. 检查地址是否在内核空间 │ │ 2. 检查是否在临界区 │ │ 3. 查找对应的 VMA │ │ 4. 检查访问权限 │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ handle_mm_fault(vma, address, │ │ flags, regs) │ │ │ │ 1. 检查 VMA 权限 │ │ 2. 调用特定处理函数 │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 根据页面类型处理: │ │ │ │ ┌────────────────────────────────┐ │ │ │ 文件映射: filemap_fault │ │ │ └────────────────────────────────┘ │ │ ┌────────────────────────────────┐ │ │ │ 匿名映射: do_anonymous_page │ │ │ └────────────────────────────────┘ │ │ ┌────────────────────────────────┐ │ │ │ 写时复制: do_wp_page │ │ │ └────────────────────────────────┘ │ │ ┌────────────────────────────────┐ │ │ │ 堆栈扩展: expand_stack │ │ │ └────────────────────────────────┘ │ └──────────────────────────────────────┘
|
9.2.2 入口函数
位置: arch/x86/mm/fault.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| DEFINE_IDTENTRY_RAW(exc_page_fault) { unsigned long address = read_cr2(); irqentry_state_t state;
state = irqentry_enter(regs);
handle_page_fault(regs, error_code, address);
irqentry_exit(regs, state); }
static __always_inline void handle_page_fault(struct pt_regs *regs, unsigned long error_code, unsigned long address) { if (unlikely(fault_in_kernel_space(address))) { do_kern_addr_fault(regs, error_code, address); } else { do_user_addr_fault(regs, error_code, address); } }
|
9.3 匿名页面缺页
9.3.1 do_anonymous_page
位置: mm/memory.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| vm_fault_t do_anonymous_page(struct vm_fault *vmf) { struct vm_area_struct *vma = vmf->vma; struct page *page; vm_fault_t ret = VM_FAULT_OOM; pte_t entry;
if (vma->vm_file) { return VM_FAULT_SIGBUS; }
if (vmf->flags & FAULT_FLAG_WRITE) { page = alloc_zeroed_user_highpage_movable(vma, vmf->address); if (!page) goto oom;
ret = set_pte_range(vma, vmf->address, vmf->address + PAGE_SIZE, page, 1); if (ret) goto oom; } else { entry = pte_mkspecial(pfn_pte(my_zero_pfn(0), vma->vm_page_prot)); ret = finish_fault(vmf); }
return ret;
oom: return VM_FAULT_OOM; }
|
9.3.2 匿名页面分配流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| do_anonymous_page(vmf) │ ▼ ┌──────────────────────────────────────┐ │ 1. 检查 VMA 是否为匿名映射 │ │ - 如果有 vm_file,返回错误 │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 2. 检查操作类型 │ │ - 写操作: 分配新页面 │ │ - 读操作: 使用共享零页 │ └──────────────────────────────────────┘ │ 写操作 ▼ ┌──────────────────────────────────────┐ │ 3. 分配零页 │ │ - alloc_zeroed_user_highpage │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 4. 设置页表项 │ │ - set_pte_range │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 5. 返回 VM_FAULT_NOPAGE │ └──────────────────────────────────────┘
|
9.4 文件映射缺页
9.4.1 filemap_fault
位置: mm/filemap.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| vm_fault_t filemap_fault(struct vm_fault *vmf) { int error; struct file *file = vmf->vma->vm_file; struct file_lock_context *file_lock_context = file->f_lock_context; struct address_space *mapping = file->f_mapping;
struct folio *folio; pgoff_t offset = vmf->pgoff;
folio = filemap_get_folio(mapping, offset); if (IS_ERR(folio)) { error = filemap_read_folio(file, mapping, ra, offset, &folio, NULL); if (error) return VM_FAULT_SIGBUS; }
if (folio_test_error(folio)) { folio_unlock(folio); folio_put(folio); return VM_FAULT_SIGBUS; }
error = vmf_insert_folio(vmf, folio);
folio_put(folio);
return error; }
|
9.4.2 文件映射缺页流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| filemap_fault(vmf) │ ▼ ┌──────────────────────────────────────┐ │ 1. 检查页缓存 │ │ - filemap_get_folio │ └──────────────────────────────────────┘ │ 不在缓存 ▼ ┌──────────────────────────────────────┐ │ 2. 从文件读取页面 │ │ - filemap_read_folio │ │ - 启动 IO 操作 │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 3. 等待 IO 完成 │ │ - folio_wait_locked │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 4. 映射到用户空间 │ │ - vmf_insert_folio │ └──────────────────────────────────────┘
|
9.5 写时复制
9.5.1 do_wp_page
位置: mm/memory.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| vm_fault_t do_wp_page(struct vm_fault *vmf) __releases(vmf->ptl) { struct vm_area_struct *vma = vmf->vma; struct folio *folio = vmf->page;
if (folio_mapcount(folio) == 1 && !folio_test_dirty(folio)) {
wp_page_reuse(vmf, folio); return VM_FAULT_WRITE; }
get_folio(folio);
struct folio *new_folio = alloc_folio(GFP_HIGHUSER_MOVABLE, 0); if (!new_folio) { folio_put(folio); return VM_FAULT_OOM; }
copy_user_highpage(new_folio, folio, vmf->address, vma);
__folio_mark_uptodate(new_folio); folio_set_dirty(new_folio);
pte_unmap_unlock(vmf->pte, vmf->ptl); vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address, &vmf->ptl); if (likely(pte_same(*vmf->pte, vmf->orig_pte))) { pte_t entry = mk_pte_folio(new_folio, vma->vm_page_prot); entry = pte_mkdirty(pte_mkwrite(entry)); set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); }
folio_put(folio); return VM_FAULT_WRITE; }
|
9.5.2 COW 流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| do_wp_page(vmf) │ ▼ ┌──────────────────────────────────────┐ │ 1. 检查页面映射计数 │ │ - mapcount == 1: 私有页面 │ │ - mapcount > 1: 共享页面 │ └──────────────────────────────────────┘ │ 私有页面 ▼ ┌──────────────────────────────────────┐ │ 2. 直接标记为可写 │ │ - wp_page_reuse │ └──────────────────────────────────────┘ │ 共享页面 ▼ ┌──────────────────────────────────────┐ │ 3. 分配新页面 │ │ - alloc_folio │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 4. 复制页面内容 │ │ - copy_user_highpage │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 5. 更新页表 │ │ - 设置新页面 PTE │ └──────────────────────────────────────┘
|
9.6 堆栈扩展
9.6.1 expand_stack
位置: mm/mmap.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| int expand_stack(struct vm_area_struct *vma, unsigned long address) { if (!(vma->vm_flags & VM_GROWSDOWN)) return -ENOMEM;
if (address < vma->vm_start) { unsigned long size = vma->vm_end - address; if (size > rlimit(RLIMIT_STACK)) return -ENOMEM;
struct mm_struct *mm = vma->vm_mm; struct vm_area_struct *next = find_vma(mm, vma->vm_end);
if (next && next->vm_start < vma->vm_end + size) return -ENOMEM; }
vma->vm_start = address;
return 0; }
|
9.6.2 栈扩展流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| expand_stack(vma, address) │ ▼ ┌──────────────────────────────────────┐ │ 1. 检查是否是栈 VMA │ │ - VM_GROWSDOWN 标志 │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 2. 检查地址范围 │ │ - 检查 RLIMIT_STACK │ │ - 检查与现有 VMA 重叠 │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 3. 扩展 VMA │ │ - 调整 vm_start │ └──────────────────────────────────────┘
|
9.7 缺页统计
9.7.1 vm_fault 结构 (完整版)
位置: include/linux/mm.h:547
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
struct vm_fault { const struct { struct vm_area_struct *vma; gfp_t gfp_mask; pgoff_t pgoff; unsigned long address; unsigned long real_address; };
enum fault_flag flags; pmd_t *pmd; pud_t *pud;
union { pte_t orig_pte; pmd_t orig_pmd; };
spinlock_t *ptl; pte_t *pte;
union { struct page *page; struct folio *folio; };
atomic_subword_t nr_pages; };
|
9.7.2 fault_flag 标志
位置: include/linux/mm.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| enum fault_flag { FAULT_FLAG_WRITE = 0x01, FAULT_FLAG_MKWRITE = 0x02, FAULT_FLAG_ALLOW_RETRY = 0x04, FAULT_FLAG_RETRY_NOWAIT = 0x08, FAULT_FLAG_KILLABLE = 0x10, FAULT_FLAG_TRIED = 0x20, FAULT_FLAG_USER = 0x40, FAULT_FLAG_REMOTE = 0x80, FAULT_FLAG_INSTRUCTION = 0x100, FAULT_FLAG_INTERRUPTIBLE = 0x200, FAULT_FLAG_VMA_LOCK = 0x400, FAULT_FLAG_ORIG_PTE_VALID = 0x800, };
|
9.7.3 缺页标志位说明
| 标志 |
值 |
描述 |
| FAULT_FLAG_WRITE |
0x01 |
写操作导致的 fault |
| FAULT_FLAG_MKWRITE |
0x02 |
创建写映射 (用于写时复制) |
| FAULT_FLAG_ALLOW_RETRY |
0x04 |
允许在信号处理后重试 |
| FAULT_FLAG_RETRY_NOWAIT |
0x08 |
不等待重试 (立即返回) |
| FAULT_FLAG_KILLABLE |
0x10 |
可被致命信号中断 |
| FAULT_FLAG_TRIED |
0x20 |
已经尝试过一次 |
| FAULT_FLAG_USER |
0x40 |
用户空间 fault |
| FAULT_FLAG_REMOTE |
0x80 |
远程节点 fault (NUMA) |
| FAULT_FLAG_INSTRUCTION |
0x100 |
指令获取 fault |
| FAULT_FLAG_INTERRUPTIBLE |
0x200 |
可中断等待 |
| FAULT_FLAG_VMA_LOCK |
0x400 |
使用 Per-VMA 锁 |
| FAULT_FLAG_ORIG_PTE_VALID |
0x800 |
orig_pte 字段有效 |
9.7.4 vm_fault 返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #define VM_FAULT_OOM 0x0001 #define VM_FAULT_SIGBUS 0x0002 #define VM_FAULT_MAJOR 0x0004 #define VM_FAULT_WRITE 0x0008 #define VM_FAULT_HWPOISON 0x0010 #define VM_FAULT_HWPOISON_LARGE 0x0020 #define VM_FAULT_SIGSEGV 0x0040 #define VM_FAULT_NOPAGE 0x0080 #define VM_FAULT_LOCKED 0x0100 #define VM_FAULT_RETRY 0x0200 #define VM_FAULT_FALLBACK 0x0400 #define VM_FAULT_DONE_COW 0x0800 #define VM_FAULT_NEEDDSYNC 0x1000 #define VM_FAULT_COMPLETED 0x2000 #define VM_FAULT_HINDEX_MASK 0x1f0000
#define VM_FAULT_ERROR (VM_FAULT_OOM | VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV) #define VM_FAULT_FAULTS (VM_FAULT_ERROR | VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE) #define VM_FAULT_PTE (VM_FAULT_COMPLETED | VM_FAULT_DONE_COW | VM_FAULT_FALLBACK | VM_FAULT_NOPAGE)
|
9.7.5 /proc 接口
1 2 3 4 5 6 7 8 9 10 11 12 13
| cat /proc/[pid]/stat
cat /proc/vmstat | grep -i fault
|
9.8 缺页处理决策树
9.8.1 完整决策流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| ┌─────────────────────────────────────────────────────────────────────┐ │ 缺页异常处理决策树 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ do_page_fault(address, error_code) │ │ │ │ │ ├──► 内核地址? ──YES─► do_kern_addr_fault() │ │ │ │ │ ├──► 找不到VMA? ──YES─► 扩展栈? ──YES─► expand_stack() │ │ │ │ │ │ │ NO │ │ │ ▼ │ │ │ SIGSEGV │ │ │ │ │ └──► 用户地址 + 找到VMA │ │ │ │ │ ▼ │ │ ┌──────────────────────┐ │ │ │ handle_mm_fault() │ │ │ └──────────────────────┘ │ │ │ │ │ ├──► 地址在页表中? ──NO──► 触发缺页处理 │ │ │ │ │ │ │ VMA类型? │ │ │ │ │ │ │ ┌───────────┼───────────┐ │ │ │ ▼ ▼ ▼ │ │ │ 匿名VMA 文件VMA 其他 │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ │ do_anonymous filemap_fault SIGBUS │ │ │ _page() │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ 分配页面 从文件读取 │ │ │ (alloc_pages) (read_folio) │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ 设置页表 设置页表 │ │ │ │ │ │ │ └────────┴───────────┘ │ │ │ │ │ 地址在页表中? ──YES──► 检查权限 │ │ │ │ │ │ ▼ ▼ │ │ 保护违例? ──YES─► do_wp_page() │ │ │ │ │ │ │ ▼ │ │ │ 写时复制 (COW) │ │ │ │ │ │ │ ├───► mapcount==1 ───► │ │ │ │ 直接写标记 │ │ │ │ │ │ │ └───► mapcount>1 ────► │ │ │ 分配新页 │ │ │ 复制内容 │ │ │ 更新页表 │ │ │ │ │ └─────────────────────────────────────│ │ │ │ 相关章节: │ │ ───────── │ │ • ch05 - 页表管理 (页表项检查) │ │ • ch06 - VMA管理 (find_vma) │ │ • ch09 - 缺页处理 (详细实现) │ │ • ch11 - mmap/mprotect (权限检查) │ │ • ch12 - 文件映射与页缓存 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
9.8.2 处理路径总结
| 路径 |
触发条件 |
处理函数 |
结果 |
| 匿名页面首次访问 |
PTE不存在,匿名VMA |
do_anonymous_page |
分配零页,设置PTE |
| 文件页面首次访问 |
PTE不存在,文件VMA |
filemap_fault |
从文件读取或使用缓存 |
| 写时复制 |
PTE存在但只读,写操作 |
do_wp_page |
复制页面或直接写标记 |
| 栈扩展 |
地址接近栈边界 |
expand_stack |
扩展VMA |
| 保护违例 |
权限不足 |
SIGSEGV |
进程终止 |
9.9 本章小结与跨章节关联
9.9.1 本章要点
本章介绍了 Linux 6.12 的缺页异常处理:
- 缺页概述: 按需加载、COW、匿名页面、文件映射
- 错误码: x86_64 缺页错误码格式
- 处理流程: do_page_fault → handle_mm_fault → 特定处理
- 匿名页面: do_anonymous_page,分配零页
- 文件映射: filemap_fault,从文件读取
- 写时复制: do_wp_page,复制共享页面
- 堆栈扩展: expand_stack,向下扩展栈
- 缺页统计: /proc 接口,minflt/majflt
9.9.2 与其他章节的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ┌────────────────────────────────────────────────────────────┐ │ 本章内容 (缺页异常处理) │ ├────────────────────────────────────────────────────────────┤ │ │ │ do_page_fault ──▶ ch05:页表管理 (检查页表项) │ │ (PTE是否有效/存在) │ │ │ │ do_page_fault ──▶ ch06:VMA管理 (find_vma) │ │ (确定地址所属VMA) │ │ │ │ do_anonymous_page ──▶ ch07:alloc_pages │ │ (分配物理页面) │ │ │ │ do_anonymous_page ──▶ ch05:页表管理 (set_pte_at) │ │ (建立虚拟→物理映射) │ │ │ │ filemap_fault ────▶ ch12:页缓存 │ │ (查找或读取文件页面) │ │ │ │ do_wp_page ────────▶ ch02:Folio/Mapcount │ │ (检查页面是否共享) │ │ │ │ do_wp_page ────────▶ ch07:alloc_pages (COW时分配新页) │ │ │ │ expand_stack ──────▶ ch06:VMA管理 (修改vm_start) │ │ │ │ 权限检查 ─────────▶ ch11:mprotect (VMA权限验证) │ │ │ │ 缺页统计 ─────────▶ ch15:vmstat (pgfault计数器) │ │ │ └────────────────────────────────────────────────────────────┘
|
关键处理路径:
匿名页面分配: 缺页异常 → do_anonymous_page() → alloc_pages() → set_pte_at()
- 相关章节: ch07 (页面分配), ch05 (页表管理)
文件页面映射: 缺页异常 → filemap_fault() → 页缓存查找 → IO读取
写时复制: 写操作缺页 → do_wp_page() → 检查mapcount → 复制或直接写
- 相关章节: ch02 (folio结构), ch07 (页面分配)
栈扩展: 栈访问缺页 → expand_stack() → 修改VMA
权限检查: 缺页异常 → 检查VMA权限 → mprotect验证 → 允许或拒绝
- 相关章节: ch06 (VMA权限), ch11 (mprotect)
下一章将介绍页面回收。