上一章重点学习了内核对于异常处理的总体的流程,从异常向量为入口,最终调用到真正的异常处理的接口__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来分配和映射新页面

  • 如果页面在物理内存中

    • 写时复制: 如果页在物理内存中,当用户向共享内存发出写请求时,它将现有的共享页面复制到新页面
Logo

更多推荐