背景
Read the fucking source code!
--By 鲁迅
A picture is worth a thousand words.
--By 高尔基
说明:
Kernel 版本: 4.14
ARM64 处理器, Contex-A53, 双核
使用工具: Source Insight 3.5, Visio
1. 概述
上篇文章分析到 malloc/mmap 函数中, 内核实现只是在进程的地址空间建立好了 vma 区域, 并没有实际的虚拟地址到物理地址的映射操作. 这部分就是在 Page Fault 异常错误处理中实现的.
Linux 内核中的 Page Fault 异常处理很复杂, 涉及的细节也很多, malloc/mmap 的物理内存映射只是它的一个子集功能, 下图大概涵盖了出现 Page Fault 的情况:
下边就开始来啃啃硬骨头吧.
2. Arm64 处理
Page Fault 的异常处理, 依赖于体系结构, 因此有必要来介绍一下 Arm64 的处理.
代码主要参考: arch/arm64/kernel/entry.S.
Arm64 在取指令或者访问数据时, 需要把虚拟地址转换成物理地址, 这个过程需要进行几种检查, 在不满足的情况下都能造成异常:
地址的合法性, 比如以 39 有效位地址为例, 内核地址的高 25 位为全 1, 用户进程地址的高 25 位为全 0;
地址的权限检查, 这里边的权限位都位于页表条目中;
从上图中可以看到, 最后都会调到 do_mem_abort 函数, 这个函数比较简单, 直接看代码, 位于 arch/arm64/mm/fault.c:
- /*
- * Dispatch a data abort to the relevant handler.
- */
- asmlinkage void __exception do_mem_abort(unsigned long addr, unsigned int esr,
- struct pt_regs *regs)
- {
- const struct fault_info *inf = esr_to_fault_info(esr);
- struct siginfo info;
- if (!inf->fn(addr, esr, regs))
- return;
- pr_alert("Unhandled fault: %s (0x%08x) at 0x%016lx\n",
- inf->name, esr, addr);
- mem_abort_decode(esr);
- info.si_signo = inf->sig;
- info.si_errno = 0;
- info.si_code = inf->code;
- info.si_addr = (void __user *)addr;
- arm64_notify_die("", regs, &info, esr);
- }
该函数中关键的处理: 根据传进来的 esr 获取 fault_info 信息, 从而去调用函数. struct fault_info 用于错误状态下对应的处理方法, 而内核中也定义了全局结构 fault_info, 存放了所有的情况.
主要的错误状态和处理函数对应如下:
- static const struct fault_info fault_info[] = {
- { do_bad, SIGBUS, 0, "ttbr address size fault" },
- { do_bad, SIGBUS, 0, "level 1 address size fault" },
- { do_bad, SIGBUS, 0, "level 2 address size fault" },
- { do_bad, SIGBUS, 0, "level 3 address size fault" },
- { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 0 translation fault" },
- { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },
- { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" },
- { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 3 translation fault" },
- { do_bad, SIGBUS, 0, "unknown 8" },
- { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 access flag fault" },
- { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 access flag fault" },
- { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 access flag fault" },
- { do_bad, SIGBUS, 0, "unknown 12" },
- { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 permission fault" },
- { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 permission fault" },
- { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 permission fault" },
- ...
- };
从代码中可以看出:
出现 0/1/2/3 级页表转换错误时, 会调用
do_translation_fault
, 实际中
do_translation_fault
最终也会调用到 do_page_fault;
出现 1/2/3 级页表访问权限的时候, 会调用 do_page_fault;
其他的错误则调用 do_bad, 其中未列出来的部分还包括 do_sea 等操作函数;
do_translation_fault
do_page_fault
do_page_fault 函数为页错误异常处理的核心函数, 与体系结构相关, 上图中的 handle_mm_fault 函数为通用函数, 也就是不管哪种处理器结构, 最终都会调用到该函数.
3. handle_mm_fault
handle_mm_fault 用于处理用户空间的页错误异常:
进程在用户模式下访问用户虚拟地址, 触发页错误异常;
进程在内核模式下访问用户虚拟地址, 触发页错误异常;
从 do_page_fault 函数的流程图中也能看出来, 当触发异常的虚拟地址属于某个 vma, 并且拥有触发页错误异常的权限时, 会调用到 handle_mm_fault 函数, 而 handle_mm_fault 函数的主要逻辑是通过__handle_mm_fault 来实现的.
流程如下图:
3.1 do_fault
do_fault 函数用于处理文件页异常, 包括以下三种情况:
读文件页错误;
写私有文件页错误;
写共享文件页错误;
3.2 do_anonymous_page
匿名页的缺页异常处理调用本函数, 在以下情况下会触发:
malloc/mmap 分配了进程地址空间区域, 但是没有进行映射处理, 在首次访问时触发;
用户栈不够的情况下, 进行栈区的扩大处理;
3.3 do_swap_page
如果访问 Swap 页面出错 (页面不在内存中), 则从 Swap cache 或 Swap 文件中读取该页面.
由于在 4.14 内核版本中, do_swap_page 调用的很多函数都是空函数, 无法进一步的了解, 大体的流程如下图:
3.4 do_wp_page
do_wp_page 函数用于处理写时复制 (copy on write), 会在以下两种情况处理:
创建子进程时, 父子进程会以只读方式共享私有的匿名页和文件页, 当试图写的时候, 触发页错误异常, 从而复制物理页, 并创建映射;
进程创建私有文件映射, 读访问后触发异常, 将文件页读入到 page cache 中, 并以只读模式创建映射, 之后发生写访问后, 触发 COW;
关键的复制工作是由 wp_page_copy 完成的:
来源: https://www.cnblogs.com/LoyenWang/p/12116570.html