通过linux源码和deepseek学习arm64 linux设备对page fault的处理。本篇文章是使用脏页检测动态hook思路的基础。
先说一下检测方案(对于r-xp可执行段):
- 检测系统库时:利用smap判断是否有dirty和匿名内存;利用pagemap检测是否为file-backend,不为匿名且不为独占映射。
- 检测自己的库时,利用smap判断是否有dirty和匿名内存;利用pagemap检测是否为file-backend且不为匿名。
为进一步提高效率,可以用lseek在记录偏移后加速定位。
将以“问题 - Deepseek回答”形式展开。
/arch/arm64/mm/fault.c
Q1
struct fault_info {
int (*fn)(unsigned long far, unsigned long esr,
struct pt_regs *regs);
int sig;
int code;
const char *name;
};
static const struct fault_info fault_info[];
static const struct fault_info fault_info[] = {
{ do_bad, SIGKILL, SI_KERNEL, "ttbr address size fault" },
{ do_bad, SIGKILL, SI_KERNEL, "level 1 address size fault" },
{ do_bad, SIGKILL, SI_KERNEL, "level 2 address size fault" },
{ do_bad, SIGKILL, SI_KERNEL, "level 3 address size fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 0 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 3 translation fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 0 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 0 permission fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 permission fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 permission fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 permission fault" },
{ do_sea, SIGBUS, BUS_OBJERR, "synchronous external abort" },
{ do_tag_check_fault, SIGSEGV, SEGV_MTESERR, "synchronous tag check fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 18" },
{ do_sea, SIGKILL, SI_KERNEL, "level -1 (translation table walk)" },
{ do_sea, SIGKILL, SI_KERNEL, "level 0 (translation table walk)" },
{ do_sea, SIGKILL, SI_KERNEL, "level 1 (translation table walk)" },
{ do_sea, SIGKILL, SI_KERNEL, "level 2 (translation table walk)" },
{ do_sea, SIGKILL, SI_KERNEL, "level 3 (translation table walk)" },
{ do_sea, SIGBUS, BUS_OBJERR, "synchronous parity or ECC error" }, // Reserved when RAS is implemented
{ do_bad, SIGKILL, SI_KERNEL, "unknown 25" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 26" },
{ do_sea, SIGKILL, SI_KERNEL, "level -1 synchronous parity error (translation table walk)" }, // Reserved when RAS is implemented
{ do_sea, SIGKILL, SI_KERNEL, "level 0 synchronous parity error (translation table walk)" }, // Reserved when RAS is implemented
{ do_sea, SIGKILL, SI_KERNEL, "level 1 synchronous parity error (translation table walk)" }, // Reserved when RAS is implemented
{ do_sea, SIGKILL, SI_KERNEL, "level 2 synchronous parity error (translation table walk)" }, // Reserved when RAS is implemented
{ do_sea, SIGKILL, SI_KERNEL, "level 3 synchronous parity error (translation table walk)" }, // Reserved when RAS is implemented
{ do_bad, SIGKILL, SI_KERNEL, "unknown 32" },
{ do_alignment_fault, SIGBUS, BUS_ADRALN, "alignment fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 34" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 35" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 36" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 37" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 38" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 39" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 40" },
{ do_bad, SIGKILL, SI_KERNEL, "level -1 address size fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 42" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level -1 translation fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 44" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 45" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 46" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 47" },
{ do_bad, SIGKILL, SI_KERNEL, "TLB conflict abort" },
{ do_bad, SIGKILL, SI_KERNEL, "Unsupported atomic hardware update fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 50" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 51" },
{ do_bad, SIGKILL, SI_KERNEL, "implementation fault (lockdown abort)" },
{ do_bad, SIGBUS, BUS_OBJERR, "implementation fault (unsupported exclusive)" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 54" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 55" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 56" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 57" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 58" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 59" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 60" },
{ do_bad, SIGKILL, SI_KERNEL, "section domain fault" },
{ do_bad, SIGKILL, SI_KERNEL, "page domain fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 63" },
};
这个内核结构是什么意思?内核异常时调用的处理函数是他定义的吗?
A1
这是一个 ARM64架构的内核异常处理表,定义了各种内存访问异常的处理方式。让我详细解释:
结构体含义
struct fault_info {
int (*fn)(...); // 处理函数指针
int sig; // 要发送给进程的信号
int code; // 信号码
const char *name; // 异常类型描述
};
关键信息
这是ARM64的异常表:处理MMU(内存管理单元)产生的各种异常
表的索引对应ESR寄存器:
- ARM64发生异常时,ESR(Exception Syndrome Register)寄存器包含异常原因
- ESR的低6位(DFSC/IFSC)作为索引查找此表
- 例如:ESR=0x04 → 索引4 → “level 0 translation fault”
常见异常处理函数:
do_bad:无法处理的严重错误,直接发送SIGKILL杀死进程do_translation_fault:页面未映射(缺页异常)do_page_fault:权限错误(无访问权限)do_sea:同步外部错误(内存硬件错误)do_tag_check_fault:内存标签检查错误(MTE特性)do_alignment_fault:对齐错误
异常分类
1. 地址大小错误(索引0-3)
- TTBR或页表配置错误
- 系统级错误,直接杀死进程
2. 转换错误(索引4-7)
- 虚拟地址没有对应的物理地址
- 触发缺页处理,可能分配新页面
3. 权限错误(索引8-15)
- 有映射但无访问权限(如写只读页面)
- 可能触发写时复制(COW)
4. 硬件错误(索引16-31)
- 内存硬件故障、ECC错误等
- 多数发送SIGBUS信号
5. 其他特殊错误
- 索引17:MTE(内存标签扩展)错误
- 索引32:对齐错误(非对齐访问)
- 索引48:TLB冲突
- 索引49:原子操作硬件不支持
工作流程
当ARM64发生数据中止异常时:
// 简化流程
asmlinkage void do_mem_abort(unsigned long addr, unsigned int esr,
struct pt_regs *regs)
{
const struct fault_info *inf;
inf = fault_info + (esr & 0x3f); // 获取低6位作为索引
// 调用对应的处理函数
if (!inf->fn(addr, esr, regs)) {
// 处理成功返回
return;
}
// 处理失败,发送信号
arm64_force_sig_fault(inf->sig, inf->code, addr, inf->name);
}
信号说明
SIGSEGV:段错误(非法内存访问)
SEGV_MAPERR:地址未映射SEGV_ACCERR:权限错误SEGV_MTESERR:内存标签错误
SIGBUS:总线错误(硬件错误)
BUS_OBJERR:对象硬件错误BUS_ADRALN:地址对齐错误
SIGKILL:无法恢复的严重错误
实际例子
用户态访问NULL指针:
- 产生”level 0 translation fault”(索引4)
- 调用
do_translation_fault - 发现是非法地址,发送SIGSEGV(SEGV_MAPERR)
写只读页面:
- 产生”level 0 permission fault”(索引12)
- 调用
do_page_fault - 尝试COW或发送SIGSEGV(SEGV_ACCERR)
硬件内存故障:
- 产生”synchronous external abort”(索引16)
- 调用
do_sea,发送SIGBUS
这个表是ARM64内核异常处理的调度中心,决定了每种硬件异常如何转化为用户可见的信号或内部处理。
Q2
static int __kprobes do_page_fault(unsigned long far, unsigned long esr,
struct pt_regs *regs)
{
const struct fault_info *inf;
struct mm_struct *mm = current->mm;
vm_fault_t fault;
vm_flags_t vm_flags;
unsigned int mm_flags = FAULT_FLAG_DEFAULT;
unsigned long addr = untagged_addr(far);
struct vm_area_struct *vma;
int si_code;
int pkey = -1;
if (kprobe_page_fault(regs, esr))
return 0;
/*
* If we're in an interrupt or have no user context, we must not take
* the fault.
*/
if (faulthandler_disabled() || !mm)
goto no_context;
if (user_mode(regs))
mm_flags |= FAULT_FLAG_USER;
/*
* vm_flags tells us what bits we must have in vma->vm_flags
* for the fault to be benign, __do_page_fault() would check
* vma->vm_flags & vm_flags and returns an error if the
* intersection is empty
*/
if (is_el0_instruction_abort(esr)) {
/* It was exec fault */
vm_flags = VM_EXEC;
mm_flags |= FAULT_FLAG_INSTRUCTION;
} else if (is_gcs_fault(esr)) {
/*
* The GCS permission on a page implies both read and
* write so always handle any GCS fault as a write fault,
* we need to trigger CoW even for GCS reads.
*/
vm_flags = VM_WRITE;
mm_flags |= FAULT_FLAG_WRITE;
} else if (is_write_abort(esr)) {
/* It was write fault */
vm_flags = VM_WRITE;
mm_flags |= FAULT_FLAG_WRITE;
} else {
/* It was read fault */
vm_flags = VM_READ;
/* Write implies read */
vm_flags |= VM_WRITE;
/* If EPAN is absent then exec implies read */
if (!alternative_has_cap_unlikely(ARM64_HAS_EPAN))
vm_flags |= VM_EXEC;
}
if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
if (is_el1_instruction_abort(esr))
die_kernel_fault("execution of user memory",
addr, esr, regs);
if (!insn_may_access_user(regs->pc, esr))
die_kernel_fault("access to user memory outside uaccess routines",
addr, esr, regs);
}
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
if (!(mm_flags & FAULT_FLAG_USER))
goto lock_mmap;
vma = lock_vma_under_rcu(mm, addr);
if (!vma)
goto lock_mmap;
if (is_invalid_gcs_access(vma, esr)) {
vma_end_read(vma);
fault = 0;
si_code = SEGV_ACCERR;
goto bad_area;
}
if (!(vma->vm_flags & vm_flags)) {
vma_end_read(vma);
fault = 0;
si_code = SEGV_ACCERR;
count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
goto bad_area;
}
if (fault_from_pkey(vma, mm_flags)) {
pkey = vma_pkey(vma);
vma_end_read(vma);
fault = 0;
si_code = SEGV_PKUERR;
count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
goto bad_area;
}
fault = handle_mm_fault(vma, addr, mm_flags | FAULT_FLAG_VMA_LOCK, regs);
if (!(fault & (VM_FAULT_RETRY | VM_FAULT_COMPLETED)))
vma_end_read(vma);
if (!(fault & VM_FAULT_RETRY)) {
count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
goto done;
}
count_vm_vma_lock_event(VMA_LOCK_RETRY);
if (fault & VM_FAULT_MAJOR)
mm_flags |= FAULT_FLAG_TRIED;
/* Quick path to respond to signals */
if (fault_signal_pending(fault, regs)) {
if (!user_mode(regs))
goto no_context;
return 0;
}
lock_mmap:
retry:
vma = lock_mm_and_find_vma(mm, addr, regs);
if (unlikely(!vma)) {
fault = 0;
si_code = SEGV_MAPERR;
goto bad_area;
}
if (!(vma->vm_flags & vm_flags)) {
mmap_read_unlock(mm);
fault = 0;
si_code = SEGV_ACCERR;
goto bad_area;
}
if (fault_from_pkey(vma, mm_flags)) {
pkey = vma_pkey(vma);
mmap_read_unlock(mm);
fault = 0;
si_code = SEGV_PKUERR;
goto bad_area;
}
fault = handle_mm_fault(vma, addr, mm_flags, regs);
/* Quick path to respond to signals */
if (fault_signal_pending(fault, regs)) {
if (!user_mode(regs))
goto no_context;
return 0;
}
/* The fault is fully completed (including releasing mmap lock) */
if (fault & VM_FAULT_COMPLETED)
return 0;
if (fault & VM_FAULT_RETRY) {
mm_flags |= FAULT_FLAG_TRIED;
goto retry;
}
mmap_read_unlock(mm);
done:
/* Handle the "normal" (no error) case first. */
if (likely(!(fault & VM_FAULT_ERROR)))
return 0;
si_code = SEGV_MAPERR;
bad_area:
/*
* If we are in kernel mode at this point, we have no context to
* handle this fault with.
*/
if (!user_mode(regs))
goto no_context;
if (fault & VM_FAULT_OOM) {
/*
* We ran out of memory, call the OOM killer, and return to
* userspace (which will retry the fault, or kill us if we got
* oom-killed).
*/
pagefault_out_of_memory();
return 0;
}
inf = esr_to_fault_info(esr);
set_thread_esr(addr, esr);
if (fault & VM_FAULT_SIGBUS) {
/*
* We had some memory, but were unable to successfully fix up
* this page fault.
*/
arm64_force_sig_fault(SIGBUS, BUS_ADRERR, far, inf->name);
} else if (fault & (VM_FAULT_HWPOISON_LARGE | VM_FAULT_HWPOISON)) {
unsigned int lsb;
lsb = PAGE_SHIFT;
if (fault & VM_FAULT_HWPOISON_LARGE)
lsb = hstate_index_to_shift(VM_FAULT_GET_HINDEX(fault));
arm64_force_sig_mceerr(BUS_MCEERR_AR, far, lsb, inf->name);
} else {
/*
* The pkey value that we return to userspace can be different
* from the pkey that caused the fault.
*
* 1. T1 : mprotect_key(foo, PAGE_SIZE, pkey=4);
* 2. T1 : set POR_EL0 to deny access to pkey=4, touches, page
* 3. T1 : faults...
* 4. T2: mprotect_key(foo, PAGE_SIZE, pkey=5);
* 5. T1 : enters fault handler, takes mmap_lock, etc...
* 6. T1 : reaches here, sees vma_pkey(vma)=5, when we really
* faulted on a pte with its pkey=4.
*/
/* Something tried to access memory that out of memory map */
if (si_code == SEGV_PKUERR)
arm64_force_sig_fault_pkey(far, inf->name, pkey);
else
arm64_force_sig_fault(SIGSEGV, si_code, far, inf->name);
}
return 0;
no_context:
__do_kernel_fault(addr, esr, regs);
return 0;
}
将函数分为几块,每块详细注释,介绍处理的情况
A2
好的,我将这个复杂的页错误处理函数分块详细注释:
/**
* do_page_fault - ARM64页错误处理函数
* @far: 错误地址(FAR寄存器值)
* @esr: 异常症状寄存器(ESR)值
* @regs: 寄存器状态
*
* 处理权限错误(access flag fault/permission fault)
*/
static int __kprobes do_page_fault(unsigned long far, unsigned long esr,
struct pt_regs *regs)
{
const struct fault_info *inf;
struct mm_struct *mm = current->mm; // 当前进程的内存描述符
vm_fault_t fault;
vm_flags_t vm_flags; // 需要的内存区域标志
unsigned int mm_flags = FAULT_FLAG_DEFAULT; // 错误处理标志
unsigned long addr = untagged_addr(far); // 去除地址标签(ARM MTE)
struct vm_area_struct *vma; // 虚拟内存区域
int si_code; // 信号代码
int pkey = -1; // 保护键
/* 块1: kprobe探测点处理 */
// 如果kprobe正在处理此错误,则直接返回
if (kprobe_page_fault(regs, esr))
return 0;
/* 块2: 上下文检查 */
/*
* 如果我们在中断中或没有用户上下文(如内核线程),不能处理错误
* faulthandler_disabled()检查是否在中断/临界区中
* mm为空表示是内核线程,没有用户空间内存
*/
if (faulthandler_disabled() || !mm)
goto no_context;
// 设置用户模式标志
if (user_mode(regs))
mm_flags |= FAULT_FLAG_USER;
/* 块3: 错误类型解析 */
/*
* 根据ESR解析错误类型,确定需要的VMA标志
* vm_flags表示VMA必须有的标志,否则是权限错误
*/
if (is_el0_instruction_abort(esr)) {
/* 执行错误 - 如执行不可执行的页面 */
vm_flags = VM_EXEC;
mm_flags |= FAULT_FLAG_INSTRUCTION;
} else if (is_gcs_fault(esr)) {
/* GCS(Guarded Control Stack)错误 */
vm_flags = VM_WRITE;
mm_flags |= FAULT_FLAG_WRITE;
} else if (is_write_abort(esr)) {
/* 写错误 - 如写入只读页面 */
vm_flags = VM_WRITE;
mm_flags |= FAULT_FLAG_WRITE;
} else {
/* 读错误 - 如读取无读权限的页面 */
vm_flags = VM_READ;
vm_flags |= VM_WRITE; // 写权限隐含读权限
// 如果没有EPAN(Enhanced Privileged Access Never)特性,执行权限隐含读权限
if (!alternative_has_cap_unlikely(ARM64_HAS_EPAN))
vm_flags |= VM_EXEC;
}
/* 块4: 内核访问用户内存检查 */
/*
* 检查内核是否合法访问用户内存
* 仅允许在uaccess例程中访问用户内存
*/
if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
if (is_el1_instruction_abort(esr))
die_kernel_fault("execution of user memory",
addr, esr, regs);
if (!insn_may_access_user(regs->pc, esr))
die_kernel_fault("access to user memory outside uaccess routines",
addr, esr, regs);
}
// 性能计数:记录页错误事件
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
/* 块5: 尝试RCU快速路径 */
// 如果不是用户模式错误,跳过RCU路径
if (!(mm_flags & FAULT_FLAG_USER))
goto lock_mmap;
// 尝试RCU方式锁定VMA(无锁读取)
vma = lock_vma_under_rcu(mm, addr);
if (!vma)
goto lock_mmap;
/* 块5.1: RCU路径下的权限检查 */
if (is_invalid_gcs_access(vma, esr)) {
vma_end_read(vma);
fault = 0;
si_code = SEGV_ACCERR;
goto bad_area;
}
// 检查VMA标志是否符合要求
if (!(vma->vm_flags & vm_flags)) {
vma_end_read(vma);
fault = 0;
si_code = SEGV_ACCERR;
count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
goto bad_area;
}
// 检查保护键权限
if (fault_from_pkey(vma, mm_flags)) {
pkey = vma_pkey(vma);
vma_end_read(vma);
fault = 0;
si_code = SEGV_PKUERR;
count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
goto bad_area;
}
/* 块5.2: RCU路径下的页错误处理 */
// 使用VMA_LOCK标志调用通用页错误处理
fault = handle_mm_fault(vma, addr, mm_flags | FAULT_FLAG_VMA_LOCK, regs);
// 如果不需要重试且未完成,释放VMA读锁
if (!(fault & (VM_FAULT_RETRY | VM_FAULT_COMPLETED)))
vma_end_read(vma);
// 如果成功完成,跳转到done
if (!(fault & VM_FAULT_RETRY)) {
count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
goto done;
}
// 需要重试,记录统计信息
count_vm_vma_lock_event(VMA_LOCK_RETRY);
if (fault & VM_FAULT_MAJOR)
mm_flags |= FAULT_FLAG_TRIED;
/* 块5.3: 信号检查(RCU路径) */
if (fault_signal_pending(fault, regs)) {
if (!user_mode(regs))
goto no_context;
return 0;
}
/* 块6: 标准mmap锁路径 */
lock_mmap:
retry:
// 获取mmap锁并查找VMA
vma = lock_mm_and_find_vma(mm, addr, regs);
if (unlikely(!vma)) {
fault = 0;
si_code = SEGV_MAPERR; // 地址未映射
goto bad_area;
}
/* 块6.1: 标准路径下的权限检查 */
if (!(vma->vm_flags & vm_flags)) {
mmap_read_unlock(mm);
fault = 0;
si_code = SEGV_ACCERR;
goto bad_area;
}
// 检查保护键权限
if (fault_from_pkey(vma, mm_flags)) {
pkey = vma_pkey(vma);
mmap_read_unlock(mm);
fault = 0;
si_code = SEGV_PKUERR;
goto bad_area;
}
/* 块6.2: 标准路径下的页错误处理 */
fault = handle_mm_fault(vma, addr, mm_flags, regs);
/* 块6.3: 信号检查(标准路径) */
if (fault_signal_pending(fault, regs)) {
if (!user_mode(regs))
goto no_context;
return 0;
}
// 如果已完成(包括释放mmap锁),直接返回
if (fault & VM_FAULT_COMPLETED)
return 0;
// 如果需要重试,设置重试标志并跳回retry
if (fault & VM_FAULT_RETRY) {
mm_flags |= FAULT_FLAG_TRIED;
goto retry;
}
// 释放mmap锁
mmap_read_unlock(mm);
/* 块7: 处理成功情况 */
done:
// 正常情况:无错误,返回0
if (likely(!(fault & VM_FAULT_ERROR)))
return 0;
// 设置默认错误代码
si_code = SEGV_MAPERR;
/* 块8: 错误处理 */
bad_area:
// 如果是内核模式错误,无法恢复
if (!user_mode(regs))
goto no_context;
/* 块8.1: 内存不足(OOM)处理 */
if (fault & VM_FAULT_OOM) {
/*
* 内存不足,调用OOM killer
* 返回用户空间后可以重试错误,或者如果被OOM杀死则结束
*/
pagefault_out_of_memory();
return 0;
}
/* 块8.2: 根据错误类型发送信号 */
inf = esr_to_fault_info(esr);
set_thread_esr(addr, esr);
// SIGBUS: 总线错误(如硬件故障)
if (fault & VM_FAULT_SIGBUS) {
arm64_force_sig_fault(SIGBUS, BUS_ADRERR, far, inf->name);
}
// 硬件内存中毒错误
else if (fault & (VM_FAULT_HWPOISON_LARGE | VM_FAULT_HWPOISON)) {
unsigned int lsb;
lsb = PAGE_SHIFT;
if (fault & VM_FAULT_HWPOISON_LARGE)
lsb = hstate_index_to_shift(VM_FAULT_GET_HINDEX(fault));
arm64_force_sig_mceerr(BUS_MCEERR_AR, far, lsb, inf->name);
}
// SIGSEGV: 段错误
else {
/*
* 注意:返回给用户空间的pkey可能与实际导致错误的pkey不同
* 因为在错误处理期间pkey可能被其他线程修改
*/
if (si_code == SEGV_PKUERR)
arm64_force_sig_fault_pkey(far, inf->name, pkey);
else
arm64_force_sig_fault(SIGSEGV, si_code, far, inf->name);
}
return 0;
/* 块9: 内核上下文错误处理 */
no_context:
__do_kernel_fault(addr, esr, regs);
return 0;
}
关键路径总结
- 权限检查路径:检查VMA标志是否匹配访问类型
- RCU快速路径:尝试无锁方式处理,减少竞争
- 标准mmap锁路径:获取完整的mmap锁进行处理
- 错误信号路径:根据错误类型发送相应的信号给进程
主要错误类型处理
- 地址未映射 (
SEGV_MAPERR):发送SIGSEGV - 权限错误 (
SEGV_ACCERR):发送SIGSEGV - 保护键错误 (
SEGV_PKUERR):发送SIGSEGV,附带pkey信息 - 总线错误 (
BUS_ADRERR):发送SIGBUS - 内存硬件错误:发送SIGBUS MCE
- 内存不足:调用OOM killer
这个函数体现了Linux内核页错误处理的复杂性和完整性,涵盖了从快速路径优化到各种错误情况的全面处理。
Q3
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags, struct pt_regs *regs)
{
/* If the fault handler drops the mmap_lock, vma may be freed */
struct mm_struct *mm = vma->vm_mm;
vm_fault_t ret;
bool is_droppable;
__set_current_state(TASK_RUNNING);
ret = sanitize_fault_flags(vma, &flags);
if (ret)
goto out;
if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
flags & FAULT_FLAG_INSTRUCTION,
flags & FAULT_FLAG_REMOTE)) {
ret = VM_FAULT_SIGSEGV;
goto out;
}
is_droppable = !!(vma->vm_flags & VM_DROPPABLE);
/*
* Enable the memcg OOM handling for faults triggered in user
* space. Kernel faults are handled more gracefully.
*/
if (flags & FAULT_FLAG_USER)
mem_cgroup_enter_user_fault();
lru_gen_enter_fault(vma);
if (unlikely(is_vm_hugetlb_page(vma)))
ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
else
ret = __handle_mm_fault(vma, address, flags);
/*
* Warning: It is no longer safe to dereference vma-> after this point,
* because mmap_lock might have been dropped by __handle_mm_fault(), so
* vma might be destroyed from underneath us.
*/
lru_gen_exit_fault();
/* If the mapping is droppable, then errors due to OOM aren't fatal. */
if (is_droppable)
ret &= ~VM_FAULT_OOM;
if (flags & FAULT_FLAG_USER) {
mem_cgroup_exit_user_fault();
/*
* The task may have entered a memcg OOM situation but
* if the allocation error was handled gracefully (no
* VM_FAULT_OOM), there is no need to kill anything.
* Just clean up the OOM state peacefully.
*/
if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
mem_cgroup_oom_synchronize(false);
}
out:
mm_account_fault(mm, regs, address, flags, ret);
return ret;
}
EXPORT_SYMBOL_GPL(handle_mm_fault);
#ifndef __PAGETABLE_P4D_FOLDED
/*
* Allocate p4d page table.
* We've already handled the fast-path in-line.
*/
int __p4d_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
{
p4d_t *new = p4d_alloc_one(mm, address);
if (!new)
return -ENOMEM;
spin_lock(&mm->page_table_lock);
if (pgd_present(*pgd)) { /* Another has populated it */
p4d_free(mm, new);
} else {
smp_wmb(); /* See comment in pmd_install() */
pgd_populate(mm, pgd, new);
}
spin_unlock(&mm->page_table_lock);
return 0;
}
同上,分析
A3
好的,我来详细注释这个重要的缺页处理函数:
/**
* handle_mm_fault - 处理内存缺页异常的核心函数
* @vma: 发生错误的虚拟内存区域
* @address: 触发错误的虚拟地址
* @flags: 错误标志,定义错误类型和处理方式
* @regs: 寄存器状态(用于统计和调试)
*
* 返回值: vm_fault_t 类型,表示错误处理结果
*/
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags, struct pt_regs *regs)
{
/* 如果错误处理程序释放了mmap_lock,vma可能被释放 */
struct mm_struct *mm = vma->vm_mm; // 获取内存描述符
vm_fault_t ret; // 返回值
bool is_droppable; // 标记vma是否可丢弃(如临时映射)
// 将当前任务状态设置为运行中
__set_current_state(TASK_RUNNING);
/* 块1: 标志位检查和清理 */
// 检查并清理错误标志,确保与vma的权限一致
ret = sanitize_fault_flags(vma, &flags);
if (ret)
goto out; // 如果标志无效,直接跳转到out
/* 块2: 架构特定的访问权限检查 */
// 检查当前架构是否允许这种访问(如ARM64的PAN特性检查)
if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
flags & FAULT_FLAG_INSTRUCTION,
flags & FAULT_FLAG_REMOTE)) {
ret = VM_FAULT_SIGSEGV; // 不允许访问,返回段错误
goto out;
}
// 检查VMA是否可丢弃(如临时映射的内存)
is_droppable = !!(vma->vm_flags & VM_DROPPABLE);
/* 块3: 内存cgroup和LRU生成管理 */
/*
* 为用户空间触发的错误启用内存cgroup OOM处理
* 内核错误处理更加优雅(不会触发OOM killer)
*/
if (flags & FAULT_FLAG_USER)
mem_cgroup_enter_user_fault(); // 进入内存cgroup用户错误上下文
// 进入LRU(最近最少使用)生成错误处理上下文
lru_gen_enter_fault(vma);
/* 块4: 根据VMA类型分发到不同的错误处理 */
if (unlikely(is_vm_hugetlb_page(vma)))
// 处理大页(HugeTLB)错误
ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
else
// 处理普通页错误
ret = __handle_mm_fault(vma, address, flags);
/*
* 警告:在此之后,不能再安全地解引用vma
* 因为__handle_mm_fault()可能已经释放了mmap_lock
* 所以vma可能在我们不知情的情况下被销毁
*/
// 退出LRU生成错误处理上下文
lru_gen_exit_fault();
/* 块5: 处理可丢弃VMA的OOM错误 */
// 如果映射是可丢弃的,那么OOM错误不是致命的
if (is_droppable)
ret &= ~VM_FAULT_OOM; // 清除OOM标志位
/* 块6: 用户空间错误的内存cgroup处理 */
if (flags & FAULT_FLAG_USER) {
// 退出内存cgroup用户错误上下文
mem_cgroup_exit_user_fault();
/*
* 任务可能已经进入内存cgroup OOM状态,但如果错误被优雅处理
* (没有VM_FAULT_OOM),则不需要杀死任何东西。
* 只需和平地清理OOM状态。
*/
if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
mem_cgroup_oom_synchronize(false); // 同步内存cgroup OOM状态
}
out:
/* 块7: 内存错误统计 */
// 记录内存错误统计信息
mm_account_fault(mm, regs, address, flags, ret);
return ret; // 返回处理结果
}
EXPORT_SYMBOL_GPL(handle_mm_fault);
关键概念解释:
1. 错误标志位 (flags)
FAULT_FLAG_USER: 用户空间触发的错误FAULT_FLAG_WRITE: 写错误FAULT_FLAG_INSTRUCTION: 指令执行错误FAULT_FLAG_REMOTE: 远程错误(如NUMA节点间访问)
2. 返回值类型 (vm_fault_t)
#define VM_FAULT_OOM 0x0001 // 内存不足
#define VM_FAULT_SIGBUS 0x0002 // 总线错误
#define VM_FAULT_MAJOR 0x0004 // 主要错误(需要I/O)
#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 0x0100 // 无页面
#define VM_FAULT_LOCKED 0x0200 // 页面已锁定
#define VM_FAULT_RETRY 0x0400 // 需要重试
#define VM_FAULT_FALLBACK 0x0800 // 回退处理
#define VM_FAULT_DONE_COW 0x1000 // COW完成
#define VM_FAULT_NEEDDSYNC 0x2000 // 需要数据同步
#define VM_FAULT_COMPLETED 0x4000 // 已完成处理
3. VM_DROPPABLE标志
- 标记VMA为可丢弃的临时映射
- 用于ephemeral映射,OOM时不会杀死进程
- 常见于:临时缓冲区、设备映射等
Q4
/*
* On entry, we hold either the VMA lock or the mmap_lock
* (FAULT_FLAG_VMA_LOCK tells you which). If VM_FAULT_RETRY is set in
* the result, the mmap_lock is not held on exit. See filemap_fault()
* and __folio_lock_or_retry().
*/
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
struct vm_fault vmf = {
.vma = vma,
.address = address & PAGE_MASK,
.real_address = address,
.flags = flags,
.pgoff = linear_page_index(vma, address),
.gfp_mask = __get_fault_gfp_mask(vma),
};
struct mm_struct *mm = vma->vm_mm;
vm_flags_t vm_flags = vma->vm_flags;
pgd_t *pgd;
p4d_t *p4d;
vm_fault_t ret;
pgd = pgd_offset(mm, address);
p4d = p4d_alloc(mm, pgd, address);
if (!p4d)
return VM_FAULT_OOM;
vmf.pud = pud_alloc(mm, p4d, address);
if (!vmf.pud)
return VM_FAULT_OOM;
retry_pud:
if (pud_none(*vmf.pud) &&
thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PUD_ORDER)) {
ret = create_huge_pud(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pud_t orig_pud = *vmf.pud;
barrier();
if (pud_trans_huge(orig_pud)) {
/*
* TODO once we support anonymous PUDs: NUMA case and
* FAULT_FLAG_UNSHARE handling.
*/
if ((flags & FAULT_FLAG_WRITE) && !pud_write(orig_pud)) {
ret = wp_huge_pud(&vmf, orig_pud);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pud_set_accessed(&vmf, orig_pud);
return 0;
}
}
}
vmf.pmd = pmd_alloc(mm, vmf.pud, address);
if (!vmf.pmd)
return VM_FAULT_OOM;
/* Huge pud page fault raced with pmd_alloc? */
if (pud_trans_unstable(vmf.pud))
goto retry_pud;
if (pmd_none(*vmf.pmd) &&
thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PMD_ORDER)) {
ret = create_huge_pmd(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
vmf.orig_pmd = pmdp_get_lockless(vmf.pmd);
if (unlikely(is_swap_pmd(vmf.orig_pmd))) {
VM_BUG_ON(thp_migration_supported() &&
!is_pmd_migration_entry(vmf.orig_pmd));
if (is_pmd_migration_entry(vmf.orig_pmd))
pmd_migration_entry_wait(mm, vmf.pmd);
return 0;
}
if (pmd_trans_huge(vmf.orig_pmd)) {
if (pmd_protnone(vmf.orig_pmd) && vma_is_accessible(vma))
return do_huge_pmd_numa_page(&vmf);
if ((flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) &&
!pmd_write(vmf.orig_pmd)) {
ret = wp_huge_pmd(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(&vmf);
return 0;
}
}
}
return handle_pte_fault(&vmf);
}
同上分析
A4
好的,我来详细注释这个缺页处理的底层函数:
/*
* 在进入时,我们持有VMA锁或mmap_lock
* (FAULT_FLAG_VMA_LOCK标志告诉你持有哪个锁)。
* 如果结果中设置了VM_FAULT_RETRY,则在退出时不持有mmap_lock。
* 参见filemap_fault()和__folio_lock_or_retry()。
*/
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
/* 块1: 初始化vm_fault结构体 */
// vm_fault结构体用于在缺页处理过程中传递状态
struct vm_fault vmf = {
.vma = vma, // 关联的VMA
.address = address & PAGE_MASK, // 页对齐的地址
.real_address = address, // 原始地址
.flags = flags, // 错误标志
.pgoff = linear_page_index(vma, address), // 在文件中的页偏移
.gfp_mask = __get_fault_gfp_mask(vma), // 内存分配标志
};
struct mm_struct *mm = vma->vm_mm; // 内存描述符
vm_flags_t vm_flags = vma->vm_flags; // VMA标志
pgd_t *pgd; // PGD页表项指针
p4d_t *p4d; // P4D页表项指针
vm_fault_t ret; // 返回值
/* 块2: PGD/P4D级别处理 */
// 获取PGD页表项(Page Global Directory)
pgd = pgd_offset(mm, address);
// 分配P4D页表项(Page 4th Directory),ARM64中通常与PGD合并
p4d = p4d_alloc(mm, pgd, address);
if (!p4d)
return VM_FAULT_OOM; // 内存不足
/* 块3: PUD级别处理 */
// 分配PUD页表项(Page Upper Directory)
vmf.pud = pud_alloc(mm, p4d, address);
if (!vmf.pud)
return VM_FAULT_OOM; // 内存不足
retry_pud:
/* 块4: PUD级别大页处理 */
// 检查PUD是否为空且允许创建PUD级别的大页(1GB大页)
if (pud_none(*vmf.pud) &&
thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PUD_ORDER)) {
// 尝试创建1GB大页
ret = create_huge_pud(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret; // 成功创建或处理了大页
// 如果返回FALLBACK,则回退到普通页处理
} else {
// PUD已存在,获取当前PUD值
pud_t orig_pud = *vmf.pud;
barrier(); // 内存屏障,确保读取的顺序性
/* 块4.1: 处理现有的PUD大页 */
if (pud_trans_huge(orig_pud)) {
/*
* TODO: 一旦我们支持匿名PUD大页,需要处理NUMA情况和
* FAULT_FLAG_UNSHARE处理
*/
// 如果是写错误且PUD没有写权限,进行写时复制(COW)
if ((flags & FAULT_FLAG_WRITE) && !pud_write(orig_pud)) {
ret = wp_huge_pud(&vmf, orig_pud); // 写保护处理
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
// 只是读访问或已有写权限,设置访问标志
huge_pud_set_accessed(&vmf, orig_pud);
return 0; // 成功处理
}
}
}
/* 块5: PMD级别处理 */
// 分配PMD页表项(Page Middle Directory)
vmf.pmd = pmd_alloc(mm, vmf.pud, address);
if (!vmf.pmd)
return VM_FAULT_OOM; // 内存不足
/* 块6: 检查PUD是否不稳定(同时有其他线程在修改) */
// PUD大页缺页与pmd_alloc竞争?如果是则重试
if (pud_trans_unstable(vmf.pud))
goto retry_pud;
/* 块7: PMD级别大页处理 */
// 检查PMD是否为空且允许创建PMD级别的大页(2MB大页)
if (pmd_none(*vmf.pmd) &&
thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PMD_ORDER)) {
// 尝试创建2MB大页
ret = create_huge_pmd(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret; // 成功创建或处理了大页
// 如果返回FALLBACK,则回退到普通页处理
} else {
// PMD已存在,获取当前PMD值
vmf.orig_pmd = pmdp_get_lockless(vmf.pmd);
/* 块7.1: 处理交换页表项 */
// 检查是否为交换页表项(页面被换出到磁盘)
if (unlikely(is_swap_pmd(vmf.orig_pmd))) {
// 调试断言:如果支持透明大页迁移,则必须是迁移项
VM_BUG_ON(thp_migration_supported() &&
!is_pmd_migration_entry(vmf.orig_pmd));
// 如果是迁移项,等待迁移完成
if (is_pmd_migration_entry(vmf.orig_pmd))
pmd_migration_entry_wait(mm, vmf.pmd);
return 0; // 等待后重新处理
}
/* 块7.2: 处理现有的PMD大页 */
if (pmd_trans_huge(vmf.orig_pmd)) {
// 检查是否为保护性无访问权限的PMD大页(PROT_NONE)
if (pmd_protnone(vmf.orig_pmd) && vma_is_accessible(vma))
return do_huge_pmd_numa_page(&vmf); // NUMA平衡处理
// 如果是写错误或UNSHARE错误且PMD没有写权限
if ((flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) &&
!pmd_write(vmf.orig_pmd)) {
ret = wp_huge_pmd(&vmf); // 写保护处理(COW)
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
// 只是读访问或已有写权限,设置访问标志
huge_pmd_set_accessed(&vmf);
return 0; // 成功处理
}
}
}
/* 块8: PTE级别处理 */
// 如果不是大页或大页处理失败,回退到普通页(4KB)处理
return handle_pte_fault(&vmf);
}
关键概念解释:
1. 页表层级和大小
- PGD:Page Global Directory,全局页目录
- P4D:Page 4th Directory,第4级目录(ARM64中通常折叠到PGD)
- PUD:Page Upper Directory,上层目录,可管理1GB大页
- PMD:Page Middle Directory,中间目录,可管理2MB大页
- PTE:Page Table Entry,页表项,管理4KB普通页
2. 大页支持
- PUD大页:1GB大小,对应
PUD_ORDER - PMD大页:2MB大小,对应
PMD_ORDER - 透明大页:由
thp_vma_allowable_order()检查是否允许
3. 错误处理模式
- VM_FAULT_FALLBACK:大页处理失败,回退到普通页处理
- VM_FAULT_OOM:内存不足
- VM_FAULT_RETRY:需要重试(如锁竞争)
4. 特殊处理情况
// 交换页表项:页面被换出到磁盘
if (unlikely(is_swap_pmd(vmf.orig_pmd)))
// 迁移项:页面正在迁移中
if (is_pmd_migration_entry(vmf.orig_pmd))
// 保护性无访问权限页(PROT_NONE)
if (pmd_protnone(vmf.orig_pmd) && vma_is_accessible(vma))
// 写时复制(COW)处理
if ((flags & FAULT_FLAG_WRITE) && !pud_write(orig_pud))
5. 并发处理机制
// 内存屏障确保读取顺序
barrier();
// 检查页表项是否在变化中
if (pud_trans_unstable(vmf.pud))
// 无锁获取PMD值(小心使用)
vmf.orig_pmd = pmdp_get_lockless(vmf.pmd);
处理流程总结:
- 页表层级遍历:从PGD→P4D→PUD→PMD逐级遍历
- 大页优先:尝试使用大页映射(1GB或2MB)
- 写时复制:处理写保护页面的COW
- 特殊状态处理:处理交换页、迁移页、NUMA平衡
- 回退机制:大页失败时回退到普通页处理
- 并发安全:使用适当的同步机制处理并发修改
关键设计思想:
- 惰性分配:页表只在需要时才分配
- 大页优化:优先使用大页减少TLB压力
- 写时复制:延迟实际的内存复制直到需要时
- 回退机制:大页不可用时优雅降级
- 并发安全:正确处理多线程同时缺页的情况
这个函数是Linux内核内存管理的核心之一,展示了现代操作系统如何高效地管理虚拟内存。
Q5
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
if (unlikely(pmd_none(*vmf->pmd))) {
/*
* Leave __pte_alloc() until later: because vm_ops->fault may
* want to allocate huge page, and if we expose page table
* for an instant, it will be difficult to retract from
* concurrent faults and from rmap lookups.
*/
vmf->pte = NULL;
vmf->flags &= ~FAULT_FLAG_ORIG_PTE_VALID;
} else {
pmd_t dummy_pmdval;
/*
* A regular pmd is established and it can't morph into a huge
* pmd by anon khugepaged, since that takes mmap_lock in write
* mode; but shmem or file collapse to THP could still morph
* it into a huge pmd: just retry later if so.
*
* Use the maywrite version to indicate that vmf->pte may be
* modified, but since we will use pte_same() to detect the
* change of the !pte_none() entry, there is no need to recheck
* the pmdval. Here we chooes to pass a dummy variable instead
* of NULL, which helps new user think about why this place is
* special.
*/
vmf->pte = pte_offset_map_rw_nolock(vmf->vma->vm_mm, vmf->pmd,
vmf->address, &dummy_pmdval,
&vmf->ptl);
if (unlikely(!vmf->pte))
return 0;
vmf->orig_pte = ptep_get_lockless(vmf->pte);
vmf->flags |= FAULT_FLAG_ORIG_PTE_VALID;
if (pte_none(vmf->orig_pte)) {
pte_unmap(vmf->pte);
vmf->pte = NULL;
}
}
if (!vmf->pte)
return do_pte_missing(vmf);
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf);
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
return do_numa_page(vmf);
spin_lock(vmf->ptl);
entry = vmf->orig_pte;
if (unlikely(!pte_same(ptep_get(vmf->pte), entry))) {
update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);
goto unlock;
}
if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
if (!pte_write(entry))
return do_wp_page(vmf);
else if (likely(vmf->flags & FAULT_FLAG_WRITE))
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
vmf->flags & FAULT_FLAG_WRITE)) {
update_mmu_cache_range(vmf, vmf->vma, vmf->address,
vmf->pte, 1);
} else {
/* Skip spurious TLB flush for retried page fault */
if (vmf->flags & FAULT_FLAG_TRIED)
goto unlock;
/*
* This is needed only for protection faults but the arch code
* is not yet telling us if this is a protection fault or not.
* This still avoids useless tlb flushes for .text page faults
* with threads.
*/
if (vmf->flags & FAULT_FLAG_WRITE)
flush_tlb_fix_spurious_fault(vmf->vma, vmf->address,
vmf->pte);
}
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
同上
A5
好的,我来详细注释这个PTE级别的缺页处理函数:
/**
* handle_pte_fault - 处理PTE级别的页错误
* @vmf: 包含错误信息的vm_fault结构体
*
* 处理PTE级别的页错误,包括缺页、交换页、NUMA页、写时复制等
*/
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry; // 临时存储PTE值
/* 块1: 获取PTE指针和原始PTE值 */
// 检查PMD是否为空(即没有分配PTE表)
if (unlikely(pmd_none(*vmf->pmd))) {
/*
* 将__pte_alloc()延迟到后面:因为vm_ops->fault可能想要分配大页,
* 如果我们临时暴露页表,那么从并发错误和反向映射查找中撤消将很困难。
*/
vmf->pte = NULL; // 标记PTE为空
vmf->flags &= ~FAULT_FLAG_ORIG_PTE_VALID; // 清除原始PTE有效标志
} else {
pmd_t dummy_pmdval; // 哑变量,用于接收可能的PMD修改
/*
* 一个常规的PMD已经建立,并且它不会因为anon khugepaged而变成大页PMD,
* 因为这需要以写模式获取mmap_lock;但是shmem或文件折叠到THP仍然可能
* 将其变成大页PMD:如果是这样,稍后重试。
*
* 使用maywrite版本表示vmf->pte可能会被修改,但是因为我们使用pte_same()
* 来检测非空条目的变化,所以不需要重新检查pmdval。这里我们选择传递一个哑变量
* 而不是NULL,以帮助新用户思考为什么这个地方是特殊的。
*/
// 获取PTE指针,同时获取自旋锁(vmf->ptl)
vmf->pte = pte_offset_map_rw_nolock(vmf->vma->vm_mm, vmf->pmd,
vmf->address, &dummy_pmdval,
&vmf->ptl);
if (unlikely(!vmf->pte))
return 0; // 获取失败(可能是大页转换中)
// 无锁获取原始的PTE值(小心并发)
vmf->orig_pte = ptep_get_lockless(vmf->pte);
vmf->flags |= FAULT_FLAG_ORIG_PTE_VALID; // 设置原始PTE有效标志
// 如果PTE为空(没有映射),则解除映射,将vmf->pte设置为NULL
if (pte_none(vmf->orig_pte)) {
pte_unmap(vmf->pte);
vmf->pte = NULL;
}
}
/* 块2: 根据PTE状态分发到不同的处理函数 */
// 如果没有PTE指针(PMD为空或PTE为空),处理缺页
if (!vmf->pte)
return do_pte_missing(vmf); // 分配新的物理页
// 如果PTE存在但不是PRESENT状态(被交换到磁盘或其他特殊状态)
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf); // 处理交换页(从磁盘换入)
// 如果PTE是保护性的(PROT_NONE)且VMA可访问,处理NUMA页迁移
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
return do_numa_page(vmf); // NUMA平衡:迁移到本地节点
/* 块3: 处理存在的PTE(页面已在内存中) */
// 获取自旋锁(因为前面已经通过pte_offset_map_rw_nolock获取了锁,但这里重新获取以确保同步)
spin_lock(vmf->ptl);
// 再次获取当前PTE的值,与之前无锁获取的值进行比较,检查是否被并发修改
entry = vmf->orig_pte;
if (unlikely(!pte_same(ptep_get(vmf->pte), entry))) {
// PTE已经被并发修改,更新MMU TLB并跳转到解锁
update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);
goto unlock;
}
/* 块4: 处理写错误或UNSHARE错误 */
// 如果是写错误或UNSHARE错误
if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
// 如果PTE没有写权限,则进行写时复制(COW)处理
if (!pte_write(entry))
return do_wp_page(vmf); // 写保护页:执行COW
// 如果有写权限且是写错误,则标记页面为脏
else if (likely(vmf->flags & FAULT_FLAG_WRITE))
entry = pte_mkdirty(entry); // 设置脏位
}
/* 块5: 更新访问标志(Accessed位) */
// 将页面标记为年轻(最近访问)
entry = pte_mkyoung(entry);
// 尝试原子地更新PTE的访问标志(Accessed位,可能还有Dirty位)
if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
vmf->flags & FAULT_FLAG_WRITE)) {
// 如果更新成功(标志位确实改变了),则更新MMU缓存
update_mmu_cache_range(vmf, vmf->vma, vmf->address,
vmf->pte, 1);
} else {
/* 对于重试的页错误,跳过虚假的TLB刷新 */
if (vmf->flags & FAULT_FLAG_TRIED)
goto unlock;
/*
* 这仅用于保护错误,但是架构代码还没有告诉我们这是否是一个保护错误。
* 这仍然可以避免对于.text页错误的多线程情况下的无用TLB刷新。
*/
// 如果是写错误,则刷新TLB以处理可能的虚假错误
if (vmf->flags & FAULT_FLAG_WRITE)
flush_tlb_fix_spurious_fault(vmf->vma, vmf->address,
vmf->pte);
}
unlock:
// 解锁并解除PTE映射
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0; // 成功处理
}
关键概念解释:
1. PTE状态分类
// 1. 完全空(无映射)
if (!vmf->pte) → do_pte_missing()
// 2. 非PRESENT状态(被交换出)
if (!pte_present(vmf->orig_pte)) → do_swap_page()
// 3. 保护性无访问权限(PROT_NONE)
if (pte_protnone(vmf->orig_pte)) → do_numa_page()
// 4. PRESENT且可访问,但没有写权限的写错误
if (!pte_write(entry)) → do_wp_page()
// 5. PRESENT且有写权限的写错误
else → 标记脏位,更新访问标志
2. PTE标志位
// PTE中的标志位(部分)
#define _PAGE_PRESENT (1UL << 0) // 页面在内存中
#define _PAGE_RW (1UL << 1) // 可写
#define _PAGE_USER (1UL << 2) // 用户空间可访问
#define _PAGE_ACCESSED (1UL << 5) // 已访问
#define _PAGE_DIRTY (1UL << 6) // 脏页(已被修改)
#define _PAGE_PROTNONE (1UL << 8) // 保护性无访问权限
3. 处理函数说明
// do_pte_missing: 分配新的物理页
// 情况:首次访问匿名页、文件映射页
// do_swap_page: 从交换空间换入页面
// 情况:页面被换出到磁盘
// do_numa_page: NUMA平衡页面迁移
// 情况:页面在远程NUMA节点,需要迁移到本地
// do_wp_page: 写时复制(Copy-on-Write)
// 情况:写入只读页面(共享的匿名页或文件映射页)
4. 并发处理机制
// 1. 无锁读取原始PTE
vmf->orig_pte = ptep_get_lockless(vmf->pte);
// 2. 获取锁后验证PTE是否被修改
if (unlikely(!pte_same(ptep_get(vmf->pte), entry)))
// 3. 原子更新PTE标志
ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry, ...)
5. TLB管理
// 更新MMU TLB缓存
update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);
// 更新MMU缓存范围
update_mmu_cache_range(vmf, vmf->vma, vmf->address, vmf->pte, 1);
// 刷新虚假TLB错误
flush_tlb_fix_spurious_fault(vmf->vma, vmf->address, vmf->pte);
处理流程总结:
- 获取PTE:尝试获取PTE指针,处理PMD为空或PTE为空的特殊情况
- 状态检查:根据PTE状态分发到不同的处理函数
- 并发验证:获取锁后验证PTE是否被其他线程修改
- 写错误处理:处理写时复制(COW)或标记脏页
- 访问标志更新:标记页面为已访问,更新硬件PTE
- TLB维护:根据需要更新TLB缓存
关键设计思想:
- 延迟分配:直到需要时才分配PTE表
- 状态驱动:根据PTE状态选择正确的处理路径
- 并发安全:使用锁和无锁读取组合处理并发
- 性能优化:避免不必要的TLB刷新
- NUMA感知:处理跨节点的页面迁移
这个函数是Linux内核缺页处理的最底层核心,直接操作页表项,展示了操作系统如何管理虚拟内存到物理内存的映射关系。
Q6
static inline int ptep_set_access_flags(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep,
pte_t entry, int dirty)
{
pte_t orig_pte = __ptep_get(ptep);
entry = pte_mknoncont(entry);
if (likely(!pte_valid_cont(orig_pte)))
return __ptep_set_access_flags(vma, addr, ptep, entry, dirty);
return contpte_ptep_set_access_flags(vma, addr, ptep, entry, dirty);
}
int __ptep_set_access_flags(struct vm_area_struct *vma,
unsigned long address, pte_t *ptep,
pte_t entry, int dirty)
{
pteval_t old_pteval, pteval;
pte_t pte = __ptep_get(ptep);
if (pte_same(pte, entry))
return 0;
/* only preserve the access flags and write permission */
pte_val(entry) &= PTE_RDONLY | PTE_AF | PTE_WRITE | PTE_DIRTY;
/*
* Setting the flags must be done atomically to avoid racing with the
* hardware update of the access/dirty state. The PTE_RDONLY bit must
* be set to the most permissive (lowest value) of *ptep and entry
* (calculated as: a & b == ~(~a | ~b)).
*/
pte_val(entry) ^= PTE_RDONLY;
pteval = pte_val(pte);
do {
old_pteval = pteval;
pteval ^= PTE_RDONLY;
pteval |= pte_val(entry);
pteval ^= PTE_RDONLY;
pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval, pteval);
} while (pteval != old_pteval);
/* Invalidate a stale read-only entry */
if (dirty)
flush_tlb_page(vma, address);
return 1;
}
int contpte_ptep_set_access_flags(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep,
pte_t entry, int dirty)
{
unsigned long start_addr;
pte_t orig_pte;
int i;
/*
* Gather the access/dirty bits for the contiguous range. If nothing has
* changed, its a noop.
*/
orig_pte = pte_mknoncont(ptep_get(ptep));
if (pte_val(orig_pte) == pte_val(entry))
return 0;
/*
* We can fix up access/dirty bits without having to unfold the contig
* range. But if the write bit is changing, we must unfold.
*/
if (pte_write(orig_pte) == pte_write(entry)) {
/*
* For HW access management, we technically only need to update
* the flag on a single pte in the range. But for SW access
* management, we need to update all the ptes to prevent extra
* faults. Avoid per-page tlb flush in __ptep_set_access_flags()
* and instead flush the whole range at the end.
*/
ptep = contpte_align_down(ptep);
start_addr = addr = ALIGN_DOWN(addr, CONT_PTE_SIZE);
/*
* We are not advancing entry because __ptep_set_access_flags()
* only consumes access flags from entry. And since we have checked
* for the whole contpte block and returned early, pte_same()
* within __ptep_set_access_flags() is likely false.
*/
for (i = 0; i < CONT_PTES; i++, ptep++, addr += PAGE_SIZE)
__ptep_set_access_flags(vma, addr, ptep, entry, 0);
if (dirty)
__flush_tlb_range(vma, start_addr, addr,
PAGE_SIZE, true, 3);
} else {
__contpte_try_unfold(vma->vm_mm, addr, ptep, orig_pte);
__ptep_set_access_flags(vma, addr, ptep, entry, dirty);
}
return 1;
}
分析怎么处理pte的标识的
A6
我来详细注释这些PTE访问标志设置函数:
/**
* ptep_set_access_flags - 设置PTE访问标志的入口函数
* @vma: 虚拟内存区域
* @addr: 虚拟地址
* @ptep: PTE指针
* @entry: 新的PTE值
* @dirty: 是否设置脏位
*
* 根据PTE是否连续,分发给不同的处理函数
*/
static inline int ptep_set_access_flags(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep,
pte_t entry, int dirty)
{
// 获取当前PTE的值
pte_t orig_pte = __ptep_get(ptep);
// 将新PTE标记为非连续(确保我们处理的是单个PTE)
entry = pte_mknoncont(entry);
// 如果原始PTE不是有效的连续PTE,使用普通处理函数
if (likely(!pte_valid_cont(orig_pte)))
return __ptep_set_access_flags(vma, addr, ptep, entry, dirty);
// 否则,使用连续PTE处理函数
return contpte_ptep_set_access_flags(vma, addr, ptep, entry, dirty);
}
/**
* __ptep_set_access_flags - 处理单个PTE的访问标志设置
* @vma: 虚拟内存区域
* @address: 虚拟地址
* @ptep: PTE指针
* @entry: 新的PTE值
* @dirty: 是否设置脏位
*
* 原子地更新PTE的访问/脏标志,处理潜在的并发修改
*/
int __ptep_set_access_flags(struct vm_area_struct *vma,
unsigned long address, pte_t *ptep,
pte_t entry, int dirty)
{
pteval_t old_pteval, pteval;
// 再次获取当前PTE(原子读取)
pte_t pte = __ptep_get(ptep);
// 如果新旧PTE相同,无需更新
if (pte_same(pte, entry))
return 0;
/* 只保留访问标志和写权限(清除其他位)*/
// 从entry中只保留我们关心的标志位:RDONLY(只读)、AF(访问)、WRITE(写)、DIRTY(脏)
pte_val(entry) &= PTE_RDONLY | PTE_AF | PTE_WRITE | PTE_DIRTY;
/*
* 设置标志必须原子地进行,以避免与硬件更新访问/脏状态的竞争。
* PTE_RDONLY位必须设置为*ptep和entry中最宽松(最低值)的值。
* 计算方法:a & b == ~(~a | ~b)
*/
pte_val(entry) ^= PTE_RDONLY; // 反转RDONLY位,以便后续计算
pteval = pte_val(pte); // 当前PTE的值
do {
old_pteval = pteval; // 保存旧值用于比较交换
// 反转当前PTE的RDONLY位
pteval ^= PTE_RDONLY;
// 合并新的标志位
pteval |= pte_val(entry);
// 再次反转RDONLY位,恢复正确的表示
pteval ^= PTE_RDONLY;
// 使用比较交换原子更新PTE值
pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval, pteval);
// 如果ptep指向的值在更新期间被其他线程修改,循环重试
} while (pteval != old_pteval);
/* 使陈旧的只读条目无效(刷新TLB)*/
// 如果设置了脏位,需要刷新TLB,因为写权限可能改变了
if (dirty)
flush_tlb_page(vma, address);
return 1; // 表示PTE已更新
}
/**
* contpte_ptep_set_access_flags - 处理连续PTE的访问标志设置
* @vma: 虚拟内存区域
* @addr: 虚拟地址
* @ptep: PTE指针(可能指向连续块中的任意一个PTE)
* @entry: 新的PTE值
* @dirty: 是否设置脏位
*
* 处理连续PTE(CONT_PTES个连续页面)的访问标志更新
*/
int contpte_ptep_set_access_flags(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep,
pte_t entry, int dirty)
{
unsigned long start_addr;
pte_t orig_pte;
int i;
/*
* 收集连续范围的访问/脏位。如果没有变化,就什么都不做。
*/
// 获取原始PTE并标记为非连续(用于比较)
orig_pte = pte_mknoncont(ptep_get(ptep));
// 如果值相同,无需更新
if (pte_val(orig_pte) == pte_val(entry))
return 0;
/*
* 我们可以在不展开连续范围的情况下修复访问/脏位。
* 但是,如果写位正在改变,我们必须展开连续块。
*/
if (pte_write(orig_pte) == pte_write(entry)) {
/*
* 对于硬件访问管理,技术上我们只需要更新范围内的一个PTE的标志。
* 但对于软件访问管理,我们需要更新所有PTE以防止额外的错误。
* 避免在__ptep_set_access_flags()中进行逐页TLB刷新,
* 而是在最后刷新整个范围。
*/
// 对齐到连续PTE块的起始地址
ptep = contpte_align_down(ptep);
start_addr = addr = ALIGN_DOWN(addr, CONT_PTE_SIZE);
/*
* 我们不推进entry,因为__ptep_set_access_flags()只从entry中消耗访问标志。
* 由于我们已经检查了整个contpte块并提前返回,因此__ptep_set_access_flags()
* 中的pte_same()很可能为false。
*/
// 更新连续块中的所有PTE
for (i = 0; i < CONT_PTES; i++, ptep++, addr += PAGE_SIZE)
__ptep_set_access_flags(vma, addr, ptep, entry, 0); // 传递dirty=0,稍后统一刷新
// 如果需要设置脏位,刷新整个范围的TLB
if (dirty)
__flush_tlb_range(vma, start_addr, addr,
PAGE_SIZE, true, 3);
} else {
// 写位改变,必须展开连续块
__contpte_try_unfold(vma->vm_mm, addr, ptep, orig_pte);
// 然后更新单个PTE
__ptep_set_access_flags(vma, addr, ptep, entry, dirty);
}
return 1; // 表示PTE已更新
}
关键概念解释:
1. 连续PTE(Contiguous PTE)
- CONT_PTES:连续PTE块中的PTE数量(ARM64通常是16个,64KB块)
- 优势:减少TLB压力,提高大块内存访问性能
- 展开(Unfold):当连续块的属性不一致时需要拆分为单个PTE
2. PTE标志位宏定义(ARM64示例)
#define PTE_VALID (_AT(pteval_t, 1) << 0) // PTE有效
#define PTE_WRITE (_AT(pteval_t, 1) << 1) // 可写
#define PTE_DIRTY (_AT(pteval_t, 1) << 2) // 脏页
#define PTE_AF (_AT(pteval_t, 1) << 3) // 已访问(Accessed)
#define PTE_NG (_AT(pteval_t, 1) << 4) // 非全局
#define PTE_CONT (_AT(pteval_t, 1) << 5) // 连续PTE
#define PTE_PXN (_AT(pteval_t, 1) << 6) // 特权执行从不
#define PTE_UXN (_AT(pteval_t, 1) << 7) // 用户执行从不
#define PTE_RDONLY (_AT(pteval_t, 1) << 8) // 只读(软件位)
3. 原子更新机制
// 使用比较交换(Compare-and-Swap)原子更新
do {
old_pteval = pteval;
// ... 计算新值 ...
pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval, pteval);
} while (pteval != old_pteval);
// 这是经典的"读取-修改-写入"循环模式
4. TLB刷新策略
// 单个页面刷新
flush_tlb_page(vma, address);
// 范围刷新(更高效)
__flush_tlb_range(vma, start_addr, addr, PAGE_SIZE, true, 3);
5. RDONLY位的特殊处理
// RDONLY位是软件位,不是硬件位
// 算法确保设置最宽松的权限:
// 1. 反转entry的RDONLY位:entry ^= PTE_RDONLY
// 2. 反转当前PTE的RDONLY位:pteval ^= PTE_RDONLY
// 3. 合并:pteval |= entry
// 4. 再次反转:pteval ^= PTE_RDONLY
// 这样保证如果任何一个PTE是可写的,整个操作结果就是可写的
处理流程总结:
单个PTE处理:
- 检查必要性:如果新旧PTE相同,直接返回
- 提取标志:只保留关心的标志位
- 原子更新:使用CAS循环原子更新PTE值
- TLB刷新:如果设置了脏位,刷新对应页面的TLB
连续PTE处理:
- 检查变化:如果整个块没有变化,直接返回
- 写位检查:
- 如果写位不变:批量更新所有PTE,最后刷新整个范围TLB
- 如果写位改变:展开连续块,然后按单个PTE处理
关键设计思想:
- 原子性:使用CAS确保PTE更新在并发情况下的正确性
- 性能优化:
- 连续PTE的批量处理减少TLB刷新次数
- 避免不必要的TLB刷新
- 兼容性:处理硬件与软件管理标志的差异
- 选择性展开:只有在必要(写权限改变)时才展开连续块
- 分层设计:ptep_set_access_flags作为分发器,隐藏实现细节
这些函数展示了Linux内核如何高效管理页表标志位,特别是在ARM64连续PTE特性下的优化处理。
Q7
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);
}
static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
return !vma->vm_ops;
}
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
unsigned long addr = vmf->address;
struct folio *folio;
vm_fault_t ret = 0;
int nr_pages = 1;
pte_t entry;
/* File mapping without ->vm_ops ? */
if (vma->vm_flags & VM_SHARED)
return VM_FAULT_SIGBUS;
/*
* Use pte_alloc() instead of pte_alloc_map(), so that OOM can
* be distinguished from a transient failure of pte_offset_map().
*/
if (pte_alloc(vma->vm_mm, vmf->pmd))
return VM_FAULT_OOM;
/* Use the zero-page for reads */
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vma->vm_page_prot));
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (!vmf->pte)
goto unlock;
if (vmf_pte_changed(vmf)) {
update_mmu_tlb(vma, vmf->address, vmf->pte);
goto unlock;
}
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto unlock;
/* Deliver the page fault to userland, check inside PT lock */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
goto setpte;
}
/* Allocate our own private page. */
ret = vmf_anon_prepare(vmf);
if (ret)
return ret;
/* Returns NULL on OOM or ERR_PTR(-EAGAIN) if we must retry the fault */
folio = alloc_anon_folio(vmf);
if (IS_ERR(folio))
return 0;
if (!folio)
goto oom;
nr_pages = folio_nr_pages(folio);
addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);
/*
* The memory barrier inside __folio_mark_uptodate makes sure that
* preceding stores to the page contents become visible before
* the set_pte_at() write.
*/
__folio_mark_uptodate(folio);
entry = folio_mk_pte(folio, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry), vma);
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, addr, &vmf->ptl);
if (!vmf->pte)
goto release;
if (nr_pages == 1 && vmf_pte_changed(vmf)) {
update_mmu_tlb(vma, addr, vmf->pte);
goto release;
} else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages)) {
update_mmu_tlb_range(vma, addr, vmf->pte, nr_pages);
goto release;
}
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto release;
/* Deliver the page fault to userland, check inside PT lock */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
folio_put(folio);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
folio_ref_add(folio, nr_pages - 1);
add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
count_mthp_stat(folio_order(folio), MTHP_STAT_ANON_FAULT_ALLOC);
folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);
folio_add_lru_vma(folio, vma);
setpte:
if (vmf_orig_pte_uffd_wp(vmf))
entry = pte_mkuffd_wp(entry);
set_ptes(vma->vm_mm, addr, vmf->pte, entry, nr_pages);
/* No need to invalidate - it was non-present before */
update_mmu_cache_range(vmf, vma, addr, vmf->pte, nr_pages);
unlock:
if (vmf->pte)
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
release:
folio_put(folio);
goto unlock;
oom:
return VM_FAULT_OOM;
}
static vm_fault_t do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct mm_struct *vm_mm = vma->vm_mm;
vm_fault_t ret;
/*
* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND
*/
if (!vma->vm_ops->fault) {
vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (unlikely(!vmf->pte))
ret = VM_FAULT_SIGBUS;
else {
/*
* Make sure this is not a temporary clearing of pte
* by holding ptl and checking again. A R/M/W update
* of pte involves: take ptl, clearing the pte so that
* we don't have concurrent modification by hardware
* followed by an update.
*/
if (unlikely(pte_none(ptep_get(vmf->pte))))
ret = VM_FAULT_SIGBUS;
else
ret = VM_FAULT_NOPAGE;
pte_unmap_unlock(vmf->pte, vmf->ptl);
}
} else if (!(vmf->flags & FAULT_FLAG_WRITE))
ret = do_read_fault(vmf);
else if (!(vma->vm_flags & VM_SHARED))
ret = do_cow_fault(vmf);
else
ret = do_shared_fault(vmf);
/* preallocated pagetable is unused: free it */
if (vmf->prealloc_pte) {
pte_free(vm_mm, vmf->prealloc_pte);
vmf->prealloc_pte = NULL;
}
return ret;
}
分析
A7
我来详细注释这两个函数,它们是缺页处理的核心部分:
/**
* do_pte_missing - 处理PTE缺失(无映射)的情况
* @vmf: 包含错误信息的vm_fault结构体
*
* 当PTE为空(无映射)时,根据VMA类型分发到不同的处理函数
*/
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); // 处理文件映射缺页
}
/**
* vma_is_anonymous - 判断VMA是否为匿名映射
* @vma: 虚拟内存区域
*
* 匿名映射没有关联的vm_ops(虚拟内存操作)
* 常见匿名映射:堆、栈、共享内存(匿名)
*/
static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
return !vma->vm_ops; // 没有文件操作表示匿名映射
}
/**
* do_anonymous_page - 处理匿名页面缺页
* @vmf: 包含错误信息的vm_fault结构体
*
* 处理匿名内存(堆、栈等)的缺页异常
* 两种情况:1. 读缺页使用零页面 2. 写缺页分配新页面
*/
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
unsigned long addr = vmf->address;
struct folio *folio; // 内存页的数据结构
vm_fault_t ret = 0; // 返回值
int nr_pages = 1; // 页面数量(可能为大页)
pte_t entry; // 要设置的PTE值
/* 文件映射但没有 ->vm_ops?这种情况不应该发生 */
// 如果是共享映射(VM_SHARED)但没有vm_ops,返回总线错误
if (vma->vm_flags & VM_SHARED)
return VM_FAULT_SIGBUS;
/*
* 使用pte_alloc()而不是pte_alloc_map(),以便区分OOM
* 和pte_offset_map()的临时失败。
*/
// 分配PTE表(如果需要)
if (pte_alloc(vma->vm_mm, vmf->pmd))
return VM_FAULT_OOM; // 内存不足
/* 块1: 读缺页处理 - 使用零页面 */
// 如果是读错误且允许使用零页面
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
// 创建指向零页面的特殊PTE
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vma->vm_page_prot));
// 获取PTE指针并锁定页表
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (!vmf->pte)
goto unlock;
// 检查PTE是否被并发修改
if (vmf_pte_changed(vmf)) {
update_mmu_tlb(vma, vmf->address, vmf->pte);
goto unlock;
}
// 检查地址空间是否稳定(如mmap序列号)
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto unlock;
/* 将缺页传递给用户空间,在PT锁内检查 */
// 如果使用userfaultfd,则让用户空间处理缺页
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
goto setpte; // 跳转到设置PTE
}
/* 块2: 写缺页处理 - 分配私有页面 */
// 准备匿名页面分配(检查资源限制等)
ret = vmf_anon_prepare(vmf);
if (ret)
return ret;
/* 分配我们自己的私有页面。 */
// 分配匿名页面:成功返回folio,NULL表示OOM,ERR_PTR(-EAGAIN)表示需要重试
folio = alloc_anon_folio(vmf);
if (IS_ERR(folio))
return 0; // 重试错误
if (!folio)
goto oom; // 内存不足
// 获取folio包含的页面数(可能是大页)
nr_pages = folio_nr_pages(folio);
// 地址对齐到大页边界
addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);
/*
* __folio_mark_uptodate中的内存屏障确保在set_pte_at()写入之前,
* 对页面内容的先前存储变得可见。
*/
// 标记页面内容已就绪(用于文件映射的预读优化)
__folio_mark_uptodate(folio);
// 创建PTE条目,包含年轻标志(Accessed位)
entry = folio_mk_pte(folio, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry);
// 如果VMA有写权限,设置PTE的写标志和脏标志
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry), vma);
// 获取PTE指针并锁定
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, addr, &vmf->ptl);
if (!vmf->pte)
goto release;
// 检查PTE是否被并发修改(单个页面情况)
if (nr_pages == 1 && vmf_pte_changed(vmf)) {
update_mmu_tlb(vma, addr, vmf->pte);
goto release;
}
// 检查大页范围的PTE是否被并发修改
else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages)) {
update_mmu_tlb_range(vma, addr, vmf->pte, nr_pages);
goto release;
}
// 再次检查地址空间稳定性
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto release;
/* 将缺页传递给用户空间,在PT锁内检查 */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
folio_put(folio);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
// 增加folio引用计数(减去已有一个引用)
folio_ref_add(folio, nr_pages - 1);
// 增加匿名页面计数器
add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
// 统计透明大页分配
count_mthp_stat(folio_order(folio), MTHP_STAT_ANON_FAULT_ALLOC);
// 添加反向映射(rmap)条目,标记为独占(无共享)
folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);
// 将页面添加到LRU链表
folio_add_lru_vma(folio, vma);
setpte:
// 如果原始PTE有userfaultfd写保护标志,则设置新PTE的uffd_wp标志
if (vmf_orig_pte_uffd_wp(vmf))
entry = pte_mkuffd_wp(entry);
// 设置PTE(可能设置多个页面)
set_ptes(vma->vm_mm, addr, vmf->pte, entry, nr_pages);
/* 无需无效化TLB - 它之前不存在 */
// 更新MMU缓存
update_mmu_cache_range(vmf, vma, addr, vmf->pte, nr_pages);
unlock:
// 解锁并解除PTE映射
if (vmf->pte)
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
release:
// 释放folio并跳转到解锁
folio_put(folio);
goto unlock;
oom:
return VM_FAULT_OOM; // 内存不足
}
/**
* do_fault - 处理文件映射缺页
* @vmf: 包含错误信息的vm_fault结构体
*
* 处理文件映射内存(mmap文件)的缺页异常
* 三种情况:读缺页、私有写缺页(COW)、共享写缺页
*/
static vm_fault_t do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct mm_struct *vm_mm = vma->vm_mm;
vm_fault_t ret;
/*
* VMA在mmap()时未完全填充或缺少VM_DONTEXPAND
*/
// 如果没有定义fault操作,说明VMA不支持缺页处理
if (!vma->vm_ops->fault) {
vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (unlikely(!vmf->pte))
ret = VM_FAULT_SIGBUS; // 获取PTE失败,返回总线错误
else {
/*
* 确保这不是PTE的临时清除。通过持有ptl并再次检查。
* PTE的读/修改/写更新涉及:获取ptl,清除PTE以便
* 我们没有硬件的并发修改,然后进行更新。
*/
if (unlikely(pte_none(ptep_get(vmf->pte))))
ret = VM_FAULT_SIGBUS; // PTE仍然为空,总线错误
else
ret = VM_FAULT_NOPAGE; // PTE存在但不是PRESENT状态
pte_unmap_unlock(vmf->pte, vmf->ptl);
}
}
// 如果是读错误
else if (!(vmf->flags & FAULT_FLAG_WRITE))
ret = do_read_fault(vmf); // 读缺页:从文件读取
// 如果是私有映射的写错误(VM_SHARED未设置)
else if (!(vma->vm_flags & VM_SHARED))
ret = do_cow_fault(vmf); // 写时复制:分配新页面并复制内容
// 如果是共享映射的写错误
else
ret = do_shared_fault(vmf); // 共享写:更新文件并保持共享
/* 预分配的页表未使用:释放它 */
if (vmf->prealloc_pte) {
pte_free(vm_mm, vmf->prealloc_pte);
vmf->prealloc_pte = NULL;
}
return ret;
}
关键概念解释:
1. 匿名映射 vs 文件映射
// 匿名映射:没有关联的文件
// - 堆(malloc)、栈、匿名共享内存
// - 缺页时:读使用零页面,写分配新页面
// 文件映射:关联到文件(mmap)
// - 可执行文件、共享库、文件IO
// - 缺页时:从文件读取数据
2. 零页面(Zero Page)
- 特殊的只读页面,所有字节为0
- 用于匿名映射的读缺页,避免分配物理内存
- 当写入时触发COW,分配真正的物理页面
- 节省内存,因为许多初始化为0的页面可以共享同一个零页面
3. 三种文件映射缺页类型
// 1. 读缺页(do_read_fault)
// - 从文件读取数据到页面
// - 设置页面为只读(如果文件是只读的)
// 2. 私有写缺页(do_cow_fault)
// - 写时复制:分配新页面,复制文件数据
// - 新页面可写,与原文件解耦
// - 用于私有映射(如可执行文件的.data段)
// 3. 共享写缺页(do_shared_fault)
// - 直接修改文件映射的页面
// - 可能写回文件(取决于同步策略)
// - 用于共享映射(如进程间通信)
4. userfaultfd机制
- 用户空间缺页处理框架
- 允许用户空间程序处理自己的缺页异常
- 用于:检查点/恢复、内存迁移、调试等
- 当检测到userfaultfd时,将缺页传递给用户空间处理
5. 透明大页(Transparent Huge Page)
// 检查是否允许分配大页
thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PMD_ORDER)
// 统计大页分配
count_mthp_stat(folio_order(folio), MTHP_STAT_ANON_FAULT_ALLOC)
6. 内存管理关键操作
// 分配匿名页面
folio = alloc_anon_folio(vmf);
// 添加反向映射(rmap)
folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);
// 添加到LRU链表
folio_add_lru_vma(folio, vma);
// 设置PTE(可能多个页面)
set_ptes(vma->vm_mm, addr, vmf->pte, entry, nr_pages);
处理流程总结:
匿名映射缺页:
- 读缺页:映射到共享的零页面,节省内存
- 写缺页:分配新的物理页面,建立映射
文件映射缺页:
- 检查VMA有效性:是否有fault操作
- 读错误:从文件读取数据到页面
- 私有写错误:写时复制,分配新页面并复制数据
- 共享写错误:直接修改共享页面,可能写回文件
关键设计思想:
- 惰性分配:物理内存只在需要时才分配
- 写时复制:延迟实际的内存复制直到写入时
- 零页面优化:共享零页面节省内存
- 大页支持:透明地使用大页提高性能
- 用户空间处理:支持userfaultfd让用户空间参与缺页处理
- 并发安全:使用锁和原子操作处理并发缺页
这些函数展示了Linux内核如何高效地处理不同类型的内存缺页,平衡性能和内存使用。
Q8
static vm_fault_t do_wp_page(struct vm_fault *vmf)
__releases(vmf->ptl)
{
const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
struct vm_area_struct *vma = vmf->vma;
struct folio *folio = NULL;
pte_t pte;
if (likely(!unshare)) {
if (userfaultfd_pte_wp(vma, ptep_get(vmf->pte))) {
if (!userfaultfd_wp_async(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_WP);
}
/*
* Nothing needed (cache flush, TLB invalidations,
* etc.) because we're only removing the uffd-wp bit,
* which is completely invisible to the user.
*/
pte = pte_clear_uffd_wp(ptep_get(vmf->pte));
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
/*
* Update this to be prepared for following up CoW
* handling
*/
vmf->orig_pte = pte;
}
/*
* Userfaultfd write-protect can defer flushes. Ensure the TLB
* is flushed in this case before copying.
*/
if (unlikely(userfaultfd_wp(vmf->vma) &&
mm_tlb_flush_pending(vmf->vma->vm_mm)))
flush_tlb_page(vmf->vma, vmf->address);
}
vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
if (vmf->page)
folio = page_folio(vmf->page);
/*
* Shared mapping: we are guaranteed to have VM_WRITE and
* FAULT_FLAG_WRITE set at this point.
*/
if (vma->vm_flags & (VM_SHARED | VM_MAYSHARE)) {
/*
* VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
* VM_PFNMAP VMA. FS DAX also wants ops->pfn_mkwrite called.
*
* We should not cow pages in a shared writeable mapping.
* Just mark the pages writable and/or call ops->pfn_mkwrite.
*/
if (!vmf->page || is_fsdax_page(vmf->page)) {
vmf->page = NULL;
return wp_pfn_shared(vmf);
}
return wp_page_shared(vmf, folio);
}
/*
* Private mapping: create an exclusive anonymous page copy if reuse
* is impossible. We might miss VM_WRITE for FOLL_FORCE handling.
*
* If we encounter a page that is marked exclusive, we must reuse
* the page without further checks.
*/
if (folio && folio_test_anon(folio) &&
(PageAnonExclusive(vmf->page) || wp_can_reuse_anon_folio(folio, vma))) {
if (!PageAnonExclusive(vmf->page))
SetPageAnonExclusive(vmf->page);
if (unlikely(unshare)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
wp_page_reuse(vmf, folio);
return 0;
}
/*
* Ok, we need to copy. Oh, well..
*/
if (folio)
folio_get(folio);
pte_unmap_unlock(vmf->pte, vmf->ptl);
#ifdef CONFIG_KSM
if (folio && folio_test_ksm(folio))
count_vm_event(COW_KSM);
#endif
return wp_page_copy(vmf);
}
static vm_fault_t wp_pfn_shared(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
if (vma->vm_ops && vma->vm_ops->pfn_mkwrite) {
vm_fault_t ret;
pte_unmap_unlock(vmf->pte, vmf->ptl);
ret = vmf_can_call_fault(vmf);
if (ret)
return ret;
vmf->flags |= FAULT_FLAG_MKWRITE;
ret = vma->vm_ops->pfn_mkwrite(vmf);
if (ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))
return ret;
return finish_mkwrite_fault(vmf, NULL);
}
wp_page_reuse(vmf, NULL);
return 0;
}
static inline void wp_page_reuse(struct vm_fault *vmf, struct folio *folio)
__releases(vmf->ptl)
{
struct vm_area_struct *vma = vmf->vma;
pte_t entry;
VM_BUG_ON(!(vmf->flags & FAULT_FLAG_WRITE));
VM_WARN_ON(is_zero_pfn(pte_pfn(vmf->orig_pte)));
if (folio) {
VM_BUG_ON(folio_test_anon(folio) &&
!PageAnonExclusive(vmf->page));
/*
* Clear the folio's cpupid information as the existing
* information potentially belongs to a now completely
* unrelated process.
*/
folio_xchg_last_cpupid(folio, (1 << LAST_CPUPID_SHIFT) - 1);
}
flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
entry = pte_mkyoung(vmf->orig_pte);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
if (ptep_set_access_flags(vma, vmf->address, vmf->pte, entry, 1))
update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
pte_unmap_unlock(vmf->pte, vmf->ptl);
count_vm_event(PGREUSE);
}
static vm_fault_t wp_page_shared(struct vm_fault *vmf, struct folio *folio)
__releases(vmf->ptl)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret = 0;
folio_get(folio);
if (vma->vm_ops && vma->vm_ops->page_mkwrite) {
vm_fault_t tmp;
pte_unmap_unlock(vmf->pte, vmf->ptl);
tmp = vmf_can_call_fault(vmf);
if (tmp) {
folio_put(folio);
return tmp;
}
tmp = do_page_mkwrite(vmf, folio);
if (unlikely(!tmp || (tmp &
(VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
folio_put(folio);
return tmp;
}
tmp = finish_mkwrite_fault(vmf, folio);
if (unlikely(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))) {
folio_unlock(folio);
folio_put(folio);
return tmp;
}
} else {
wp_page_reuse(vmf, folio);
folio_lock(folio);
}
ret |= fault_dirty_shared_page(vmf);
folio_put(folio);
return ret;
}
/*
* Handle the case of a page which we actually need to copy to a new page,
* either due to COW or unsharing.
*
* Called with mmap_lock locked and the old page referenced, but
* without the ptl held.
*
* High level logic flow:
*
* - Allocate a page, copy the content of the old page to the new one.
* - Handle book keeping and accounting - cgroups, mmu-notifiers, etc.
* - Take the PTL. If the pte changed, bail out and release the allocated page
* - If the pte is still the way we remember it, update the page table and all
* relevant references. This includes dropping the reference the page-table
* held to the old page, as well as updating the rmap.
* - In any case, unlock the PTL and drop the reference we took to the old page.
*/
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
struct vm_area_struct *vma = vmf->vma;
struct mm_struct *mm = vma->vm_mm;
struct folio *old_folio = NULL;
struct folio *new_folio = NULL;
pte_t entry;
int page_copied = 0;
struct mmu_notifier_range range;
vm_fault_t ret;
bool pfn_is_zero;
delayacct_wpcopy_start();
if (vmf->page)
old_folio = page_folio(vmf->page);
ret = vmf_anon_prepare(vmf);
if (unlikely(ret))
goto out;
pfn_is_zero = is_zero_pfn(pte_pfn(vmf->orig_pte));
new_folio = folio_prealloc(mm, vma, vmf->address, pfn_is_zero);
if (!new_folio)
goto oom;
if (!pfn_is_zero) {
int err;
err = __wp_page_copy_user(&new_folio->page, vmf->page, vmf);
if (err) {
/*
* COW failed, if the fault was solved by other,
* it's fine. If not, userspace would re-fault on
* the same address and we will handle the fault
* from the second attempt.
* The -EHWPOISON case will not be retried.
*/
folio_put(new_folio);
if (old_folio)
folio_put(old_folio);
delayacct_wpcopy_end();
return err == -EHWPOISON ? VM_FAULT_HWPOISON : 0;
}
kmsan_copy_page_meta(&new_folio->page, vmf->page);
}
__folio_mark_uptodate(new_folio);
mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
vmf->address & PAGE_MASK,
(vmf->address & PAGE_MASK) + PAGE_SIZE);
mmu_notifier_invalidate_range_start(&range);
/*
* Re-check the pte - we dropped the lock
*/
vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
if (likely(vmf->pte && pte_same(ptep_get(vmf->pte), vmf->orig_pte))) {
if (old_folio) {
if (!folio_test_anon(old_folio)) {
dec_mm_counter(mm, mm_counter_file(old_folio));
inc_mm_counter(mm, MM_ANONPAGES);
}
} else {
ksm_might_unmap_zero_page(mm, vmf->orig_pte);
inc_mm_counter(mm, MM_ANONPAGES);
}
flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
entry = folio_mk_pte(new_folio, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry);
if (unlikely(unshare)) {
if (pte_soft_dirty(vmf->orig_pte))
entry = pte_mksoft_dirty(entry);
if (pte_uffd_wp(vmf->orig_pte))
entry = pte_mkuffd_wp(entry);
} else {
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
}
/*
* Clear the pte entry and flush it first, before updating the
* pte with the new entry, to keep TLBs on different CPUs in
* sync. This code used to set the new PTE then flush TLBs, but
* that left a window where the new PTE could be loaded into
* some TLBs while the old PTE remains in others.
*/
ptep_clear_flush(vma, vmf->address, vmf->pte);
folio_add_new_anon_rmap(new_folio, vma, vmf->address, RMAP_EXCLUSIVE);
folio_add_lru_vma(new_folio, vma);
BUG_ON(unshare && pte_write(entry));
set_pte_at(mm, vmf->address, vmf->pte, entry);
update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
if (old_folio) {
/*
* Only after switching the pte to the new page may
* we remove the mapcount here. Otherwise another
* process may come and find the rmap count decremented
* before the pte is switched to the new page, and
* "reuse" the old page writing into it while our pte
* here still points into it and can be read by other
* threads.
*
* The critical issue is to order this
* folio_remove_rmap_pte() with the ptp_clear_flush
* above. Those stores are ordered by (if nothing else,)
* the barrier present in the atomic_add_negative
* in folio_remove_rmap_pte();
*
* Then the TLB flush in ptep_clear_flush ensures that
* no process can access the old page before the
* decremented mapcount is visible. And the old page
* cannot be reused until after the decremented
* mapcount is visible. So transitively, TLBs to
* old page will be flushed before it can be reused.
*/
folio_remove_rmap_pte(old_folio, vmf->page, vma);
}
/* Free the old page.. */
new_folio = old_folio;
page_copied = 1;
pte_unmap_unlock(vmf->pte, vmf->ptl);
} else if (vmf->pte) {
update_mmu_tlb(vma, vmf->address, vmf->pte);
pte_unmap_unlock(vmf->pte, vmf->ptl);
}
mmu_notifier_invalidate_range_end(&range);
if (new_folio)
folio_put(new_folio);
if (old_folio) {
if (page_copied)
free_swap_cache(old_folio);
folio_put(old_folio);
}
delayacct_wpcopy_end();
return 0;
oom:
ret = VM_FAULT_OOM;
out:
if (old_folio)
folio_put(old_folio);
delayacct_wpcopy_end();
return ret;
}
A8
好的,我来详细注释这个复杂的写时复制(COW)处理函数:
/**
* do_wp_page - 处理写保护页错误(写时复制)
* @vmf: 包含错误信息的vm_fault结构体
*
* 处理写保护页面的缺页异常,包括COW和UNSHARE操作
*/
static vm_fault_t do_wp_page(struct vm_fault *vmf)
__releases(vmf->ptl) // 函数会释放ptl锁
{
const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE; // 是否是UNSHARE操作
struct vm_area_struct *vma = vmf->vma; // 虚拟内存区域
struct folio *folio = NULL; // 内存页数据结构
pte_t pte; // 临时PTE值
/* 块1: userfaultfd写保护处理 */
if (likely(!unshare)) { // 如果不是UNSHARE操作
// 检查原始PTE是否有userfaultfd写保护标志
if (userfaultfd_pte_wp(vma, ptep_get(vmf->pte))) {
// 如果不是异步模式,需要用户空间处理
if (!userfaultfd_wp_async(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_WP);
}
/*
* 不需要做任何事(缓存刷新、TLB无效等),因为我们只是移除uffd-wp位,
* 这对用户是完全不可见的。
*/
// 清除uffd写保护标志
pte = pte_clear_uffd_wp(ptep_get(vmf->pte));
// 更新PTE
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
/*
* 更新orig_pte,为后续的COW处理做准备
*/
vmf->orig_pte = pte;
}
/*
* Userfaultfd写保护可能延迟刷新。确保在复制之前TLB被刷新。
*/
if (unlikely(userfaultfd_wp(vmf->vma) &&
mm_tlb_flush_pending(vmf->vma->vm_mm)))
flush_tlb_page(vmf->vma, vmf->address); // 刷新单个页面的TLB
}
/* 块2: 获取对应的物理页 */
// 从PTE获取对应的物理页(如果是正常映射)
vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
// 如果有物理页,获取对应的folio
if (vmf->page)
folio = page_folio(vmf->page);
/*
* 共享映射:此时我们保证有VM_WRITE和FAULT_FLAG_WRITE设置
*/
// 检查是否是共享映射
if (vma->vm_flags & (VM_SHARED | VM_MAYSHARE)) {
/*
* VM_MIXEDMAP !pfn_valid()情况,或者在VM_PFNMAP VMA上清除VM_SOFTDIRTY。
* FS DAX也希望调用ops->pfn_mkwrite。
*
* 我们不应该在共享可写映射中COW页面。
* 只需标记页面可写和/或调用ops->pfn_mkwrite。
*/
// 如果没有物理页或是FS DAX页面,处理特殊情况
if (!vmf->page || is_fsdax_page(vmf->page)) {
vmf->page = NULL; // 清空page指针
return wp_pfn_shared(vmf); // 处理PFN特殊映射
}
// 处理普通的共享页面COW
return wp_page_shared(vmf, folio);
}
/*
* 私有映射:如果无法重用,则创建独占的匿名页面副本。
* 我们可能因为FOLL_FORCE处理而缺少VM_WRITE。
*
* 如果我们遇到标记为独占的页面,我们必须重用该页面而不进行进一步检查。
*/
// 检查是否可以重用匿名页面(是否独占)
if (folio && folio_test_anon(folio) &&
(PageAnonExclusive(vmf->page) || wp_can_reuse_anon_folio(folio, vma))) {
// 如果页面还不是独占的,设置为独占
if (!PageAnonExclusive(vmf->page))
SetPageAnonExclusive(vmf->page);
// 如果是UNSHARE操作,直接返回
if (unlikely(unshare)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
// 重用页面:直接修改PTE标志位
wp_page_reuse(vmf, folio);
return 0;
}
/*
* 好的,我们需要复制。唉,好吧...
*/
// 增加页面的引用计数
if (folio)
folio_get(folio);
// 释放页表锁
pte_unmap_unlock(vmf->pte, vmf->ptl);
#ifdef CONFIG_KSM
// 如果启用了KSM,统计KSM COW事件
if (folio && folio_test_ksm(folio))
count_vm_event(COW_KSM);
#endif
// 执行页面复制
return wp_page_copy(vmf);
}
/**
* wp_pfn_shared - 处理共享PFN映射的写保护
* @vmf: 包含错误信息的vm_fault结构体
*
* 处理没有实际页面结构(只有PFN)的共享映射写保护
*/
static vm_fault_t wp_pfn_shared(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
// 如果定义了pfn_mkwrite操作,调用它
if (vma->vm_ops && vma->vm_ops->pfn_mkwrite) {
vm_fault_t ret;
pte_unmap_unlock(vmf->pte, vmf->ptl);
// 检查是否可以调用fault操作
ret = vmf_can_call_fault(vmf);
if (ret)
return ret;
// 设置MKWRITE标志,调用pfn_mkwrite
vmf->flags |= FAULT_FLAG_MKWRITE;
ret = vma->vm_ops->pfn_mkwrite(vmf);
if (ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))
return ret;
// 完成mkwrite操作
return finish_mkwrite_fault(vmf, NULL);
}
// 如果没有pfn_mkwrite操作,直接重用页面
wp_page_reuse(vmf, NULL);
return 0;
}
/**
* wp_page_reuse - 重用现有页面(不复制)
* @vmf: 包含错误信息的vm_fault结构体
* @folio: 要重用的folio
*
* 直接修改PTE的写权限位,而不进行页面复制
*/
static inline void wp_page_reuse(struct vm_fault *vmf, struct folio *folio)
__releases(vmf->ptl) // 函数会释放ptl锁
{
struct vm_area_struct *vma = vmf->vma;
pte_t entry;
// 调试断言:必须是写错误
VM_BUG_ON(!(vmf->flags & FAULT_FLAG_WRITE));
// 警告:不能是零页面
VM_WARN_ON(is_zero_pfn(pte_pfn(vmf->orig_pte)));
if (folio) {
// 调试断言:如果是匿名页,必须是独占的
VM_BUG_ON(folio_test_anon(folio) &&
!PageAnonExclusive(vmf->page));
/*
* 清除folio的cpupid信息,因为现有的信息可能属于一个现在完全无关的进程。
*/
folio_xchg_last_cpupid(folio, (1 << LAST_CPUPID_SHIFT) - 1);
}
// 刷新缓存
flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
// 设置年轻标志和脏标志
entry = pte_mkyoung(vmf->orig_pte);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
// 原子更新PTE访问标志
if (ptep_set_access_flags(vma, vmf->address, vmf->pte, entry, 1))
update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
pte_unmap_unlock(vmf->pte, vmf->ptl);
count_vm_event(PGREUSE); // 统计页面重用事件
}
/**
* wp_page_shared - 处理共享页面的写保护
* @vmf: 包含错误信息的vm_fault结构体
* @folio: 要处理的folio
*
* 处理共享映射页面的COW,可能调用page_mkwrite操作
*/
static vm_fault_t wp_page_shared(struct vm_fault *vmf, struct folio *folio)
__releases(vmf->ptl) // 函数会释放ptl锁
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret = 0;
// 增加引用计数
folio_get(folio);
// 如果定义了page_mkwrite操作
if (vma->vm_ops && vma->vm_ops->page_mkwrite) {
vm_fault_t tmp;
pte_unmap_unlock(vmf->pte, vmf->ptl);
// 检查是否可以调用fault操作
tmp = vmf_can_call_fault(vmf);
if (tmp) {
folio_put(folio);
return tmp;
}
// 执行page_mkwrite操作
tmp = do_page_mkwrite(vmf, folio);
if (unlikely(!tmp || (tmp &
(VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
folio_put(folio);
return tmp;
}
// 完成mkwrite操作
tmp = finish_mkwrite_fault(vmf, folio);
if (unlikely(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))) {
folio_unlock(folio);
folio_put(folio);
return tmp;
}
} else {
// 没有page_mkwrite操作,直接重用页面
wp_page_reuse(vmf, folio);
folio_lock(folio); // 锁定页面
}
// 标记页面为脏
ret |= fault_dirty_shared_page(vmf);
folio_put(folio); // 减少引用计数
return ret;
}
/*
* 处理需要复制到新页面的情况,无论是由于COW还是UNSHARE。
*
* 在持有mmap_lock且旧页面被引用但未持有ptl的情况下调用。
*
* 高级逻辑流程:
*
* - 分配一个页面,将旧页面的内容复制到新页面。
* - 处理簿记和统计 - cgroups、mmu-notifiers等。
* - 获取PTL。如果pte已更改,则退出并释放分配的页面。
* - 如果pte仍然是我们记忆中的样子,更新页表和所有相关引用。
* 这包括减少页表对旧页面的引用,以及更新反向映射(rmap)。
* - 无论如何,解锁PTL并减少我们对旧页面持有的引用。
*/
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE; // 是否是UNSHARE操作
struct vm_area_struct *vma = vmf->vma; // 虚拟内存区域
struct mm_struct *mm = vma->vm_mm; // 内存描述符
struct folio *old_folio = NULL; // 旧页面
struct folio *new_folio = NULL; // 新页面
pte_t entry; // 新PTE值
int page_copied = 0; // 是否成功复制页面
struct mmu_notifier_range range; // MMU通知器范围
vm_fault_t ret; // 返回值
bool pfn_is_zero; // 是否是零页面
// 开始延迟统计
delayacct_wpcopy_start();
// 获取旧页面的folio
if (vmf->page)
old_folio = page_folio(vmf->page);
// 准备匿名页面分配
ret = vmf_anon_prepare(vmf);
if (unlikely(ret))
goto out;
// 检查是否是零页面
pfn_is_zero = is_zero_pfn(pte_pfn(vmf->orig_pte));
// 预分配新页面
new_folio = folio_prealloc(mm, vma, vmf->address, pfn_is_zero);
if (!new_folio)
goto oom; // 内存不足
// 如果不是零页面,需要复制内容
if (!pfn_is_zero) {
int err;
// 复制页面内容
err = __wp_page_copy_user(&new_folio->page, vmf->page, vmf);
if (err) {
/*
* COW失败,如果错误已被其他方式解决,那也没关系。
* 如果没有,用户空间将在相同的地址上重新触发错误,
* 我们将从第二次尝试中处理错误。
* -EHWPOISON情况不会被重试。
*/
folio_put(new_folio);
if (old_folio)
folio_put(old_folio);
delayacct_wpcopy_end();
return err == -EHWPOISON ? VM_FAULT_HWPOISON : 0;
}
// 复制KMSAN元数据
kmsan_copy_page_meta(&new_folio->page, vmf->page);
}
// 标记新页面为就绪状态
__folio_mark_uptodate(new_folio);
// 初始化MMU通知器范围
mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
vmf->address & PAGE_MASK,
(vmf->address & PAGE_MASK) + PAGE_SIZE);
// 开始MMU通知器范围
mmu_notifier_invalidate_range_start(&range);
/*
* 重新检查pte - 我们之前释放了锁
*/
vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
// 检查PTE是否仍然是原始值(没有被并发修改)
if (likely(vmf->pte && pte_same(ptep_get(vmf->pte), vmf->orig_pte))) {
// 更新内存计数器
if (old_folio) {
// 如果是文件映射,减少文件页面计数,增加匿名页面计数
if (!folio_test_anon(old_folio)) {
dec_mm_counter(mm, mm_counter_file(old_folio));
inc_mm_counter(mm, MM_ANONPAGES);
}
} else {
// 如果没有旧页面(零页面),增加匿名页面计数
ksm_might_unmap_zero_page(mm, vmf->orig_pte);
inc_mm_counter(mm, MM_ANONPAGES);
}
// 刷新缓存
flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
// 创建新PTE条目
entry = folio_mk_pte(new_folio, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry); // 设置年轻标志
// 根据unshare设置不同的标志
if (unlikely(unshare)) {
if (pte_soft_dirty(vmf->orig_pte))
entry = pte_mksoft_dirty(entry);
if (pte_uffd_wp(vmf->orig_pte))
entry = pte_mkuffd_wp(entry);
} else {
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
}
/*
* 清除pte条目并首先刷新它,然后在用新条目更新pte之前,
* 以保持不同CPU上的TLB同步。这个代码过去先设置新PTE然后刷新TLB,
* 但那样会留下一个窗口期,新PTE可能被加载到一些TLB中,
* 而旧PTE仍然保留在其他TLB中。
*/
// 清除旧PTE并刷新TLB
ptep_clear_flush(vma, vmf->address, vmf->pte);
// 添加反向映射
folio_add_new_anon_rmap(new_folio, vma, vmf->address, RMAP_EXCLUSIVE);
// 添加到LRU链表
folio_add_lru_vma(new_folio, vma);
// 调试断言:unshare时PTE不能有写权限
BUG_ON(unshare && pte_write(entry));
// 设置新PTE
set_pte_at(mm, vmf->address, vmf->pte, entry);
// 更新MMU缓存
update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
if (old_folio) {
/*
* 只有在将pte切换到新页面后,我们才能在这里移除反向映射计数。
* 否则另一个进程可能在我们将pte切换到新页面之前发现反向映射计数减少,
* 并"重用"旧页面,向其中写入内容,而我们的pte仍然指向它,
* 并且可以被其他线程读取。
*
* 关键问题是使这个folio_remove_rmap_pte()与上面的ptp_clear_flush顺序正确。
* 这些存储操作是通过(如果没有其他东西)folio_remove_rmap_pte()
* 中的atomic_add_negative屏障来排序的。
*
* 然后ptep_clear_flush中的TLB刷新确保在减少的映射计数可见之前,
* 没有进程可以访问旧页面。而旧页面在减少的映射计数可见之前不能被重用。
* 因此,传递性地,在旧页面可以被重用之前,到旧页面的TLB将被刷新。
*/
// 移除旧页面的反向映射
folio_remove_rmap_pte(old_folio, vmf->page, vma);
}
/* 释放旧页面... */
new_folio = old_folio; // 现在new_folio指向旧页面(要释放的)
page_copied = 1; // 标记为已复制
pte_unmap_unlock(vmf->pte, vmf->ptl); // 解锁
} else if (vmf->pte) {
// PTE已改变,更新MMU TLB
update_mmu_tlb(vma, vmf->address, vmf->pte);
pte_unmap_unlock(vmf->pte, vmf->ptl);
}
// 结束MMU通知器范围
mmu_notifier_invalidate_range_end(&range);
// 释放不再需要的页面
if (new_folio)
folio_put(new_folio);
if (old_folio) {
if (page_copied)
free_swap_cache(old_folio); // 如果已复制,释放交换缓存
folio_put(old_folio); // 释放旧页面
}
delayacct_wpcopy_end();
return 0; // 成功
oom:
ret = VM_FAULT_OOM; // 内存不足
out:
if (old_folio)
folio_put(old_folio); // 释放旧页面
delayacct_wpcopy_end();
return ret;
}
关键概念解释:
1. 写时复制(Copy-on-Write)
- 原理:当多个进程共享同一内存页时,只读共享;当有进程尝试写入时,内核会复制该页面,让写入进程拥有自己的副本
- 应用场景:
fork()创建子进程时- 私有文件映射(如可执行文件的.data段)
- 共享匿名映射的写操作
2. UNSHARE标志
- 用途:用于内存去共享操作,如
madvise(MADV_UNSHARE) - 与COW的区别:UNSHARE会立即复制页面,即使还没有写入操作
3. userfaultfd机制
- userfaultfd_pte_wp:检查PTE是否有userfaultfd写保护标志
- 异步模式:允许内核直接处理,无需用户空间介入
- 同步模式:需要用户空间处理缺页
4. 页面类型
- 匿名页面:没有关联的文件(堆、栈)
- 文件映射页面:关联到文件(代码段、数据段)
- 零页面:特殊的全零页面,用于惰性分配
5. 关键标志位
// VM区域标志
VM_SHARED // 共享映射(多个进程可见)
VM_MAYSHARE // 可能共享
VM_WRITE // 可写
// 错误处理标志
FAULT_FLAG_WRITE // 写错误
FAULT_FLAG_UNSHARE // 去共享操作
FAULT_FLAG_MKWRITE // 需要调用mkwrite操作
// 页面标志
PageAnonExclusive // 匿名页面独占标志
6. mmu_notifier机制
- 用于通知其他子系统页表变化
- 如KVM、GPU驱动等需要知道内存映射变化
- invalidate_range_start/end:标记TLB无效化范围
7. 反向映射(Reverse Mapping)
- 目的:从物理页找到所有映射它的虚拟地址
- 操作:
folio_add_new_anon_rmap:添加新的匿名反向映射folio_remove_rmap_pte:移除PTE级别的反向映射
- 重要性:用于页面回收、迁移等操作
处理流程总结:
1. 权限检查
- 检查是否是userfaultfd写保护页面
- 检查是否UNSHARE操作
- 刷新TLB(如果需要)
2. 映射类型判断
- 共享映射:调用pfn_mkwrite或page_mkwrite
- 私有映射:检查是否可以重用页面
3. 页面重用条件
- 页面必须是匿名的
- 页面必须是独占的(或可以安全重用的)
- 不是UNSHARE操作
4. 页面复制流程
- 预分配新页面
- 复制内容(如果不是零页面)
- 重新检查PTE(防止并发修改)
- 更新页表和统计信息
- 刷新TLB和MMU缓存
- 更新反向映射
- 清理旧页面
关键设计思想:
- 惰性复制:只有在真正需要时才复制页面
- 并发安全:使用锁和原子操作处理并发访问
- 性能优化:避免不必要的复制和TLB刷新
- 内存重用:尽可能重用现有页面
- 通知机制:使用mmu_notifier通知相关子系统
这个函数展示了Linux内核写时复制机制的精妙设计,平衡了内存使用效率和性能。
Q9
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
vm_fault_t ret = 0;
struct folio *folio;
/*
* Let's call ->map_pages() first and use ->fault() as fallback
* if page by the offset is not ready to be mapped (cold cache or
* something).
*/
if (should_fault_around(vmf)) {
ret = do_fault_around(vmf);
if (ret)
return ret;
}
ret = vmf_can_call_fault(vmf);
if (ret)
return ret;
ret = __do_fault(vmf);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
ret |= finish_fault(vmf);
folio = page_folio(vmf->page);
folio_unlock(folio);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
folio_put(folio);
return ret;
}
/* Return true if we should do read fault-around, false otherwise */
static inline bool should_fault_around(struct vm_fault *vmf)
{
/* No ->map_pages? No way to fault around... */
if (!vmf->vma->vm_ops->map_pages)
return false;
if (uffd_disable_fault_around(vmf->vma))
return false;
/* A single page implies no faulting 'around' at all. */
return fault_around_pages > 1;
}
/*
* do_fault_around() tries to map few pages around the fault address. The hope
* is that the pages will be needed soon and this will lower the number of
* faults to handle.
*
* It uses vm_ops->map_pages() to map the pages, which skips the page if it's
* not ready to be mapped: not up-to-date, locked, etc.
*
* This function doesn't cross VMA or page table boundaries, in order to call
* map_pages() and acquire a PTE lock only once.
*
* fault_around_pages defines how many pages we'll try to map.
* do_fault_around() expects it to be set to a power of two less than or equal
* to PTRS_PER_PTE.
*
* The virtual address of the area that we map is naturally aligned to
* fault_around_pages * PAGE_SIZE rounded down to the machine page size
* (and therefore to page order). This way it's easier to guarantee
* that we don't cross page table boundaries.
*/
static vm_fault_t do_fault_around(struct vm_fault *vmf)
{
pgoff_t nr_pages = READ_ONCE(fault_around_pages);
pgoff_t pte_off = pte_index(vmf->address);
/* The page offset of vmf->address within the VMA. */
pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
pgoff_t from_pte, to_pte;
vm_fault_t ret;
/* The PTE offset of the start address, clamped to the VMA. */
from_pte = max(ALIGN_DOWN(pte_off, nr_pages),
pte_off - min(pte_off, vma_off));
/* The PTE offset of the end address, clamped to the VMA and PTE. */
to_pte = min3(from_pte + nr_pages, (pgoff_t)PTRS_PER_PTE,
pte_off + vma_pages(vmf->vma) - vma_off) - 1;
if (pmd_none(*vmf->pmd)) {
vmf->prealloc_pte = pte_alloc_one(vmf->vma->vm_mm);
if (!vmf->prealloc_pte)
return VM_FAULT_OOM;
}
rcu_read_lock();
ret = vmf->vma->vm_ops->map_pages(vmf,
vmf->pgoff + from_pte - pte_off,
vmf->pgoff + to_pte - pte_off);
rcu_read_unlock();
return ret;
}
static inline vm_fault_t vmf_can_call_fault(const struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
if (vma->vm_ops->map_pages || !(vmf->flags & FAULT_FLAG_VMA_LOCK))
return 0;
vma_end_read(vma);
return VM_FAULT_RETRY;
}
/*
* The mmap_lock must have been held on entry, and may have been
* released depending on flags and vma->vm_ops->fault() return value.
* See filemap_fault() and __lock_page_retry().
*/
static vm_fault_t __do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct folio *folio;
vm_fault_t ret;
/*
* Preallocate pte before we take page_lock because this might lead to
* deadlocks for memcg reclaim which waits for pages under writeback:
* lock_page(A)
* SetPageWriteback(A)
* unlock_page(A)
* lock_page(B)
* lock_page(B)
* pte_alloc_one
* shrink_folio_list
* wait_on_page_writeback(A)
* SetPageWriteback(B)
* unlock_page(B)
* # flush A, B to clear the writeback
*/
if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) {
vmf->prealloc_pte = pte_alloc_one(vma->vm_mm);
if (!vmf->prealloc_pte)
return VM_FAULT_OOM;
}
ret = vma->vm_ops->fault(vmf);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
VM_FAULT_DONE_COW)))
return ret;
folio = page_folio(vmf->page);
if (unlikely(PageHWPoison(vmf->page))) {
vm_fault_t poisonret = VM_FAULT_HWPOISON;
if (ret & VM_FAULT_LOCKED) {
if (page_mapped(vmf->page))
unmap_mapping_folio(folio);
/* Retry if a clean folio was removed from the cache. */
if (mapping_evict_folio(folio->mapping, folio))
poisonret = VM_FAULT_NOPAGE;
folio_unlock(folio);
}
folio_put(folio);
vmf->page = NULL;
return poisonret;
}
if (unlikely(!(ret & VM_FAULT_LOCKED)))
folio_lock(folio);
else
VM_BUG_ON_PAGE(!folio_test_locked(folio), vmf->page);
return ret;
}
/**
* finish_fault - finish page fault once we have prepared the page to fault
*
* @vmf: structure describing the fault
*
* This function handles all that is needed to finish a page fault once the
* page to fault in is prepared. It handles locking of PTEs, inserts PTE for
* given page, adds reverse page mapping, handles memcg charges and LRU
* addition.
*
* The function expects the page to be locked and on success it consumes a
* reference of a page being mapped (for the PTE which maps it).
*
* Return: %0 on success, %VM_FAULT_ code in case of error.
*/
vm_fault_t finish_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page;
struct folio *folio;
vm_fault_t ret;
bool is_cow = (vmf->flags & FAULT_FLAG_WRITE) &&
!(vma->vm_flags & VM_SHARED);
int type, nr_pages;
unsigned long addr;
bool needs_fallback = false;
fallback:
addr = vmf->address;
/* Did we COW the page? */
if (is_cow)
page = vmf->cow_page;
else
page = vmf->page;
folio = page_folio(page);
/*
* check even for read faults because we might have lost our CoWed
* page
*/
if (!(vma->vm_flags & VM_SHARED)) {
ret = check_stable_address_space(vma->vm_mm);
if (ret)
return ret;
}
if (!needs_fallback && vma->vm_file) {
struct address_space *mapping = vma->vm_file->f_mapping;
pgoff_t file_end;
file_end = DIV_ROUND_UP(i_size_read(mapping->host), PAGE_SIZE);
/*
* Do not allow to map with PTEs beyond i_size and with PMD
* across i_size to preserve SIGBUS semantics.
*
* Make an exception for shmem/tmpfs that for long time
* intentionally mapped with PMDs across i_size.
*/
needs_fallback = !shmem_mapping(mapping) &&
file_end < folio_next_index(folio);
}
if (pmd_none(*vmf->pmd)) {
if (!needs_fallback && folio_test_pmd_mappable(folio)) {
ret = do_set_pmd(vmf, folio, page);
if (ret != VM_FAULT_FALLBACK)
return ret;
}
if (vmf->prealloc_pte)
pmd_install(vma->vm_mm, vmf->pmd, &vmf->prealloc_pte);
else if (unlikely(pte_alloc(vma->vm_mm, vmf->pmd)))
return VM_FAULT_OOM;
}
nr_pages = folio_nr_pages(folio);
/* Using per-page fault to maintain the uffd semantics */
if (unlikely(userfaultfd_armed(vma)) || unlikely(needs_fallback)) {
nr_pages = 1;
} else if (nr_pages > 1) {
pgoff_t idx = folio_page_idx(folio, page);
/* The page offset of vmf->address within the VMA. */
pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
/* The index of the entry in the pagetable for fault page. */
pgoff_t pte_off = pte_index(vmf->address);
/*
* Fallback to per-page fault in case the folio size in page
* cache beyond the VMA limits and PMD pagetable limits.
*/
if (unlikely(vma_off < idx ||
vma_off + (nr_pages - idx) > vma_pages(vma) ||
pte_off < idx ||
pte_off + (nr_pages - idx) > PTRS_PER_PTE)) {
nr_pages = 1;
} else {
/* Now we can set mappings for the whole large folio. */
addr = vmf->address - idx * PAGE_SIZE;
page = &folio->page;
}
}
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
addr, &vmf->ptl);
if (!vmf->pte)
return VM_FAULT_NOPAGE;
/* Re-check under ptl */
if (nr_pages == 1 && unlikely(vmf_pte_changed(vmf))) {
update_mmu_tlb(vma, addr, vmf->pte);
ret = VM_FAULT_NOPAGE;
goto unlock;
} else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages)) {
needs_fallback = true;
pte_unmap_unlock(vmf->pte, vmf->ptl);
goto fallback;
}
folio_ref_add(folio, nr_pages - 1);
set_pte_range(vmf, folio, page, nr_pages, addr);
type = is_cow ? MM_ANONPAGES : mm_counter_file(folio);
add_mm_counter(vma->vm_mm, type, nr_pages);
ret = 0;
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
}
/**
* page_folio - Converts from page to folio.
* @p: The page.
*
* Every page is part of a folio. This function cannot be called on a
* NULL pointer.
*
* Context: No reference, nor lock is required on @page. If the caller
* does not hold a reference, this call may race with a folio split, so
* it should re-check the folio still contains this page after gaining
* a reference on the folio.
* Return: The folio which contains this page.
*/
#define page_folio(p) (_Generic((p), \
const struct page *: (const struct folio *)_compound_head(p), \
struct page *: (struct folio *)_compound_head(p)))
/**
* folio_put - Decrement the reference count on a folio.
* @folio: The folio.
*
* If the folio's reference count reaches zero, the memory will be
* released back to the page allocator and may be used by another
* allocation immediately. Do not access the memory or the struct folio
* after calling folio_put() unless you can be sure that it wasn't the
* last reference.
*
* Context: May be called in process or interrupt context, but not in NMI
* context. May be called while holding a spinlock.
*/
static inline void folio_put(struct folio *folio)
{
if (folio_put_testzero(folio))
__folio_put(folio);
}
A9
好的,我来详细注释这些文件映射缺页处理的关键函数:
/**
* do_read_fault - 处理文件映射的读缺页
* @vmf: 包含错误信息的vm_fault结构体
*
* 处理文件映射的读错误(第一次访问文件页)
*/
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
vm_fault_t ret = 0; // 返回值
struct folio *folio; // 内存页数据结构
/*
* 让我们先调用->map_pages(),如果偏移处的页面尚未准备好映射(冷缓存等),
* 则使用->fault()作为回退。
*/
// 尝试fault-around优化:预读周围页面
if (should_fault_around(vmf)) {
ret = do_fault_around(vmf);
if (ret)
return ret;
}
// 检查是否可以调用fault处理函数
ret = vmf_can_call_fault(vmf);
if (ret)
return ret;
// 执行主要的fault处理:从文件读取页面
ret = __do_fault(vmf);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
// 完成fault处理:建立页表映射
ret |= finish_fault(vmf);
// 获取页面的folio并解锁
folio = page_folio(vmf->page);
folio_unlock(folio);
// 如果出错,释放页面引用
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
folio_put(folio);
return ret;
}
/* 如果应该执行读fault-around,返回true,否则返回false */
static inline bool should_fault_around(struct vm_fault *vmf)
{
/* 没有->map_pages?无法进行fault-around... */
if (!vmf->vma->vm_ops->map_pages)
return false;
// 如果userfaultfd禁用了fault-around,返回false
if (uffd_disable_fault_around(vmf->vma))
return false;
/* 单个页面意味着根本不进行fault-around。 */
// 只有fault_around_pages > 1时才启用预读
return fault_around_pages > 1;
}
/*
* do_fault_around()尝试映射故障地址周围的几个页面。
* 希望这些页面很快会被需要,从而减少需要处理的故障数量。
*
* 它使用vm_ops->map_pages()来映射页面,如果页面尚未准备好映射
*(未更新、锁定等),则会跳过该页面。
*
* 此函数不跨越VMA或页表边界,以便只调用一次map_pages()和获取一次PTE锁。
*
* fault_around_pages定义了我们尝试映射的页面数量。
* do_fault_around()期望它被设置为小于或等于PTRS_PER_PTE的2的幂。
*
* 我们映射区域的虚拟地址自然对齐到fault_around_pages * PAGE_SIZE
* 向下舍入到机器页面大小(因此也向下舍入到页面顺序)。
* 这样更容易保证我们不跨越页表边界。
*/
static vm_fault_t do_fault_around(struct vm_fault *vmf)
{
pgoff_t nr_pages = READ_ONCE(fault_around_pages); // 预读页面数
pgoff_t pte_off = pte_index(vmf->address); // 地址在PTE中的偏移
/* vmf->address在VMA内的页面偏移。 */
pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
pgoff_t from_pte, to_pte; // 预读范围的起始和结束PTE偏移
vm_fault_t ret;
/* 起始地址的PTE偏移,钳制到VMA范围内。 */
from_pte = max(ALIGN_DOWN(pte_off, nr_pages),
pte_off - min(pte_off, vma_off));
/* 结束地址的PTE偏移,钳制到VMA和PTE范围内。 */
to_pte = min3(from_pte + nr_pages, (pgoff_t)PTRS_PER_PTE,
pte_off + vma_pages(vmf->vma) - vma_off) - 1;
// 如果PMD为空,预分配PTE表
if (pmd_none(*vmf->pmd)) {
vmf->prealloc_pte = pte_alloc_one(vmf->vma->vm_mm);
if (!vmf->prealloc_pte)
return VM_FAULT_OOM;
}
// 在RCU保护下调用map_pages进行预读
rcu_read_lock();
ret = vmf->vma->vm_ops->map_pages(vmf,
vmf->pgoff + from_pte - pte_off, // 起始文件偏移
vmf->pgoff + to_pte - pte_off); // 结束文件偏移
rcu_read_unlock();
return ret;
}
/**
* vmf_can_call_fault - 检查是否可以调用fault处理函数
* @vmf: 包含错误信息的vm_fault结构体
*
* 当使用VMA_LOCK时,需要检查是否允许调用fault操作
*/
static inline vm_fault_t vmf_can_call_fault(const struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
// 如果有map_pages操作,或者不是VMA_LOCK模式,可以直接调用
if (vma->vm_ops->map_pages || !(vmf->flags & FAULT_FLAG_VMA_LOCK))
return 0;
// 如果是VMA_LOCK模式但没有map_pages,需要释放锁并重试
vma_end_read(vma);
return VM_FAULT_RETRY;
}
/*
* 在进入时必须持有mmap_lock,根据flags和vma->vm_ops->fault()返回值
* 可能已释放。参见filemap_fault()和__lock_page_retry()。
*/
static vm_fault_t __do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct folio *folio;
vm_fault_t ret;
/*
* 在获取page_lock之前预分配pte,因为这可能导致memcg回收的死锁,
* 后者等待处于写回状态的页面:
* lock_page(A)
* SetPageWriteback(A)
* unlock_page(A)
* lock_page(B)
* lock_page(B)
* pte_alloc_one
* shrink_folio_list
* wait_on_page_writeback(A)
* SetPageWriteback(B)
* unlock_page(B)
* # 刷新A,B以清除写回状态
*/
// 如果PMD为空且没有预分配PTE,则预分配一个PTE
if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) {
vmf->prealloc_pte = pte_alloc_one(vma->vm_mm);
if (!vmf->prealloc_pte)
return VM_FAULT_OOM;
}
// 调用文件系统或驱动提供的fault处理函数
ret = vma->vm_ops->fault(vmf);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
VM_FAULT_DONE_COW)))
return ret;
// 获取页面对应的folio
folio = page_folio(vmf->page);
// 检查页面是否硬件中毒
if (unlikely(PageHWPoison(vmf->page))) {
vm_fault_t poisonret = VM_FAULT_HWPOISON;
if (ret & VM_FAULT_LOCKED) {
// 如果页面有映射,解除映射
if (page_mapped(vmf->page))
unmap_mapping_folio(folio);
/* 如果从缓存中移除了一个干净的folio,则重试。 */
if (mapping_evict_folio(folio->mapping, folio))
poisonret = VM_FAULT_NOPAGE;
folio_unlock(folio);
}
folio_put(folio);
vmf->page = NULL;
return poisonret;
}
// 如果fault操作没有锁定页面,则锁定它
if (unlikely(!(ret & VM_FAULT_LOCKED)))
folio_lock(folio);
else
// 如果已锁定,进行调试检查
VM_BUG_ON_PAGE(!folio_test_locked(folio), vmf->page);
return ret;
}
/**
* finish_fault - 在准备好要缺页的页面后完成页面错误处理
*
* @vmf: 描述错误的结构体
*
* 此函数处理完成页面错误所需的所有操作:锁定PTE、为给定页面插入PTE、
* 添加反向页面映射、处理memcg计数和LRU添加。
*
* 该函数期望页面被锁定,并且在成功时会消耗被映射页面的引用(对于映射它的PTE)。
*
* 返回值:成功时为%0,错误时为%VM_FAULT_代码。
*/
vm_fault_t finish_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page;
struct folio *folio;
vm_fault_t ret;
bool is_cow = (vmf->flags & FAULT_FLAG_WRITE) &&
!(vma->vm_flags & VM_SHARED); // 是否为COW(写私有映射)
int type, nr_pages;
unsigned long addr;
bool needs_fallback = false; // 是否需要回退到单页处理
fallback:
addr = vmf->address;
/* 我们是否执行了COW? */
if (is_cow)
page = vmf->cow_page; // COW页面
else
page = vmf->page; // 普通页面
folio = page_folio(page);
/*
* 即使是读错误也要检查,因为我们可能丢失了COW页面
*/
if (!(vma->vm_flags & VM_SHARED)) {
ret = check_stable_address_space(vma->vm_mm);
if (ret)
return ret;
}
// 检查是否需要进行回退(页面超出文件大小)
if (!needs_fallback && vma->vm_file) {
struct address_space *mapping = vma->vm_file->f_mapping;
pgoff_t file_end;
// 计算文件总页数
file_end = DIV_ROUND_UP(i_size_read(mapping->host), PAGE_SIZE);
/*
* 不要允许映射超出i_size的PTE,以及跨越i_size的PMD,
* 以保留SIGBUS语义。
*
* 为shmem/tmpfs做一个例外,它们长期以来故意用跨越i_size的PMD映射。
*/
needs_fallback = !shmem_mapping(mapping) &&
file_end < folio_next_index(folio);
}
// 如果PMD为空,尝试使用大页映射
if (pmd_none(*vmf->pmd)) {
// 如果folio支持PMD映射且不需要回退,尝试设置PMD大页
if (!needs_fallback && folio_test_pmd_mappable(folio)) {
ret = do_set_pmd(vmf, folio, page);
if (ret != VM_FAULT_FALLBACK)
return ret;
}
// 安装预分配的PTE或分配新的PTE
if (vmf->prealloc_pte)
pmd_install(vma->vm_mm, vmf->pmd, &vmf->prealloc_pte);
else if (unlikely(pte_alloc(vma->vm_mm, vmf->pmd)))
return VM_FAULT_OOM;
}
nr_pages = folio_nr_pages(folio); // folio包含的页面数
/* 使用每页错误来维护uffd语义 */
if (unlikely(userfaultfd_armed(vma)) || unlikely(needs_fallback)) {
nr_pages = 1; // userfaultfd或需要回退时只处理单页
} else if (nr_pages > 1) {
// 检查大页是否在VMA和PTE边界内
pgoff_t idx = folio_page_idx(folio, page);
/* vmf->address在VMA内的页面偏移。 */
pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
/* 错误页面在页表中的条目索引。 */
pgoff_t pte_off = pte_index(vmf->address);
/*
* 如果页面缓存中的folio大小超过VMA限制和PMD页表限制,
* 则回退到每页错误。
*/
if (unlikely(vma_off < idx ||
vma_off + (nr_pages - idx) > vma_pages(vma) ||
pte_off < idx ||
pte_off + (nr_pages - idx) > PTRS_PER_PTE)) {
nr_pages = 1;
} else {
/* 现在我们可以为整个大folio设置映射。 */
addr = vmf->address - idx * PAGE_SIZE;
page = &folio->page; // 指向folio的第一个页面
}
}
// 获取PTE指针和锁
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
addr, &vmf->ptl);
if (!vmf->pte)
return VM_FAULT_NOPAGE;
/* 在ptl下重新检查 */
if (nr_pages == 1 && unlikely(vmf_pte_changed(vmf))) {
// 单页被并发修改,更新TLB
update_mmu_tlb(vma, addr, vmf->pte);
ret = VM_FAULT_NOPAGE;
goto unlock;
} else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages)) {
// 大页范围被占用,需要回退
needs_fallback = true;
pte_unmap_unlock(vmf->pte, vmf->ptl);
goto fallback;
}
// 增加folio引用计数
folio_ref_add(folio, nr_pages - 1);
// 设置PTE映射
set_pte_range(vmf, folio, page, nr_pages, addr);
// 根据是否为COW选择计数器类型
type = is_cow ? MM_ANONPAGES : mm_counter_file(folio);
// 增加内存计数器
add_mm_counter(vma->vm_mm, type, nr_pages);
ret = 0;
unlock:
// 解锁并解除PTE映射
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
}
/**
* page_folio - 从page转换为folio
* @p: 页面指针
*
* 每个页面都是一个folio的一部分。此函数不能在NULL指针上调用。
*
* 上下文:不需要引用或锁定@p。如果调用者不持有引用,
* 此调用可能与folio拆分竞争,因此应该在获得folio上的引用后
* 重新检查该folio是否仍包含此页面。
* 返回值:包含此页面的folio。
*/
#define page_folio(p) (_Generic((p), \
const struct page *: (const struct folio *)_compound_head(p), \
struct page *: (struct folio *)_compound_head(p)))
/**
* folio_put - 减少folio的引用计数
* @folio: folio
*
* 如果folio的引用计数达到零,内存将被释放回页面分配器,
* 并可能立即被另一个分配使用。在调用folio_put()后,
* 除非你能确定它不是最后一个引用,否则不要访问内存或struct folio。
*
* 上下文:可以在进程或中断上下文中调用,但不能在NMI上下文中调用。
* 可以在持有自旋锁时调用。
*/
static inline void folio_put(struct folio *folio)
{
if (folio_put_testzero(folio))
__folio_put(folio);
}
关键概念解释:
1. Fault-around预读优化
// 原理:预读周围的页面到内存,减少后续缺页异常
// 触发条件:fault_around_pages > 1(通常16或32)
// 机制:调用vm_ops->map_pages()预读多个页面
2. 文件映射的fault处理流程
// 1. 尝试预读(do_fault_around)
// 2. 检查是否可以调用fault(vmf_can_call_fault)
// 3. 执行主要读取(__do_fault)
// 4. 完成映射(finish_fault)
3. vm_ops操作结构
// 文件系统/驱动提供的虚拟内存操作
struct vm_operations_struct {
vm_fault_t (*fault)(struct vm_fault *vmf); // 主要fault处理
vm_fault_t (*map_pages)(struct vm_fault *vmf, // 预读多个页面
pgoff_t start, pgoff_t end);
void (*page_mkwrite)(struct vm_fault *vmf); // 页面变为可写
vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf); // PFN映射变为可写
};
4. 页面引用计数管理
// 引用计数操作:
folio_ref_add(folio, nr_pages - 1); // 增加引用
folio_put(folio); // 减少引用
// 重要规则:finish_fault成功后会消耗一个引用
5. 大页映射与回退机制
// 尝试使用大页(PMD级别)映射
if (folio_test_pmd_mappable(folio))
ret = do_set_pmd(vmf, folio, page);
// 回退条件:
// 1. 超出文件大小
// 2. 超出VMA边界
// 3. 超出页表边界
// 4. 被userfaultfd保护
6. COW与共享映射
// COW(写私有映射)
bool is_cow = (vmf->flags & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED);
// 共享映射:多个进程共享同一物理页
// 私有映射:写时复制,每个进程有自己的副本
7. 内存计数器类型
// 匿名页面计数
MM_ANONPAGES
// 文件页面计数
mm_counter_file(folio) // 根据folio类型返回MM_FILEPAGES或MM_SHMEMPAGES
处理流程总结:
1. 预读阶段
- 检查是否启用fault-around
- 计算预读范围(确保不跨越VMA/页表边界)
- 调用map_pages预读多个页面
2. 主要读取阶段
- 预分配PTE(避免死锁)
- 调用文件系统的fault处理函数
- 处理硬件中毒页面
- 确保页面已锁定
3. 映射完成阶段
- 检查是否需要回退(超出边界等)
- 尝试大页映射
- 建立PTE映射
- 更新反向映射和内存统计
- 处理引用计数
关键设计思想:
- 预读优化:fault-around减少后续缺页次数
- 大页优先:尝试使用大页映射提高性能
- 安全回退:大页失败时优雅回退到单页
- 并发安全:使用锁保护页表操作
- 内存统计:准确统计不同类型的内存使用
- 引用计数:精确管理页面生命周期
这些函数展示了Linux内核如何高效处理文件映射的缺页异常,结合了性能优化(预读、大页)和鲁棒性(错误处理、边界检查)。
Q10
/**
* filemap_fault - read in file data for page fault handling
* @vmf: struct vm_fault containing details of the fault
*
* filemap_fault() is invoked via the vma operations vector for a
* mapped memory region to read in file data during a page fault.
*
* The goto's are kind of ugly, but this streamlines the normal case of having
* it in the page cache, and handles the special cases reasonably without
* having a lot of duplicated code.
*
* vma->vm_mm->mmap_lock must be held on entry.
*
* If our return value has VM_FAULT_RETRY set, it's because the mmap_lock
* may be dropped before doing I/O or by lock_folio_maybe_drop_mmap().
*
* If our return value does not have VM_FAULT_RETRY set, the mmap_lock
* has not been released.
*
* We never return with VM_FAULT_RETRY and a bit from VM_FAULT_ERROR set.
*
* Return: bitwise-OR of %VM_FAULT_ codes.
*/
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
int error;
struct file *file = vmf->vma->vm_file;
struct file *fpin = NULL;
struct address_space *mapping = file->f_mapping;
struct inode *inode = mapping->host;
pgoff_t max_idx, index = vmf->pgoff;
struct folio *folio;
vm_fault_t ret = 0;
bool mapping_locked = false;
max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
if (unlikely(index >= max_idx))
return VM_FAULT_SIGBUS;
trace_mm_filemap_fault(mapping, index);
/*
* Do we have something in the page cache already?
*/
folio = filemap_get_folio(mapping, index);
if (likely(!IS_ERR(folio))) {
/*
* We found the page, so try async readahead before waiting for
* the lock.
*/
if (!(vmf->flags & FAULT_FLAG_TRIED))
fpin = do_async_mmap_readahead(vmf, folio);
if (unlikely(!folio_test_uptodate(folio))) {
filemap_invalidate_lock_shared(mapping);
mapping_locked = true;
}
} else {
ret = filemap_fault_recheck_pte_none(vmf);
if (unlikely(ret))
return ret;
/* No page in the page cache at all */
count_vm_event(PGMAJFAULT);
count_memcg_event_mm(vmf->vma->vm_mm, PGMAJFAULT);
ret = VM_FAULT_MAJOR;
fpin = do_sync_mmap_readahead(vmf);
retry_find:
/*
* See comment in filemap_create_folio() why we need
* invalidate_lock
*/
if (!mapping_locked) {
filemap_invalidate_lock_shared(mapping);
mapping_locked = true;
}
folio = __filemap_get_folio(mapping, index,
FGP_CREAT|FGP_FOR_MMAP,
vmf->gfp_mask);
if (IS_ERR(folio)) {
if (fpin)
goto out_retry;
filemap_invalidate_unlock_shared(mapping);
return VM_FAULT_OOM;
}
}
if (!lock_folio_maybe_drop_mmap(vmf, folio, &fpin))
goto out_retry;
/* Did it get truncated? */
if (unlikely(folio->mapping != mapping)) {
folio_unlock(folio);
folio_put(folio);
goto retry_find;
}
VM_BUG_ON_FOLIO(!folio_contains(folio, index), folio);
/*
* We have a locked folio in the page cache, now we need to check
* that it's up-to-date. If not, it is going to be due to an error,
* or because readahead was otherwise unable to retrieve it.
*/
if (unlikely(!folio_test_uptodate(folio))) {
/*
* If the invalidate lock is not held, the folio was in cache
* and uptodate and now it is not. Strange but possible since we
* didn't hold the page lock all the time. Let's drop
* everything, get the invalidate lock and try again.
*/
if (!mapping_locked) {
folio_unlock(folio);
folio_put(folio);
goto retry_find;
}
/*
* OK, the folio is really not uptodate. This can be because the
* VMA has the VM_RAND_READ flag set, or because an error
* arose. Let's read it in directly.
*/
goto page_not_uptodate;
}
/*
* We've made it this far and we had to drop our mmap_lock, now is the
* time to return to the upper layer and have it re-find the vma and
* redo the fault.
*/
if (fpin) {
folio_unlock(folio);
goto out_retry;
}
if (mapping_locked)
filemap_invalidate_unlock_shared(mapping);
/*
* Found the page and have a reference on it.
* We must recheck i_size under page lock.
*/
max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
if (unlikely(index >= max_idx)) {
folio_unlock(folio);
folio_put(folio);
return VM_FAULT_SIGBUS;
}
vmf->page = folio_file_page(folio, index);
return ret | VM_FAULT_LOCKED;
page_not_uptodate:
/*
* Umm, take care of errors if the page isn't up-to-date.
* Try to re-read it _once_. We do this synchronously,
* because there really aren't any performance issues here
* and we need to check for errors.
*/
fpin = maybe_unlock_mmap_for_io(vmf, fpin);
error = filemap_read_folio(file, mapping->a_ops->read_folio, folio);
if (fpin)
goto out_retry;
folio_put(folio);
if (!error || error == AOP_TRUNCATED_PAGE)
goto retry_find;
filemap_invalidate_unlock_shared(mapping);
return VM_FAULT_SIGBUS;
out_retry:
/*
* We dropped the mmap_lock, we need to return to the fault handler to
* re-find the vma and come back and find our hopefully still populated
* page.
*/
if (!IS_ERR(folio))
folio_put(folio);
if (mapping_locked)
filemap_invalidate_unlock_shared(mapping);
if (fpin)
fput(fpin);
return ret | VM_FAULT_RETRY;
}
A10
我来详细注释这个文件映射缺页处理的核心函数:
/**
* filemap_fault - 为页面错误处理读取文件数据
* @vmf: 包含错误详细信息的struct vm_fault
*
* filemap_fault()通过映射内存区域的vma操作向量调用,
* 在页面错误期间读取文件数据。
*
* goto语句有点丑陋,但这简化了页面缓存的正常情况,
* 并在没有大量重复代码的情况下合理地处理特殊情况。
*
* 进入时必须持有vma->vm_mm->mmap_lock。
*
* 如果我们的返回值设置了VM_FAULT_RETRY,那是因为
* 在执行I/O之前或lock_folio_maybe_drop_mmap()可能释放了mmap_lock。
*
* 如果我们的返回值没有设置VM_FAULT_RETRY,则mmap_lock尚未释放。
*
* 我们从不返回同时设置了VM_FAULT_RETRY和VM_FAULT_ERROR位的值。
*
* 返回值:%VM_FAULT_代码的按位或。
*/
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
int error;
struct file *file = vmf->vma->vm_file; // 关联的文件
struct file *fpin = NULL; // 用于预读的文件指针
struct address_space *mapping = file->f_mapping; // 地址空间(页缓存)
struct inode *inode = mapping->host; // 文件的inode
pgoff_t max_idx, index = vmf->pgoff; // 页面索引(文件偏移)
struct folio *folio; // 内存页数据结构
vm_fault_t ret = 0; // 返回值
bool mapping_locked = false; // 地址空间锁是否已获取
// 计算文件的最大页面索引(文件大小除以页面大小)
max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
// 如果索引超出文件大小,返回SIGBUS
if (unlikely(index >= max_idx))
return VM_FAULT_SIGBUS;
// 跟踪文件映射错误事件
trace_mm_filemap_fault(mapping, index);
/*
* 页面缓存中已经有内容了吗?
*/
// 尝试从页缓存中获取页面
folio = filemap_get_folio(mapping, index);
if (likely(!IS_ERR(folio))) {
/*
* 我们找到了页面,所以在等待锁之前尝试异步预读。
*/
// 如果尚未尝试过预读,执行异步预读
if (!(vmf->flags & FAULT_FLAG_TRIED))
fpin = do_async_mmap_readahead(vmf, folio);
// 如果页面不是最新的(需要从磁盘读取)
if (unlikely(!folio_test_uptodate(folio))) {
// 获取地址空间的共享锁
filemap_invalidate_lock_shared(mapping);
mapping_locked = true;
}
} else {
// 页面不在缓存中,需要从磁盘读取
// 重新检查PTE是否为空(防止竞争)
ret = filemap_fault_recheck_pte_none(vmf);
if (unlikely(ret))
return ret;
/* 页面缓存中完全没有页面 */
// 统计主要错误事件(需要磁盘I/O)
count_vm_event(PGMAJFAULT);
count_memcg_event_mm(vmf->vma->vm_mm, PGMAJFAULT);
ret = VM_FAULT_MAJOR; // 标记为主要错误
// 执行同步预读
fpin = do_sync_mmap_readahead(vmf);
retry_find:
/*
* 参见filemap_create_folio()中的注释,了解为什么需要invalidate_lock
*/
// 确保地址空间锁已获取
if (!mapping_locked) {
filemap_invalidate_lock_shared(mapping);
mapping_locked = true;
}
// 再次尝试获取页面,如果不存在则创建
folio = __filemap_get_folio(mapping, index,
FGP_CREAT|FGP_FOR_MMAP,
vmf->gfp_mask);
if (IS_ERR(folio)) {
// 获取失败,如果有关联的预读文件,重试
if (fpin)
goto out_retry;
// 释放地址空间锁,返回内存不足
filemap_invalidate_unlock_shared(mapping);
return VM_FAULT_OOM;
}
}
/*
* 锁定页面,可能会释放mmap_lock
* 如果不能锁定,跳转到out_retry重试
*/
if (!lock_folio_maybe_drop_mmap(vmf, folio, &fpin))
goto out_retry;
/* 页面是否被截断了? */
// 检查页面的映射是否改变(文件可能被截断)
if (unlikely(folio->mapping != mapping)) {
folio_unlock(folio);
folio_put(folio);
goto retry_find; // 重新查找页面
}
// 调试检查:页面必须包含请求的索引
VM_BUG_ON_FOLIO(!folio_contains(folio, index), folio);
/*
* 我们在页面缓存中有一个锁定的folio,现在我们需要检查
* 它是否是最新的。如果不是,那将是由于错误,
* 或者因为预读无法检索它。
*/
if (unlikely(!folio_test_uptodate(folio))) {
/*
* 如果没有持有invalidate锁,folio在缓存中并且是最新的,
* 但现在不是了。奇怪但可能,因为我们没有一直持有页面锁。
* 让我们丢弃所有东西,获取invalidate锁并重试。
*/
if (!mapping_locked) {
folio_unlock(folio);
folio_put(folio);
goto retry_find;
}
/*
* 好的,folio确实不是最新的。这可能是因为
* VMA设置了VM_RAND_READ标志,或者因为发生了错误。
* 让我们直接读取它。
*/
goto page_not_uptodate;
}
/*
* 我们已经走到这一步,并且我们可能已经释放了mmap_lock,
* 现在是时候返回到上层,让它重新查找vma并重新执行错误。
*/
if (fpin) {
folio_unlock(folio);
goto out_retry;
}
// 释放地址空间锁
if (mapping_locked)
filemap_invalidate_unlock_shared(mapping);
/*
* 找到了页面并持有其引用。
* 我们必须在页面锁下重新检查i_size。
*/
// 再次检查文件大小(在页面锁保护下)
max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
if (unlikely(index >= max_idx)) {
folio_unlock(folio);
folio_put(folio);
return VM_FAULT_SIGBUS; // 页面超出文件大小
}
// 设置返回的页面指针
vmf->page = folio_file_page(folio, index);
// 返回成功,并标记页面已锁定
return ret | VM_FAULT_LOCKED;
page_not_uptodate:
/*
* 嗯,如果页面不是最新的,请处理错误。
* 尝试重新读取它_一次_。我们同步执行此操作,
* 因为这里确实没有性能问题,我们需要检查错误。
*/
// 为I/O准备解锁mmap
fpin = maybe_unlock_mmap_for_io(vmf, fpin);
// 从文件读取页面内容
error = filemap_read_folio(file, mapping->a_ops->read_folio, folio);
if (fpin)
goto out_retry;
// 释放页面引用
folio_put(folio);
// 如果没有错误或页面被截断,重试查找
if (!error || error == AOP_TRUNCATED_PAGE)
goto retry_find;
// 释放地址空间锁,返回总线错误
filemap_invalidate_unlock_shared(mapping);
return VM_FAULT_SIGBUS;
out_retry:
/*
* 我们释放了mmap_lock,需要返回到错误处理程序以
* 重新查找vma,然后回来找到我们希望仍然存在的页面。
*/
if (!IS_ERR(folio))
folio_put(folio); // 释放页面引用
if (mapping_locked)
filemap_invalidate_unlock_shared(mapping); // 释放地址空间锁
if (fpin)
fput(fpin); // 释放预读文件引用
return ret | VM_FAULT_RETRY; // 返回重试标志
}
/**
* folio_file_page - 获取特定索引的页面
* @folio: 包含此索引的folio
* @index: 我们想要查找的索引
*
* 有时在页面缓存中查找folio后,我们需要获取特定索引的页面(例如页面错误处理)。
*
* 原理:folio可能包含多个连续的物理页面(大页),
* 这个函数计算给定文件偏移在folio内的相对页面位置。
*
* 例子:如果folio包含16个页面(64KB),且支持大页,
* 那么index & (folio_nr_pages(folio) - 1)计算在folio内的偏移。
*
* 返回值:包含此索引文件数据的页面。
*/
static inline struct page *folio_file_page(struct folio *folio, pgoff_t index)
{
// 计算索引在folio内的位置:
// 1. folio_nr_pages(folio): 获取folio包含的页面数
// 2. folio_nr_pages(folio) - 1: 创建掩码(例如16个页面则掩码为0b1111)
// 3. index & mask: 获取在folio内的相对偏移
return folio_page(folio, index & (folio_nr_pages(folio) - 1));
}
/**
* folio_page - 从folio中返回一个页面
* @folio: folio
* @n: 要返回的页面号
*
* @n是相对于folio起始位置的页面号。
* 此函数不检查页面号是否在@folio内;假定调用者已经持有页面的引用。
*
* 原理:folio本质上是多个连续物理页面的复合结构,
* 通过folio->page + n可以获取第n个页面的page结构。
*
* 宏定义:避免函数调用开销,直接指针运算
*/
#define folio_page(folio, n) (&(folio)->page + (n))
关键概念解释:
1. 页缓存(Page Cache)
- Linux内核的文件缓存系统
- 缓存最近访问的文件页面
- 通过
address_space结构管理 filemap_get_folio()从页缓存获取页面
2. 页面状态标志
// folio标志位
folio_test_uptodate(folio) // 页面内容是最新的(与磁盘一致)
folio_test_locked(folio) // 页面被锁定(防止并发修改)
3. 预读(Readahead)
- 异步预读:当页面在缓存中但不是最新时触发
- 同步预读:当页面完全不在缓存中时触发
- 目的:预读后续页面,减少后续缺页异常
4. 锁管理
// 地址空间锁(invalidate_lock)
filemap_invalidate_lock_shared(mapping) // 获取共享锁
filemap_invalidate_unlock_shared(mapping) // 释放共享锁
// 页面锁
folio_lock(folio) // 锁定页面
folio_unlock(folio) // 解锁页面
// mmap_lock管理
lock_folio_maybe_drop_mmap() // 锁定页面,可能释放mmap_lock
5. 错误类型
VM_FAULT_MAJOR // 主要错误(需要磁盘I/O)
VM_FAULT_LOCKED // 页面已锁定
VM_FAULT_RETRY // 需要重试(锁被释放)
VM_FAULT_SIGBUS // 总线错误(如超出文件大小)
VM_FAULT_OOM // 内存不足
6. 文件截断(Truncation)处理
// 检查页面是否被截断
if (unlikely(folio->mapping != mapping))
// 处理截断的页面
if (error == AOP_TRUNCATED_PAGE)
goto retry_find;
处理流程总结:
1. 边界检查
- 检查请求的页面是否超出文件大小
- 如果超出,返回
VM_FAULT_SIGBUS
2. 缓存查找
- 命中缓存:检查页面是否最新,尝试异步预读
- 未命中缓存:执行同步预读,创建新页面
3. 页面锁定
- 锁定页面,可能释放mmap_lock
- 检查页面是否被截断
4. 数据有效性检查
- 检查页面内容是否最新
- 如果不是最新,从磁盘读取
5. 最终检查
- 在页面锁保护下重新检查文件大小
- 设置返回的页面指针
6. 错误处理
- 处理I/O错误
- 处理页面截断
- 清理资源,返回适当的错误码
关键设计思想:
- 缓存优先:优先从页缓存获取,减少磁盘I/O
- 惰性读取:只在需要时才从磁盘读取数据
- 预读优化:预测性读取后续页面
- 锁管理优化:必要时释放mmap_lock避免死锁
- 错误恢复:处理截断、I/O错误等特殊情况
- 重试机制:使用
VM_FAULT_RETRY让上层重试
性能优化点:
- 异步预读:当页面在缓存中但不是最新时
- 同步预读:当页面不在缓存中时
- 锁降级:
lock_folio_maybe_drop_mmap()可能释放mmap_lock - 无效锁:保护页面缓存免受截断操作影响
这个函数展示了Linux内核如何处理文件映射的缺页异常,包括缓存管理、锁优化和错误处理等复杂逻辑。