上一章重点学习了内核对于异常处理的总体的流程,从异常向量为入口,最终调用到真正的异常处理的接口__do_page_fault,本章主要是学习之前提到的内存缺页异常的常见场景中如何实现
static int __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
unsigned int flags, struct task_struct *tsk)
{
struct vm_area_struct *vma;
int fault;
vma = find_vma(mm, addr); -------------------(1)
fault = VM_FAULT_BADMAP;
if (unlikely(!vma))
goto out;
if (unlikely(vma->vm_start > addr)) --------------------(2)
goto check_stack;
/*
* Ok, we have a good vm_area for this
* memory access, so we can handle it.
*/
good_area: --------------------(3)
if (access_error(fsr, vma)) {
fault = VM_FAULT_BADACCESS;
goto out;
}
return handle_mm_fault(vma, addr & PAGE_MASK, flags); --------------------(4)
check_stack:
/* Don't allow expansion below FIRST_USER_ADDRESS */
if (vma->vm_flags & VM_GROWSDOWN &&
addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr))
goto good_area;
out:
return fault;
}
-
首先find_vma通过失效地址addr来查找vma,如果没有找到vma,说明addr地址还没有在进程地址空间中分配任何一个VMA的线性区,这将是一种严重的错误,返回VM_FAULT_BADMAP错误,内核将会杀掉该进程
-
找到了vma,如果发现addr不在vma的映射区,可能是下面两种原因
- 该区域的VM_GROWSDOWN标志位置位,意味着该区域是栈区,自顶向下增长,接下来调用expand_stack适当地增大栈
- 找到的区域不是栈,访问无效
expand_stack函数主要完成
- 开展堆栈区间的VMA,重新调整VMA的起始地址以可以容纳addr,这个前提是不会导致进程区间超限或者进程动态分配的页面超限
- expand_stack只是更改了堆栈区的vm_area_struct结构,没有建立物理内存映射
- 当返回值为非0 ,就返回对应的错误码,并退出,交由上级程序处理
-
当地址存在后,调用access_error判断当前vma是否具有可写或可执行权限,如果发生一个写错误的缺页异常,首先判断vma属性是否具有可写,如果没有就返回VM_FAULT_BADACCESS
-
最后调用handle_mm_fault函数,它是缺页中断的核心处理函数,正常情况下将返回VM_FAULT_MAJOR或VM_FAULT_MINOR,返回错误码fault并加一task的maj_flt或min_flt成员;
确定异常是在允许的地址触发,内核必须确定将所需数据读取到物理内存的适当方法,该任务委托给handle_mm_fault是一个体系结构无关的,用于选择适当的异常恢复方法(按需调页/换入等),并应用选择的方法。其大致的处理流程如下图所示
handle_mm_fault为引发缺页的进程分配一个物理页框,它先确定与引发缺页的线性地址对应的各级页目录项是否存在,如不存在则分进行分配。具体如何分配这个页框是通过调用handle_pte_fault()完成的,注意最后一个参数flag,它来源于fsr。
- 创建各级页表目录,该函数支持四级页表,而ARM32只支持2级页表,那么pgd = pua = pmd,该函数主要是分配pmd,然后调用handle_pte_fault
handle_mm_fault函数确认在各级页目录中,通向对应于异常地址页表项的各个页目录都存在,handle_pte_fault函数分析缺页的原因,并进行处理,其大致的处理流程如下
-
如果页不在物理内存中,那么就分为以下3中情况
-
匿名映射: 如果PTE的内容为空,没有找到对应的页表项,对于anonymous page,将通过调用do_anonymous_page()函数来分配和映射新页面,也就是按需分配。
用户空间使用malloc()进行内存申请时(对应底层的实现是mmap或者brk),内核并不会立刻为其分配物理内存,而只是为请求的进程的rbtree管理的vma信息中记录(添加或更改)诸如内存范围和标志之类的信息。只有当内存被真正使用,触发page fault,才会真正分配物理页面和对应的页表项,即demand alloction,对应的函数实现是 do_anonymous_page() 。通过mmap映射建立的heap和stack等内存区域,在初始未使用时,也适用于这样的规则。
-
文件映射: 如果PTE的内容为空,处理文件页发生异常,将会通过do_fault来分配和映射新页面
对于page cache, 在发生内存回收后,部分text(code)段的页面会被discard,部分data段的页面会被writeback,之后再次访问这些页面,也将出现page fault。此时,需要从外部存储介质中,将页面内容调回内存,即demand paging,对应的函数实现是 do_fault()
-
换入或按需调页: 如果该页标记为不存在,而页表中保存了相关的信息,则意味着页已经被换出,因而必须从系统的某个交换分区换入,则调用do_swap_page来分配和映射新页面
-
-
如果页面在物理内存中
- 写时复制: 如果页在物理内存中,当用户向共享内存发出写请求时,它将现有的共享页面复制到新页面