1.什么是缺页异常
在Linux虚拟内存管理中,缺页异常(Page Fault) 是CPU在访问虚拟地址时发现对应物理页未就绪时触发的中断。根据触发原因,缺页异常分为两类:
次要缺页(Minor Fault):物理页已存在(如缓存或共享内存),只需建立映射。
主要缺页(Major Fault):需要分配物理页或从磁盘加载数据(如匿名页首次访问或文件页未缓存)。
匿名页面(Anonymous Page) 是指不与任何文件关联的内存页(如进程堆、栈或mmap(MAP_ANONYMOUS)分配的内存),其生命周期与进程绑定,可能被交换到磁盘(Swap)。
当进程访问一个虚拟内存地址时,若该地址对应的物理页尚未分配、权限不足或已被换出到磁盘,CPU 会触发缺页异常。此时,内核接管异常处理流程,完成内存分配、权限修复或数据加载后,进程才能继续执行。
2.缺页异常的内核处理流程
Linux 内核的缺页异常处理入口为 do_page_fault(x86 架构),其核心逻辑如下:
1. 硬件触发异常:CPU 将触发异常的虚拟地址存入 CR2 寄存器。进入内核态,保存现场并调用缺页处理函数。
2. 异常原因检查
内核通过以下步骤判断异常原因:
// 伪代码逻辑
if (地址超出进程虚拟空间范围) {
触发 SIGSEGV 信号(段错误);
} else if (访问权限不足) {
检查是否可修复(如 COW);
} else {
进入物理页分配流程;
}
3. 物理页分配与映射
内核调用 handle_mm_fault,根据虚拟内存区域(VMA)类型处理:
匿名页(Anonymous Page):分配物理页并填充零(Zero Page)。
文件映射页(File-backed Page):从文件系统读取数据到物理页。
Swap 页:从 Swap 分区加载数据到物理页。
4. 关键数据结构
VMA(vm_area_struct):描述进程虚拟内存区域(如堆、栈、文件映射)。页表项(PTE):存储虚拟地址到物理地址的映射关系及权限标志。
反向映射(Reverse Mapping):加速物理页的回收和 Swap 操作。
2.缺页异常的核心函数分析
1.函数do_page_fault
1.函数定义(以 x86 架构为例)
// arch/x86/mm/fault.c
void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code) {
unsigned long address = read_cr2(); // 获取触发异常的虚拟地址
struct vm_area_struct *vma;
struct task_struct *tsk;
struct mm_struct *mm;
int fault;
tsk = current;
mm = tsk->mm;
// 检查异常是否发生在内核态(如内核模块访问非法地址)
if (unlikely(fault_in_kernel_space(address))) {
if (vmalloc_fault(address) >= 0) // 处理 vmalloc 异常
return;
// 触发内核 oops 或 panic
bad_area_nosemaphore(regs, error_code, address);
return;
}
// 检查进程是否处于中断上下文或未分配内存
if (unlikely(!mm || pagefault_disabled())) {
bad_area_nosemaphore(regs, error_code, address);
return;
}
// 查找虚拟地址对应的 VMA(虚拟内存区域)
vma = find_vma(mm, address);
if (unlikely(!vma)) {
bad_area(regs, error_code, address);
return;
}
// 检查地址是否在 VMA 的合法范围内
if (likely(vma->vm_start <= address && address < vma->vm_end)) {
// 调用 handle_mm_fault 处理具体缺页逻辑
fault = handle_mm_fault(vma, address, flags);
if (unlikely(fault & VM_FAULT_ERROR)) {
// 处理错误(如权限不足)
__bad_area(regs, error_code, address, vma, fault);
return;
}
// 成功处理缺页,返回用户态
return;
}
// 地址不在 VMA 范围内,触发段错误(SIGSEGV)
bad_area(regs, error_code, address);
}
2.do_page_fault 核心流程
1. 硬件触发异常
CPU 将触发异常的虚拟地址存入 CR2 寄存器(x86 特性)。
保存寄存器状态到 pt_regs 结构体,进入内核态。
2. 异常地址合法性检查
内核空间地址:检查是否由 vmalloc 区域访问引发,尝试修复映射。
用户空间地址:检查进程的 mm_struct 是否存在,确认地址是否合法。
3. 查找虚拟内存区域(VMA)
通过 find_vma 函数在进程的 VMA 红黑树中查找包含 address 的 VMA。
VMA 描述了进程虚拟地址空间的属性(如权限、文件映射、堆栈等)。
4. 权限与类型检查
检查访问权限(读/写/执行)是否与 VMA 的 vm_flags 匹配。
根据错误码 error_code 判断异常原因:
写操作触发:error_code & PF_WRITE
用户态触发:error_code & PF_USER
5. 调用 handle_mm_fault
fault = handle_mm_fault(vma, address, flags);
handle_mm_fault 进一步调用 handle_pte_fault,根据页表项(PTE)状态处理:
PTE 不存在:分配物理页(匿名页或文件映射页)。
PTE 存在但权限不足:处理写时复制(COW)或权限升级。
6. 错误处理
若 handle_mm_fault 返回错误(如 VM_FAULT_OOM),向进程发送 SIGSEGV 信号。
2.函数handle_mm_fault
// mm/memory.c
int handle_mm_fault(struct vm_area_struct *vma, unsigned long address, unsigned int flags) {
pgd_t *pgd; // 页全局目录项
p4d_t *p4d; // 四级页目录项(x86 五级分页时为 p4d,否则映射到 pgd)
pud_t *pud; // 页上级目录项
pmd_t *pmd; // 页中间目录项
pte_t *pte; // 页表项
// 逐级查找页表项
pgd = pgd_offset(vma->vm_mm, address);
p4d = p4d_alloc(mm, pgd, address);
pud = pud_alloc(mm, p4d, address);
pmd = pmd_alloc(mm, pud, address);
pte = pte_offset_map(pmd, address);
// 处理 PTE 状态
return handle_pte_fault(vma, address, pte, pmd, flags);
}
3.函数handle_pte_fault
// mm/memory.c
static int handle_pte_fault(struct vm_fault *vmf) {
pte_t entry;
if (!vmf->pte) { // PTE 不存在
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf); // 分配匿名页
else
return do_fault(vmf); // 文件映射页
}
entry = *vmf->pte;
if (!pte_present(entry)) { // PTE 存在但页不在内存
if (pte_none(entry))
return do_swap_page(vmf); // 从 Swap 分区加载页
}
if (pte_protnone(entry)) // 权限不足(如 COW)
return do_numa_page(vmf);
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry)) // 写操作触发 COW
return do_wp_page(vmf);
}
// 其他错误处理
return VM_FAULT_SIGBUS;
}
3.缺页异常的分类
1.缺页异常的基本分类
Linux内核将缺页异常分为两大类:Major Fault**(主要缺页)和Minor Fault(次要缺页)。
两者的核心区别在于是否涉及磁盘I/O操*:
| 类型