目录

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


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

在 Linux 里,进程使用 mmap 等函数从进程的地址空间分配一段虚拟内存,如果采用 LazyAlloc 的方式分配,那么这些分配出来的虚拟内存是没有和物理内存建立页表的,因此当进程访问这段虚拟内存时,MMU 检查到虚拟内存对应的物理内存不存在,那么 MMU 直接触发缺页异常。缺页异常调用缺页异常处理函数进行处理,其核心两个任务就是分配新的物理内存,然后建立虚拟内存到物理内存的页表,待缺页异常处理函数返回之后,进程就可以正常访问这端虚拟内存了。通过分析可以知道缺页异常是建立虚拟内存到物理内存页表的一个很好契机. 本文就详细分析一下缺页异常处理函数如何构建页表.

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

在研究缺页异建立页表之前,现将发生缺页的虚拟内存进行一下分配,分类的依据来自缺页异常处理函数的处理逻辑,正如上图分作这几类内存:

  • Normal MEM: 进程通过 malloc/brk/mmap 函数分配的普通虚拟内存,其通过缺页最终映射到随机的 4KiB 物理页上. 可以是匿名内存,也可以是共享内存,也可以是文件映射内存.
  • TH MEM: 进程通过 mmap 分配不小于 2MiB 的虚拟内存,被 THP 机制合成的透明大页内存. 可以是匿名透明大页,也可以是文件透明大页. 透明大页有 2MiB 和 1Gig 两种.
  • HugeTLB MEM: 进程通过 mmap 从 HugeTLB 大页池子里分配的内存,HugeTLB 内存相对独立的大页内存,具有 2MiB 和 1Gig 两种粒度大页.
  • DAX MEM: 进行绕过文件 I/O 和块设备层,进程直接将虚拟内存映射到物理区域上(PFN), 具有 2MiB 和 4KiB 两种粒度.
  • PFNMAP: 进程通过 mmap 分配虚拟内存映射到设备管理的物理内存上,具有 4KiB、2MiB 和 1Gig 粒度
虚拟地址对齐(Alignment)

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

缺页异常函数建立何种页表与虚拟内存的对齐方式和虚拟内存的大小有直接关系,例如按 4KiB 对齐的 4KiB 虚拟地址,那么一定映射 4KiB 的物理内存,因此 PTE 是最后一级页表; 如果按 2MiB 对齐的 2MiB 虚拟区域,有可能映射 2MiB 的物理内存,也就是说这个条件是映射 2MiB 物理页的前提条件; 同理按 1Gig 对齐和虚拟内存大小不小于 1Gig 是映射 1Gig 物理内存的前提.

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

缺页异常处理函数建立何种页表还与虚拟内存的对齐方式有关,例如当进程使用 mmap 分配虚拟内存时,其第一个参数可以指定虚拟地址, 第二个参数则是指定虚拟区域的大小,两者配合使用可以让虚拟内存映射不同粒度的物理页,具体映射什么粒度还需要看虚拟内存的类型.

页表与缺页

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

缺页异常处理函数在不同的路径上为不同类型的内存建立页表,其中 hugetlb_fault 函数为 HugeTLB 内存建立页表; __handle_mm_fault 函数为其余类型内存建立了 PGD/P4D/PUD/PMD 页表,并且对于 THP 大页建立最后一级页表 PMD. handle_pte_fault 函数为普通内存和 PFNMAP 以及 DAX 内存建立最后一级 PTE 页表.

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


PGD/PUD/PMD 页表构建

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

缺页异常处理函数在调用 __handle_mm_fault 函数处理非 HugeTLB 内存 PGD/P4D/PUD/PMD 页表,其逻辑如下:

  • 获得 PGD Entry: 4998 行调用 pgd_offset 函数获得缺页地址对应的 PGD Entry
  • 获得 P4D Entry: 4999 行调用 p4d_alloc 函数检查 P4D 页表是否存在,不存在直接分配 P4D 页表页,并获得缺页地址对应的 P4D Entry. 在通常架构里 PGD 和 P4D 是同一个页表. 如果系统没有足够的内存分配给页表页,那么直接在 5001 行直接返回 VM_FAULT_OOM.
  • 获得 PUD Entry: 5003 行调用 pud_alloc 函数检查 PUD 页表页是否存在,如果不存在分配一个新的 PUD 页表页,并获得缺页地址对应的 PUD Entry. 同理系统没有足够内存分配给 PUD 页表页,那么直接在 5005 行返回 VM_FAULT_OOM.
  • PUD 大页填充页表: 5007 行调用 pud_none 函数确认 PUD Entry 为空,另外 hugepage_vma_check 函数检查某些类型内存满足条件之后,可以调用 create_huge_pud 为虚拟内存构建 PUD 作为最后一级页表. PFNMAP 的 PUDMAPPED 大页内存满足这个条件.
  • 修改 PUD 大页页表: 5016 行在 PUD Entry 不为空的情况下,调用 pud_trans_huge 函数检查页表映射了透明大页,或者调用 pud_devmap 检查到页表映射了 DEVMAP,接着在 5021 行检查到缺页的原因中包括写操作,并且 PUD Entry 是只读的,那么判断为写保护(Write-Protect) 引起的缺页,因此 5022 行调用 wp_huge_pud 函数进行相应的写保护操作,并将页表修改为可读可写的.
  • 分配 PMD Entry: 此时 PUD Entry 非空,函数在 5032 行调用 pmd_alloc 函数检查缺页地址对应的 PMD 页表页是否存在,如果不存在则新分配一个 PMD 页表页,然后获得缺页地址对应的 PMD Entry.
  • PMD 大页填充页表: 5040 行调用 pmd_none 函数确认 PMD Entry 为空,另外 ugepage_vma_check 函数检查某些类型内存满足条件之后,可以调用 create_huge_pmd 为虚拟内存构建 PMD 作为最后一级页表的映射. PFNMAP 的 PUDMAPPED 大页内存和透明大页满足这个条件.
  • 修改 PMD 大页页表: 5056 行在 PMD Entry 不为空的情况下,调用 pmd_trans_huge 函数检查页表映射了透明大页,或者调用 pmd_devmap 检查到页表映射了 DEVMAP,接着在 5057 行检查 PMD Entry 是否为 PMD_PROTNONE, 如果是则可判断是因为 NUMA Balancing 引起的缺页,因此在 5058 行调用 do_huge_pmd_numa_page 进行页表迁移操作. 反之 5060 行检查到缺页的原因中包括写操作,并且 PMD Entry 是只读的,那么判断为写保护(Write-Protect) 引起的缺页,因此 5062 行调用 wp_huge_pmd 函数进行相应的写保护操作,并将 PMD 页表修改为可读可写的.
PTE 页表构建

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

缺页异常处理函数在调用 handle_pte_fault 函数处理 PTE 页表,其逻辑如下:

  • 获得 PTE Entry: 4875 行调用 pmd_none 函数检查 PMD Entry 是否为空,正常情况下 PMD Entry 不能为空,如果为空那么说明 PTE 页表页不存在, 则将 FAULT_FLAG_ORIG_PTE_VALID 标志清零; 反之 PTE 页表页是存在的,于是在 4905 行调用 pte_offset_map 函数获得 PTE Entry,然后将 PTE Entry 的内容保存在 vmf->orig_pte 里,并添加 FAULT_FLAG_ORIG_PTE_VALID 标志.
  • 填充 PTE 页表: 4918 行调用 pte_none 检查到 PTE Entry 是否为空,为空说明缺页是首次缺页,需要分配新的内存,然后在 4925 行根据 vma_is_anonymous 函数区分是匿名内存还是非匿名内存,并在指定内存缺页流程里将 PTE Entry 进行设置和更新.
  • 修改 PTE 页表: 4930 行之后的场景,PTE Entry 一般非空以及被修改为特定的模式,以满足不同的内存行为需求,缺页异常处理函数只需根据特定场景区修改 PTE 页表即可. PTE 非空说明虚拟内存已经建立页表映射到物理内存上,只是为了满足特定内存行为而将页表进行修改,以便进程再次访问而发生缺页,最后能够更正页表.
HugeTLB 页表构建

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

缺页异常处理在调用 hugetlb_fault 函数处理 HugeTLB 内存页表,其逻辑如下:

  • 获取最后一级页表: 5735 行调用 huge_pte_offset 函数获得最后一页页表,由于 HugeTLB 内存存在不同粒度的大页,因此这里的最后一级页表可能是 PMD,也可能是 PUD. 如果最后一级页表存在,那么在 5742 行调用 huge_ptep_get 函数获得缺页地址对应最后一级页表的 Entry. 缺页异常处理函数直接检查 Entry 的内容,以此查看 Entry 是否为 POISON 和 MIGRATE
  • 再次获得最后一级页表: 5764 行调用 huge_pte_alloc 函数检查最后一级页表页是否存在,如果不存在则先分配一个页表页,然后再获得最后一个页表的 Entry,如果此时分配页表页失败,那么直接在 5767 行返回 VM_FAULT_OOM.
  • 填充最后一级页表: 5779 行调用 huge_ptep_get 函数获得最后一级页表 Entry 的内容,然后在 5781 行调用 huge_pte_none_mostly 检查 Entry 是否为空. 当页表 Entry 为空时,那么说明发生缺页的虚拟内存还没有建立页表映射,于是调用 hugetlb_no_page 分配一个 HugeTLB 大页,并建立对应的页表,使虚拟内存映射到新的 HugeTLB 大页上.
  • 修改最后一级页表: 5781 行调用 huge_pte_none_mostly 函数缺页最后一级页表 Entry 不为空,那么说明满足不同的内存行为需求,例如写保护等,待相应的内存行为处理完毕之后,函数将更新页表为正常进程可以访问的页表.

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


Normal Memory 缺页页表构建场景

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

可读可写匿名内存 是可以对匿名内存进行读写操作,其可以通过 mmap()/malloc()/brk() 函数进行分配,亦或者从堆栈上分配的匿名内存也是可读可写, 可读可写的匿名内存具有以下特点:

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

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

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with Anonymous Memory --->

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

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

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

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

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

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

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

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

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

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

对于可读可写的匿名内存,其缺页异常处理流程如上图,如果是首次读操作,那么最终进入映射 “R ZERO Page” 分支; 如果首次是读接下来发生写操作,那么进入 “WP AnonMEM” 分支; 如果首次是写,那么进入 “R/W AnonMem” 分支.

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

对于首次发起对可读可写匿名内存写操作导致的缺页do_anonymous_page 函数作为核心处理流程其逻辑如上图,其首先在 4085 行调用 alloc_zeroed_user_highpage_movable 函数从 Buddy 分配器分配一个物理页,然后在 4098 行调用 __SetPageUptodate 函数将物理页标记为物理页的内容是最新的,无需更新。接下来构造 PTE Entry 的内容,首先从 VMA 的 vm_page_prot 成员获得 VMA 设置的页表属性,此时检查到 VMA 可写,那么调用 pte_mkwrite 添加页表的 Write 标志。接下来函数在 4105 行调用 pte_offset_map_lock 获得 PTE Entry,并进行一些检测,函数接着在 4123 行调用 inc_mm_counter_fast 函数增加对 MM_ANONPAGES 的统计,以此表示系统的匿名内存增加的数量,然后调用 page_add_new_anon_rmap 建立物理内存的逆向映射,最后调用 lru_cache_add_inactive_or_unevictable 函数将物理页添加到 LRU 链表,当系统内存紧张的时候会将该物理页交换到 SWAP Space,处理完这些之后,函数在 4127 行调用 set_pte_at 函数更新页表.

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

对于首次发起对可读可写匿名内存读操作导致的缺页do_anonymous_page 函数作为核心处理流程其逻辑如上图, 函数首先判断到此时缺页是读操作引起的,并且 mm_forbids_zeropage 返回 0 说明系统支持 ZERO Page,那么缺页异常处理会为读操作映射到 ZERO Page. 所谓 ZERO Page 就是一块特殊的内存,内存的内容全部为 0,并且有 4KiB 粒度和 2MiB 粒度的,只要符合条件的进程都可以将匿名内存映射到 ZERO Page 上,这样可以起到节省内存的作用. 因此函数在 4063 行调用 pte_mkspecial 函数构造了一个指向 ZERO Page 的 PTE Entry 内容,并在 4065 函数调用 pte_offset_map_lock 函数获得 PTE Entry 的地址,如果发现此时 PTE Entry 不为空,那么可能是页表的内容和 TLB 的内容不一致,此时函数进入 4068 行分支调用 update_mmu_tlb 函数更新 TLB,并跳转到 unlock 处; 反之如果此时 PTE Entry 为空,说明是第一次访问匿名内存,符合预期,那么跳转到 setpte 处将准备好的 PTE Entry 内容更新到 PTE Entry 里,待缺页中断返回之后进程可以继续对匿名内存进行读操作而不会引起缺页. 细心的开发者是否发现,整个过程没有设置页表的 _PAGE_RW 标志位,那么说明这段虚拟内存是写保护的,因此接下来只要进程对虚拟内存首次发起写操作,那么还是会触发缺页.

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

对于首次发起对可读可写匿名内存读操作之后再次发起写操作导致的缺页do_wp_page 函数作为核心处理流程其逻辑如上图,函数获得匿名内存对于的物理页之后,如果在 3419 行发现匿名页是独占的,也就是此时只有一块虚拟内存映射到该物理页上,那么直接跳转到 reuse 处调用 wp_page_reuse() 函数,该函数的核心作用就是添加 PTE Entry 页表的 _PAGE_RW 标志,以此让匿名内存具有可读可写的权限. 其余逻辑均不再此种场景考虑的范围.

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


透明大页内存缺页页表构建场景

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

在 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 行设置更新页表。完成以上动作基本建立好页表映射,那么进程以后对虚拟内存的读写操作都不会再次触发缺页.

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


HugeTLB 内存缺页页表构建场景

在 Linux 里,进程可以分配一段虚拟内存映射到 HugeTLB 大页池子里的大页,以此让进程使用上大页,进而减少 TLB 缺失. 进程可以使用 mmap 函数完成这个任务,同时可以使用惰性分配 HugeTLB 内存. 当进程首次访问这段虚拟内存时,MMU 发现物理内存不存在并触发缺页异常,缺页异常处理函数识别出异常的虚拟内存映射了 HugeTLB 大页,于是从大页池子的预留小池子里获得一个 HugeTLB 大页,然后将页表映射到该 HugeTLB 大页上,待缺页异常处理函数返回之后,进程可以正常访问这段虚拟内存,并且不会触发缺页. 这就是最基础的 HugeTLB 内存缺页场景,HugeTLB 机制并没有提供类似 Huge Zero Page 机制,因此首次对 HugeTLB 虚拟内存读和写操作过程都是一样的. 接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Support Host CPU Feature Passthrough
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with HUGETLB Memory --->

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

BiscuitOS-PAGING-PF-HUGETLB-default Source Code on Gitee

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

实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配一段 2MiB 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,因此绑定了公共 2MiB HugeTLB 大页池子了的大页. 程序接着在 33 行对这段虚拟内存执行写操作,由于此时页表没有建立会触发缺页异常,程序接着在 35 行对虚拟内存执行读操作,此时页表已经建立,因此不会触发缺页,最后释放内存. 以上便是一个最基础的实践案例,可以知道 33 行写操作会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 33 行前后加上 BS_DEBUG 开关:

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

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

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

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

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

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

缺页异常处理函数在 handle_mm_fault 函数调用 is_vm_hugetlb_page 函数判断是否为 HugeTLB 内存缺页,并调用 hugetlb_fault 函数进行 HUGETLB 缺页处理,HUGETLB 缺页与其他内存缺页流程不一样. HUGETLB 缺页流程首先检查对应的 PMD/PUD Entry 是否为空,为空说明 HUGETLB 内存首次缺页,然后调用 alloc_huge_page 从预留内存池子里分配大页,从之前的分析可知,正常情况下进程分配 HUGETLB 虚拟内存时就会预留相应的 HUGETLB 大页,当获得 HUGETLB 大页时判断映射是匿名映射还是文件映射,以此建立逆向映射。如果是匿名映射,则调用 hugepage_add_new_anon_rmap; 反之如果是文件映射则调用 page_dup_file_rmap 建立逆向映射. 最后就是更新页表操作和将 HUGELTB 大页更新为激活态.

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

HUGETLB 内存缺页之后,对于来自公共 HugeTLB 大页池子的虚拟内存均采用匿名映射,因此会构建如上的逆向映射,可以看到没有 HugeTLB 大页指向了 anon_vma, 并可以从 anon_vma 的红黑树知道哪些进程的 VMA 映射了该 HugeTLB 大页.

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


DAX 内存缺页页表构建过程

DAX(Direct Access) 是 Linux 内核中的一项功能,允许应用程序以直接的方式从非易失性内存(如非易失性双端口 RAM、NVDIMM 或 3D XPoint 存储类内存)中访问数据,而无需通过标准的文件 I/O 路径或页缓存(Page Cache). DAX 提供了低延迟和高性能的特性,适用于需要快速和直接访问非易失性内存的工作负载,例如数据库或虚拟机。以下是关于 DAX 的一些关键特点和用途:

  • 直接内存访问: DAX 允许应用程序以直接的方式在非易失性内存中读取和写入数据,而无需将数据复制到内核中的页缓存。这减少了内核的介入,提高了数据访问的效率
  • 低延迟: 由于 DAX 绕过了页缓存,因此可以实现更低的 I/O 延迟,这对于响应时间敏感的应用程序非常有用
  • 高性能: DAX 可提供更高的数据访问性能,特别是在大规模内存中和大数据集的情况下
  • 一致性和原子性: DAX 确保在非易失性内存中的数据访问是原子性的,并且提供了数据一致性的保证。这对于要求数据可靠性的应用程序非常重要

XFS 文件系统上是 DAX 支持,XFS 是一种高性能的文件系统,支持在适当的硬件上启用 DAX 以获得更好的性能。启用 XFS-DAX 后,XFS 文件系统可以利用 DAX 的优势,直接从非易失性内存中读取和写入数据,而无需通过页缓存。这对于某些工作负载(如大规模数据库或虚拟机磁盘)来说是非常有益的. 要启用 XFS-DAX,您需要在文件系统挂载时使用 “-o dax” 选项,或者在 “/etc/fstab” 中的相应条目中添加 dax 选项. 在 Linux 启用 XFS-DAX 需要打开如下内核宏:

CONFIG_NVDIMM_PFN=y
CONFIG_NVDIMM_DAX=y
CONFIG_DAX=y
CONFIG_DEV_DAX=y
CONFIG_DEV_DAX_PMEM=y
CONFIG_DEV_DAX_KMEM=y
CONFIG_NVMEM=y
CONFIG_NVMEM_SYSFS=y
CONFIG_FS_DAX=y
CONFIG_FS_DAX_PMD=y
CONFIG_XFS_FS=y

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

XFS-DAX 文件系统提供映射文件的 mmap 接口使用了 xfs_file_mmap 函数,xfs_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 xfs_file_vm_ops,该数据接口实现了 fault 接口 xfs_filemap_fault,那么文件映射 VMA 发生缺页时 xfs_filemap_fault 函数会被调用.

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

在 XFS-DAX 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,XFS-DAX 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。那么接下来通过一个实践案例了解这种异常场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      (memmap=16M!0x10000000) CMDLINE on Kernel
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped XFS-DAX FS --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-XFS-DAX-default/
# 部署源码
make download
# 安装依赖工具
make prepare
# 在 BiscuitOS 中实践
make build

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

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

实践案例由一个应用程序构成,进程在 23 行在 “/mnt/xfs-dax/” 目录下打开文件 BiscuitOS.txt 文件,该目录已经挂载为 XFS-DAX 文件系统,进程接着在 29 行调用 mmap 函数将文件映射到进程的地址空间,并在 40 行对文件对应的虚拟内存进行写操作,然后在 42 行对虚拟内存进行读操作,操作完毕之后就是释放虚拟内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 40 行前后加上 BS_DEBUG 开关:

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

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

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

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

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

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

对于 XFS-DAX 文件系统映射文件到地址空间之后,进程访问该虚拟内存时,由于 MMU 发现对于的物理内存不存在,那么触发缺页异常。在缺页异常处理函数里,其主要做三个事情,首先是分配 PAGECACHE,如上图调用 filemap_alloc_folio 函数进行分配; 当分配 PAGECACHE 之后 XFS-DAX 文件系统向 BIO 层发送请求从磁盘里读取读取多个页表内容到 PAGECACHE, 由于磁盘 I/O 延迟无法立即获得文件内存,因此缺页异常会通过 VM_FAULT_RETRY 再次进行缺页处理,以确保文件内容已经更新到 PAGECACHE 里; 最后一个任务就是更新页表指向新的 PAGECACHE,以及更新页表标记为脏页等. 完成三个任务之后缺页异常处理函数就返回,那么进程可以正常访问虚拟内存. 因为 VM_FAULT_RETRY 的缘故,handle_mm_fault 会被执行两次,这么做的原因有如下:

  • 磁盘 I/O 延迟: 当页面不在物理内存中,需要从磁盘加载时,可能由于磁盘 I/O 操作的延迟,内核无法立即获取所需页面。此时,内核可能会将页面标记为需要重新尝试,并等待磁盘 I/O 完成
  • 页面锁定: 在某些情况下,内核可能会锁定某些页面,以确保它们在内存中不会被交换出去或释放。如果页面被锁定,内核可能会等待解锁页面后才能重新加载
  • 其他临时情况: 还可能存在其他临时情况,导致页面无法立即加载到内存中。在这种情况下,内核可能会标记页面并尝试重新加载

VM_FAULT_RETRY 并不是 Linux 文件映射的特定特性,而是内核内存管理的一般机制。它在多种上下文中都有用,不仅限于文件映射。具体的行为可能因 Linux 内核版本、硬件配置和文件系统类型而异,因此要根据具体情况考虑如何处理 VM_FAULT_RETRY 错误码.

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

do_shared_fault 函数作为 XFS-DAX 文件系统映射内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 XARRY 等映射这里不放开讲。当分配物理内存之后,文件映射内存没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异. 另外 XFS-DAX 文件系统提供的 VMA 缺页处理函数是 filemap_fault,该函数负责分配 PAGECACHE 和发起 BIO 请求读取文件内存.

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


Huge-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 区域上.

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


PFNMAP 内存缺页页表构建过程

当用户进程分配一段虚拟内存用于映射设备驱动管理的 RSVDMEM 内存,当用户空间调用 mmap 函数分配虚拟内存,此时只是分配虚拟内存并没有建立实际的页表和分配 RSVDMEM 内存动作,因此当进程对这段虚拟内存发起写操作时,MMU 发现对应页表的 PRESENT 标志位不存在,那么 MMU 触发缺页异常. 缺页异常处理函数最终会调用到设备驱动为 VMA 提供的缺页函数,该缺页函数负责 RSVDMEM 内存的分配,以及 PTE 页表的填充,整个过程不需要使用 struct page,直接使用 PFN 即可完成。缺页异常处理函数返回之后,进程再次执行写操作,写操作可以正确将数据写入对应的内存里. 那么接下来通过一个实践案例了解整个过程的实现,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with PFNMAP(RSVDMEM) --->

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

BiscuitOS-PAGING-PF-PFNMAP-default Source Code on Gitee

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

实践案例由两部分组成,其中一部分如上图的内核模块,该模块由 MSIC 驱动框架构成,MSIC 驱动框架向用户空间提供 “/dev/BiscuitOS-PFNMAP” 文件,并为文件实现了 mmap 接口,也就是用户空间打开该文件之后,并调用 mmap 分配内存时会调用到 BiscuitOS_mmap 函数,该函数的主要任务是设备 VMA 的 vm_ops, vm_ops 里实现了 fault 接口,也就是访问 mmap 分配的虚拟内存触发缺页时 vm_fault 函数会被调用。vm_fault 的主要任务就是分配 RSVDMEM 内存和填充页表,因此函数在 33 行先将 VMA 标记为 PFNMAP 的虚拟区域,然后在 34 行调用 apply_to_page_range() 函数查询虚拟内存对应的 PTE 页表,当 PTE 页表找到之后对调用 PFNMAP_pfn() 函数,此时 RSVDMEM 内存来自与 CMDLINE=”memmap=1M$0x1000000” 预留的物理内存,此时使用 PFNMAP_PFN 表示 RSVDMEM 可用内存,接着函数在 22 行调用 set_pte_at() 函数填充 PTE Entry 页表. 当 vm_fault() 函数执行完毕之后返回 VM_FAULT_NOPAGE 完成缺页异常处理.

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

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

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

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

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

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

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

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

PFNMAP 映射 4KiB 可读写物理内存场景的缺页异常流程如上图,对于该场景异常的原因包括 PF_WRITE,那么 do_fault 函数了选择了 do_shared_fault 函数处理异常,该函数又通过 __do_fault 调用到 PFNMAP 驱动模块提供的 vm_fault 函数,该函数主要通过 apply_to_pange_range 函数遍历到 PTE 页表,此时调用 PFNMAP_pfn 函数进行最后的 RSVDMEM 内存分配和页表填充.

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

对于可读可写虚拟内存的缺页,do_shared_fault 函数的处理逻辑如上图,函数在 4583 行调用 __do_fault 逻辑分配物理内存,其会根据 VMA 的 vm_ops 找到对应的 fault 回调函数,对于 FPNMAP 内存其回调函数是有内核模块提供的 vm_fault 函数, vm_fault 函数的主要任务是分配 RSVDMEM 内存和填充 PTE 页表,由于 vm_fault 处理完毕之后直接返回 VM_FAULT_NOPAGE, 那么缺页异常处理在次就直接返回。

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

apply_to_page_range 函数是内核为内核模块提供的机制,该机制用于查询虚拟地址对应的 PTE Entry,因此只能查 PTE 映射的页表。该函数从 PGD、P4D、PUD、PMD、PTE 一级一级查询下去,当查询到 PTE 页表时会调用 apply_to_page_range 函数的 fn 参数对应的回调函数,因此在该回调函数了添加了对 RSVDMEM 内存的分配和页表填充功能.

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