__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 task_struct *tsk)
{
struct vm_area_struct *vma;
vm_fault_t fault;
//找到vma 如果找不到 就返回VM_FAULT_BADMAP
vma = find_vma(mm, addr);
fault = VM_FAULT_BADMAP;
if (unlikely(!vma))
goto out;
//如果找到的vma的起始地址大于addr 跳转到check_stack
if (unlikely(vma->vm_start > addr))
goto check_stack;
/*
* Ok, we have a good vm_area for this memory access, so we can handle
* it.
*/
good_area:
/*
* Check that the permissions on the VMA allow for the fault which
* occurred.
*/
//通过对比vm_flags 是否具备可读属性,如果不具备可读的属性说明这次访问是错的
if (!(vma->vm_flags & vm_flags)) {
fault = VM_FAULT_BADACCESS;
goto out;
}
// handle_mm_fault 缺页中断的核心处理函数
//PAGE_MASK --- > ~(PAGE_SIZE-1) PAGE_SIZE ---> 1<<12
return handle_mm_fault(vma, addr & PAGE_MASK, mm_flags);
check_stack:
//判断是否VMA地址拓展到addr,如果可以这说明VMA是一个ok的VMA 如果不可以返回fault = VM_FAULT_BADMAP;
if (vma->vm_flags & VM_GROWSDOWN && !expand_stack(vma, addr))
goto good_area;
out:
return fault;
}
最终使用handle_mm_fault
这个函数处理缺页中断 ,handle_mm_fault
使用 __handle_mm_fault
处理当前错误
__handle_mm_fault函数分析
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
//定义一个vm_fault 用于填充参数并且传递给进程的地址空间的fault函数
// 通过 ---> return handle_pte_fault(&vmf);传递给进程的地址空间的fault函数
struct vm_fault vmf = {
.vma = vma,//目标VMA
.address = address & PAGE_MASK,//发生缺页中断时的虚拟地址
.flags = flags,//与进程内存描述符相关的标志位
.pgoff = linear_page_index(vma, address),//在VMA中的偏移量
.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_offset_raw((mm)->pgd, (addr)))
//((pgd) + pgd_index(addr))
//(((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
pgd = pgd_offset(mm, address);//计算出PGD页表项
p4d = p4d_alloc(mm, pgd, address);//计算出P4D页表项
if (!p4d)
return VM_FAULT_OOM;
vmf.pud = pud_alloc(mm, p4d, address);//计算PUD页表项
...
vmf.pmd = pmd_alloc(mm, vmf.pud, address);//计算出PMD页表项
...
return handle_pte_fault(&vmf);
}
__handle_mm_fault
函数分配 一个vm_fault
并填充这个这个结构体。给到handle_pte_fault
函数使用
handle_pte_fault 函数分析
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
//判断*vmf->pmd 是否为空
if (unlikely(pmd_none(*vmf->pmd))) {
vmf->pte = NULL;
} else {
/* See comment in pte_alloc_one_map() */
if (pmd_devmap_trans_unstable(vmf->pmd))
return 0;
vmf->pte = pte_offset_map(vmf->pmd, vmf->address);//计算出pte
vmf->orig_pte = *vmf->pte;
barrier();
if (pte_none(vmf->orig_pte)) {
pte_unmap(vmf->pte);
vmf->pte = NULL;
}
}
//判断pte是否为空,页表项为空有两种可能,1 页表项还没有建立,2 页表项的内容被清空了
if (!vmf->pte) {
//当前VMA 是匿名页 使用do_anonymous_page, 否则就是文件映射使用 do_fault
if (vma_is_anonymous(vmf->vma))// return !vma->vm_ops; 当前VMA是否有方法集
return do_anonymous_page(vmf);
else
return do_fault(vmf);
}
//页面没有在内存中,PTE没有映射页物理页面,真正的缺页,从交换分区回读页面
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf);
//页面在内存中,但是页面被设置为NUMA调度的页面,所以会使用do_numa_page
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);//获取进程的内存描述符mm中定义的自旋锁
spin_lock(vmf->ptl);
entry = vmf->orig_pte;
//判断这段时间PTE是否被修改了
if (unlikely(!pte_same(*vmf->pte, entry)))
goto unlock;
//判断vmf->flags 是否设置了FAULT_FLAG_WRITE
//通过ESR 的WnR判断是否 因为写内容触发的缺页异常
if (vmf->flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))//如果PTE是只读属性
return do_wp_page(vmf);//触发写时复制的缺页中断,父子进程共享内存,当其中一个需要写入新的内容的时候,就会触发写时复制
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);//对于ARM64 架构会设置PET_AF
//判断PTE内容是否发生变化,如果改变就需要更新新的内容到PTE中,并且要刷新对应的TLB和高速缓存
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 {
//这只适用于保护故障,但arch代码还没有告诉我们这是否是保护故障。这仍然可以避免线程对.text页故障进行无用的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;
}
缺页中断流程
异常addr出现在用户空间--->
vma=find_vma(addr)
没有找到VMA
如果发生在内核模式则发生Oops
如果发生在用户模式,给线程发送SIGSEGV或者SIGBUS信号终止进程
找到VMA
__handle_mm_fault重新建立页面到VMA的映射关系
遍历页表找到addres对应的PTE
PTE页表是否存在 ?
N 是否为匿名页面
Y do_anonymous_page
N do_fault
Y PTE的PRESENT是否设置,确定是否真正的缺页 ?
!N do_swap_page
!Y pte_protnone
Y do_numa_page
N 写内存导致的缺页异常,并且PTE是只读属性 ?
Y do_wp_page
N 设置AF 更新PTE内容 刷新对应TLB和高速缓存
小结
发生缺页中断的时候,会根据PTE中的PRESENT位 PTE内容是否为空以及是否为文件映射等条件,执行不同的函数处理不类型的缺页异常。
1 do_anonymous_page() ---- 处理匿名页面的缺页异常
判断条件:PTE的内容为空并且没有设置 vma->vm_ops 方法集
应用场景: malloc() 分配内存或者使用 mmap() 函数来分配匿名页面内存
2 do_fault() ------ 处理文件映射缺页异常
PTE的内容为空并且设置了 vma->vm_ops 方法集。
如果发生了读错误,那么调用 do_read_fault() 函数读取这个页面
如果在私有映射VMA中发生写保护错误,那么发生写时复制,新分配一个页面,旧的页面内容要复制到新的页面中,利用新页面生成一个PTE并将这个页面设置到硬件页表项中。
如果写保护错误发生在共享映射VMA中,就会产生脏页,调用系统的回写机制来回写这个脏页
应用场景:使用mmap读文件内容,如驱动使用mmap映射设备内存到用户空间
动态库映射,如不同的进程可以通过文件映射来共享同一个动态库
3 do_swp_page () ------ 处理swap缺页异常
判断条件:PTE中的PRESENT位没有置位并且PTE内容不为空
4 do_wp_page() ---- 处理写时复制缺页异常
复用旧页面(对 _mapcount=0 的页面和可以写的共享页面)
写时复制旧页面(对 _mapcount!=0 只读或者非共享的文件映射页面)
判断条件:PTE中的PRESENT置位并且发生了写错误缺页中断
应用场景:通过fork调用父进程创建子进程,父子进程都共享父进程的匿名页面,其中一方修改内容的时候就会发生写时复制