主要参考了《深入linux内核》和《Linux内核深度解析》,另外简单浅析了一下相关内容

文章目录

  • 页错误异常处理
    • 特定于架构部分(x86)的异常处理流程
    • 用户空间页错误异常
      • handle_pte_fault
      • 私有匿名页的缺页异常
      • 文件页的缺页异常
        • 处理读文件页错误
        • 处理写私有文件页错误的方法
        • 处理写共享文件页错误的方法
      • 写时复制
    • 内核模式页错误异常

页错误异常处理

在取指令或数据的时候,处理器的内存管理单元需要把虚拟地址转换成物理地址。

如果虚拟页没有映射到物理页,或者没有访问权限,处理器将生成页错误异常(page fault)。

  1. 虚拟页没有映射到物理页,这种情况通常称为缺页异常,有以下几种情况。

(1)访问用户栈的时候,超出了当前用户栈的范围,需要扩大用户栈。
(2)当进程申请虚拟内存区域的时候,通常没有分配物理页,进程第一次访问的时候触发页错误异常。
(3)内存不足的时候,内核把进程的匿名页换出到交换区。
(4)一个文件页被映射到进程的虚拟地址空间,内存不足的时候,内核回收这个文件页,在进程的页表中删除这个文件页的映射。
(5)程序错误,访问没有分配给进程的虚拟内存区域。

前面四种情况,如果页错误异常处理程序成功地把虚拟页映射到物理页,处理程序返回后,处理器重新执行触发异常的指令。

第五种情况,页错误异常处理程序将会发送段违法(SIGSEGV)信号以杀死进程。

  1. 没有访问权限,有以下两种情况。

(1)可能是软件有意造成的,典型的例子是写时复制(Copy on Write,CoW):进程分叉生成子进程的时候,为了避免复制物理页,子进程和父进程以只读方式共享所有私有的匿名页和文件页。当其中一个进程试图写只读页时,触发页错误异常,页错误异常处理程序分配新的物理页,把旧的物理页的数据复制到新的物理页,然后把虚拟页映射到新的物理页。
(2)程序错误,例如试图写只读的代码段所在的物理页。

第一种情况,如果页错误异常处理程序成功地把虚拟页映射到物理页,处理程序返回后,处理器重新执行触发异常的指令。
第二种情况,页错误异常处理程序将会发送段违法(SIGSEGV)信号以杀死进程。

不同处理器架构实现的页错误异常不同,页错误异常处理程序的前面一部分是各种处理器架构自定义的部分,后面从函数handle_mm_fault开始的部分是所有处理器架构共用的部分。

特定于架构部分(x86)的异常处理流程

page fault基本流程:
从cr2中获取发生异常的地址
缺页地址位于内核态
位于vmalloc区?->从主内核页表同步数据到进程页表
非vmalloc区 ->不应该产生page fault->oops
缺页地址位于用户态
缺页上下文发生在内核态
exception table中有相应的处理项? ->进行修正
没有 ->oops
查找vma
找到?-> 是否expand stack?->堆栈扩展
不是->正常的缺页处理:handle_mm_fault
没找到->bad_area

arch\x86\mm\fault.c (用的是3.10版本和4.x版本差不多)

/**缺页异常主处理函数。*regs:异常时的寄存器信息;*error_code-当异常发生时,硬件压入栈中的错误代码。*             当第0位被清0时,则异常是由一个不存在的页所引起的。否则是由无效的访问权限引起的。*             如果第1位被清0,则异常由读访问或者执行访问所引起,如果被设置,则异常由写访问引起。*             如果第2位被清0,则异常发生在内核态,否则异常发生在用户态。*/
static void __kprobes
__do_page_fault(struct pt_regs *regs, unsigned long error_code)
{struct vm_area_struct *vma;struct task_struct *tsk;unsigned long address;struct mm_struct *mm;int fault;int write = error_code & PF_WRITE;unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE |(write ? FAULT_FLAG_WRITE : 0);tsk = current;mm = tsk->mm;/* Get the faulting address: *///缺页异常的地址默认存放于CR2寄存器中,x86硬件决定address = read_cr2();/** Detect and handle instructions that would cause a page fault for* both a tracked kernel page and a userspace page.*/if (kmemcheck_active(regs))kmemcheck_hide(regs);prefetchw(&mm->mmap_sem);// mmio不应该发生缺页,通常都会ioremap到vmalloc区,然后进行访问if (unlikely(kmmio_fault(regs, address)))return;/** We fault-in kernel-space virtual memory on-demand. The* 'reference' page table is init_mm.pgd.** We MUST NOT take any locks for this case. We may* be in an interrupt or a critical region, and should* only copy the information from the master page table,* nothing more.** This verifies that the fault happens in kernel space* (error_code & 4) == 0, and that the fault was not a* protection error (error_code & 9) == 0.*//** 缺页地址位于内核空间。并不代表异常发生于内核空间,有可能是用户* 态访问了内核空间的地址。*/if (unlikely(fault_in_kernel_space(address))) {if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {/** 检查发生缺页的地址是否在vmalloc区,是则进行相应的处理* 主要是从内核主页表向进程页表同步数据*/if (vmalloc_fault(address) >= 0)return;if (kmemcheck_fault(regs, address, error_code))return;}/* Can handle a stale RO->RW TLB: *//** 检查是否是由于陈旧的TLB导致的假的pagefault(由于TLB的延迟flush导致,* 因为提前flush会有比较大的性能代价)。*/if (spurious_fault(error_code, address))return;/* kprobes don't want to hook the spurious faults: */if (notify_page_fault(regs))return;/** Don't take the mm semaphore here. If we fixup a prefetch* fault we could otherwise deadlock:*//** 有问题了: 由于异常地址位于内核态,触发内核异常,因为vmalloc* 区的缺页异常前面已经处理过了,内核态的缺页异常只能发生在* vmalloc区,如果不是,那就是内核异常了。*/bad_area_nosemaphore(regs, error_code, address);return;}// 进入到这里,说明异常地址位于用户态/* kprobes don't want to hook the spurious faults: */if (unlikely(notify_page_fault(regs)))return;/** It's safe to allow irq's after cr2 has been saved and the* vmalloc fault has been handled.** User-mode registers count as a user access even for any* potential system fault or CPU buglet:*//** 开中断,这种情况下,是安全的,可以缩短因缺页异常导致的关中断时长。* 老内核版本中(2.6.11)没有这样的操作*/if (user_mode_vm(regs)) {local_irq_enable();error_code |= PF_USER;} else {if (regs->flags & X86_EFLAGS_IF)local_irq_enable();}if (unlikely(error_code & PF_RSVD))pgtable_bad(regs, error_code, address);if (static_cpu_has(X86_FEATURE_SMAP)) {if (unlikely(smap_violation(error_code, regs))) {bad_area_nosemaphore(regs, error_code, address);return;}}perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);/** If we're in an interrupt, have no user context or are running* in an atomic region then we must not take the fault:*//** 当缺页异常发生于中断或其它atomic上下文中时,则产生异常。* 这种情况下,不应该再产生page fault*/if (unlikely(in_atomic() || !mm)) {bad_area_nosemaphore(regs, error_code, address);return;}/** When running in the kernel we expect faults to occur only to* addresses in user space. All other faults represent errors in* the kernel and should generate an OOPS. Unfortunately, in the* case of an erroneous fault occurring in a code path which already* holds mmap_sem we will deadlock attempting to validate the fault* against the address space. Luckily the kernel only validly* references user space from well defined areas of code, which are* listed in the exceptions table.** As the vast majority of faults will be valid we will only perform* the source reference check when there is a possibility of a* deadlock. Attempt to lock the address space, if we cannot we then* validate the source. If this is invalid we can skip the address* space check, thus avoiding the deadlock:*/if (unlikely(!down_read_trylock(&mm->mmap_sem))) {/** 缺页发生在内核上下文,这种情况发生缺页的地址只能位于用户态地址空间* 这种情况下,也只能为exceptions table中预先定义好的异常,如果exceptions* table中没有预先定义的处理,或者缺页的地址位于内核态地址空间,则表示* 错误,进入oops流程。*/if ((error_code & PF_USER) == 0 &&!search_exception_tables(regs->ip)) {bad_area_nosemaphore(regs, error_code, address);return;}
retry:// 如果发生在用户态或者有exception table,说明不是内核异常down_read(&mm->mmap_sem);} else {/** The above down_read_trylock() might have succeeded in* which case we'll have missed the might_sleep() from* down_read():*/might_sleep();}// 在当前进程的地址空间中寻找发生异常的地址对应的VMA。vma = find_vma(mm, address);// 如果没找到VMA,则释放mem_sem信号量后,进入__bad_area_nosemaphore处理。if (unlikely(!vma)) {bad_area(regs, error_code, address);return;}/* 找到VMA,且发生异常的虚拟地址位于vma的有效范围内,则为正常的缺页* 异常,请求调页,分配物理内存 */if (likely(vma->vm_start <= address))goto good_area;/* 如果异常地址不是位于紧挨着堆栈区的那个区域,同时又没有相应VMA,则* 进程访问了非法地址,进入bad_area处理*/if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {bad_area(regs, error_code, address);return;}if (error_code & PF_USER) {/** Accessing the stack below %sp is always a bug.* The large cushion allows instructions like enter* and pusha to work. ("enter $65535, $31" pushes* 32 pointers and then decrements %sp by 65535.)*//** 压栈操作时,操作的地址最大的偏移为65536+32*sizeof(unsigned long),* 该操作由pusha命令触发(老版本中,pusha命令最大只能操作32字节,即* 同时压栈8个寄存器)。如果访问的地址距栈顶的距离超过了,则肯定是非法* 地址访问了。*/if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {bad_area(regs, error_code, address);return;}}/** 运行到这里,说明设置了VM_GROWSDOWN标记,表示缺页异常地址位于堆栈区* 需要扩展堆栈。说明: 堆栈区的虚拟地址空间也是动态分配和扩展的,不是* 一开始就分配好的。*/if (unlikely(expand_stack(vma, address))) {bad_area(regs, error_code, address);return;}/** Ok, we have a good vm_area for this memory access, so* we can handle it..*//** 运行到这里,说明是正常的缺页异常,则进行请求调页,分配物理内存*/
good_area:if (unlikely(access_error(error_code, vma))) {bad_area_access_error(regs, error_code, address);return;}/** If for any reason at all we couldn't handle the fault,* make sure we exit gracefully rather than endlessly redo* the fault:*//** 分配物理内存,缺页异常的正常处理主函数* 可能的情况有:1、请求调页/按需分配;2、COW;3、缺的页位于交换分区,* 需要换入。*/fault = handle_mm_fault(mm, vma, address, flags);if (unlikely(fault & (VM_FAULT_RETRY|VM_FAULT_ERROR))) {if (mm_fault_error(regs, error_code, address, fault))return;}/** Major/minor page fault accounting is only done on the* initial attempt. If we go through a retry, it is extremely* likely that the page will be found in page cache at that point.*/if (flags & FAULT_FLAG_ALLOW_RETRY) {if (fault & VM_FAULT_MAJOR) {tsk->maj_flt++;perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,regs, address);} else {tsk->min_flt++;perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,regs, address);}if (fault & VM_FAULT_RETRY) {/* Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk* of starvation. */flags &= ~FAULT_FLAG_ALLOW_RETRY;flags |= FAULT_FLAG_TRIED;goto retry;}}// VM86模式(兼容老环境)相关检查check_v8086_mode(regs, address, tsk);up_read(&mm->mmap_sem);
}

用户空间页错误异常

**在结束对缺页异常的特定于体系结构的分析之后,确认异常是在允许的地址触发,内核必须确定将所需数据读取到物理内存的适当方法。该任务委托给handle_mm_fault,它不依赖于底层体系结构,而是在内存管理的框架下、独立于系统而实现。**该函数确认在各级页目录中,通向对应于异常地址的页表项的各个页目录项都存在。

用户空间页错误异常是指进程访问用户虚拟地址生成的页错误异常,可以分为两种情况:

  • 进程在用户模式下访问用户虚拟地址。生成页错误异常。
  • 进程在内核模式下访问用户虚拟地址,生成页错误异常。
    进程通过系统调用进入内核模式,系统调用传入用户空间的缓冲区,进程在内核模式下访问用户空间的缓冲区。

如果虚拟内存区域使用标准巨型页,则调用函数hugetlb_fault处理标准巨型页的页错误异常。
如果虚拟内存区域使用普通页,则调用__handle_mm_fault处理普通页的页错误异常。

如果页错误异常处理程序确认虚拟地址属于分配给进程的虚拟内存区域,并且虚拟内存区域授予触发页错误异常的访问权限,就会运行到函数handle_mm_fault,执行流程如下:

/** By the time we get here, we already hold the mm semaphore** The mmap_sem may have been released depending on flags and our* return value.  See filemap_fault() and __lock_page_or_retry().*/
static int __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),};struct mm_struct *mm = vma->vm_mm;pgd_t *pgd;p4d_t *p4d;int 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);...// 在页中间目录中查找虚拟地址对应的表项,如果页中间目录不存在,那么先创建页中间目录。vmf.pmd = pmd_alloc(mm, vmf.pud, address);...// 处理页表项return handle_pte_fault(&vmf);
}

handle_pte_fault

static int handle_pte_fault(struct vm_fault *vmf)
{pte_t entry;// 直接在页表中查找虚拟地址对应的表项if (unlikely(pmd_none(*vmf->pmd))) {// 如果页中间目录表项是空表项,说明直接页表不存在,则把vmf->pte设置为空vmf->pte = NULL;} else {/* 如果页中间目录表项不是空表项,说明直接页表存在,那么在直接页表中查找虚拟地址对应的表项,vmf->pte存放表项的地址,vmf->orig_pte存放页表项的值,如果页表项是空表项,vmf->pte没必要存放表项的地址,设置成空指针*/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_sem 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);vmf->orig_pte = *vmf->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 or ACCESS_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;}}//    如果页表项不存在(直接页表不存在或者页表项是空表项if (!vmf->pte) {// 如果是私有匿名映射,则调用函数do_anonymous_page处理匿名页的缺页异常if (vma_is_anonymous(vmf->vma))return do_anonymous_page(vmf);else/* 如果是文件映射或者共享匿名映射,调用函数do_fault处理文件页的缺页异常 */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);// 开始处理"页表项存在,并且页在物理内存中”这种情况,页错误异常是由访问权限触发的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)))goto unlock;// 如果页错误异常是由写操作触发if (vmf->flags & FAULT_FLAG_WRITE) {// 如果页表项没有写权限,调用函数do_wp_page执行写时复制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以更新处理器的内存管理单位的页表缓存update_mmu_cache(vmf->vma, vmf->address, vmf->pte);} else {/** 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.*/// 如果页表项没有变化,并且页错误异常是由写操作触发的,说明页错误异常可能是TLB表项和页表项不一致导致的,那么使TLB表项失效。if (vmf->flags & FAULT_FLAG_WRITE)flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);}
unlock: // 释放页表锁pte_unmap_unlock(vmf->pte, vmf->ptl);return 0;
}

私有匿名页的缺页异常

什么情况下会发生匿名页缺页异常呢?

  • 函数的局部变量比较大,或者函数调用的层次比较深,导致当前栈不够用,需要扩大栈;
  • 进程调用malloc,从堆申请了内存块,只分配虚拟内存区域,还没有映射到物理页,第一次访问时触发缺页异常。
  • 进程直接调用mmap,创建匿名的内存映射,只分配了虚拟内存区域,还没有映射到物理页,第一次访问时触发缺页异常。

函数do_anonymous_page处理私有匿名页的缺页异常,执行流程及源码分析如下:

mm\memory.c

static int do_anonymous_page(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;struct mem_cgroup *memcg;struct page *page;pte_t entry;// 如果是共享的匿名映射,但是虚拟内存区域没有提供虚拟内存操作集合(vm_area_struct.vm_ops),那么返回错误号VM_FAULT_SIGBUS。// 判断vma_is_anonymous 是根据 !vma->vm_opsif (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 down_write(mmap_sem) or when* parallel threads are excluded by other means.** Here we only have down_read(mmap_sem).*/// 如果直接页表不存在,分配页表if (pte_alloc(vma->vm_mm, vmf->pmd, vmf->address))return VM_FAULT_OOM;/* See the comment in pte_alloc_one_map() */if (unlikely(pmd_trans_unstable(vmf->pmd)))return 0;// 如果缺页异常是由读操作触发的,并且进程允许使用零页,那么我们就把虚拟页映射到一个专用的零页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))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);}// 跳转道标号setpte区设置页表项goto setpte;}// 分配自己的私有页if (unlikely(anon_vma_prepare(vma)))goto oom;// 分配物理页,优先从高端内存区域分配,并且用零初始化page = alloc_zeroed_user_highpage_movable(vma, vmf->address);if (!page)goto oom;if (mem_cgroup_try_charge(page, vma->vm_mm, GFP_KERNEL, &memcg, false))goto oom_free_page;/** The memory barrier inside __SetPageUptodate makes sure that* preceeding stores to the page contents become visible before* the set_pte_at() write.*/// 设置页描述符的标志位,表示物理页包含有效的数据__SetPageUptodate(page);// 使用页帧号和访问权限生成相应的页表项entry = mk_pte(page, vma->vm_page_prot);// 如果虚拟内存区域又写权限,设置页表项的脏标志位和写权限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))goto release;/* Deliver the page fault to userland, check inside PT lock */if (userfaultfd_missing(vma)) {pte_unmap_unlock(vmf->pte, vmf->ptl);mem_cgroup_cancel_charge(page, memcg, false);put_page(page);return handle_userfault(vmf, VM_UFFD_MISSING);}inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);// 建立物理页到虚拟页的反向映射page_add_new_anon_rmap(page, vma, vmf->address, false);mem_cgroup_commit_charge(page, memcg, false, false);// 把物理页添加到活动LRU或不可回收LRU链表,页回收算法需要从LRU链表选择需要回收物理页lru_cache_add_active_or_unevictable(page, vma);
setpte:// 设置页表项set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);// 不需要从页表缓存删除页表项,因为从前虚拟页未映射到物理页update_mmu_cache(vma, vmf->address, vmf->pte);
unlock:// 释放页表的锁pte_unmap_unlock(vmf->pte, vmf->ptl);return 0;
release:mem_cgroup_cancel_charge(page, memcg, false);put_page(page);goto unlock;
oom_free_page:put_page(page);
oom:return VM_FAULT_OOM;
}

文件页的缺页异常

共享匿名页某种程度也算是文件页,用的是特殊的fd

何时会触发文件页的缺页异常呢?

  1. 启动程序的时候,内核为程序的代码段和数据段创建私有的文件映射,映射到进程的虚拟地址空间,第一次访问的时候触发文件页的缺页异常。
  2. 进程使用mmap创建文件映射,把文件的一个区间映射到进程的虚拟地址空间,第一次访问的时候触发文件页的缺页异常。

函数__do_fault处理文件页和共享匿名页的缺页异常,执行流程及源码分析如下:

mm\memory.c

static int do_fault(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;int ret;// 如果虚拟内存区域没有提供页错误异常方法vma->vm_ops->fault,返回错误号VM_FAULT_SIGBUSif (!vma->vm_ops->fault)ret = VM_FAULT_SIGBUS;// 如果缺页异常是由读文件页触发的else if (!(vmf->flags & FAULT_FLAG_WRITE))ret = do_read_fault(vmf);// 如果缺页异常是由写私有文件页触发的,调用函数do_cow_fault以处理写私有文件页错误,执行写时复制else if (!(vma->vm_flags & VM_SHARED))ret = do_cow_fault(vmf);else// 如果缺页异常是由写共享文件页触的发的,调用函数do_shared_faultret = do_shared_fault(vmf);/* preallocated pagetable is unused: free it */if (vmf->prealloc_pte) {pte_free(vma->vm_mm, vmf->prealloc_pte);vmf->prealloc_pte = NULL;}return ret;
}

处理读文件页错误

具体处理读文件页错误的方法如下:

  • 把文件页从存储设备上的文件系统读到文件的缓存 (每个文件有一个缓存,因为以页为单位,所以称为页缓存)中。
  • 设置进程的页表项,把虚拟页映射到文件的页缓存的物理页。

函数do_read_fault处理读文件页错误,执行流程及源码分析如下:

static int do_read_fault(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;int ret = 0;/*为了减少页错误异常的次数,如果正在访问的文件页后面的几个文件页也被映射到进程的虚拟地址空间,那么预先读取到页缓存中,全局变量fault_around_bytes控制总长度,默认值是64kb,如果页长度是4kb,就一次读取16页*/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;
}

函数__do_fault需要使用虚拟内存区域的虚拟内存操作集合中的fault方法(vm_area_struct.vm_ops->fault)来把文件页读到内存中。

**进程调用mmap创建文件映射的时候,文件所属的文件系统会注册虚拟内存区域的虚拟内存操作集合,fault方法负责处理文件页的缺页异常。**例如,EXT4文件系统注册的虚拟内存操作集合是ext4_file_vm_ops,fault方法是函数ext4_filemap_fault。许多文件系统注册的fault方法是通用的函数filemap_fault。

给定一个虚拟内存区域vma,函数filemap_fault读文件页的方法如下:

  • 根据vma->vm_file得到文件的打开实例file;
  • 根据file->f_mapping得到文件的地址空间mapping;
  • 使用地址空间操作集合中的readpage方法(mapping->a_ops->readpage)把文件页读到内存中。

函数finish_fault负责设备项表项,把主要工作委托给函数alloc_set_pte,执行流程及源码分析如下:

int alloc_set_pte(struct vm_fault *vmf, struct mem_cgroup *memcg,struct page *page)
{struct vm_area_struct *vma = vmf->vma;bool write = vmf->flags & FAULT_FLAG_WRITE;pte_t entry;int ret;if (pmd_none(*vmf->pmd) && PageTransCompound(page) &&IS_ENABLED(CONFIG_TRANSPARENT_HUGE_PAGECACHE)) {/* THP on COW? */VM_BUG_ON_PAGE(memcg, 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;}// 锁住页表后重新检查if (unlikely(!pte_none(*vmf->pte)))return VM_FAULT_NOPAGE;// 如果锁住页表以后发现页表项不是空表项,说明其他处理器修改同一页表,那么当前处理器放弃处理flush_icache_page(vma, page);// 使用页帧号和访问权限生成对应的页表项entry = mk_pte(page, vma->vm_page_prot);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);page_add_new_anon_rmap(page, vma, vmf->address, false);mem_cgroup_commit_charge(page, memcg, false, false);// 把物理页添加到活动的lru链表或者不可回收lru链表lru_cache_add_active_or_unevictable(page, vma);} else {inc_mm_counter_fast(vma->vm_mm, mm_counter_file(page));page_add_file_rmap(page, false);}// 设置页表项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;
}

处理写私有文件页错误的方法

  • 把文件页从存储设备上的文件系统读到文件的页缓存中;
  • 执行写时复制,为文件的页缓存中的物理页创建一个副本,这个副本是进程的私有匿名页,和文件脱离系统,修改副本不会导致文件变化;
  • 设备进程的页表项,把虚拟页映射到副本

函数do_cow_fault处理写私有文件页错误,执行流程及源码分析如下:

static int do_cow_fault(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;int ret;// 关联一个实例到虚拟内存区域if (unlikely(anon_vma_prepare(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_try_charge(vmf->cow_page, vma->vm_mm, GFP_KERNEL,&vmf->memcg, false)) {put_page(vmf->cow_page);return VM_FAULT_OOM;}// 把文件页读取到文件的页缓存中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);// 设置页表项,把虚拟页映射到副本的物理页。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:mem_cgroup_cancel_charge(vmf->cow_page, vmf->memcg, false);put_page(vmf->cow_page);return ret;
}

处理写共享文件页错误的方法

  • 把文件页从存储设备上的文件系统读到文件的页缓存中;
  • 设备进程的页表项,把虚拟页映射到文件的页缓存中的物理页。

函数do_shared_fault处理写共享文件页错误,执行流程及源码分析如下:

static int do_shared_fault(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;int ret, tmp;// 把文件页读取到文件的页缓存中ret = __do_fault(vmf);// 如果创建内存映射的时候文件所属的文件系统注册了虚拟内存操作集合中的page-write方法,// 调用这个方法通知文件系统“页即将变成可写的”。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) {unlock_page(vmf->page);tmp = do_page_mkwrite(vmf);if (unlikely(!tmp ||(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;}// 设置页的脏标志位,表示页的数据被修改。如果文件所属文件系统没有注册虚拟内存操作集合方法// 那么更新方法的修改时间fault_dirty_shared_page(vma, vmf->page);return ret;
}

写时复制

有两种情况会执行写时复制(Copy on Write,CoW)。

(1)进程分叉生成子进程的时候,为了避免复制物理页,子进程和父进程以只读方式共享所有私有的匿名页和文件页。当其中一个进程试图写只读页时,触发页错误异常,页错误异常处理程序分配新的物理页,把旧的物理页的数据复制到新的物理页,然后把虚拟页映射到新的物理页。

**(2)进程创建私有的文件映射,然后读访问,触发页错误异常,异常处理程序把文件读到页缓存,然后以只读模式把虚拟页映射到文件的页缓存中的物理页。**接着执行写访问,触发页错误异常,异常处理程序执行写时复制,为文件的页缓存中的物理页创建一个副本,把虚拟页映射到副本。这个副本是进程的私有匿名页,和文件脱离关系,修改副本不会导致文件变化。

函数do_wp_page处理写时复制(第一种情况),执行流程如图3.94所示,执行过程如下。

函数wp_page_copy执行写时复制(第二种情况),执行流程如图3.95所示,其代码如下:

内核模式页错误异常

内核访问内核虚拟地址,正常情况下不会出现虚拟页没有映射到物理页的状况,内核使用线性映射区域的虚拟地址,在内存管理子系统初始化的时候就会把虚拟地址映射到物理地址,运行过程中可能使用vmalloc()函数从vmalloc区域分配虚拟内存区域,vmalloc()函数会分配并且映射到物理页(x86不确定)。如果出现虚拟页没有映射到物理页的情况,一定是程序错误,内核将会崩溃。

内核可能访问用户虚拟地址,进程通过系统调用进入内核模式,有些系统调用会传入用户空间的缓冲区,内核必须使用头文件“uaccess.h”定义的专用函数访问用户空间的缓冲区,这些专用函数在异常表中添加了可能触发异常的指令地址和异常修正程序的地址。

如果访问用户空间的缓冲区时生成页错误异常,页错误异常处理程序发现用户虚拟地址没有被分配给进程,就在异常表中查找指令地址对应的异常修正程序,如果找到了,使用异常修正程序修正异常,避免内核崩溃。

页错误异常处理(page fault)的实现相关推荐

  1. 性能-Windows内存中断-页面错误(page fault)

    最近准备项目上线,做了大量的压测工作,发现了在有些机器上会出现大量的页面错误,这些错误会导致内存中断. 然而实际的测试数据来看,分两种情况,有大量中断不影响性能和有大量中断影响性能. 经过调查发现:大 ...

  2. Linux内存page,【原创】(十四)Linux内存管理之page fault处理

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  3. 页错误 Page Fault /缺页异常 详解

    ​​​​​目录 ​​​​​​ 1. 第一部分:如果你看得懂 1.1 页错误定义 1.2 页错误的处理 2. 第二部分:如果你看不懂上面的,请看这里 2.1. 举例子(背景) 2.1.1 进程及页映射 ...

  4. 图解|什么是缺页错误Page Fault

    1.号外号外 各位老铁,大家好! 上周大白有事停更1次,最近在想如何让大家在10分钟中有所收获,于是准备搞一个"什么是xxx"系列,写一些精悍的知识点. 先抛一道阿里面试题给大家热 ...

  5. 趣味图解 | 什么是缺页错误 Page Fault?

    来源 | 后端技术指南针 号外号外 各位老铁,大家好! 最近在想如何让大家在10分钟中有所收获,写一些精悍的知识点. 先抛一道阿里面试题给大家热热身,引出今天的主角-缺页异常Page Fault. 谈 ...

  6. 什么是缺页错误 Page Fault?

    谈谈对缺页异常Page Fault的理解. 话不多说,集合上车. 术语约定 VA:Virtual Address 虚拟地址 PA:Physical Address 物理地址 MMU:Memory Ma ...

  7. linux那些事之page fault(AMD64架构)(user space)(2)

    do_user_addr_fault 用户空间地址处理是page fault主要处理流程,x86 64位系统主要是do_user_addr_fault()函数 该处理部分是x86架构特有部分 即与架构 ...

  8. linux 内存管理 page fault带来的性能问题

    Linux进程如何访问内存 Linux下,进程并不是直接访问物理内存,而是通过内存管理单元(MMU)来访问内存资源. 原因后面会讲到. 为什么需要虚拟内存地址空间 假设某个进程需要4MB的空间,内存假 ...

  9. linux 内存越界判断_虚拟内存 和 page fault 的解释

    Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的.这样进程就可以很方便地访问内存,更确切地说是访问虚拟内存. 1.什么是虚拟内存 假设某个进程需要100MB的空间,而 ...

  10. Linux系统下深究一个malloc/brk/sbrk新内存后的page fault问题

    有耳可听的,就应当听 -<马可福音> 周四的休假团建又没有去,不因别的,只因年前东北行休假太多了,想缓缓-不过真实原因也确实因为假期剩余无几了-思考了一些问题,写下本文.   本文的缘起来 ...

最新文章

  1. HIVE 查询显示列名 及 行转列显示
  2. Percona XtraBackup 关于 MySQL备份还原的详细测试
  3. r包调用legend函数_R语言实现基于朴素贝叶斯构造分类模型数据可视化
  4. LeetCode--88.合并两个有序数组(插入法,排序法)
  5. 文件上传input简便美化方案
  6. python如何将多张excel表内数据求和_Excel批量操作,把你的工作效率提升10倍以上(1)...
  7. 修改已经创建的docker容器的端口映射
  8. 贝叶斯判别分析的基本步骤_环境感知算法-目标追踪1.2- 贝叶斯方法
  9. ajax如何用编号查询姓名,Ajax js 使用Ajax检测用户名是否存在
  10. C++之---友元函数
  11. 运维必读:避免故障、拒绝背锅的六大原则!
  12. 简单的php商城,简单的php商城
  13. 基于FPGA/数字IC的数字信号处理课程
  14. python可视化计算器_使用Python自带GUI tkinter编写一个期权价格计算器
  15. 第三届泰迪杯全国大学生数据挖掘竞赛通知
  16. 如何解决Invalid quadratic form: product is complex
  17. 《MFC添加语音功能》
  18. Springboot定时任务【多线程处理】
  19. jsp的include标签
  20. win10连接win7共享打印机提示无法连接到打印机

热门文章

  1. 计算机自动关机时间如何设置在哪设置方法,Win8设置电脑在某一个时间段自动关机的三种方法...
  2. 小程序逆向——某书小程序反编译(一)
  3. Linux的进程管理之进程与线程—2
  4. 360前端校招2019笔试编程题
  5. Android Button设置边框 和背景
  6. java 刻度尺_用java代码如何实现画坐标刻度尺图
  7. 诚之和:谁在抢救瑞幸咖啡?
  8. C语言编程我爱你心形,用c语言写出变色的心形图案
  9. 魔法门之英雄无敌3 android,魔法门之英雄无敌3 v0.86.04
  10. 《编程小白的第一本python入门书》——读书笔记