目录

图片无法显示,请右键点击新窗口打开图片


初识 PageFault

图片无法显示,请右键点击新窗口打开图片

在分页大专题对缺页形成的原因进行从上层进行了分析,这里再重新温习一下。Linux 用户进程有三种分配内存的方式,其中一种是 LazyAlloc(惰性分配),其特点是在分配内存的时候只分配虚拟内存,而不分配虚拟内存,那么只有在进程访问虚拟内存的时候,MMU 通过查询页表发现物理内存不存在,于是直接触发缺页异常(PageFault Exception),接着缺页异常处理函数会负责分配物理内存,以及建立页表并映射到物理内存上,待缺页异常处理函数处理完毕之后,控制权返回给用户进程,由于是异常的缘故,用户进程会重新执行发生缺页的指令,此时 MMU 发现物理内存存在因此进程可以正常访问内存. 对应内核线程也是同样的逻辑,只要内核线程访问内核空间虚拟内存时,MMU 发现物理内存不存在,那么同样也会触发缺页异常,与用户空间触发的缺页不同的是缺页异常处理函数并不会为内核线程分配物理内存,而是直接报错,这是因为内核不允许物理内存不存在的情况. 本专题将带领开发者详细了解缺页机制底层原理, 那么接下来通过一个实践案例感受什么是缺页异常,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000001000),然后在 32 行对虚拟内存执行写操作,然后在 34 行又对虚拟内存执行读操作,最后再释放内存. 以上便是一个最基础的缺页实践案例,接下来在 BiscuitOS 上进行实践:

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以应用程序从 0x6000000000 直接读取到刚刚写入的字符 ‘B’, 从结果来看虚拟内存最终与物理内存建立页表,进程可以正常访问内存. 那么光凭一个例子怎么证明发生了缺页,以及要怎么才能看到内存在缺页里流动。各位开发者不要着急,本文将一一带大家了解,最终让各位开发者看到这看不到的内存在缺页里的流动.

图片无法显示,请右键点击新窗口打开图片


PageFault 导论

图片无法显示,请右键点击新窗口打开图片

在 Linux 内存管理里,开启分页之后,CPU 直接访问的虚拟内存,虚拟内存与物理内存之间建立页表,那么 CPU 才能正常访问虚拟内存. 当虚拟内存没有与物理内存建立页表之前,CPU 就访问这段虚拟内存会引起异常(Exception), 在 Linux 里将这个异常称为缺页异常(Page Fault Exception), Linux 为缺页异常设置了缺页异常处理函数(或缺页中断处理函数). 缺页异常处理函数的主要任务是在合理的请求下,为虚拟内存分配物理内存,然后建立虚拟内存到物理内存的页表,并在缺页异常处理函数返回之后重新执行发生异常的指令, 那么 CPU 可以继续访问虚拟内存.

图片无法显示,请右键点击新窗口打开图片

进程分配内存的方式有三种,三种方式虽然不同,但只有建立了虚拟内存到物理内存的页表之后,CPU 才可以访问,三种分配方式的特点如下:

  • PreAlloc(预分配): 即进程分配虚拟内存的同时也分配物理内存,并建立虚拟内存到物理内存之间的页表,因此这类型的内存不会发生缺页,例如 malloc() 函数分配的小粒度内存,或者 mmap() 函数添加了 MAP_POPULATE 标志. 由于其分配特点,存在分配内存时会消耗很多时间,另外分配多少虚拟内存就会分配多少虚拟内存,那么可能会造成内存浪费,一旦内存分配好,那么 CPU 初次访问内存时速度会比缺页的快很多,因此这种方式适合对运行时内存访问速度要求高的场景.
  • LazyAlloc(惰性分配): 即进程起初只分配虚拟内存,只有当 CPU 访问虚拟内存时,触发缺页异常之后,缺页中断为其分配物理内存并建立页表,在缺页中断返回之后再次执行发送异常的指令,例如使用 mmap() 函数不带 MAP_POPULATE 标志分配内存,或者 malloc 分配大粒度内存. 由于其特点,存在分配内存时速度很快,并只有用到才会去分配内存,因此很节省内存,但是 CPU 初次访问内存会触发缺页相当耗时,因此这种方式适合内存资源紧缺的场景.
  • On-Demand(按需分配): 即进程起初只分配虚拟内存,但进程觉得需要为虚拟内存分配物理内存时,那么会为虚拟内存分配物理内存并建立页表,这样 CPU 访问内存时不会发送缺页异常。例如 GUP(Get User Page) 机制 或者 madvise 机制的 MADV_HUGEPAGE 请求。由于其特点,存在分配内存时速度很快,初次访问时无需缺页就可以访问,访问速度很快,但需要进程介入控制分配.

无论是使用何种分配方式,需要借助缺页机制来分配物理内存和建立页表,只是 LazyAlloc 会触发缺页异常完成物理内存分配和页表建立,而 PreAllocOn-Demand 会主动调用缺页机制提供的接口,以此完成物理内存的分配和页表建立. 因此开发者在学习缺页机制时候一定要记住的一个定理: 什么时候分配物理内存,什么时候建立页表.

Page Fault Exception

图片无法显示,请右键点击新窗口打开图片

页面错误异常(Page Fault Exception) 是当 CPU 尝试访问一个当前不在物理内存中的虚拟页时,由内存管理单元(MMU)触发的异常。此异常可能是由于该页尚未加载到 RAM、访问违规,或者对应的页表条目无效所引起的. 缺页异常发送之后,不同架构会给出发生异常的原因. 例如 Intel X86 架构会提供上图引起缺页异常的原因,其他架构与之类似:

  • PRSENT: 由于虚拟内存没有映射物理内存引起的页面故障
  • READ: 对虚拟区域写操作引起的页面故障
  • WRITE: 对写保护的虚拟区域执行写操作引起的页面故障
  • User: 缺页故障发生在用户空间
  • Super: 缺页故障发生在内核空间
  • Instruct: 缺页故障由执行指令引起的页面故障

图片无法显示,请右键点击新窗口打开图片

缺页异常在给出故障原因的同时,也会给出发生缺页的虚拟地址。例如在 Intel X86 架构下,当发生缺页异常时,CR2 寄存器存储了发生缺页的虚拟地址,然后结合当前进程和虚拟地址,缺页异常处理函数可以找到对应的 VMA.

图片无法显示,请右键点击新窗口打开图片

缺页异常处理函数(PageFault Exception handler) 是当发生缺页异常之后,专门用来处理该异常的代码。缺页异常处理函数的大概逻辑如上图,其流程如下:

  • 触发缺页异常: 当进程试图访问一个尚未在物理内存中的虚拟页时,内存管理单元(MMU)会触发一个缺页异常
  • 异常向量表处理: CPU 首先跳转到预设的地址,通常这个地址指向一个异常向量表,这个表里保存着处理各种异常的程序的地址。对于缺页异常,它将跳转到专门处理这种异常的代码
  • 检查错误码: 异常的原因可以是多种多样的,例如尝试访问非法的内存地址、访问权限不足等. Linux将根据错误码来确定下一步的动作
  • 非法访问检测: 如果是一个非法的内存访问(例如,进程试图访问它没有权限的内存),则进程通常会收到一个信号,如 SIGSEGV,然后终止或采取其他行动
  • 页换入: 如果该页确实是有效的但尚未加载到系统物理地址空间,那么 Linux 需要从磁盘(例如,交换分区或文件系统)上将其换入
    • 如果该页是匿名页(例如,由 malloc() 或 mmap() 创建的),它可能会从交换空间中恢复
    • 如果该页是文件映射的,它将从对应的文件中读取
  • 分配新页: 如果进程访问的是一个尚未分配的页(例如,堆上的延迟分配或新的堆栈页),系统需要分配一个新的物理页并将其关联到该虚拟地址
  • 更新页表: 一旦页面被加载到系统物理地址空间,对应的页表项需要更新,以表示该虚拟页与物理页的新映射
  • 恢复进程: 一旦处理完成,进程会从触发缺页异常的指令重新开始执行

图片无法显示,请右键点击新窗口打开图片