Page Fault

Page Fault

上文中提及,当我们向操作系统申请内存时,操作系统并不是直接分配给我们物理内存,而是只标记当前进程拥有该段内存,当真正使用这段段内存时才会分配。这种延迟分配物理内存的方式就通过page fault机制来实现的。当我们访问一个内存地址时,如果该地址非法,或者我们对其没有访问权限,或者该地址对应的物理内存还未分配,cpu都会生成一个page fault,进而执行操作系统的page fault handler

这个page fault handler里会检查该fault产生的原因,如果是地址非法或没有权限,则会向当前进程发送一个SIGSEGV signal,该signal默认会kill掉当前进程,并提示我们segmentation fault异常。如果是因为还未分配物理内存,操作系统会立即分配物理内存给当前进程,然后重试产生这个page fault的内存访问指令,一般情况下都可以正常向下执行。

下面我们来看下对应的内核源码:

// arch/x86/mm/fault.c
dotraplinkage void notrace
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
        unsigned long address = read_cr2(); /* Get the faulting address */
        ...
        __do_page_fault(regs, error_code, address);
        ...
}
NOKPROBE_SYMBOL(do_page_fault);

该方法先从cr2寄存器中读出产生这个page fault的虚拟内存地址,然后再调用 __do_page_fault 方法。

// arch/x86/mm/fault.c
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long hw_error_code,
                unsigned long address)
{
        ...
        /* Was the fault on kernel-controlled part of the address space? */
        if (unlikely(fault_in_kernel_space(address)))
                do_kern_addr_fault(regs, hw_error_code, address);
        else
                do_user_addr_fault(regs, hw_error_code, address);
}
NOKPROBE_SYMBOL(__do_page_fault);

该方法会检查该地址是属于kernel space还是user space,如果是user space,则会调用do_user_addr_fault方法。继续do_user_addr_fault方法:

// arch/x86/mm/fault.c
static inline
void do_user_addr_fault(struct pt_regs *regs,
                        unsigned long hw_error_code,
                        unsigned long address)
{
        struct vm_area_struct *vma;
        struct task_struct *tsk;
        struct mm_struct *mm;
        ...
        tsk = current;
        mm = tsk->mm;
        ...
        vma = find_vma(mm, address);
        if (unlikely(!vma)) {
                bad_area(regs, hw_error_code, address);
                return;
        }
        if (likely(vma->vm_start <= address))
                goto good_area;
        ...
good_area:
        ...
        fault = handle_mm_fault(vma, address, flags);
        ...
}
NOKPROBE_SYMBOL(do_user_addr_fault);

该方法会先从mm中找包含address的内存段,如果没有,则说明我们访问了一个非法地址,该方法进而会调用bad_area方法,向当前进程发送一个SIGSEGV signal。如果找到了对应的内存段,则会调用handle_mm_fault方法继续处理。

// mm/memory.c
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
                unsigned int flags)
{
        vm_fault_t ret;
        ...
        if (unlikely(is_vm_hugetlb_page(vma)))
                ...
        else
                ret = __handle_mm_fault(vma, address, flags);
        ...
        return ret;
}
EXPORT_SYMBOL_GPL(handle_mm_fault);

该方法又调用了 __handle_mm_fault 方法:

// mm/memory.c
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,
                ...
        };
        ...
        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);
        ...
        vmf.pud = pud_alloc(mm, p4d, address);
        ...
        vmf.pmd = pmd_alloc(mm, vmf.pud, address);
        ...
        return handle_pte_fault(&vmf);
}

此时,vmf->pte应该为null。该方法通过vma_is_anonymous方法,判断vmf->vma对应的内存段是否是anonymous的,如果是,则调用do_anonymous_page,如果不是,比如mmap file产生的vma,则调用do_fault

// mm/memory.c
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
        struct vm_area_struct *vma = vmf->vma;
        ...
        struct page *page;
        ...
        pte_t entry;
        ...
        page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
        ...
        entry = mk_pte(page, vma->vm_page_prot);
        ...
        set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
        ...
        return ret;
        ...
}

该方法先调用alloc_zeroed_user_highpage_movable分配一个新的page,这个就是物理内存了。然后调用mk_pte方法,把page的地址信息等记录到entry里。最后,把这个entry写入到vmf->pte指向的内存中。这样在下次再访问这个page对应的虚拟内存地址时,page walk就可以在pte中找到这个page了。