为什么有缺页异常
在malloc()
和mmap()
这两个用户态接口函数的内核实现时,它们只是建立了进程地址空间,在用户空间里可以看到虚拟内存,但没有建立虚拟内存和物理内存之间的映射关系。当进程访问这些还没有建立映射关系的虚拟内存时,处理器自动触发缺页异常,并且linux内核必须处理此异常。
异常分为同步异常和异步异常两种,通常异步异常指的是中断,同步异常指的是异常。
处理器发生缺页异常怎么处理
当处理器发生异常时,处理器会首先跳转到ARM64的异常向量表中。
linux5.10内核中关于异常向量表的描述是arch/arm64/kernel/entry.S
汇编文件中。
/*
* 异常向量表
*/
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
kernel_ventry 1, sync_invalid // 同步异常,发生在异常级别1(EL1)且来自当前任务。
kernel_ventry 1, irq_invalid // 中断请求异常,发生在异常级别1(EL1)且来自当前任务。
kernel_ventry 1, fiq_invalid // 快速中断请求异常,发生在异常级别1(EL1)且来自当前任务。
kernel_ventry 1, error_invalid//错误异常,发生在异常级别1(EL1)且来自当前任务。
kernel_ventry 1, sync // 同步异常,发生在异常级别1(EL1)且来自低优先级的任务。
kernel_ventry 1, irq // 中断请求异常,发生在异常级别1(EL1)且来自低优先级的任务。
kernel_ventry 1, fiq_invalid // 快速中断请求异常,发生在异常级别1(EL1)且来自低优先级的任务。
kernel_ventry 1, error // 错误异常,发生在异常级别1(EL1)且来自低优先级的任务。
kernel_ventry 0, sync // 同步异常,发生在64位异常级别0(EL0)。
kernel_ventry 0, irq // 中断请求异常,发生在64位异常级别0(EL0)。
kernel_ventry 0, fiq_invalid //快速中断请求异常,发生在64位异常级别0(EL0)。
kernel_ventry 0, error // 错误异常,发生在64位异常级别0(EL0)。
#ifdef CONFIG_COMPAT //兼容模式
//特定的兼容处理程序
kernel_ventry 0, sync_compat, 32 //同步异常,发生在32位异常级别0(EL0)。
kernel_ventry 0, irq_compat, 32 // 中断请求异常,发生在32位异常级别0(EL0)。
kernel_ventry 0, fiq_invalid_compat, 32 // 快速中断请求异常,发生在32位异常级别0(EL0)。
kernel_ventry 0, error_compat, 32 // 错误异常,发生在32位异常级别0(EL0)。
#else
//默认的无效处理程序
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 //快速中断请求异常,发生在32位异常级别0(EL0)。
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
SYM_CODE_END(vectors)
ARM架构中有一个与存储访问失效有关的寄存器,即异常综合信息寄存器(ESR)
除了ESR之外,ARM架构还有一个寄存器,失效地址寄存器(FAR),这个寄存器保存了发生异常时的虚拟地址。
以发生在EL1下的数据异常为例,当异常发生后,处理器会首先跳转到ARM64的异常向量表中,在查询异常向量表后跳转到ell_sync()
函数里,并使用ell_sync()
函数读取ESR的值以判断异常类型。根据异常类型,跳转到不同的处理函数里。对于发生在EL1下的数据异常,会跳转到ell_da()
汇编函数里。在ell_da()
汇编函数里读取失效地址寄存器的值,直接调用C的do_mem_abort()
函数。系统通过异常状态表预先列出常见的地址失效处理方案,以页面转换失效和页面访问权限失效为例,do_mem_abort()
函数最后的解决方案是调用do_page_fault
来修复。
缺页流程分析
缺页有下面五种情况
- 访问用户栈的时候,超出了当前用户栈的范围,需要扩大用户栈。
- 当进程申请虚拟内存区域的时候,通常没有分配物理页,进程第一次访问的时候触发页错误异常。
- 内存不足的时候,内核把进程的匿名页换出到交换区。
- 一个文件页被映射到进程的虚拟地址空间,内存不足的时候,内核回收这个文件页,在进程的页表中删除这个文件页的映射。
- 程序错误,访问没有分配给进程的虚拟内存区域。
前四种情况如果页错误异常成功的把虚拟页面映射到物理页面,处理程序返回后,处理器重新执行出发异常的指令。
第五种情况,页错误异常处理程序将会发送段违法信号来杀死程序。
在kmalloc之后,仅仅分配了虚拟地址,但是对应的物理地址还没有分配,这时候,我们操作虚拟地址的时候,就会在页表找不到相应的物理地址,就是产生缺页异常。
处理器生成也错误异常,也错误异常属于同步异常,页错误异常通常交给do_mem_abort函数处理。
do_mem_abort
//addr保存了发生异常的虚拟地址,esr是异常综合信息寄存器,regs为发生异常时的pt_regs指针
void do_mem_abort(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
//根据esr寄存器的ISS字段找到fault_info结构体中相应的处理函数
const struct fault_info *inf = esr_to_fault_info(esr);
if (!inf->fn(addr, esr, regs))//执行fault_info结构体中相应的处理函数
return;//处理成功后返回
//处理失败,后果很严重,下面会输出一些信息后卡死,我们没有必要看
if (!user_mode(regs)) {//如果发生异常时候的pt指针不是用户模式
pr_alert("Unhandled fault at 0x%016lx\n", addr);
mem_abort_decode(esr);//输出内存相关寄存器值
show_pte(addr);//输出当前活动mm中与'addr'相关的页表。
}
//卡死
arm64_notify_die(inf->name, regs,
inf->sig, inf->code, (void __user *)addr, esr);
}
首先通过esr_to_fault_info
函数查询esr寄存器的ISS字段(表示具体的异常指令编码,异常指令编码依赖于不同的异常类型,不同的异常类型有不同的编码格式),然后根据ISS字段去到struct 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_bad, SIGKILL, SI_KERNEL, "unknown 8" },
{ 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_bad, SIGKILL, SI_KERNEL, "unknown 12" },
{ 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_alignment_fault, SIGBUS, BUS_ADRALN, "alignment fault" },
...
{ do_bad, SIGKILL, SI_KERNEL, "unknown 63" },
};
主要的处理函数有几种:
do_translation_fault
:虚拟页没有映射到物理页的do_page_fault
:权限错误,访问错误,无效描述符do_alignment_fault
:没有对齐do_bad
:其他错误
do_translation_fault
static int __kprobes do_translation_fault(unsigned long addr,
unsigned int esr,
struct pt_regs *regs)
{
if (is_ttbr0_addr(addr))//如果是用户虚拟地址
return do_page_fault(addr, esr, regs);//调用do_page_fault申请物理页
do_bad_area(addr, esr, regs);//内核虚拟地址
return 0;
}
虚拟页没有映射到物理页,用户空间的虚拟地址是通过页表映射的,存在申请了虚拟内存但是由于没有使用过导致没有对应的物理内存的情况,而内核空间的虚拟地址是线性映射的,不存在没有映射的情况。
do_translation_fault
会判断一下没有映射的是用户虚拟地址还是内核虚拟地址,如果是用户虚拟地址,就调用do_page_fault
申请物理页并且简历映射关系,如果是内核虚拟地址,就调用do_bad_area
处理这些错误区域。
先看do_bad_area
:
static void do_bad_area(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
/*
* If we are in kernel mode at this point, we have no context to
* handle this fault with.
*/
if (user_mode(regs)) {//处于用户模式,找个进程处理一下
//根据esr寄存器的ISS字段找到fault_info结构体中相应的信号
const struct fault_info *inf = esr_to_fault_info(esr);
set_thread_esr(addr, esr);
//把相应的信号发送到对应的用户态进程,让用户态进程处理信号
arm64_force_sig_fault(inf->sig, inf->code, (void __user *)addr,
inf->name);
} else {//处于内核模式,则没有上下文来处理此错误,卡死吧
__do_kernel_fault(addr, esr, regs);
}
}
如果处于用户模式,根据esr寄存器的ISS字段找到fault_info
结构体中相应的信号,然后调用amr64_force_sig_fault
函数给该进程发送相应的信号。如果处于内核模式,那就没有任何办法了,调用__do_kernel_fault
输出一些信息后卡死。
do_page_fault
static int __kprobes do_page_fault(unsigned long addr, unsigned int esr,
struct pt_regs *regs)
{
const struct fault_info *inf;
struct mm_struct *mm = current->mm;
vm_fault_t fault;
unsigned long vm_flags = VM_ACCESS_FLAGS;
unsigned int mm_flags = FAULT_FLAG_DEFAULT;
//kprobes处理了错误,但是这是不可能的。
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.
*/
//如果current不可以处理fault或者没有mm结构体的时候,说明没有上下文,就去no_context吧
if (faulthandler_disabled() || !mm)
goto no_context;
if (user_mode(regs))//如果是在用户模式下生成的异常
mm_flags |= FAULT_FLAG_USER;//那么 mm_flags 设置标志位FAULT_FLAG_USER
if (is_el0_instruction_abort(esr)) {//如果指令从较低的异常级别中止
vm_flags = VM_EXEC;
mm_flags |= FAULT_FLAG_INSTRUCTION;
} else if (is_write_abort(esr)) {//写数据时生成页错误异常
vm_flags = VM_WRITE;
mm_flags |= FAULT_FLAG_WRITE;
}
//如果虚拟地址是用户态虚拟地址,并且EL1允许fault
if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
//进程在内核模式下把地址上界设置为内核虚拟地址空间上界不能访问用户虚拟地址
if (regs->orig_addr_limit == KERNEL_DS)
die_kernel_fault("access to user memory with fs=KERNEL_DS",
addr, esr, regs);
//如果指令中止没有改变异常级别,说明进程在内核模式下试图执行用户空间的指令
if (is_el1_instruction_abort(esr))
die_kernel_fault("execution of user memory",
addr, esr, regs);
//根据触发异常的指令的虚拟地址在异常表中没有找到异常修正程序
if (!search_exception_tables(regs->pc))
die_kernel_fault("access to user memory outside uaccess routines",
addr, esr, regs);
}
//perf报告缺页时间发生的信息
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
/*
* As per x86, we may deadlock here. However, since the kernel only
* validly references user space from well defined areas of the code,
* we can bug out early if this is from code which shouldn't.
*/
if (!mmap_read_trylock(mm)) {
//如果异常是发生在内核模式,并且在异常表中找到PC
if (!user_mode(regs) && !search_exception_tables(regs->pc))
goto no_context;
retry:
mmap_read_lock(mm);
} else {
/*
* The above down_read_trylock() might have succeeded in which
* case, we'll have missed the might_sleep() from down_read().
*/
might_sleep();
#ifdef CONFIG_DEBUG_VM
//如果异常是发生在内核模式,并且在异常表中没有找到异常修正程序
if (!user_mode(regs) && !search_exception_tables(regs->pc)) {
mmap_read_unlock(mm);
goto no_context;
}
#endif
}
fault = __do_page_fault(mm, addr, mm_flags, vm_flags, regs);//重要的缺页异常处理函数
//如果需要VM_FAULT_RETRY但是current是pending
if (fault_signal_pending(fault, regs)) {
if (!user_mode(regs))//内核进程可以卡死了
goto no_context;
return 0;//用户进程返回0
}
//如果需要VM_FAULT_RETRY但是current不是pending
if (fault & VM_FAULT_RETRY) {
if (mm_flags & FAULT_FLAG_ALLOW_RETRY) {
mm_flags |= FAULT_FLAG_TRIED;
goto retry;//回到上面进行重试
}
}
mmap_read_unlock(mm);
//成功地处理页错误异常,返回。
if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP |
VM_FAULT_BADACCESS))))
return 0;
//来到这里说明处理页错误异常有点问题
//如果current处于内核模式,去到no_context
if (!user_mode(regs))
goto no_context;
if (fault & VM_FAULT_OOM) {//如果是因为内存空间耗尽,就调用oom杀死进程吧
pagefault_out_of_memory();
return 0;
}
//来到这里,说明处理页错误异常有点问题并且是在用户模式下生成的异常
//根据esr寄存器的ISS字段找到fault_info结构体
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, (void __user *)addr,
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, (void __user *)addr, lsb,
inf->name);
} else {
/*
* Something tried to access memory that isn't in our memory
* map.
*/
arm64_force_sig_fault(SIGSEGV,
fault == VM_FAULT_BADACCESS ? SEGV_ACCERR : SEGV_MAPERR,
(void __user *)addr,
inf->name);
}
return 0;
no_context:
__do_kernel_fault(addr, esr, regs);//卡死吧
return 0;
}
do_page_fault
首先判断触发异常的情况是否为执行硬中断、执行软中断、禁止硬中断、禁止软中断、禁止内核抢占这几类原子上下文,这几种情况可以直接卡死,也就是到__do_kernel_fault
。如果在内核模式访问用户虚拟地址的情况也是开始,然后调用_do_page_fault
这个缺页异常处理函数,根据返回值,如果oom或者其他错误都是发送信号杀死进程,成功则返回0。
接着看_do_page_fault
这个函数
static vm_fault_t __do_page_fault(struct mm_struct *mm, unsigned long addr,
unsigned int mm_flags, unsigned long vm_flags,
struct pt_regs *regs)
{
//从current的mm_struct中根据触发异常单的虚拟地址找到对应的vma
struct vm_area_struct *vma = find_vma(mm, addr);
//如果没有找到vma,说明该虚拟地址没有分配给进程,虚拟地址是非法的,
if (unlikely(!vma))
return VM_FAULT_BADMAP;
/*
* Ok, we have a good vm_area for this memory access, so we can handle
* it.
*/
//如果找到的虚拟内存区域的起始地址比触发异常的虚拟地址大
if (unlikely(vma->vm_start > addr)) {
if (!(vma->vm_flags & VM_GROWSDOWN))//这个虚拟内存区域不是栈,返回错误
return VM_FAULT_BADMAP;
if (expand_stack(vma, addr))//扩大栈的虚拟内存区域,失败则返回错误
return VM_FAULT_BADMAP;
}
/*
* Check that the permissions on the VMA allow for the fault which
* occurred.
*/
if (!(vma->vm_flags & vm_flags))//如果虚拟内存区域没有授予触发页错误异常的访问权限
return VM_FAULT_BADACCESS;
return handle_mm_fault(vma, addr & PAGE_MASK, mm_flags, regs);//处理页错误异常的函数
}
可以看到_do_page_fault
首先通过find_vma
根据出发异常的虚拟地址找到对应的vma(进程地址空间),然后判断vma是否合法和是否拥有触发页错误异常的访问权限,没有则返回错误,有的话通过handle_mm_fault
处理页错误异常。
find_vma
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct rb_node *rb_node;
struct vm_area_struct *vma;
/* Check the cache first. */
//在task_struct结构中的vmacache(存放最近访问过的VMA的数组)查找addr
vma = vmacache_find(mm, addr);
if (likely(vma))
return vma;
rb_node = mm->mm_rb.rb_node;//取出current的mm_struct的rb_node
while (rb_node) {//遍历红黑树,找到vma
struct vm_area_struct *tmp;
tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if (tmp->vm_end > addr) {
vma = tmp;
if (tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)//如果找到vma,更新vmacache
vmacache_update(addr, vma);
return vma;
}
首先从current
的task_struct
结构中的vmacache
(存放最近访问过的VMA的数组)查找,看是否有合适的vma
,找不到的话再从其中的mm_struct
查找所有的vma,这些vma组成的是一个红黑树,遍历这个红黑树来查找vma,找到vma则更新vmaacache
,返回vma,找不到就返回null。
handle_mm_fault
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags, struct pt_regs *regs)
{
vm_fault_t ret;
__set_current_state(TASK_RUNNING);//把当前的进程设置为running状态
count_vm_event(PGFAULT);//当前cpu的PGFAULT这个事件数量加一
count_memcg_event_mm(vma->vm_mm, PGFAULT);//对应的memcg的PGFAULT这个事件数量加一
/* do counter updates before entering really critical section. */
check_sync_rss_stat(current);//空函数
if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
flags & FAULT_FLAG_INSTRUCTION,
flags & FAULT_FLAG_REMOTE))
return VM_FAULT_SIGSEGV;
/*
* 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();//设置current的in_user_fault为1
if (unlikely(is_vm_hugetlb_page(vma)))//如果是巨型页
ret = hugetlb_fault(vma->vm_mm, vma, address, flags);//巨型页缺页处理
else//不是巨型页
ret = __handle_mm_fault(vma, address, flags);//普通页缺页处理
if (flags & FAULT_FLAG_USER) {//如果是用户空间触发的故障,需要解除进程状态
mem_cgroup_exit_user_fault();//设置current的in_user_fault为0
/*
* 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.
*/
//如果任务进入了memcg OOM,但是如果正确地处理了分配错误(没有VM_FAULT_OOM),
if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
mem_cgroup_oom_synchronize(false);//直接返回错误,所以看不懂
}
mm_account_fault(regs, address, flags, ret);//统计缺页异常计数
return ret;
}
首先把当前进程设置为running状态,然后设置进程current的in_user_fault
状态为1。接着判断vma是巨型页还是普通页,巨型页则调用hugetlb_fault
进行巨型页缺页处理,普通页则调用_handle_mm_fau
lt进行普通页缺页处理。最后设置进程current的in_user_fault
状态为0,调用mm_account_fault
统计缺页异常计数,返回缺页处理结果。
handle_pte_fault
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,
.flags = flags,
.pgoff = linear_page_index(vma, address),
.gfp_mask = __get_fault_gfp_mask(vma),
};
unsigned int dirty = flags & FAULT_FLAG_WRITE;
struct mm_struct *mm = vma->vm_mm;
pgd_t *pgd;
p4d_t *p4d;
vm_fault_t ret;
pgd = pgd_offset(mm, address);//查找页全局目录表项
p4d = p4d_alloc(mm, pgd, address);//在pgd中查找页四级目录表项,如果不存在则创建页四级目录表项
if (!p4d)
return VM_FAULT_OOM;
vmf.pud = pud_alloc(mm, p4d, address);//在p4d中查找页上层目录表项,如果不存在则创建页上层目录表项
if (!vmf.pud)
return VM_FAULT_OOM;
retry_pud:
if (pud_none(*vmf.pud) && __transparent_hugepage_enabled(vma)) {
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) || pud_devmap(orig_pud)) {
/* NUMA case for anonymous PUDs would go here */
if (dirty && !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);//在pud中查找页中间目录表项,如果不存在则创建页中间目录表项
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) && __transparent_hugepage_enabled(vma)) {
ret = create_huge_pmd(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pmd_t orig_pmd = *vmf.pmd;
barrier();
if (unlikely(is_swap_pmd(orig_pmd))) {
VM_BUG_ON(thp_migration_supported() &&
!is_pmd_migration_entry(orig_pmd));
if (is_pmd_migration_entry(orig_pmd))
pmd_migration_entry_wait(mm, vmf.pmd);
return 0;
}
if (pmd_trans_huge(orig_pmd) || pmd_devmap(orig_pmd)) {
if (pmd_protnone(orig_pmd) && vma_is_accessible(vma))
return do_huge_pmd_numa_page(&vmf, orig_pmd);
if (dirty && !pmd_write(orig_pmd)) {
ret = wp_huge_pmd(&vmf, orig_pmd);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(&vmf, orig_pmd);
return 0;
}
}
}
return handle_pte_fault(&vmf);//在pmd中肯定找不到页表,在这里处理。
}
__handle_mm_fault
函数是普通页缺页处理函数,首先通过mm_struct
找到页全局目录表项,然后在pgd中查找页四级目录表项,如果不存在则创建四级目录表项,然后在p4d中查找页上层目录表项秒如果不存在则创建页上层目录表项,然后在pud中查找页中间目录表项,如果不存在则创建页中间目录表项,最后在pmd中肯定找不到页表,调用handle_pte_fault
处理
handle_pte_fault
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
if (unlikely(pmd_none(*vmf->pmd))) {//如果页中间目录表项是空表项
vmf->pte = NULL;//说明直接页表不存在,vmf->pte 设置成空指针。
} else {//页中间目录表项存在
if (pmd_devmap_trans_unstable(vmf->pmd))
return 0;
/*
* A regular pmd is established and it can't morph into a huge
* pmd from under us anymore at this point because we hold the
* mmap_lock read mode and khugepaged takes it in write mode.
* So now it's safe to run pte_offset_map().
*/
vmf->pte = pte_offset_map(vmf->pmd, vmf->address);//在pmd中查找页表项
vmf->orig_pte = *vmf->pte;//vmf->pte存放表项的地址,vmf->orig_pte存放页表项的值
/*
* some architectures can have larger ptes than wordsize,
* e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and
* CONFIG_32BIT=y, so READ_ONCE cannot guarantee atomic
* accesses. The code below just needs a consistent view
* for the ifs and we later double check anyway with the
* ptl lock held. So here a barrier will do.
*/
barrier();
if (pte_none(vmf->orig_pte)) {//如果页表项是空表项,
pte_unmap(vmf->pte);//空操作
vmf->pte = NULL;//vmf->pte没必要存放表项的地址,设置成空指针
}
}
if (!vmf->pte) {//如果页表项不存在(直接页表不存在或者页表项是空表项)
if (vma_is_anonymous(vmf->vma))//如果是私有匿名映射,
return do_anonymous_page(vmf);//处理匿名页的缺页异常
else
return do_fault(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);//NUMA的情况,涉及的PAGE的迁移
//到这里说明页表项存在,并且页在物理内存中,也就是页错误异常是由访问权限触发的。
vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);//获取页表锁
spin_lock(vmf->ptl);//给页表加锁
entry = vmf->orig_pte;
if (unlikely(!pte_same(*vmf->pte, entry))) {//重新读取页表项的值,如果和前面读取的不相同
update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);//说明其他处理器可能正在修改同一个页表项,不是问题,等待其他处理器处理完就好
goto unlock;
}
if (vmf->flags & FAULT_FLAG_WRITE) {//如果页错误异常是由写操作触发的
if (!pte_write(entry))//如果页表项没有写权限
return do_wp_page(vmf);//执行写时复制
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(vmf->vma, vmf->address, vmf->pte);
} else {//页表项没有发生变化,
//如果这个错误已经试过一次了,返回吧
if (vmf->flags & FAULT_FLAG_TRIED)
goto unlock;
if (vmf->flags & FAULT_FLAG_WRITE)//页错误异常是由写操作触发的
//页错误异常可能是TLB表项和页表项不一致导致的,那么使TLB表项失效
flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);
}
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);//给页表解锁
return 0;
}
处理主要分为两种
- 页表指向的页不在物理内存中
- 页表指向的页在物理内存中
页表指向的页不在物理内存中,就需要根据页的种类来进行物理页的申请和映射了,分别是匿名页缺页、文件页缺页、被交换到swap中、在其他内存节点上。
页表指向的页在物理内存中,首先重新读取页表项的值,如果和前面读取的不相同,说明其他处理器可能正在修改同一个页表项,不是问题,等待其他处理器处理完就好,返回。如果页错误异常是由写操作触发的,并且页表项没有写权限,那就是cow了,需要调用do_wp_page
执行写时复制后返回。
do_alignment_fault
static int do_alignment_fault(unsigned long addr, unsigned int esr,
struct pt_regs *regs)
{
do_bad_area(addr, esr, regs);
return 0;
}
看到do_alignment_fault
代码,我们知道如果遇到内存没有对齐的问题,他直接调用do_bad_area
处理。
do_bad
static int do_bad(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
return 1; /* "fault" */
}
do_bad
里面也没有什么处理,仅仅返回1。
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/7cfca121e5b0452c851da531722e92cd.png
匿名页缺页:do_anonymous_page
在linux内核中,没有关联到文件映射的页面称为匿名页面,例如采用malloc()函数分配的内存或者采用mmap机制分配的匿名映射的内存。
触发匿名页的缺页的情况
- 函数的局部变量较大,或者函数调用的层次比较深,导致当前的栈不够用,需要扩大栈。
- 进程调用 malloc ,从堆申请了内存块,只分配了虚拟内存区域,还没有映射到物理页,第一次访问时触发缺页异常。
- 进程直接调用 mmap ,创建匿名的内存映射,只分配了虚拟内存区域,还没有映射到物理页,第一次访问时触发缺页异常。
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page;
vm_fault_t ret = 0;
pte_t entry;
if (vma->vm_flags & VM_SHARED)//如果是共享的匿名映射(正常的共享映射是文件映射的)
return VM_FAULT_SIGBUS;
/*
* Use pte_alloc() instead of pte_alloc_map(). We can't run
* pte_offset_map() on pmds where a huge pmd might be created
* from a different thread.
*
* pte_alloc_map() is safe to use under mmap_write_lock(mm) or when
* parallel threads are excluded by other means.
*
* Here we only have mmap_read_lock(mm).
*/
if (pte_alloc(vma->vm_mm, vmf->pmd))//如果直接页表不存在,那么分配页表。
return VM_FAULT_OOM;
/* See the comment in pte_alloc_one_map() */
if (unlikely(pmd_trans_unstable(vmf->pmd)))
return 0;
/* 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 (!pte_none(*vmf->pte)) {//如果页表项不是空表项,说明其他cpu在处理,需要等待他完成
update_mmu_tlb(vma, vmf->address, vmf->pte);
goto unlock;
}
ret = check_stable_address_space(vma->vm_mm);//检查给定的内存是否从用户拷贝过来的
if (ret)
goto unlock;//从用户拷贝过来的内存不稳定,不用处理
//userfaultfd(用户页错误文件描述符)作用是解决QEMU/KVM虚拟机动态迁移的问题,我们不看
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
goto setpte;//跳转到标号 setpte 去设置页表项
}
/* Allocate our own private page. */
if (unlikely(anon_vma_prepare(vma)))//初始化vma中的anon_vma_chain和anon_vma
goto oom;
//分配物理页,优先从高端内存区域分配,并且用零初始化。
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
if (!page)
goto oom;
if (mem_cgroup_charge(page, vma->vm_mm, GFP_KERNEL))//cgroup相关,不看
goto oom_free_page;
cgroup_throttle_swaprate(page, GFP_KERNEL);
/*
* The memory barrier inside __SetPageUptodate makes sure that
* preceding stores to the page contents become visible before
* the set_pte_at() write.
*/
__SetPageUptodate(page);//设置页描述符的标志位 PG_uptodate ,表示物理页包含有效的数据
entry = mk_pte(page, vma->vm_page_prot);//使用页帧号和访问权限生成页表项。
entry = pte_sw_mkyoung(entry);//直接返回entry,不知道有什么用
if (vma->vm_flags & VM_WRITE)//如果虚拟内存区域有写权限,
//设置页表项的脏标志位和写权限,脏标志位表示页的数据被修改过
entry = pte_mkwrite(pte_mkdirty(entry));
//在直接页表中查找虚拟地址对应的表项,并且锁住页表。
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
&vmf->ptl);
if (!pte_none(*vmf->pte)) {//如果页表项不是空表项,说明其他处理器可能正在修改同一个页表项,
update_mmu_cache(vma, vmf->address, vmf->pte);
goto release;//处理器只需要等着使用其他处理器设置的页表项,没必要继续处理页错误异常。
}
ret = check_stable_address_space(vma->vm_mm);//检查给定的内存是否从用户拷贝过来的
if (ret)
goto release;//从用户拷贝过来的内存不稳定,不用处理
//userfaultfd(用户页错误文件描述符)作用是解决QEMU/KVM虚拟机动态迁移的问题,我们不看
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
put_page(page);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);//task或者mm的rss_stat的匿名页数量加一
page_add_new_anon_rmap(page, vma, vmf->address, false);//建立物理页到虚拟页的反向映射
lru_cache_add_inactive_or_unevictable(page, vma);//把物理页添加到活动LRU链表或者不可回收LRU链表
setpte:
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);//设置页表项
/* No need to invalidate - it was non-present before */
update_mmu_cache(vma, vmf->address, vmf->pte);//更新处理器的页表缓存
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);//释放页表的锁
return ret;
release:
put_page(page);
goto unlock;
oom_free_page:
put_page(page);
oom:
return VM_FAULT_OOM;
}
首先判断匿名页是否是共享的,如果是共享的匿名映射,但是虚拟内存区域没有提供虚拟内存操作集合,就返回错误;然后判断pte页表是否存在,如果页表不存在就分配页表;接着判断缺页异常是由读操作触发还是写操作触发m,如果是读操作触发的,生成特殊的页表项,映射到专用的零页,设置页表项后返回;如果是写操作触发的,需要初始化vma中的anon_vma_chain
和anon_vma
,分配物理页用于匿名映射,调用mk_pte
函数生成页表项,设置页表项的脏标志位和写权限,设置页表项后返回。
文件页缺页:do_fault
触发文件页缺页异常的情况
- 启动程序的时候,内核为程序的代码段和数据段创建私有的文件映射,映射带进程的虚拟地址空间,第一次访问的时间触发文件页的缺页异常。
- 进程使用mmap创建文件映射,把文件的一个区间映射到进程的虚拟地址空间,第一次访问的时候触发文件页的缺页异常。
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;
if (!vma->vm_ops->fault) {//如果虚拟内存区域没有提供处理页错误异常的方法,返回错误
if (unlikely(!pmd_present(*vmf->pmd)))
ret = VM_FAULT_SIGBUS;
else {
vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm,
vmf->pmd,
vmf->address,
&vmf->ptl);
/*
* 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(*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);//处理写共享文件页错误
if (vmf->prealloc_pte) {//如果预分配的分页未使用
pte_free(vm_mm, vmf->prealloc_pte);//释放预分配的页表项
vmf->prealloc_pte = NULL;
}
return ret;
}
do_fault
如果没有提供处理页错误异常的方法,直接返回错误;如果缺页异常是由读文件页触发的,调用do_read_fault
处理读文件页错误;如果缺页异常是由写私有文件页触发的,抵用do_cow_fault
处理写私有文件页错误,执行写时复制;否则缺页异常就是由共享文件页触发的,调用do_shared_fault
处理写共享文件页错误。最后查看vmf->prealloc_pte
,如果没有开启预分配,则释放预分配的页表项。
主要看读文件错误do_read_fault
、写私有文件错误do_cow_fault
和写共享文件错误do_shared_fault
。
do_read_fault
处理读文件页错误的方法
- 把文件页从存储设备上的文件系统读取到文件的页缓存
- 设置进程的页表项,把虚拟页映射到文件的页缓存中的物理页
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret = 0;
//调用map_pages映射到物理页,如果需要预读
if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
ret = do_fault_around(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);//设置页表项
unlock_page(vmf->page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
put_page(vmf->page);
return ret;
}
首先调用vma中的map_pages
方法,把文件页映射到物理内存上,然后判断如果需要预读,则调用do_fault_around
把预读的文件页也映射到物理内存上;然后调用__do_fault
函数读文件到物理页缓存中;最后调用finish_fault
设置页表项。其中do_fault_around
主要是调用map_pages
方法把预读的文件页映射到物理内存上,__do_fault
则是调用vma中的falut方法读文件到物理页缓存中,finish_fault
设置页表项。
vm_fault_t finish_fault(struct vm_fault *vmf)
{
struct page *page;
vm_fault_t ret = 0;
//如果异常是写操作导致的,并且vma不是共享的
if ((vmf->flags & FAULT_FLAG_WRITE) &&
!(vmf->vma->vm_flags & VM_SHARED))
page = vmf->cow_page;//page指向vmf->cow_page
else
page = vmf->page;//page指向vmf->page
/*
* check even for read faults because we might have lost our CoWed
* page
*/
if (!(vmf->vma->vm_flags & VM_SHARED))//如果是读操作导致的,
ret = check_stable_address_space(vmf->vma->vm_mm);//检查给定的内存是否从用户拷贝过来的
if (!ret)
ret = alloc_set_pte(vmf, page);//设置页表项的主要工作函数
if (vmf->pte)
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
}
finish_fault
负责设置页表项,把主要工作委托给函数 alloc_set_pte
vm_fault_t alloc_set_pte(struct vm_fault *vmf, struct page *page)
{
struct vm_area_struct *vma = vmf->vma;
bool write = vmf->flags & FAULT_FLAG_WRITE;
pte_t entry;
vm_fault_t ret;
if (pmd_none(*vmf->pmd) && PageTransCompound(page)) {
ret = do_set_pmd(vmf, page);
if (ret != VM_FAULT_FALLBACK)
return ret;
}
if (!vmf->pte) {//如果直接页表不存在
ret = pte_alloc_one_map(vmf);//查找页表项
if (ret)
return ret;
}
/* Re-check under ptl */
if (unlikely(!pte_none(*vmf->pte))) {//如果在锁住页表以后发现页表项不是空表项
update_mmu_tlb(vma, vmf->address, vmf->pte);//说明其他处理器修改了同一页表项,那么当前处理器放弃处理
return VM_FAULT_NOPAGE;
}
flush_icache_page(vma, page);//从指令缓存中冲刷页
entry = mk_pte(page, vma->vm_page_prot);//使用页帧号和访问权限生成页表项的值
entry = pte_sw_mkyoung(entry);//直接返回entry,不知道有什么用
if (write)
entry = maybe_mkwrite(pte_mkdirty(entry), vma);//设置页表项的脏标志位和写权限位
/* copy-on-write page */
if (write && !(vma->vm_flags & VM_SHARED)) {//如果写私有文件页
inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);//task或者mm的rss_stat的匿名页数量加一
page_add_new_anon_rmap(page, vma, vmf->address, false);//建立物理页到虚拟页的反向映射
lru_cache_add_inactive_or_unevictable(page, vma);//把物理页添加到活动LRU链表或者不可回收LRU链表
} else {//写私有文件页除外
inc_mm_counter_fast(vma->vm_mm, mm_counter_file(page));//task或者mm的rss_stat的某个种类页数量加一
page_add_file_rmap(page, false);//添加pte映射到文件页面
}
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);设置页表项,把刚刚申请的物理地址写入到页表
/* no need to invalidate: a not-present page won't be cached */
update_mmu_cache(vma, vmf->address, vmf->pte);//更新处理器的页表缓存
return 0;
}
如果发现直接页表不存在,则需要调用pte_alloc_one_map
查找页表项;如果在锁住页表以后发现页表不是空表项,说明其他处理器修改了同一页表项,那么当前处理器不用处理,仅仅更新一下TLB就好;然后调用flush_icache_page
从指令缓存中冲刷页,使用页帧号和访问权限生成页表项的值;如果是写操作导致的缺页异常,则需要设置页表项的脏标志位和写权限位;如果是写私有文件页,需要建立物理页到虚拟页的反向映射,把物理页添加到活动LRU链表或者不可回收LRU链表;如果不是写私有文件,就是写共享文件或者写匿名页或者读操作,则需要添加pte映射到文件页面;最后调用set_pte_at
函数完成页表项的设置,更新处理器的页表缓存。
do_cow_fault
处理私有文件页错误会执行写时复制
- 把文件页从存储设备上的文件系统读到文件的页缓存上
- 执行写时复制时,为文件的页缓存中的物理页创建一个副本,这个副本是进程的私有匿名页,和文件脱离关系,修改副本不会导致文件变化
- 设置进程的页表项,把虚拟页映射到副本
static vm_fault_t do_cow_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret;
if (unlikely(anon_vma_prepare(vma)))//初始化vma中的anon_vma_chain和anon_vma
return VM_FAULT_OOM;
//因为后面需要执行写时复制,所以预先为副本分配一个物理页
vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
if (!vmf->cow_page)
return VM_FAULT_OOM;
if (mem_cgroup_charge(vmf->cow_page, vma->vm_mm, GFP_KERNEL)) {
put_page(vmf->cow_page);
return VM_FAULT_OOM;
}
cgroup_throttle_swaprate(vmf->cow_page, GFP_KERNEL);
ret = __do_fault(vmf);//把文件页读到文件的页缓存中
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
if (ret & VM_FAULT_DONE_COW)
return ret;
//把文件的页缓存中物理页的数据复制到副本物理页
copy_user_highpage(vmf->cow_page, vmf->page, vmf->address, vma);
__SetPageUptodate(vmf->cow_page);//设置副本页描述符的标志位PG_uptodate,表示物理页包含有效的数据
ret |= finish_fault(vmf);//设置页表项,把虚拟页映射到副本物理页
unlock_page(vmf->page);
put_page(vmf->page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
return ret;
uncharge_out:
put_page(vmf->cow_page);
return ret;
}
do_cow_fault
首先调用anon_vma_prepare
初始化vma
中的anon_vma_chain
和anon_vma
,因为后面需要执行写时复制,所以调用alloc_page_vma预
先为副本分配一个物理页,然后调用__do_fault
把文件页读到文件的页缓存中,接着调用copy_user_highpage
把文件的页缓存中物理页的数据复制到副本物理页,同时设置副本页描述符的标志位PG_uptodate
,表示物理页包含有效的数据,最后调用finish_fault
设置页表项。
do_shared_fault
处理写共享文件页错误的方法
- 把文件页从存储设备上的文件系统读到文件的页缓存
- 设置进程的页表项,把虚拟页映射到文件的页缓存中的物理页
static vm_fault_t do_shared_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret, tmp;
ret = __do_fault(vmf);//把文件页读到文件的页缓存中
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
/*
* Check if the backing address space wants to know that the page is
* about to become writable
*/
if (vma->vm_ops->page_mkwrite) {//如果page_mkwrite存在
unlock_page(vmf->page);
tmp = do_page_mkwrite(vmf);//调用page_mkwrite方法通知文件系统“页即将变成可写的”
if (unlikely(!tmp ||s
(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
put_page(vmf->page);
return tmp;
}
}
ret |= finish_fault(vmf);//设置页表项,把虚拟页映射到文件的页缓存中的物理页。
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE |
VM_FAULT_RETRY))) {
unlock_page(vmf->page);
put_page(vmf->page);
return ret;
}
ret |= fault_dirty_shared_page(vmf);//设置页的脏标志位,表示页的数据被修改
return ret;
}
do_shared_fault
首先调用__do_fault
把文件页读到文件的页缓存中;接着判断page_mkwrite
方法是否存在,如果存在则通过do_page_mkwrite
函数调用page_mkwrite
方法通知文件系统“页即将变成可写的”,然后通过finish_fault
函数设置页表项,最后调用fault_dirty_shared_page
设置页的脏标志位,表示页的数据被修改,在fault_dirty_shared_page
函数中会判断,如果page_mkwrite
方法不存在,则仅仅更新文件的修改时间,不会设置页的脏标志位。
页在交换分区中:do_swap_page
执行流程
- 调用函数
pte_to_swap_entry
,把页表项转换为交换项,交换项包含了交换区的索引和偏移 - 调用函数
lookup_swap_cache
,在交换缓存中根据交换区的偏移查找页 - 调用函数
swap_readpage
或者swapin_readahead
从交换区换入页面
vm_fault_t do_swap_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page = NULL, *swapcache;
swp_entry_t entry;
pte_t pte;
int locked;
int exclusive = 0;
vm_fault_t ret = 0;
void *shadow = NULL;
//比较pte页表项的内容和orig_pte,如果不相同则返回作物
if (!pte_unmap_same(vma->vm_mm, vmf->pmd, vmf->pte, vmf->orig_pte))
goto out;
entry = pte_to_swp_entry(vmf->orig_pte);//通过物理地址找到entry
if (unlikely(non_swap_entry(entry))) {//如果是非迁移类型的swap_entry,返回各种错误
if (is_migration_entry(entry)) {
migration_entry_wait(vma->vm_mm, vmf->pmd,
vmf->address);
} else if (is_device_private_entry(entry)) {
vmf->page = device_private_entry_to_page(entry);
ret = vmf->page->pgmap->ops->migrate_to_ram(vmf);
} else if (is_hwpoison_entry(entry)) {
ret = VM_FAULT_HWPOISON;
} else {
print_bad_pte(vma, vmf->address, vmf->orig_pte, NULL);
ret = VM_FAULT_SIGBUS;
}
goto out;
}
delayacct_set_flag(DELAYACCT_PF_SWAPIN);//设置current状态为正在swapin
page = lookup_swap_cache(entry, vma, vmf->address);//在交换缓存中查找页
swapcache = page;
if (!page) {//如果页不在交换缓存中
struct swap_info_struct *si = swp_swap_info(entry);
if (data_race(si->flags & SWP_SYNCHRONOUS_IO) &&
__swap_count(entry) == 1) {//如果是高效同步IO并且数量是1
/* skip swapcache */
page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
vmf->address);//分配一个物理页
if (page) {
int err;
__SetPageLocked(page);
__SetPageSwapBacked(page);
set_page_private(page, entry.val);
/* Tell memcg to use swap ownership records */
SetPageSwapCache(page);
err = mem_cgroup_charge(page, vma->vm_mm,
GFP_KERNEL);
ClearPageSwapCache(page);
if (err) {
ret = VM_FAULT_OOM;
goto out_page;
}
shadow = get_shadow_from_swap_cache(entry);
if (shadow)
workingset_refault(page, shadow);
lru_cache_add(page);//将页面添加到LRU列表中
swap_readpage(page, true);//快速从交换区换入1个页
}
} else {//不是高效IO的情况
page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
vmf);//慢慢的从交换区换入多个页面
swapcache = page;
}
if (!page) {
/*
* Back out if somebody else faulted in this pte
* while we released the pte lock.
*/
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (likely(pte_same(*vmf->pte, vmf->orig_pte)))
ret = VM_FAULT_OOM;
delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
goto unlock;
}
/* Had to read the page from swap area: Major fault */
ret = VM_FAULT_MAJOR;
count_vm_event(PGMAJFAULT);
count_memcg_event_mm(vma->vm_mm, PGMAJFAULT);
} else if (PageHWPoison(page)) {
/*
* hwpoisoned dirty swapcache pages are kept for killing
* owner processes (which may be unknown at hwpoison time)
*/
ret = VM_FAULT_HWPOISON;
delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
goto out_release;
}
locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags);
delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
if (!locked) {
ret |= VM_FAULT_RETRY;
goto out_release;
}
/*
* Make sure try_to_free_swap or reuse_swap_page or swapoff did not
* release the swapcache from under us. The page pin, and pte_same
* test below, are not enough to exclude that. Even if it is still
* swapcache, we need to check that the page's swap has not changed.
*/
if (unlikely((!PageSwapCache(page) ||
page_private(page) != entry.val)) && swapcache)
goto out_page;
page = ksm_might_need_to_copy(page, vma, vmf->address);
if (unlikely(!page)) {
ret = VM_FAULT_OOM;
page = swapcache;
goto out_page;
}
cgroup_throttle_swaprate(page, GFP_KERNEL);//cgroup记账
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
&vmf->ptl);//锁住页表
//直接页表项和锁住页表之前不同,说明其他处理器已经换入页
if (unlikely(!pte_same(*vmf->pte, vmf->orig_pte)))
goto out_nomap;
if (unlikely(!PageUptodate(page))) {
ret = VM_FAULT_SIGBUS;
goto out_nomap;
}
inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);//anonpage数加1,匿名页从swap空间交换出来,所以加1
dec_mm_counter_fast(vma->vm_mm, MM_SWAPENTS);//swap page个数减1,
pte = mk_pte(page, vma->vm_page_prot);//生成页表项的值
if ((vmf->flags & FAULT_FLAG_WRITE) && reuse_swap_page(page, NULL)) {
pte = maybe_mkwrite(pte_mkdirty(pte), vma);
vmf->flags &= ~FAULT_FLAG_WRITE;
ret |= VM_FAULT_WRITE;
exclusive = RMAP_EXCLUSIVE;
}
flush_icache_page(vma, page);
if (pte_swp_soft_dirty(vmf->orig_pte))
pte = pte_mksoft_dirty(pte);
if (pte_swp_uffd_wp(vmf->orig_pte)) {
pte = pte_mkuffd_wp(pte);
pte = pte_wrprotect(pte);
}
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);//将新生成的页表项的值写入页表
arch_do_swap_page(vma->vm_mm, vma, vmf->address, pte, vmf->orig_pte);//空函数
vmf->orig_pte = pte;//更新orig_pte
/* ksm created a completely new copy */
if (unlikely(page != swapcache && swapcache)) {
page_add_new_anon_rmap(page, vma, vmf->address, false);//建立物理页到虚拟页的反向映射
lru_cache_add_inactive_or_unevictable(page, vma);//把物理页添加到活动LRU链表或者不可回收LRU链表
} else {
do_page_add_anon_rmap(page, vma, vmf->address, exclusive);//建立物理页到虚拟页的反向映射
}
swap_free(entry);//通过entry找到swap_info_struct并且释放他
if (mem_cgroup_swap_full(page) ||
(vma->vm_flags & VM_LOCKED) || PageMlocked(page))
try_to_free_swap(page);
unlock_page(page);
if (page != swapcache && swapcache) {
unlock_page(swapcache);
put_page(swapcache);
}
if (vmf->flags & FAULT_FLAG_WRITE) {
ret |= do_wp_page(vmf);//执行的写时复制
if (ret & VM_FAULT_ERROR)
ret &= VM_FAULT_ERROR;
goto out;
}
/* No need to invalidate - it was non-present before */
update_mmu_cache(vma, vmf->address, vmf->pte);//保持缓存一致
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);//释放页表锁
out:
return ret;
out_nomap:
pte_unmap_unlock(vmf->pte, vmf->ptl);
out_page:
unlock_page(page);
out_release:
put_page(page);
if (page != swapcache && swapcache) {
unlock_page(swapcache);
put_page(swapcache);
}
return ret;
}
do_swap_page
首先比较pte页表项的内容和orig_pte
,如果不相同则返回错误;调用函数pte_to_swp_entry
通过物理地址找到entry,如果entry是非迁移类型的,返回各种错误;调用函数delayacct_set_flag
设置current状态为正在swapin,调用函数lookup_swap_cache
在交换缓存中查找页;如果找不到页,说明页不在缓存中,在判断缓存页数量,如果只有一个,并且是高效的swap IO,调用swap_readpage
函数快速读入页面,如果有多个缓存页,则调用swapin_readahead
稍后读入页面;接着anonpage数加1,匿名页从swap空间交换出来,所以加1,swap page个数减1,然后调用函数mk_pte
生成页表项的值和调用函数set_pte_at
将新生成的页表项的值写入页表;最后如果页面具有写权限,调用函数do_wp_page
执行的写时复制。
页在其他内存节点中:do_numa_page
static vm_fault_t do_numa_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page = NULL;
int page_nid = NUMA_NO_NODE;
int last_cpupid;
int target_nid;
bool migrated = false;
pte_t pte, old_pte;
bool was_writable = pte_savedwrite(vmf->orig_pte);
int flags = 0;
/*
* The "pte" at this point cannot be used safely without
* validation through pte_unmap_same(). It's of NUMA type but
* the pfn may be screwed if the read is non atomic.
*/
vmf->ptl = pte_lockptr(vma->vm_mm, vmf->pmd);//获取pmd锁
spin_lock(vmf->ptl);//上锁
//如果页表项和锁住以前的页表项不同
if (unlikely(!pte_same(*vmf->pte, vmf->orig_pte))) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
goto out;//说明其他处理器修改了同一页表项,那么当前处理器放弃更新页表项
}
//启动pte保护read-modify-write事务,防止对pte进行异步硬件修改
old_pte = ptep_modify_prot_start(vma, vmf->address, vmf->pte);
pte = pte_modify(old_pte, vma->vm_page_prot);//保存临时修改的pte
pte = pte_mkyoung(pte);
if (was_writable)
pte = pte_mkwrite(pte);//调用mkwrite方法通知问价那系统可读变为可写
ptep_modify_prot_commit(vma, vmf->address, vmf->pte, old_pte, pte);//更新pte后完成pte的保护
update_mmu_cache(vma, vmf->address, vmf->pte);//刷新mmu的cache
page = vm_normal_page(vma, vmf->address, pte);//获取与pte关联page
if (!page) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
/* TODO: handle PTE-mapped THP */
if (PageCompound(page)) {//如果是复合页
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
/*
* Avoid grouping on RO pages in general. RO pages shouldn't hurt as
* much anyway since they can be in shared cache state. This misses
* the case where a mapping is writable but the process never writes
* to it but pte_write gets cleared during protection updates and
* pte_dirty has unpredictable behaviour between PTE scan updates,
* background writeback, dirty balancing and application behaviour.
*/
if (!pte_write(pte))
flags |= TNF_NO_GROUP;
/*
* Flag if the page is shared between multiple address spaces. This
* is later used when determining whether to group tasks together
*/
if (page_mapcount(page) > 1 && (vma->vm_flags & VM_SHARED))
flags |= TNF_SHARED;
last_cpupid = page_cpupid_last(page);
page_nid = page_to_nid(page);
target_nid = numa_migrate_prep(page, vma, vmf->address, page_nid,
&flags);
pte_unmap_unlock(vmf->pte, vmf->ptl);
if (target_nid == NUMA_NO_NODE) {
put_page(page);
goto out;
}
/* Migrate to the requested node */
migrated = migrate_misplaced_page(page, vma, target_nid);
if (migrated) {
page_nid = target_nid;
flags |= TNF_MIGRATED;
} else
flags |= TNF_MIGRATE_FAIL;
out:
if (page_nid != NUMA_NO_NODE)
task_numa_fault(last_cpupid, page_nid, 1, flags);//去到node上的页面发生PROT NONE错误。
return 0;
}
写时复制:do_wp_page
两种情况下会执行写时复制:
- 进程分叉生成子进程时,为了避免复制物理页,子进程和父进程以只读方式共享所有私有匿名页和文件页,当其中一个进程试图写只读页时,触发页错误异常,页错误异常处理程序分配新的物理页,把旧的物理页的数据复制到新的物理页,然后把虚拟页映射到新的物理页。
- 进程创建私有的文件映射,然后读访问,触发页错误异常,异常处理程序把文件读到页缓存,然后以只读模式把虚拟页映射到文件页的缓存中的物理页。接着执行写访问,触发页错误异常,异常处理程序执行写时复制,为文件的页缓存中的物理页创建一个副本,把虚拟页映射到副本。这个副本是进程的私有匿名页,和文件脱离关系,修改副本不会导致文件变化。
do_wp_page的执行流程
- 调用函数
vm_normal_page
,从页表项得到页帧号,然后得到页帧号对应的页描述符。特殊映射不希望关联页描述符,直接使用页帧号,可能是因为页描述符不存在,也可能是因为不想使用页描述符。 - 如果页描述符为空,说明使用页帧号的特殊映射。 如果是共享的可写特殊映射,不需要复制物理页,调用函数
wp_pfn_shared
来设置页表项的写权限位。如果是私有的可写特殊映射,调用函数wp_page_copy
以复制物理页,然后把虚拟页映射到新的物理页。 - 如果页描述符存在,说明使用页描述符的正常映射。如果是共享的可写正常映射,不需要复制物理页,调用函数
wp_page_shared
来设置页表项的写权限位;如果是私有的可写正常映射,调用函数wp_page_copy
以复制物理页,然后把虚拟页映射到新的物理页。
static vm_fault_t do_wp_page(struct vm_fault *vmf)
__releases(vmf->ptl)
{
struct vm_area_struct *vma = vmf->vma;
if (userfaultfd_pte_wp(vma, *vmf->pte)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_WP);
}
/*
* 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) {//找不到page,说明使用页帧号的特殊映射
/*
* VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
* VM_PFNMAP VMA.
*
* We should not cow pages in a shared writeable mapping.
* Just mark the pages writable and/or call ops->pfn_mkwrite.
*/
if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))
return wp_pfn_shared(vmf);//共享的可写映射的cow过程,其实是调用pfn_mkwrite通知文件系统可读变为可写
pte_unmap_unlock(vmf->pte, vmf->ptl);
return wp_page_copy(vmf);//私有的可写映射的cow过程,复制物理页,然后把虚拟页映射到新的物理页。
}
//来到这里,说明找到page结构体,也就是使用页描述符的正常映射
if (PageAnon(vmf->page)) {
struct page *page = vmf->page;
/* PageKsm() doesn't necessarily raise the page refcount */
if (PageKsm(page) || page_count(page) != 1)
goto copy;
if (!trylock_page(page))
goto copy;
if (PageKsm(page) || page_mapcount(page) != 1 || page_count(page) != 1) {
unlock_page(page);
goto copy;
}
/*
* Ok, we've got the only map reference, and the only
* page count reference, and the page is locked,
* it's dark out, and we're wearing sunglasses. Hit it.
*/
unlock_page(page);
wp_page_reuse(vmf);
return VM_FAULT_WRITE;
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))) {
return wp_page_shared(vmf);//共享的可写映射cow过程,其实是调用page_mkwrite通知文件系统可读变为可写
}
copy:
/*
* Ok, we need to copy. Oh, well..
*/
get_page(vmf->page);
pte_unmap_unlock(vmf->pte, vmf->ptl);
return wp_page_copy(vmf);//私有的可写映射的cow过程,复制物理页,然后把虚拟页映射到新的物理页。
}
do_wp_page
调用函数vm_normal_page
从页表项得到页帧号,然后得到页帧号对应的页描述符;然后判断能否找到页,如果找不到说明使用页帧号的特殊映射,如果找到页则说明使用页描述符的正常映射。如果是特殊映射,还要判断是共享的还是私有的,如果是共享的特殊映射,调用wp_pfn_shared
函数处理;如果是私有的特殊映射,则调用wp_page_copy
函数处理。如果是正常映射,也是要判断是共享的还是私有的,如果是共享的正常映射,调用wp_page_shared
函数处理;如果是私有的正常映射,调用wp_page_copy
处理。
因为wp_pfn_shared和wp_page_shared都是共享的,所以不需要复制物理页,这两个函数仅仅调用vma的page_mkwrite
方法,通知文件系统把可读修改问可写。wp_page_copy
就比较复杂了,下面我们看看wp_page_copy
。
wp_page_copy
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct mm_struct *mm = vma->vm_mm;
struct page *old_page = vmf->page;
struct page *new_page = NULL;
pte_t entry;
int page_copied = 0;
struct mmu_notifier_range range;
if (unlikely(anon_vma_prepare(vma)))//初始化vma中的anon_vma_chain和anon_vma
goto oom;
if (is_zero_pfn(pte_pfn(vmf->orig_pte))) {//如果是零页,
new_page = alloc_zeroed_user_highpage_movable(vma,
vmf->address);//那么分配一个物理页,然后用零初始化
if (!new_page)
goto oom;
} else {//否则就是一个物理页,
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
vmf->address);//分配一个物理页
if (!new_page)
goto oom;
if (!cow_user_page(new_page, old_page, vmf)) {//把数据复制到新的物理页
/*
* 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.
*/
put_page(new_page);
if (old_page)
put_page(old_page);
return 0;
}
}
if (mem_cgroup_charge(new_page, mm, GFP_KERNEL))
goto oom_free_new;
cgroup_throttle_swaprate(new_page, GFP_KERNEL);
__SetPageUptodate(new_page);//设置新页的标志位PG_uptodate,表示物理页包含有效的数据
mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma, mm,
vmf->address & PAGE_MASK,
(vmf->address & PAGE_MASK) + PAGE_SIZE);
mmu_notifier_invalidate_range_start(&range);
//给页表上锁,锁住以后重新读页表项
vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
if (likely(pte_same(*vmf->pte, vmf->orig_pte))) {//页表项和锁住以前的页表项相同
if (old_page) {
if (!PageAnon(old_page)) {
dec_mm_counter_fast(mm,
mm_counter_file(old_page));
inc_mm_counter_fast(mm, MM_ANONPAGES);
}
} else {
inc_mm_counter_fast(mm, MM_ANONPAGES);
}
flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));//从缓存中冲刷页
entry = mk_pte(new_page, vma->vm_page_prot);//使用新的物理页和访问权限生成页表项的值
entry = pte_sw_mkyoung(entry);//直接返回entry,不知道有什么用
entry = maybe_mkwrite(pte_mkdirty(entry), vma);//设置为脏
ptep_clear_flush_notify(vma, vmf->address, vmf->pte);//把页表项清除,并且冲刷页表缓存
page_add_new_anon_rmap(new_page, vma, vmf->address, false);//建立新物理页到虚拟页的反向映射
lru_cache_add_inactive_or_unevictable(new_page, vma);//把物理页添加到活动 LRU 链表或不可回收 LRU 链表中
/*
* We call the notify macro here because, when using secondary
* mmu page tables (such as kvm shadow page tables), we want the
* new page to be mapped directly into the secondary page table.
*/
set_pte_at_notify(mm, vmf->address, vmf->pte, entry);//修改页表项
update_mmu_cache(vma, vmf->address, vmf->pte);//更新页表缓存
if (old_page) {
page_remove_rmap(old_page, false);//删除旧物理页到虚拟页的反向映射
}
/* Free the old page.. */
new_page = old_page;
page_copied = 1;
} else {//页表项和锁住以前的页表项不同
update_mmu_tlb(vma, vmf->address, vmf->pte);
}
if (new_page)
put_page(new_page);
pte_unmap_unlock(vmf->pte, vmf->ptl);//释放页表的锁
/*
* No need to double call mmu_notifier->invalidate_range() callback as
* the above ptep_clear_flush_notify() did already call it.
*/
mmu_notifier_invalidate_range_only_end(&range);
if (old_page) {//如果页表项映射到新的物理页,并且旧的物理页被锁定在内存中,
/*
* Don't let another task, with possibly unlocked vma,
* keep the mlocked page.
*/
if (page_copied && (vma->vm_flags & VM_LOCKED)) {
lock_page(old_page); /* LRU manipulation */
if (PageMlocked(old_page))
munlock_vma_page(old_page);
unlock_page(old_page);
}
put_page(old_page);//把旧的物理页释放
}
return page_copied ? VM_FAULT_WRITE : 0;
oom_free_new:
put_page(new_page);
oom:
if (old_page)
put_page(old_page);
return VM_FAULT_OOM;
}
wp_page_copy
首先调用函数anon_vma_prepare
初始化vma中的anon_vma_chain
和anon_vma
,然后判断这个也是否为零页,如果是零页,则调用函数alloc_zeroed_user_highpage_movable
分配一个物理页并且初始化为0;如果不是零页,则调用函数alloc_page_vma
分配物理页,调用函数cow_user_page
把数据复制到新的物理页。接着调用函数__SetPageUptodate
设置新页的标志位PG_uptodate
,表示物理页包含有效的数据,调用函数flush_cache_page
从缓存中刷新页,调用函数mk_pte
生成页表项的值,使用函数maybe_mkwrite
设置为脏页,把旧的页表项清除,并且冲刷页表缓存,建立新物理页到虚拟页的反向映射,把物理页添加到活动 LRU 链表或不可回收 LRU 链表中,调用函数set_pte_at_notify
修改页表项,还要把旧的物理页释放。