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 了。