目录

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


Huge-PageFault 机制

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

在 Linux 里,当进程使用 LazyAlloc 方式分配虚拟内存时,进程一旦访问虚拟内存时,MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数为虚拟内存分配物理内存并建立页表,那么进程可以正常访问虚拟内存。在这个过程中,如果虚拟内存和物理内存粒度都是 4KiB,那么这里称为标准的 PageFault 场景. 如果虚拟内存和物理内存的粒度都是 2MiB,并且都按 2MiB 粒度对齐,那么这里称为 Huge-PageFault 场景. 在某些情况下 Huge-PageFault 也支持 1Gig 粒度的缺页场景.

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

目前支持 Huge-PageFault 的内存类型包括: 匿名透明大页、采用 DAX 的文件透明大页、RSVD PMDMAPPED 和 RSVD PUDMAPPED. 这些内存类型在发生缺页时都采用 Huge-PageFault 流程进行处理,与普通 4KiB 粒度内存缺页不相同.

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

对于 Huge-PageFault 场景的内存,在映射文件的场景,文件映射都会提供相应的 mmap 接口,并且为每个 VMA 提供的 vm_ops 里支持 huge_fault 接口,缺页异常处理函数缺页的时候会调用 huge_fault 对应的接口进行缺页处理. 目前支持 huge_fault 接口的文件系统是 EXT4 和 XFS,但两种文件系统挂载时需要使用 “-o dax” 参数,这样才能进入到 Huge-PageFault 路径.

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

对于匿名透明大页要支持 Huge-PageFault,那么需要通过 hugepage_vma_check 的检查,如果这些条件检查返回 true,缺页就可以使用 Huge-PageFault, 这些条件包括:

  • 77: VMA 不属于指定的用户进程则失败
  • 85: VMA 不支持大页或者进程不支持 THP 则失败
  • 91: 透明大页机制不支持 HUGE DAX 则失败
  • 95: VMA 采用的是 DAX 映射但该函数不再缺页流程里则失败
  • 103: VMA 不被 Khugepaged 扫描则失败
  • 112: 透明大页还没有成型且该函数不在缺页流程里则失败
  • 121: 该函数不在缺页流程里且采用共享映射,则看共享内存是否支持大页
  • 124: 系统不支持大页则失败
  • 128: VMA 映射大页但 THP 不支持 Always 合并功能则失败
  • 132: 该函数不在缺页流程且支持文件 THP 则成功
  • 135: VMA 不是匿名映射则失败
  • 138: VMA 是临时堆栈则失败
  • 148: VMA 没有建立逆向映射

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

Huge-PageFault 虚拟内存发生缺页场景的场景是: 当 CPU 访问了未建立页表映射内存,MMU 检查到物理内存不存在即触发缺页异常. 但在 Linux 里还有很多场景会导致 Huge-PageFault 缺页,总结包括如下场景:

  • 可读可写匿名透明大页场景: 当进程分配一个匿名透明大页虚拟内存,当进程访问这段虚拟内存时,MMU 检查到物理内存不存在直接触发缺页异常,在缺页异常处理函数里检查到内存为匿名透明大页内存,并且此时发现 PMD Entry 为空,另外虚拟内存也符合 Huge-PageFault 的条件,于是分配物理大页并建立 PMD 页表映射,接下来进程可以对虚拟内存进行读写操作.
  • 只读匿名透明大页场景: 当进程分配一个只读匿名透明大页虚拟内存,当进程对这段虚拟内存首次发起读操作,MMU 检查到物理内存不存在直接触发缺页异常,在缺页异常处理函数里检查到为匿名透明大页,并且此时发现 PMD Entry 为空,另外虚拟内存也符合 Huge-PageFault 的条件,另外如果系统支持 HUGE ZERO PAGE,那么缺页异常处理函数将虚拟内存映射到 HUGE ZERO PAGE,否则就映射到普通大页上,最后建立写保护的页表。进程接下来可以读虚拟内存进行读操作,一旦发生写操作将再次触发缺页并直接 SegmentFault.
  • 写保护的匿名透明大页场景: 当进程一块写保护的匿名透明大页虚拟内存,当进程对这段虚拟内存进行读操作时,不会触发缺页可以正常读取. 一旦进程对虚拟内存发起写操作时,MMU 发现权限不对而触发缺页,缺页流程里检查到是匿名透明大页,那么会将匿名透明大页拆分(Split)成小页,然后将这些小页页表设置为写保护,作为这些之后缺页异常处理函数将这次缺页视为对 4KiB 粒度写保护写操作缺页来处理,处理完之后会发现只有缺页的 4KiB 小页变成可读可写匿名页,原先匿名大页内存的其他小页变成写保护匿名页. 接下来进程可以对 4KiB 虚拟内存进行读写操作.
  • COW 的匿名透明大页场景: 当进程分配一块匿名透明大页虚拟内存之后,发生了 FORK 操作,匿名透明大页变成写保护。当父子进程对这段虚拟内存进行读操作时,不会触发缺页可以正常读取. 一旦父子其一进程对虚拟内存发起写操作时,MMU 发现权限不对而触发缺页,缺页流程里检查到是匿名透明大页,那么会将匿名透明大页拆分(Split)成小页,然后将这些小页页表设置为写保护,作为这些之后缺页异常处理函数将这次缺页视为对 4KiB 粒度写保护写操作缺页来处理,处理完之后会发现只有缺页的 4KiB 小页变成可读可写匿名页,原先匿名大页内存的其他小页变成写保护匿名页. 接下来进程可以对 4KiB 虚拟内存进行读写操作. 那么当另外一个进程对该内存写操作时,也会发生缺页,缺页的过程和前一个进程一致.
  • NUMA Balancing 匿名透明大页场景: 当在支持多 NUMA NODE 的系统里,进程跨 NUMA 分配了一个匿名透明大页. 系统支持 NUMA Balancing 机制,并定期将跨 NUMA 的内存迁移会本地 NUMA NODE. 在这个场景下,进程开始可以正常访问匿名透明大页,但随着 NUMA Balancing 的运行,发现匿名透明大页跨 NUMA,于是修改其页表为 PROT_NONE,那么当进程再次访问匿名透明大页之后,MMU 检查物理内存不存在并触发缺页异常,在缺页异常处理函数里,匿名透明大页的页表是 PROT_NONE 的,于是从页表中获得跨 NUMA 的物理地址,于是将匿名透明大页迁移到 LOCAL NUMA NODE. 待缺页异常处理函数处理完毕之后,进程可以正常访问匿名透明大页内存.
  • EXT4/XFS HUGE-DAX 文件透明大页场景: 在支持 DAX 的 EXT4/XFS 文件系统里,进程可以将虚拟内存直接映射到 PMEM 上,并且可以支持 HUGE-DAX 映射,也就是将 2MiB 的虚拟内存映射到 2MiB 的 PMEM 区域上。进程将文件映射到进程地址空间,并对该段虚拟内存进行访问,MMU 发现物理内存不存在并触发缺页,缺页异常处理发现文件对应的文件系统提供了 huge_fault 接口,并调用该接口将虚拟内存映射到 PMEM 上. 待缺页中断访问之后,进程可以正常访问这段内存.
  • PMDMAPPED 1G 内存场景: 进程分配 1G 虚拟内存,准备映射到系统预留物理内存上,当进程访问这段虚拟内存时,MMU 发现物理内存不存在并触发缺页异常,缺页异常处理函数发现其属于文件映射,并且支持 huge_fault 接口,另外虚拟地址也是按 1G 对齐,那么调用 huge_fault 接口将 1G 的虚拟内存直接映射到 1G RSVD 物理内存上,待缺页异常处理函数返回之后进程可以正常访问虚拟内存.

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

HUGE-PageFault 缺页处理函数主要逻辑位于 __handle_mm_fault 函数,包括 1Gig 大页缺页的 PUD-HUGE-PF 缺页逻辑,以及匿名透明大页的 ANON-THP-PF 缺页逻辑,另外还是写保护的 WP-HUGE-PF 和 NUMA Balancing 的 NUMA-HUGE-PF 逻辑,最后就是文件透明大页的 FILE-THP-PF, 有了代码逻辑分析之后,接下来就是针对这些代码逻辑的场景分析,总结有如下场景:

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


Huge-PageFault: 可读可写透明大页场景

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

在 Linux 里,透明大页有很多种,其中匿名透明大页(Anonymous THP) 是由多个匿名页转化而成,因此匿名大页具有匿名页的属性。可读可写的匿名透明大页可以通过 mmap 函数进行分配,其使用 PROT_READ 和 PROT_WRITE 属性。对于可读可写的匿名透明大页内存,当还没有建立页表时,进程对其的访问有如下情况:

  • 当 CPU 首次对可读可写匿名透明大页内存写操作: MMU 因发现没有对应的物理内存而触发缺页异常,在缺页异常处理函数里会新分配一个物理大页作为匿名透明大页,并更新页表映射到该匿名透明大页上,这样 CPU 可以对匿名内存进行写操作
  • 当 CPU 首次对可读可写匿名透明大页内存读操作: MMU 同样检查到没有对应的物理内存,那么也会触发缺页异常,在缺页异常处理函数里,如果系统支持 Huge ZERO Page,那么会将页表映射到 Huge Zero Page 上,并将页表修改为写保护,那么 CPU 可以对匿名透明大页内存进行读操作,但如果 CPU 接下来对匿名透明大页内存发起写操作,MMU 检查权限异常,那么再次触发缺页异常,缺页异常处理函数此时新分配一个物理大页作为匿名透明大页,并更新页表映射到该匿名透明大页上,最后将页表修改为可读可写,这样 CPU 就可以对匿名透明大页内存进行写操作.
  • 当 CPU 首次对可读可写匿名透明大页内存读操作: MMU 同样检查到没有对应的物理内存,那么也会触发缺页异常,在缺页异常处理函数里,如果系统不支持 Huge ZERO Page,那么缺页异常处理函数此时新分配一个物理大页作为匿名透明大页,并更新页表映射到该匿名透明大页上,这样 CPU 就可以对匿名透明大页内存进行读操作,并且后续的写操作不会触发缺页异常.

虽然都是可读可写匿名透明大页内存,但访问内存的行为不同,以及 Linux 支持的功能不同,最终导致缺页的行为也是大相径庭。为了更加深刻了解可读可写匿名透明大内存,接下来通过一个实践案例了解这种异常场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Huge PageFault with Anonymous THP --->

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

BiscuitOS-PAGING-HUGE-PF-ANON-THP-default Source Code on Gitee

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

实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志分配一段匿名透明大页内存,然后在 32 行对匿名透明大页内存进行写操作,并在 34 行对匿名透明大页内存进行读操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 32 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 32 行前后加上 BS_DEBUG 开关:

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

接着在匿名透明大页内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_huge_pmd_anonymous_page 函数的 788 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-ANON-THP-default/
# 编译内核
make kernel
# 编译实践案例
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “ANON-THP HUGE-PF on do_huge_pmd_anonymous_page 0x6000000000”, 那么说明实践案例分配的匿名透明大页内存,同时也可以看到匿名透明大页内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名透明大页内存在缺页异常处理流程里的流动.

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

对于可读可写的匿名透明大页内存,其缺页异常处理流程如上图,当 PMD 为空且 hugepage_vma_check 检查通过之后,才进入 HugeFault 路径,并且 vma_is_anonymous 满足之后,才是真正的匿名透明大页异常处理流程,该流程首先调用 vma_alloc_folio 分配物理大页,然后构造 PMD Entry 内容,最后在填充页表,整个过程都是循规蹈矩.

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

do_huge_pmd_anonymous_page 函数是处理匿名透明大页缺页的核心流程,789 行 transhuge_vma_suitable 检查到透明大页不稳定则直接返回 VM_FAULT_FALLBACK. 791 行通过 anon_vma_prepare 行检查匿名透明大页的逆向映射是否可用,如果不可用直接分配一个新的. 795 行的检查条件中,如果匿名透明大页缺页时发生了读操作,并且 mm_forbids_zeropage 返回假,即系统支持 HUGE ZERO PAGE,最后 transparent_hugepage_use_zero_page 也返回真,即 THP 支持 HUGE ZERO Page,那么代码流程进入 798 分支进行处理,该分支主要将虚拟内存映射到一个写保护的 Huge ZERO Page,进程只会读到 0,一旦进程发起写操作,那么会再次触发缺页; 反之 795 的条件不满足或者发生缺页时是写操作,那么直接跳到 835 行调用 vma_alloc_folio 分配一个匿名大页,并调用 __do_huge_pmd_anonymous_page 函数进行页表建立等操作.

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

__do_huge_pmd_anonymous_page 函数完成页表的映射和逆向映射等操作。671 行调用 pte_alloc_one 函数缺页 PMD Entry 存在,677 行调用 clear_huge_page 清除大页内容,685 行获得 PMD Entry,接下来 705-706 构造 PMD Entry 内容,707 行设置匿名透明大页的逆向映射,708 行将匿名透明大页加入到 LRU 链表,710 行设置更新页表。完成以上动作基本建立好页表映射,那么进程以后对虚拟内存的读写操作都不会再次触发缺页.

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


Huge-PageFault: 只读透明大页场景

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

在 Linux 里,透明大页有很多种,其中匿名透明大页(Anonymous THP) 是由多个匿名页转化而成,因此匿名大页具有匿名页的属性。只读匿名透明大页可以通过 mmap 函数进行分配,其只使用 PROT_READ 属性。对于只读的匿名透明大页,当还没有建立页表时,进程对其的访问有如下情况:

  • 当 CPU 首先对只读匿名透明大页内存读操作: MMU 因发现物理内存不存在而触发缺页异常,如果系统支持匿名 HUGE ZERO PAGE,那么缺页异常处理函数会将虚拟内存映射到一个 HUGE ZERO PAGE 上,并且页表为写保护, 这样 CPU 可以一直从虚拟内存里读数据,只不过这些数据都是 ZERO.
  • 当 CPU 首先对只读匿名透明大页内存读操作: MMU 因发现物理内存不存在而触发缺页异常,如果系统不支持匿名 HUGE ZERO PAGE,那么缺页异常处理函数会先分配一个物理大页,然后清除大页的内容,最后建立写保护页表映射到该大页上, 这样 CPU 可以一直从虚拟内存里读数据,只不过这些数据都是 ZERO.
  • 当 CPU 首次对只读匿名透明大页写操作: MMU 发现物理内存不存在而触发缺页异常处理函数,缺页异常处理函数在处理过程中发现虚拟内存是只读的,那么直接触发 SIG_BUS 导致进程 SegmentFault 退出.
  • 当 CPU 对建立页表的只读匿名透明大页写操作: MMU 发现权限异常而触发缺页异常,缺页异常处理函数同样检查到虚拟区域是只读,那么直接触发 SIG_BUS 导致进程 SegmentFault 退出.

虽然都是只读匿名透明大页内存,但访问内存的行为不同,以及 Linux 支持的功能不同,最终导致缺页的行为也是大相径庭。为了更加深刻了解只读匿名透明大内存,接下来通过一个实践案例了解这种异常场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Huge PageFault with RO Anonymous THP(Huge ZERO Page) --->

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

BiscuitOS-PAGING-HUGE-PF-RO-ANON-THP-default Source Code on Gitee

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

实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志分配一段匿名透明大页内存,然后在 32 行对匿名透明大页内存进行读操作,并在 37 行对匿名透明大页内存进行写操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 32 行和 37 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 32 行前后加上 BS_DEBUG 开关:

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

接着在匿名透明大页内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_huge_pmd_anonymous_page 函数的 801 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-RO-ANON-THP-default/
# 编译内核
make kernel
# 编译实践案例
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “RO ANON-THP HUGE-PF on do_huge_pmd_anonymous_page 0x6000000000”, 那么说明实践案例分配了只读匿名透明大页内存,同时也可以看到匿名透明大页内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名透明大页内存在缺页异常处理流程里的流动.

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

对于只读的匿名透明大页内存,其缺页异常处理流程如上图,当 PMD 为空且 hugepage_vma_check 检查通过之后,才进入 HugeFault 路径,并且 vma_is_anonymous 满足之后,才是真正的匿名透明大页异常处理流程,对于只读匿名透明大页有两种分支,当支持透明大页 ZERO PAGE,那么进入 RO-HUGE-ZEROPAGE 分支将页表映射到 Huge ZERO Page; 对于不支持 Huge ZERO Page,而进入 NORMAL-RO-THP 分支,分配一个物理大页,然后将写保护的页表映射到物理大页上.

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

当系统支持透明 HUGE ZERO PAGE,也就是 796 行的 transparent_hugepage_use_zero_page 条件满足,那么缺页流程采用 801 行分支逻辑处理只读匿名透明大页缺页. 函数首先 801 行调用 pte_alloc_one 函数确保 PMD 页表页存在,然后 804 行调用 mm_get_huge_zero_page 函数获得一个 HUGE ZERO PAGE,接着 810 行获得 PMD Entry,此时 PMD Entry 为空,否则直接返回; 函数 823 行调用 set_huge_zero_page 函数将虚拟内存写保护的页表映射到 HUGE ZERO PAGE 上. 另外如果透明大页要支持 HUGE ZERO PAGE, 使用如下命令:

echo 1 > /sys/kernel/mm/transparent_hugepage/use_zero_page

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

当系统不支持透明大页 HUGE ZERO PAGE 时, do_huge_pmd_anonymous_page 函数是处理只读匿名透明大页缺页的核心流程,789 行 transhuge_vma_suitable 检查到透明大页不稳定则直接返回 VM_FAULT_FALLBACK. 791 行通过 anon_vma_prepare 行检查匿名透明大页的逆向映射是否可用,如果不可用直接分配一个新的. 795 行的条件不满足,那么直接跳到 835 行调用 vma_alloc_folio 分配一个匿名大页,并调用 __do_huge_pmd_anonymous_page 函数进行页表建立等操作.

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

__do_huge_pmd_anonymous_page 函数完成页表的映射和逆向映射等操作。671 行调用 pte_alloc_one 函数缺页 PMD Entry 存在,677 行调用 clear_huge_page 清除大页内容,685 行获得 PMD Entry,接下来 705-706 构造写保护的 PMD Entry 内容,707 行设置匿名透明大页的逆向映射,708 行将匿名透明大页加入到 LRU 链表,710 行设置更新页表。完成以上动作基本建立好页表映射,那么进程以后对虚拟内存的读操作都不会再次触发缺页. 一旦对只读内存发起写操作,那么会再次触发缺页,但此时缺页异常处理函数发现虚拟内存只读,那么直接发送 SIG_BUS 导致进程 Segment Fault 异常退出.

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


Huge-PageFault: 写保护透明大页场景

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

写保护(Write-Protection)匿名透明大页内存是可读可写的虚拟内存,但对应的页表没有 _PAGE_RW 写权限,于是对写保护匿名透明大页内存发起写操作会引起缺页. 写保护匿名透明大页内存可以通过 mmap 函数进行分配,其可以包含 PROT_READ 和 PROT_WRITE 属性。写保护匿名透明大页内存存在以下几种缺页场景:

  • 当 CPU 首次对 HUGE ZERO PAGE 写保护匿名内存发起写操作: MMU 因为发现权限异常,于是触发缺页异常,在缺页异常处理函数里分配一个新的物理大页,并将页表更新映射新的物理页,并向页表添加 _PAGE_RW 属性,那么 CPU 后续的读写操作不会触发缺页.
  • 当 CPU 首次对 Normal HUGE PAGE 写保护匿名内存发起写操作: MMU 因为发现权限异常,于是触发缺页异常,在缺页异常处理函数向页表添加 _PAGE_RW 写属性,那么 CPU 后续的读写操作不会引发缺页.

写保护内存的出现是为了某些特定的场景,比如 COW、或者为了拦截某次页表修改之后的写操作等,但无论如何写保护内存的基本缺页逻辑如上。为了更加深刻了解写保护匿名透明大页内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Huge PageFault with WP(Write-Protect) Anonymous THP --->

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

BiscuitOS-PAGING-HUGE-PF-WP-ANON-THP-default Source Code on Gitee

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

实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志分配一段匿名透明大页内存,然后在 32 行对匿名透明大页内存进行读操作,并在 36 行对匿名透明大页内存进行写操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 32 行和 36 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 36 行前后加上 BS_DEBUG 开关:

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

接着在匿名透明大页内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_huge_pmd 函数的 4802 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-WP-ANON-THP-default/
# 编译内核
make kernel
# 编译实践案例
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “ANON-THP HUGE-PF on wp_huge_pmd 0x6000000000”, 那么说明实践案例分配了写保护匿名透明大页内存,同时也可以看到匿名透明大页内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名透明大页内存在缺页异常处理流程里的流动.

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

对于写保护的匿名透明大页内存,其缺页异常处理流程如上图,当 PMD 不为空且缺页原因包括 FAULT_FLAG_WRITE 检查通过之后,才进入 WP-HUGEPAGE 路径. wp_huge_pmd 函数处理多种写包括场景,例如本案例的 ZERO HUGE PAGE 的写保护场景,也包括 MPROTECT 的写保护场景等. 无论那种写保护场景,当匿名透明大页不是独占(Exclusived) 时,缺页异常处理流程都会将匿名透明大页拆分成小页, 然后再修改页表为可读可写.

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

wp_huge_pmd 函数处理大页写保护场景,4802 行调用 vma_is_anonymous 函数检查到大页是匿名透明大页,那么调用 do_huge_pmd_wp_page 函数处理写保护场景; 4808 行检查到 VMA 支持 huge_fault, 该场景是文件映射大页的场景,例如 EXT4 和 XFS 文件系统支持映射文件大页,他们的写保护场景就会调用 4809 行提供的专有异常处理函数. 最后函数返回 VM_FAULT_FALLBACK 继续执行接下来的缺页异常处理函数.

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


Huge-PageFault: COW 透明大页场景

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

写时拷贝(COW)匿名透明大页内存是当进程调用 fork() 函数构造子进程时,子进程继承了父进程的匿名透明大页内存,但 fork 系统调用会将父进程和子进程的匿名透明大页内存都修改为写保护,那么可以确保父子进程读到同样的内容,而父子进程对匿名透明大页内存要写的时候,才会在属于自己的匿名内存(匿名透明大页内存)上执行写操作。COW 的核心是在写的时候才会分配物理内存承接写操作,其存在两种缺页场景:

  • 当 CPU 写入父子进程还在共享的 COW 匿名透明大页内存: MMU 因发现权限异常而触发缺页异常,缺页异常处理函数是被该内存为匿名透明大页,并且此时页的引用计数大于 1,那么缺页异常函数将匿名透明大页拆分成写保护的匿名页,然后按匿名页写保护缺页的流程处理,此时缺页异常处理函数会新分配一个匿名页,然后将原先内容拷贝到新的匿名页上,并将页表更新到新的匿名页上。此时匿名透明大页依旧存在,但名存实亡,其与内存也变成了写保护的匿名页。最后进程可以对该匿名内存进行读写操作.
  • 当 CPU 写入父子进程独占的 COW 匿名透明大页内存: MMU 因发现权限异常而触发缺页异常,缺页异常处理函数是被该内存为匿名透明大页,并且此时页的引用计数为 1,同样的缺页异常函数将匿名透明大页拆分成写保护的匿名页,然后按匿名页写保护缺页的流程处理,此时缺页异常处理函数会新分配一个匿名页,然后将原先内容拷贝到新的匿名页上,并将页表更新到新的匿名页上。此时匿名透明大页已经不存在,其余内存也变成了写保护的匿名页。最后进程可以对该匿名内存进行读写操作.

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

在 Linux 里使用 struct anon_vma 和 struct anon_vma_chain 两个数据结构体维护匿名透明大页内存的逆向映射,所有逆向映射就是通过匿名页被哪些 VMA 映射。AV 是 struct anon_vma 的简称,AVC 是 struct anon_vma_chain 的简称,每个 VMA 一一对应一个 AV,每个 AV 维护一颗红黑树,红黑树的节点表示映射到 AV 的不同 VMA,每个 VMA 通过 AVC 进行表示,那么只要遍历这颗红黑树就可以知道有多少进程的 VMA 映射到该 AV,那么可以看到一个 AVC 就是一次映射,可以通过 AVC 找到对应的 VMA吗,那么可以将匿名透明大页指向 AV,那么就可以知道有多少进程映射到自己. 每当进程发送一次 fork 动作时,子进程会在父进程 AV 红黑树下新增一个 AVC 节点,AVC 节点指向子进程的 VMA,并且子进程 VMA 对应的 AV 的 Parent 指向父进程 VMA 对应 AV. 以此类推无论进程 fork 多少次,都可以知道一个匿名透明大页被多少进程映射. 为了更加深刻了解 COW 匿名透明大页内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Huge PageFault with COW(Copy-On-Write) Anonymous THP --->

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

BiscuitOS-PAGING-HUGE-PF-COW-ANON-THP-default Source Code on Gitee

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

实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志分配一段匿名透明大页内存, 并且赋予 PROT_READ 和 PROT_WRITE 属性,然后在 31 行对匿名透明大页内存进行写操作. 程序接着在 33 行调用 fork 函数创建子进程,此时匿名透明大页内存变成写保护匿名内存,接着为了构造另外一个场景,35 行为子进程分支,其首先对 COW 匿名透明大页内存进行写操作,由于此时父子进程都映射到 COW 匿名透明大页内存,那么 35 行的写操作会触发缺页,但缺页异常处理函数会分配一个新匿名页,然后将内容拷贝到新匿名页上,那么接下来子进程在新的匿名页上执行写操作; 同理 39 行是父进程分支,其先 sleep 一段时间之后,COW 匿名透明大页内存此时只有父进程映射,因此 39 行的写操作同样会引起缺页,但缺页异常处理函数的处理和子进程访问时一致,新分配一个匿名页,并将原先内容拷贝到该匿名页上,那么父进程继续在新的匿名页上进行写操作. 以上便是一个最基础的实践案例,可以知道 35 行读操作和 39 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 35 行前后加上 BS_DEBUG 开关:

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

接着在匿名透明大页内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_wp_page 函数的 3368 行和 do_huge_pmd_wp_page 函数 1315 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-COW-ANON-THP-default/
# 编译内核
make kernel
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “COW ANON-THP HUGE-PF on do_huge_pmd_wp_page 0x6000000000”“COW ANON-THP HUGE-PF on do_wp_page 0x6000000000”, 子进程访问 COW 匿名透明大页内存时,缺页中断将原始内容拷贝到新匿名页上,子进程接下来都是访问新的匿名页. 通过实践可以看到写保护匿名内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.

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

对于匿名透明大页 COW 的缺页处理流程主要完成两个任务: 首先是将写保护的匿名透明大页拆分成更小粒度的匿名页,然后将匿名页内容拷贝到一个新的匿名页上,最后更新页表到新的匿名页上. 缺页异常处理函数首先会进入到 do_huge_pmd_wp_page 函数,通过一些列检查之后发现需要将透明大页拆分成匿名页,那么调用 __split_huge_pmd_locked 函数进行拆分,接下来将拆分之后的匿名页对应的页表修改为写保护,此时直接返回 VM_FAULT_FALLBACK,那么缺页异常处理函数继续处理而不是返回. 接下来利用 handle_pte_fault 提供的 do_wp_page 来处理 COW,此时调用 alloc_page_vma 函数分配一个新的匿名页,接着调用 __wp_page_copy_user 将内容拷贝到新的匿名页上并更新页表.

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

上图是将匿名透明大页拆分成匿名页的过程,主要逻辑位于 2182-2202 行,此时 page 指向匿名透明大页,2183 行调用 mk_pte 函数都在 PTE Entry 内容,此时仅包含 PFN 信息和 VMA 使用的页表信息,2184 行调用 maybe_mkwrite 函数判断如果 VMA 具有写权限,那么向 PTE Entry 里添加 _PAGE_RW 标志,2187 行如果 write 变量不为真,从上下文可知这里不为真,那么调用 pte_wrprotect 函数将 PTE Entry 修改为写保护的,最后 2196 行调用 pte_offset_map 函数获得 PTE Entry 页表的位置,最后调用 set_pte_at 函数设置最终的页表.

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

一个有意思的地方,也是和写保护匿名内存不同的地方,当对一个写保护的匿名透明大页写操作之后,被写的那部分变成了可读可写的匿名内存,而匿名透明大页会被拆分,其余部分均变成了写保护的匿名页. 匿名透明大页 COW 其余流程和匿名页场景一样,因此共用了这部分代码.

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

上图是 fork 系统调用处理逻辑,其中核心逻辑是新进程会调用 dup_mmap 函数拷贝父进程的地址空间,其遍历父进程的所有的 VMA,对于匿名映射的 VMA,那么调用 anon_vma_fork 函数建立逆向映射,最后调用 copy_page_range 函数遍历 VMA 对于的页表,将 COW 匿名内存对应页表的 _PAGE_RW 标志去掉,此时可以看到调用 ptep_set_wrprotect 函数进行写保护,最终更新页表. 那么无论是父进程还是子进程的 COW 匿名内存都变成了写保护.

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


Huge-PageFault: NUMA Balancing 透明大页场景

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

在 Linux 和其他操作系统中,UMA(Uniform Memory Access)NUMA(Non-Uniform Memory Access) 都是与内存访问模型和多处理器系统设计相关的术语。这两个术语描述的是处理器如何访问系统中的内存区域以及相关的性能特性, 上图大致描述了 UMA 和 NUMA 之间的差异,总结如下:

  • UMA(Uniform Memory Access): UMA 是一种内存架构,其中所有处理器访问内存所需的时间是均匀的,无论内存是哪一部分. 处理器共享同一内存资源,并且每次访问的延迟都是相同的. 缺点是当多个处理器尝试同时访问内存时,可能会导致内容竞争,从而限制系统的可扩展性.
  • NUMA(Non-Uniform Memory Access): NUMA 是一种内存架构,针对多处理器系统设计,其中处理器访问内存的时间取决于内存所在的位置. 在 NUMA 架构中,系统内存被划分为多个节点,每个节点与一个或多个处理器相关联. 当处理器访问其本地节点的内存时,访问速度最快。但是访问其他节点(非本地或远程节点)的内存时,访问速度会变慢. 这种架构的主要优势在于,它能更好地扩展到具有大量处理器的系统,并尽量减少内存访问的竞争状况.

在 NUMA 架构里,正常情况下 CPU 优先分配本地(LOCAL)节点上的内存,但在内存紧缺或者特殊需求场景下,CPU 也可以分配远端(REMOTE)节点的内存,这样做的好处是解决内存压力,但降低内存访问的性能.

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

在 Linux 中,NUMA Balancing 是一个内核特性,用于自动改善多处理器系统上的应用程序性能,特别是在 NUMA 架构上。这一特性旨在确保内存分配和进程调度与 NUMA 架构保持一致,以便获得最佳的性能. NUMA Balancing 机制一般会做以下几个操作:

  • 巡检: 在进程创建的时候,NUMA Balancing 机制会为每个进程启动一个定时任务, 该任务会扫描进程里跨 NUMA 的物理页
  • 标记: 巡检过程中,NUMA Balancing 机制会对跨 NUMA 物理页进行标记,将其标记为 PROTNONE,这里不是 PROT_NONE, 被标记的内存没有读写权限.
  • 替换: 当进程访问被标记的内存会触发缺页,缺页异常处理函数识别出被标记的页之后,将其迁移(Migrate)到本地(LOCAL)节点上,并恢复正常的页表属性.

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

NUMA Balancing 机制一个重要任务就是将跨 NUMA Page 对应的页表修改为 PROTNONE,这里的 PROTNONE 是一种特殊的页表,对应的匿名内存即不可读也不可写,与 PROT_NONE 不同的是,PROTNONE 是将原先的 PMD Entry 分作两部分,高于 BIT12 的字段存储物理页帧的反码,低于 BIT12 的字段则将 BIT8 置位,其余全部清零,因此缺页异常处理函数首先判断 _PAGE_PRESENT 清零,然后判断 BIT8(_PAGE_PROTNONE) 置位,那么知道该 Entry 对应着 NUMA Balancing 的页表,因此接下来的逻辑就按 NUMA Balancing HUGE Page Fault 进行处理.

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

上图为 NUMA Balancing 机制的运作逻辑,其为每个进程维护一个 NUMA Balancing 的内核线程,该内核线程通过多个 sysctl 变量进行控制,当 NUMA Balancing 内核线程被唤醒,其将执行 task_numa_work 函数,该函数会检查进程地址空间中那些跨 NUMA 的区域,然后获得对应的 PTE Entry,并调用 pte_modify 函数将页表修改为 PROTNONE. 在不同版本 Linux 里,四个 sysctl 有的导到 sysctl 系统工具里,但有的内核没有导出,开发者可以修改下面几个变量控制任务的执行:

  • numa_balancing_scan_period_min_ms: 以毫秒为单位扫描任务虚拟内存的最小时间, 它有效地控制了每个任务的最大扫描速率.
  • numa_balancing_scan_delay_ms: 任务初始分叉时使用的起始“扫描延迟
  • numa_balancing_scan_period_max_ms: 以毫秒为单位扫描任务虚拟内存的最大时间, 它有效地控制了每个任务的最小扫描速率.
  • numa_balancing_scan_size_mb: 为给定的扫描扫描的页面价值多少兆字节.

结合起来,扫描延迟扫描大小确定了扫描速率。当扫描延迟减小时,扫描速率增加。扫描延迟和因此每个任务的扫描速率都是自适应的,并取决于历史行为。如果页面被正确放置,那么扫描延迟增加,否则扫描延迟减小。 扫描大小不是自适应的,但是扫描大小越大,扫描速率就越高。更高的扫描速率会导致更高的系统开销,因为必须捕获页面错误,并且可能需要迁移数据。但是扫描速率越高,如果工作负载模式发生变化,任务内存就会更快地迁移到本地节点,从而减小由于远程内存访问而引起的性能影响。这些 sysctl 参数控制了扫描延迟和扫描页面数量的阈值。经过对 NUMA Balancing 机制的研究,理论上已经知道其如何工作了,接下来通过一个实践案例进一步了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下(实践之前需要打开内核 CONFIG_NUM-A_BALANCING 宏):

cd BiscuitOS
make menuconfig
  
  [*] DIY BiscuitOS/Broiler Hardware  --->
      (numa_balancing=disable) CMDLINE on Kernel
      [*] Support NUMA Topology
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Huge PageFault with NUMA Balancing Anonymous THP --->

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

BiscuitOS-PAGING-HUGE-PF-NUAM-ANON-THP-default Source Code on Gitee

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

实践案例由一个应用程序构成,其分作三部分,首先是 30 行调用 mmap 函数结合 MAP_PRIVATE 和 MAP_ANONYMOUS 分配一段可读可写的匿名透明大页内存,并在 40 行对匿名内存执行写操作. 第二部分是利用 libnuma 提供的库函数,从 43-54 行构造一个跨 NUMA 的匿名透明大页. 最后是第三部分,程序在 57 行再次对匿名透明大页内存执行写操作,此时会再次触发缺页并被识别为 NUMA Balancing Huge Page Fault. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 57 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 57 行前后加上 BS_DEBUG 开关:

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

接着在匿名透明大页内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_huge_pmd_numa_page 函数的 1489 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,另外为了加速调试进度,需要修改 “kernel/sched/fair.c” 文件中 sysctl 变量的值,这里全部设置为 0,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-NUMA-ANON-THP-default/
# 编译内核
make kernel
# 安装 libnuma
make prepare
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “NUMA Balancing ANON-THP on do_huge_pmd_numa_page 0x0x6000000000”, 说明缺页异常处理函数执行过 NUMA Balancing PROTNONE 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.

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

对于 HUGE Page 场景下 NUMA Balancing 引起的缺页,缺页异常出来函数在 __handle_mm_fault 函数里判断发生缺页的内存是匿名透明大页,然后检查到页表是 PROT_NONE 并且 VMA 可以访问,那么这些条件符合 NUMA Balancing Huge Fault 的场景,于是调用 do_huge_pmd_numa_page 函数进行处理,该函数的主要目的是将匿名透明大页迁移(Migration) 到指定的 NUMA NODE 上,并更新页表指向新的匿名透明大页.

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

do_huge_pmd_numa_page 函数是 NUMA Balancing HUGE Fault 的核心处理函数,其主要任务是将匿名透明大页迁移到指定的 NUMA NODE. 函数在迁移大页之前在 1495 行调用 pmd_modify 函数修改页表,然后在 1496 调用 vm_normal_page_pmd 函数获得匿名透明大页,1504-1512 检查目的 NUMA NODE 是否合理,不合理则跳转到 out_map 处。接着函数在 1516 行调用 migrate_misplaced_page 函数进行透明大页迁移,最后返回 0.

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


Huge-PageFault: EXT4 DAX 文件透明大页场景

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

在 Linux 中,PMEM 通常指的是持久内存(Persistent Memory),这是一种存储介质,它在性能上接近传统的随机存取内存(RAM),但在断电后能保持数据不丢失,类似于传统的硬盘或固态硬盘(SSD). 从某种意义上来说,PMEM 确实可以同时具备磁盘内存的某些特性,但其具体的使用方式还取决于系统的配置和应用场景。在通用的文件系统框架里,进程访问磁盘上的文件的方式包括: Buffer-IO、MMAP 和 DirectIO. PMEM 出现之后,进程可以直接将虚拟内存映射到 PMEM 上,此时采用了 DAX 技术. DAX 是 Linux 中的一种文件系统机制,它允许文件系统绕过页面缓存(PAGE CACHE),直接从用户空间对 PMEM 等设备进行内存映射访问。这种方式可以减少数据复制次数,避免了传统的块层 I/O 堆栈,从而提高了性能. 目前支持 DAX 的文件系统包括 EXT4 和 XFS.

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

DAX 支持进程直接将虚拟内存映射到 PMEM 上,并且不同的文件系统支持映射不同粒度的 PMEM 区域,例如进程可以将 4KiB 的虚拟内存映射到 4KiB 的 PMEM 区域上,同理也可以将 2MiB 的虚拟内存区域映射到 2MiB 的 PMEM 区域上. 另外进程可以先分配虚拟内存,然后当进程访问这段虚拟内存的时候,通过缺页来建立实际的映射,因此对于映射 2MiB PMEM 区域的场景,其发生缺页时也属于 HUGE FAULT 场景.

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

针对 DAX 的场景,EXT4 文件系统也提供了相应的 HUGE FAULT 接口,可以看出对于采用 DAX 映射的文件,其 mmap 对应 ext4_file_mmap 函数,该函数为 DAX 映射的 VMA 提供的 vm_ops 是 ext4_dax_vm_ops,ext4_dax_vm_ops 中包括了 4KiB 粒度的区域缺页处理函数 ext4_dax_fault, 也提供了大页粒度区域缺页处理函数 ext4_dax_huge_fault, 如果 DAX 映射的文件发生 HUGE PAGE FAULT,那么 ext4_dax_huge_fault 函数将会被调用. 接下来在支持 DAX 的 EXT4 文件系统上实践该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] DIY BiscuitOS/Broiler Hardware  --->
      (memmap=32M!128M) CMDLINE on Kernel
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Huge PageFault with EXT4 HUGE DAX --->

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

BiscuitOS-PAGING-HUGE-PF-EXT4-HUGE-DAX-default Source Code on Gitee

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

实践案例由一个应用程序构成,首先在 23 行调用 open 函数在支持 EXT4 DAX 机制的 “/mnt/ext4-DAX/” 目录下打开文件 “BiscuitOS.txt”, 然后在 31 行调用 mmap 函数将文件映射到进程地址空间,接着在 42 行对映射之后的虚拟内存执行写操作,此时会触发缺页,然后在 44 行再次对虚拟内存进行读操作,最后释放内存. 以上便是一个最基础的实践案例,可以知道 42 行写操作会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 42 行前后加上 BS_DEBUG 开关:

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

接着在 HUGE-DAX 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 ext4_dax_huge_fault 函数的 709 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-EXT4-HUGE-DAX-default/
# 编译内核
make kernel
# 安装 mkfs 工具
make prepare
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGE-EXT4 HUGE PF on ext4_dax_huge_fault 0x6000000000”, 说明缺页异常处理函数执行过 HUGE PAGE FAULT 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.

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

当进程将 HUGE-DAX 文件映射到进程地址空间之后,进程发起对该虚拟内存的访问,MMU 发现物理页不存在则触发缺页异常,缺页异常处理函数在 __handle_mm_fault 里发现缺页的虚拟地址对应的 PMD_ENTRY 为空,并且 hugepage_vma_check 检查虚拟内存对应映射支持 HUGE FAULT,于是调用 create_huge_pmd 函数进行处理. 接着缺页异常处理函数识别处虚拟内存映射的是 EXT4 HUGE DAX,于是调用相应的 huge_fault 处理函数 ext4_dax_huge_fault,接下来的事就是 DAX 相关的内容,其主要是在 PMEM 里找到一个按 PMD_SIZE 对齐且可用的物理地址,然后将页表直接映射到物理页帧上,最后返回 VM_FAULT_NOPAGE,那么进程可以访问 2MiB 的虚拟内存.

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

缺页异常在处理 EX4 HUGE DAX 映射时,调用 ext4_dax_huge_fault 函数,709-723 为 EXT4 写入日志,725-738 为结束日志写入,缺页核心通过调用 dax_iomap_fault 函数,该函数实现从 PMEM 中找到一块可用的区域,并且该区域按 2MiB 对齐,于是缺页异常处理函数直接将虚拟内存映射到 2MiB 区域上.

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


Huge-PageFault: XFS DAX 文件透明大页场景

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

在 Linux 中,PMEM 通常指的是持久内存(Persistent Memory),这是一种存储介质,它在性能上接近传统的随机存取内存(RAM),但在断电后能保持数据不丢失,类似于传统的硬盘或固态硬盘(SSD). 从某种意义上来说,PMEM 确实可以同时具备磁盘内存的某些特性,但其具体的使用方式还取决于系统的配置和应用场景。在通用的文件系统框架里,进程访问磁盘上的文件的方式包括: Buffer-IO、MMAP 和 DirectIO. PMEM 出现之后,进程可以直接将虚拟内存映射到 PMEM 上,此时采用了 DAX 技术. DAX 是 Linux 中的一种文件系统机制,它允许文件系统绕过页面缓存(PAGE CACHE),直接从用户空间对 PMEM 等设备进行内存映射访问。这种方式可以减少数据复制次数,避免了传统的块层 I/O 堆栈,从而提高了性能. 目前支持 DAX 的文件系统包括 EXT4 和 XFS.

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

DAX 支持进程直接将虚拟内存映射到 PMEM 上,并且不同的文件系统支持映射不同粒度的 PMEM 区域,例如进程可以将 4KiB 的虚拟内存映射到 4KiB 的 PMEM 区域上,同理也可以将 2MiB 的虚拟内存区域映射到 2MiB 的 PMEM 区域上. 另外进程可以先分配虚拟内存,然后当进程访问这段虚拟内存的时候,通过缺页来建立实际的映射,因此对于映射 2MiB PMEM 区域的场景,其发生缺页时也属于 HUGE FAULT 场景.

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

针对 DAX 的场景,XFS 文件系统也提供了相应的 HUGE FAULT 接口,可以看出对于采用 DAX 映射的文件,其 mmap 对应 xfs_file_mmap 函数,该函数为 DAX 映射的 VMA 提供的 vm_ops 是 xfs_file_vm_ops,xfs_file_vm_ops 中包括了 4KiB 粒度的区域缺页处理函数 xfs_filemap_fault, 也提供了大页粒度区域缺页处理函数 xfs_filemap_huge_fault, 如果 DAX 映射的文件发生 HUGE PAGE FAULT,那么 xfs_filemap_huge_fault 函数将会被调用. 接下来在支持 DAX 的 XFS 文件系统上实践该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] DIY BiscuitOS/Broiler Hardware  --->
      (memmap=32M!128M) CMDLINE on Kernel
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Huge PageFault with XFS HUGE DAX --->

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

BiscuitOS-PAGING-HUGE-PF-XFS-HUGE-DAX-default Source Code on Gitee

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

实践案例由一个应用程序构成,首先在 25 行调用 open 函数在支持 XFS DAX 机制的 “/mnt/xfs-huge-dax/” 目录下打开文件 “BiscuitOS.txt”, 然后在 31 行调用 mmap 函数将文件映射到进程地址空间,接着在 42 行对映射之后的虚拟内存执行写操作,此时会触发缺页,然后在 44 行再次对虚拟内存进行读操作,最后释放内存. 以上便是一个最基础的实践案例,可以知道 42 行写操作会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 42 行前后加上 BS_DEBUG 开关:

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

接着在 HUGE-DAX 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 xfs_filemap_huge_fault 函数的 1362 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-XFS-HUGE-DAX-default/
# 编译内核
make kernel
# 安装 mkfs 工具
make prepare
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “XFS HUGE-DAX HUGE PF on xfs_filemap_huge_fault 0x6000000000”, 说明缺页异常处理函数执行过 HUGE PAGE FAULT 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.

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

当进程将 HUGE-DAX 文件映射到进程地址空间之后,进程发起对该虚拟内存的访问,MMU 发现物理页不存在则触发缺页异常,缺页异常处理函数在 __handle_mm_fault 里发现缺页的虚拟地址对应的 PMD_ENTRY 为空,并且 hugepage_vma_check 检查虚拟内存对应映射支持 HUGE FAULT,于是调用 create_huge_pmd 函数进行处理. 接着缺页异常处理函数识别处虚拟内存映射的是 XFS HUGE DAX,于是调用相应的 huge_fault 处理函数 xfs_filemap_huge_fault,接下来的事就是 DAX 相关的内容,其主要是在 PMEM 里找到一个按 PMD_SIZE 对齐且可用的物理地址,然后将页表直接映射到物理页帧上,最后返回 VM_FAULT_NOPAGE,那么进程可以访问 2MiB 的虚拟内存.

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

缺页异常在处理 XFS HUGE DAX 映射时,调用 xfs_filemap_huge_fault 函数,其核心函数是 __xfs_filemap_fault,对于 DAX 映射的文件,其缺页使用的是 1316 分支,其调用 xfs_dax_fault 函数处理缺页,该函数实现从 PMEM 中找到一块可用的区域,并且该区域按 2MiB 对齐,于是缺页异常处理函数直接将虚拟内存映射到 2MiB 区域上.

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


Huge-PageFault: PUD HUGE FAULT 场景

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

用户进程可以将虚拟内存映射到 4KiB/2MiB 大小的 RSVDMEM 内存上,那么同理也可以将虚拟内存映射到 1Gig 的 RSVDMEM 内存上. 当用户进程调用 mmap 函数分配虚拟内存,起初只是分配虚拟内存,当用户进程访问虚拟内存时,MMU 发现虚拟内存对应的页表不存在,那么 MMU 触发缺页异常. 当缺页异常处理函数基于 PFNMAP 构建时,PFNMAP 可以将虚拟内存映射到 1Gig 的 RSVDMEM 内存上,此时需要一个 PUD Entry 是空的,然后缺页异常处理函数只需将可用的 1Gig RSVDMEM 对应的 PFN 填充到 PUD Entry,待缺页异常处理返回之后,进程就可以正常访问 1Gig 的 RSVDMEM 内存. 那么接下来通过一个实践案例了解整个过程的实现,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] DIY BiscuitOS/Broiler Hardware  --->
          (4096) Memory Size(MiB)
      [*] Paging Mechanism  --->
          [*] Huge PageFault with PUDMAPPED(1G) --->

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

BiscuitOS-PAGING-HUGE-PF-PUDMAPPED-default Source Code on Gitee

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

实践案例由两部分组成,其中一部分如上图的内核模块,该模块由 MSIC 驱动框架构成,MSIC 驱动框架向用户空间提供 “/dev/BiscuitOS-HUGEPFN” 文件,并为文件实现了 mmap 接口,也就是用户空间打开该文件之后,并调用 mmap 分配内存时会调用到 BiscuitOS_mmap 函数,该函数的主要任务是设备 VMA 的 vm_ops, vm_ops 里实现了 huge_fault 接口,也就是访问 mmap 分配的虚拟内存触发缺页时 vm_huge_fault 函数会被调用,另外 mmap 回调函数里还将 “filp->f_inode->i_flags” 的 S_DAX 标志置位,这是实现 PFNMAP PmdMapped 的关键. vm_huge_fault 的主要任务就是分配 RSVDMEM 内存和填充页表, 其通过调用 vmf_insert_pfn_pud() 函数实现. 函数还为文件提供了 get_unmapped_area 回调函数,当 mmap 分配虚拟内存时会调用到 BiscuitOS_get_unmapped_area 函数,该函数的目的是分配 1Gig 对齐的虚拟内存。

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

实践案例另外一部分由一个应用程序构成,进程首先在 24 行调用 open 函数打开 “/dev/BiscuitOS-HUGEPFN”, 然后在 30 行调用 mmap 函数并使用 MAP_SHARED 标志分配一段内存,接着在 42 行对虚拟内存进行写操作,并在 44 行再次对内存进行读操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 42 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 42 行前后加上 BS_DEBUG 开关:

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

接着在 PFNMAP PUDMAPPED 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 __handle_mm_fault 函数的 5003 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-PUDMAPPED-default/
# 编译内核
make kernel
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “PFNMAP PUDMAPPED __handle_mm_fault 0x7f2100000000”, 那么说明实践案例分配了 PFNMAP PUDMAPPED 内存的可能,同时也可以看到 PFNMAP PUDMAPPED 内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PFNMAP PUDMAPPED 内存在缺页异常处理流程里的流动.

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

PFNMAP 映射 PUD-Mapped 1Gig 物理内存场景的缺页异常流程如上图,与 4KiB PFNMAP 不同的时缺页异常处理函数在 __handle_mm_fault 里只要判断虚拟内存对应的 PUD Entry 为空,且 hugepage_vma_check 函数检查了 VMA 支持大页缺页,那么流程进入到 create_huge_pud,函数的最终核心位于 vmf_insert_pfn_pud,该函数负责了 PUD Entry 的构建以及页表的填充操作,最后返回 VM_FAULT_NOPAGE.

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

hugepage_vma_check 函数用于检测 VMA 是否支持大页缺页,判断条件很多,其中 PFNMAP 比较关心的是 95 行的 vma_is_dax() 函数,还记得驱动程序里添加 S_DAX 标志吗? 因为 PFNMAP 无法满足其他大页的条件,只有这里可以满足,另外 DAX 就是 PFNMAP 的应用,因此 vma_is_dax 判断是成功的,因此可以使用 PFNMAP PmdMapped.

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

create_huge_pud 函数进行 PFNMAP 的大页缺页逻辑,对于 PFNMAP 的内存采用 4826 行的逻辑,其会调用到驱动模块提供的 huge_fault 接口,也就是本实践案例提供的 vm_huge_fault 函数.

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

PFNMAP PmdMapped 缺页通过调用 vmf_insert_pfn_pud 函数实现,其核心逻辑位于 insert_pfn_pud 函数,可以看到该函数 951-964 行用于处理 Write-Protection 异常逻辑,而 966 行的逻辑则是正常的 PudMapped 处理逻辑,其首先调用 pmd_mkhuge 函数将页表标记为映射 1Gig 大页的,然后 pmd_mkwrite() 函数向页表添加 _PAGE_RW 标志,然后在 873 行调用 set_pud_at() 函数更新页表.

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