目录
HugeTLB Memory With PageFault
在 Linux 里,用户进程可以使用 malloc/mmap 分配虚拟内存,通常情况下虚拟内存最终映射到 4KiB 的物理内存上. 用户进程的虚拟内存除了可以映射 4KiB 的物理内存,其也可以映射更大粒度的物理大页,映射大页的好处简单来说就是节省页表内存开销和减少 TLB Entry 占用,从而降低内存峰值带来的性能损耗。目前 Linux 支持用户空间虚拟内存映射 2MiB/1Gig 物理大页的方法有:
- 透明大页方案: 可以自动、无需修改应用程序代码的情况下,将零散的 4KiB 物理页映射迁移合并成 2MiB 物理页映射, 减少 TLB 缺失从而提供应用程序和系统的整体性能.
- PMDMAPPED PFNMAP 方案: 在驱动模块的支持下,应用程序将虚拟内存直接映射到 2MiB 的系统预留物理内存上,优点是应用自我内存管理,减少 TLB 缺失.
- HUGETLB 方案: 系统提供的大页池化机制,应用程序可以从大页池子中获得 2MiB/1Gig 物理页,然后将进程虚拟内存映射到 HugeTLB 大页上,核心优点就是减少 TLB 缺失.
- 共享大页内存方案: 多个进程在使用共享内存时,可以采用共享内存大页,这样有利于减少 TLB 缺失和内存消耗.
- HUGE-DAX 方案: 在支持 PMEM 的系统里,用户进程可以将虚拟内存直接映射到 2MiB 区域的 PMEM 内存上,同样可以减少 TLB 缺失.
虽然系统支持多种应用程序虚拟内存映射 2MiB 物理内存的方案,有的是针对特定场景,而有的是需要过多的前置条件,但对于 HugeTLB 机制来说,其具有普适性,可以应用到很多场景,并且可以做到对应用程序透明. HugeTLB 机制将物理大页维持在内存池内,内存池和系统管理的物理内存是相互独立的,也就是说 HugeTLB 机制可以控制的内存池子的伸缩,另外系统在通用内存的分配和回收时,内存池子的内存不受影响。HugeTLB 机制默认建立两个公共的大页内存池子,其也支持基于 Hugetlbfs 文件系统建立 N 个私有的大页内存池子,最后应用程序可以从指定的大页池子中分配映射 2MiB/1Gig 的物理大页.
HugeTLB 大页池子使用 STRUCT hstate 数据结构进行维护,其成员记录了某种粒度 HugeTLB 大页的使用情况,并维护了两类链表,其中一个链表维护了已经分配出去的 HugeTLB 大页,另外一类链表是为每个 NUMA NODE 维护的没有使用的 HugeTLB 大页。HugeTLB 大页存在多种状态,分别是: 空闲状态(HugePages_Free)、预留状态(HugePages_Rsvd) 、超发状态(HugePages_Surp) 以及激活状态(Active).
HugeTLB 大页池子使用不同的成员统计大页内存的使用情况,这些统计量让系统知道 HugeTLB 池子里大页使用情况, 并且各成员之间存在如下关系:
- nr_huge_pages: 该成员用于指明在指定粒度的大页内存池子中总共包含大页的数量
- max_huge_pages: 该成员用于指明指定粒度大页内存池子中固定大页的数量, 所谓固定大页就是一次性从系统中分配,并一直维护在大页内存池子中,不会被动态释放会系统的大页.
- free_huge_pages: 该成员用于表示指定粒度大页内存池子中空闲大页的数量,空闲但不代表可用(可能被预留)
- resv_huge_page: 该成员用于表示指定粒度大页内存池子中预留池子的大页数量, 预留表示已经分配但未被真正使用的大页
- nr_overcommit_huge_pages: 该成员用于指明指定粒度大页能可以超发大页的数量,即最多可以从系统动态申请大页的数量
- suplus_huge_pages: 该成员用于表示指定粒度大页内存池子中通过超发机制动态分配的大页数量
# 指定粒度 HugeTLB 大页池子大页总数: nr_huge_pages
nr_huge_pages = max_huge_pages + surplus_huge_pages
# 指定粒度 HugeTLB 大页池子固定大页总数: Persistent HugePages
max_huge_pages = nr_huge_pages - surplus_huge_pages
# 指定粒度 HugeTLB 大页池子超发大页总数: Surplus HugePages
surplus_huge_pages = nr_huge_pages - max_huge_pages
# 指定粒度 HugeTLB 大页池子可分配大页数量:
nr_Alloc_HugePage = free_huge_pages - resv_huge_pages + (nr_overcommit_huge_pages - surplus_huge_pages)
# 指定粒度 HugeTLB 大页池子还可以超发大页数量:
nr_may_Surplus_pages = nr_overcommit_huge_pages - surplus_huge_pages
# 指定粒度 HugeTLB 大页池子激活(正在使用)大页总数:
nr_active_pages = nr_huge_pages - free_huge_pages
在 Linux 系统里,可以通过 ‘/proc/meminfo’ 查看系统默认粒度 HugeTLB 大页池子的使用情况,其中 HugePages_Total 对应 nr_huge_pages 成员,HugePages_Free 对应 free_huge_pages 成员,HugePages_Rsvd 对应 resv_huge_page 成员,HugePages_Surp 对应 suplus_huge_pages 成员。对于某种粒度的大页池子内存情况,可以在 “/sys/kernel/mm/hugepages” 目录下查看.
用户进程可以使用两种方式分配 HugeTLB 大页,两种都基于 mmap 函数进行分配. 首先是从系统公共 HugeTLB 大页池子中分配大页,其核心是使用 MAP_ANONYMOUS 和 MAP_HUGETLB 标志,另外也可以使用 MAP_HUGETLB_64KB、MAP_HUGETLB_2MB、MAP_HUGETLB_4-MB、MAP_HUGETLB_32MB 和 MAP_HUGETLB_1G 选择不同粒度的 HugeTLB 大页; 第二种方式则利用 Hugetlbfs 文件系统,通过在挂载 Hugetlbfs 文件系统的目录下创建或打开文件,然后将文件映射到进程地址空间即可.
用户进程采用匿名映射(Anonymous Mapping)与 HugeTLB 建立页表映射之后,HUGETLB 大页与用户进程之间也建立逆向映射关系,那么正向映射、逆向映射、进程与 HugeTLB 之间的关系如上图: 每个 VMA 的 anon_vma(AV) 成员维护着一颗红黑树,红黑树上的节点 AVC 指向了映射到该 HugeTLB 大页的 VMA,VMA 的虚拟内存可以通过页表获得映射的 HugeTLB 大页,HugeTLB 大页的 mapping 指向了 anon_vma(AV),那么 HugeTLB 可以遍历 AV 的红黑树知道哪些进程(VMA) 映射到自身.
用户进程采用文件映射(File-Mapping) 与 HugeTLB 建立页表映射之后,HUGETLB 大页与用户进程之间也建立逆向映射关系,那么正向映射、逆向映射、进程 与 HugeTLB 之间的关系如上图: 每个 VMA 对应一个 Hugetlbfs 文件, 该文件对应唯一的 STRUCT inode, 其成员 STRUCT address_space 维护了所有的映射,其中 i_mmap 维护的红黑树记录了那些进程(VMA) 映射到该文件,然后 i_pages 维护的 XARRAY 数组知道 PAGECACHE(HugeTLB 大页) 与文件偏移的关系,最后 HugeTLB 大页的 mapping 指向了 STRUCT address_space. 因此 HugeTLB 大页可以遍历 address_space 维护的红黑树知道那些进程映射到自身.
HugeTLB 预留
当用户进程通过上面的方式分配指定粒度 HugeTLB 时,进程不仅会分配一段虚拟内存,并且会从大页池子中预留指定数量的 HugeTLB 大页,虽然此时虚拟内存并没有与 HugeTLB 大页建立映射,但 HugeTLB 一旦预留就不能被分配,直到进程真正访问到这段内存,被预留的 HugeTLB 才会转换成激活态的 HugeTLB. 这样做的目的是防止 HugeTLB 大页内存发生缺页时分配不出内存导致核心进程异常退出,保证 HugeTLB 大页池子分配的可靠。但预留 HugeTLB 策略也带来了另外一个问题,就是进程预留了很多大页,但真正使用的很少,而且系统内存特别紧缺也无法回收 HugeTLB,这就出现旱的旱死涝的涝死. 为了缓解这个问题,可以在分配时使用 MAP_NORESERVE 标志不让进程预留 HugeTLB 大页.
HugeTLB 超发
HugeTLB 大页池子可以一次性从系统中申请多个大页,这些大页只有 HugeTLB 大页池子主动释放的时候,系统才能回收,否则就算系统内存资源紧缺也无可奈何,这里将这种方式申请的大页称为 HugeTLB 固定大页(Persistent HugePages). HugeTLB 固定大页的多少直接会影响内存紧缺场景下的性能,那么为了解决这个问题,HugeTLB 大页池子也支持另外一种方法申请大页,简单来说就是需要的时候从系统申请,不需要的时候释放会系统,这类型的大页称为超发大页(Surplus Hugepages). 超发大页虽好,但也有一定问题,例如动态从系统申请导致 HugeTLB 大页申请慢,另外不采用预留机制的场景可能出现无大页可用从而导致进程崩溃, 因此超发大页的时候应该结合实际情况进行使用.
HugeTLB 缺页场景
- 可读可写 HugeTLB 内存缺页场景: 进程分配一段映射 HugeTLB 大页的虚拟内存,当进程首次访问这段虚拟内存时,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数识别出是 HugeTLB 大页内存,于是从指定粒度大页池子的预留池子里获得一个大页,然后将页表映射该大页上,待缺页异常处理返回之后,进程可以正常访问该段虚拟内存而不会触发缺页.
- 写保护 HugeTLB 内存缺页场景: 进程分配的一段映射 HugeTLB 大页的虚拟内存,这段由于某些原因变成了写保护的内存. 当进程可以对该端虚拟内存正常读操作,一旦进程对该段虚拟内存执行写操作时,MMU 发现权限异常而触发缺页异常,缺页异常处理函数发现 VMA 不具有写权限,那么直接发送 SIG_BUS 导致程序异常退出.
- HugeTLB 内存 COW 缺页异常场景: 进程分配一段映射 HugeTLB 大页的虚拟内存,并可以正常访问这段虚拟内存。接着进程调用 fork 拷贝生成子进程, fork 函数会将这段虚拟内存标记为写保护,起初父子进程都可以正常对这段虚拟内存发起读操作,当子进程或者父进程首先发起写操作时,MMU 检查到权限异常而触发缺页,缺页异常处理函数会新分配一个 HugeTLB 大页,然后将原先大页内容拷贝到新的 HugeTLB 大页上,最后建立页表映射到新的 HugeTLB 大页上,那么该进程可以正常访问这段虚拟内存; 当另外一个进程访问这段内存时,同样 MMU 发现权限异常而触发缺页异常,缺页异常处理函数检查到写保护 HugeTLB 内存发生写操作,并且此时只有一个进程映射到 HugeTLB 大页上,那么直接修改页表为可读可写,那么该进程可以正常访问该虚拟内存.
- MCE HugeTLB 内存缺页场景: 当进程分配一段映射 HugeTLB 大页的虚拟内存,并且可以正常访问这段虚拟内存. 由于内存条硬件故障导致 HugeTLB 大页发生 UE 故障,此时进程再次访问这段虚拟内存,MMU 检查到物理内存不存在并触发缺页异常,缺页异常处理函数通过对应的 PMD Entry 发现 HugeTLB 发生了 MCE(UE), 那么直接发送 SIG_BUS 让程序异常退出.
- SYSV 共享 HugeTLB 内存缺页场景: 多个进程之间通过 SYSV 机制共享了一个 HugeTLB 大页,进程从地址空间分配一段虚拟内存映射该 HugeTLB 大页,当进程首次访问这段虚拟内存,MMU 检查到物理内存不存在并触发缺页异常,缺页异常处理检查到是 HugeTLB 内存,然后通过文件映射方式找到共享的 HugeTLB 大页,最后将页表映射到共享 HugeTLB 大页上,待缺页异常返回之后,进程可以正常访问这段虚拟内存.
- POSIX 共享 HugeTLB 内存缺页场景: 多个进程之间通过 POSIX 机制共享了一个 Hugetlbfd 文件系统的 HugeTLB 大页,进程从地址空间分配一段虚拟内存映射该 HugeTLB 大页,当进程首次访问这段虚拟内存,MMU 检查到物理内存不存在并触发缺页异常,缺页异常处理检查到是 HugeTLB 内存,然后通过文件映射方式找到共享的 HugeTLB 大页,最后将页表映射到共享 HugeTLB 大页上,待缺页异常返回之后,进程可以正常访问这段虚拟内存.
- MEMFD 共享 HugeTLB 内存缺页场景: 多个进程之间通过 MEMFD 机制共享了一个来自公共 HugeTLB 大页池子的 HugeTLB 大页,进程从地址空间分配一段虚拟内存映射该 HugeTLB 大页,当进程首次访问这段虚拟内存,MMU 检查到物理内存不存在并触发缺页异常,缺页异常处理检查到是 HugeTLB 内存,然后通过文件映射方式找到共享的 HugeTLB 大页,最后将页表映射到共享 HugeTLB 大页上,待缺页异常返回之后,进程可以正常访问这段虚拟内存.
- 超发大页缺页场景: 进程分配一段映射 HugeTLB 大页的虚拟内存,由于大页池子固定大页已经分配完,此时大页池子支持超发大页并且可以动态分配超发大页,那么大页池子并没有将超发大页进行预留(进程不让大页预留),那么进程首次访问该段虚拟内存时,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数发现是 HugeTLB 内存,但是由于某些原因系统此时无法提供超发大页,那么缺页异常处理函数会因无可用内存发送 SIG_BUS,最终导致进程异常退出.
HugeTLB 大页相关的缺页均在 handle_mm_fault 函数里通过 is_vm_hugetlb_page 函数可以识别处理,然后调用 hugetlb_fault 进行统一处理,无论进程采用文件映射还是匿名映射,HugeTLB 大页均不是用 VMA vm_ops 提供的 fault 接口, 因为 HugeTLB 与普通文件映射内存存在很大差异,因此使用 hugetlb_fault 统一处理。HugeTLB 大页支持的缺页场景不仅仅包括上面的所述的,开发者可以在看完本文之后发觉更多的场景:
HugeTLB Memory: R/W 缺页场景
在 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
实践案例由一个应用程序构成,程序首先在 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 大页.
HugeTLB Memory: WP 缺页场景
在 Linux 里,当一块虚拟内存被标记为写保护(Write-Protection) 之后,如果对这块虚拟内存发起写操作,MMU 会检查到权限异常而触发缺页. 写保护起始就是将页表里的 _PAGE_RW 标志清零,以此让映射的虚拟内存没有写的权限。映射 HugeTLB 的虚拟内存同样也可以设置为写保护,但发起写操作触发缺页之后,缺页异常处理函数可能会有以下两种策略:
- SegmentFault: 当对映射 HugeTLB 的写保护虚拟内存发起写操作之后,MMU 检查到权限异常而触发缺页,此时发现缺页的原因是写操作,但 VMA 没有写权限,那么直接发送 SIG_BUS 导致进程异常退出.
- COPY-PAGE: 当对映射 HugeTLB 的写保护虚拟内存发起写操作之后,MMU 检查到权限异常而触发缺页,此时发现缺页的原因是写操作,但 VMA 有写权限且私有,那么检查对应的 HugeTLB 有多个 VMA 映射,于是缺页异常处理函数新分配一个 HugeTLB 大页,然后将原先页的内容拷贝到新的 HugeTLB 大页,最后建立页表映射到新的 HugeTLB 大页上,那么后面进程可以正常访问新的 HugeTLB 大页.
- REUSE-PAGE: 当对映射 HugeTLB 的写保护虚拟内存发起写操作之后,MMU 检查到权限异常而触发缺页,此时发现缺页的原因是写操作,但 VMA 有写权限且私有,那么检查对应的 HugeTLB 只有一个 VMA 映射,于是缺页异常处理函数修改页表为可读可写,那么进程可以正常访问虚拟内存.
结合上面的分析,写保护的场景需要结合 VMA 的私有和共享情况,以及 VMA 是否为可读可写还是只读. 写保护内存的出现是为了某些特定的场景,比如 COW、或者为了拦截某次页表修改之后的写操作等。为了更加深刻了解写保护匿名内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Support Host CPU Feature Passthrough
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on WP --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-WP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配一段 2MiB 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,因此绑定了公共 2MiB HugeTLB 大页池子了的大页. 程序接着在 33 行对这段虚拟内存执行写操作,由于此时页表没有建立会触发缺页异常,程序接着在 35 行对虚拟内存执行读操作,此时页表已经建立,因此不会触发缺页,程序然后在 38 行调用 mprotect 函数将这段虚拟内存设置为写保护,并在 41 行对虚拟内存进行写操作,由于对写保护的内存写操作会再次触发缺页,最后释放内存. 以上便是一个最基础的实践案例,可以知道 33 行和 41 行写操作会触发缺页,为了可以看到写保护内存在缺页异常里的流动,本次在 41 行前后加上 BS_DEBUG 开关:
接着在 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_user_addr_fault 函数的 1381 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-WP-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB WP PF on do_user_addr_fault 0x6000000000”, 说明缺页异常处理函数执行过 HUGETLB WP 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
缺页异常处理函数针对写保护处理逻辑如上图,对于只读 VMA 发起写操作,do_user_addr_fault 函数在获得 VMA 信息之后,调用 access_error 函数就可以知道在 VMA 没有写权限的情况下,X86_PF_WRITE 为缺页原因,那么会触发缺页异常处理函数发送 SIG_BUS,最终导致应用程序异常退出. HugeTLB 写保护的场景不仅仅如此,后续 COW 场景也是一种写保护场景.
HugeTLB Memory: COW 缺页场景
写时拷贝(COW)匿名内存是当进程调用 fork() 函数构造子进程时,子进程继承了父进程的 HugeTLB 内存,但 fork 系统调用会将父进程和子进程的 HugeTLB 内存都修改为写保护,那么可以确保父子进程读到同样的内容,而父子进程对 HugeTLB 内存要写的时候,才会在属于自己的 HugeTLB 内存上执行写操作。COW 的核心是在写的时候才会分配物理内存承接写操作,其存在两种缺页场景:
- 当 CPU 写入父子进程还在共享的 COW HugeTLB 内存: MMU 因为写保护异常,于是触发缺页异常,在缺页异常处理函数发现 HugeTLB 大页被多个进程映射,于是分配一个新的 HugeTLB 大页,并将页表更新映射到新的 HugeTLB 大页上,然后添加 _PAGE_RW 属性,最后将原始物理页的内容拷贝到新的物理页上,那么 CPU 后续的读写操作不会触发缺页.
- 当 CPU 写入父进程或者子进程独占的 COW HugeTLB 内存: MMU 因为写保护异常,于是触发缺页异常,缺页异常处理函数发现 HugeTLB 大页只被一个进程映射,那么缺页异常处理函数会重新 REUSE 该物理页,并添加页表的 _PAGE_RW 属性,那么 CPU 后续的读写操作不会触发缺页.
在 Linux 里使用 struct anon_vma 和 struct anon_vma_chain 两个数据结构体维护匿名 HugeTLB 内存的逆向映射,所谓逆向映射就是知道匿名页被哪些 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 多少次,都可以知道一个匿名 HugeTLB 页被多少进程映射. 为了更加深刻了解 COW 匿名 HugeTLB 内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Support Host CPU Feature Passthrough
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on COW(Copy-On-Write) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-COW-default Source Code on Gitee
实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配一段 2MiB 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,因此绑定了公共 2MiB HugeTLB 大页池子了的大页. 程序接着在 32 行对这段虚拟内存执行写操作,由于此时页表没有建立会触发缺页异常. 程序在 34 行调用 fork 函数创建子进程,然后在 36 行子进程首先对虚拟内存发起写操作,紧随其后父进程在 40 行也对虚拟内存发起了写操作,最后父子进程在 44 行再次对虚拟内存执行读操作. 以上便是一个最基础的实践案例,可以知道 36 行和 40 行写操作会触发缺页,为了可以看到 COW 内存在缺页异常里的流动,本次在 36 行前后加上 BS_DEBUG 开关:
接着在 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 5733 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-COW-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB COW PF on hugetlb_fault 0x6000000000”, 说明缺页异常处理函数执行过 HUGETLB COW 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
当 COW HugeTLB 内存发生缺页时,其缺页处理函数流程如上,缺页异常函数识别出异常内存是 HugeTLB 内存,于是调用 hugetlb_fault 函数进行处理,函数检查到 PMD Entry 存在,并且 VMA 不是共享的,另外 HugeTLB 大页只有被一个进程映射,那么进入 COW-RESUE 分支,其核心目的是将页表修改为可读可写,于是调用 set_huge_ptep_writable 函数完成该任务; 而对于有多个进程映射的 HugeTLB 大页,缺页异常处理函数进入 COW-COPY 分支,并调用 alloc_huge_page 新分配一个 HugeTLB 大页,然后将可读可写的页表映射新的 HugeTLB 大页上,并且调用 hugepage_add_new_anon_rmap 更新 HugeTLB 大页的逆向映射,那么进程后续就在各自的 HugeTLB 大页上进行读写操作.
上图是 HugeTLB 场景下的 fork 系统调用处理逻辑,其中核心逻辑是新进程会调用 dup_mmap 函数拷贝父进程的地址空间,其遍历父进程的所有的 VMA,对于匿名映射的 VMA,那么调用 anon_vma_fork 函数建立逆向映射,最后针对 HugeTLB 调用 reset_vma_resv_huge_pages 函数遍历 VMA 对应的页表,将 COW HugeTLB 内存对应页表的 _PAGE_RW 标志去掉,此时可以看到调用 huge_ptep_set_wrprotect 函数进行写保护,最终更新页表. 那么无论是父进程还是子进程的 COW HugeTLB 内存都变成了写保护.
HugeTLB COW 导致进程 SegmentFault
HugeTLB 大页池子在为用户进程分配虚拟内存时,会将对应数量的 HugeTLB 大页进行预留,这样做的好处是防止 HugeTLB 虚拟内存缺页时保证可以分配到 HugeTLB 大页,坏处也很明显就是 HugeTLB 虚拟内存没有被使用,那么大量的预留 HugeTLB 大页也就浪费了,另外结合 FORK 系统调用对 HugeTLB 的处理过程,如果某个进程分配的 HugeTLB 虚拟内存发生了多次 FORK 动作,FORK 动作并不会为新创建的子进程预留 HugeTLB 大页,那么会出现某个子进程访问这段虚拟内存时,缺页异常处理函数无法从 HugeTLB 大页池子里分配到可用的 HugeTLB 内存,那么缺页进程只能向进程发送 SIG_BUS,最后导致子进程 SegmentFault 异常退出. 为了对该问题有更深刻的认知,开发者可以通过一个实践案例进行了解,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on COW SegmentFault --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-COW-FAULT-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-COW-FAULT-default Source Code on Gitee
实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配一段 2MiB 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,因此绑定了公共 2MiB HugeTLB 大页池子了的大页. 程序接着在 32 行对这段虚拟内存执行写操作,由于此时页表没有建立会触发缺页异常. 程序在 34 行调用 fork 函数创建子进程,然后在 37 行子进程首先对虚拟内存发起写操作,紧随其后父进程在 41 行也对虚拟内存发起了写操作,最后父子进程在 45 行再次对虚拟内存执行读操作. 以上便是一个最基础的实践案例,可以知道 37 行和 41 行写操作会触发缺页,为了可以看到 COW 内存在缺页异常里的流动,本次在 37 行前后加上 BS_DEBUG 开关:
接着在 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 5733 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-COW-FAULT-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB COW FAULT PF on hugetlb_fault 0x6000000000”, 另外只有父进程打印了读操作的数据, 说明缺页异常处理函数执行过 HUGETLB COW FAULT 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
同 COW HugeTLB 内存发生缺页时处理逻辑类似,可以直接看 WP-HUGETLB 分支,当在 hugetlb_wp 函数时,缺页异常函数发现 HugeTLB 大页被多个进程映射,因此它会调用 alloc_huge_page 为新进程分配新的 HugeTLB 大页,但 dequeue_huge_page_vma 函数并没有从 HugeTLB 大页池子里获得可用的大页,于是继续调用 alloc_surplus_huge_page 函数通过超发方式从系统里分配新的大页,但是此时并没有超发名额,因此分配内存失败,此时缺页异常处理函数直接返回 VM_FAULT_SIGBUS,有了这个返回值之后,缺页异常处理函数直接发送 SIG_BUS,让进程 SegmentFault 异常退出.
HugeTLB Memory: 不同粒度 HugeTLB 缺页场景
HugeTLB 支持不同粒度的 HugeTLB 大页,因此系统里可以同时存在多个不同粒度的大页池子,那么当应用程序将虚拟内存映射到不同粒度的大页池子时,当进程首次访问这段虚拟内存时,MMU 发现物理内存不存在并触发缺页异常,缺页异常处理函数识别出异常的虚拟内存映射了 HugeTLB 大页,于是从大页池子的预留小池子里获得一个 HugeTLB 大页,然后将页表映射到该 HugeTLB 大页上,待缺页异常处理函数返回之后,进程可以正常访问这段虚拟内存,并且不会触发缺页. 这就是最基础的 HugeTLB 内存缺页场景,那么不同粒度的大页池子在缺页流程中是否存在差异? 接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
(6144) Memory Size(MiB)
(hugepagesz=1G hugepages=1) CMDLINE on Kernel
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on 1G --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-1G-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,程序首先在 27 行调用 mmap 函数分配一段 1Gig 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,另外还使用了 MAP_HUGE_1GB 标志,因此绑定了公共 1Gig HugeTLB 大页池子了的大页. 程序接着在 39 行对这段虚拟内存执行写操作,由于此时页表没有建立会触发缺页异常,程序接着在 41 行对虚拟内存执行读操作,此时页表已经建立,因此不会触发缺页,最后释放内存. 以上便是一个最基础的实践案例,可以知道 39 行写操作会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 39 行前后加上 BS_DEBUG 开关:
接着在 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 5733 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-1G-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB-1G FAULT PF on hugetlb_fault 0x6000000000”, 说明缺页异常处理函数执行过 HUGETLB 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
缺页异常处理函数在 handle_mm_fault 函数调用 is_vm_hugetlb_page 函数判断是否为 HugeTLB 内存缺页,并调用 hugetlb_fault 函数进行 HUGETLB 缺页处理,HUGETLB 缺页与其他内存缺页流程不一样. HUGETLB 缺页流程首先调用 hstate_vma 知道是何种粒度的 HugeTLB 大页,然后提供对应粒度大页池子的 STRUCT hstate, 接着检查对应的 PMD/PUD Entry 是否为空,为空说明 HUGETLB 内存首次缺页,然后调用 alloc_huge_page 从预留内存池子里分配大页,从之前的分析可知,正常情况下进程分配 HUGETLB 虚拟内存时就会预留相应的 HUGETLB 大页,当获得 HUGETLB 大页时判断映射是匿名映射还是文件映射,以此建立逆向映射。如果是匿名映射,则调用 hugepage_add_new_anon_rmap; 反之如果是文件映射则调用 page_dup_file_rmap 建立逆向映射. 最后就是更新页表操作和将 HUGELTB 大页更新为激活态.
#ifndef MAP_HUGE_1GB
#define HUGETLB_FLAG_ENCODE_SHIFT 26
#define MAP_HUGE_1GB (30 << HUGETLB_FLAG_ENCODE_SHIFT)
#endi
#ifndef MAP_HUGE_2MB
#define HUGETLB_FLAG_ENCODE_SHIFT 26
#define MAP_HUGE_2MB (21 << HUGETLB_FLAG_ENCODE_SHIFT)
#endif
#ifndef MAP_HUGE_4MB
#define HUGETLB_FLAG_ENCODE_SHIFT 26
#define MAP_HUGE_4MB (22 << HUGETLB_FLAG_ENCODE_SHIFT)
#endif
#ifndef MAP_HUGE_32MB
#define HUGETLB_FLAG_ENCODE_SHIFT 26
#define MAP_HUGE_32MB (25 << HUGETLB_FLAG_ENCODE_SHIFT)
#endif
#ifndef MAP_HUGE_64KB
#define HUGETLB_FLAG_ENCODE_SHIFT 26
#define MAP_HUGE_64KB (16 << HUGETLB_FLAG_ENCODE_SHIFT)
#endif
应用程序在使用 mmap 映射 HUGETLB 大页时,可以使用 MAP_HUGE_1GB、MAP_HUGE_2MB、MAP_HUGE_4MB、MAP_HUGE_32MB、MAP_HUGE_64KB 宏选择不同粒度的 HugeTLB 大页.
HugeTLB Memory: SYSV 缺页场景
SystemV 共享内存是 Linux 中一种共享内存机制,用于不同进程之间共享数据。它是 SystemV IPC(Inter-Process Communication,进程间通信) 机制的一部分,与其他 IPC 机制如信号量和消息队列一起提供了进程间通信的方式, SystemV 共享内存的主要特点包括:
- 共享内存段: SystemV 共享内存允许多个进程创建或连接到共享内存段(shared memory segment),并在这些段中读取和写入数据
- 高效的共享: 与其他 IPC 机制相比,共享内存提供了一种高效的方式来在进程之间共享大量的数据, 这是因为它不涉及数据复制,而是直接在内存中共享数据
- 操作系统持久性: 共享内存段通常具有操作系统的持久性。这意味着它们可以在不同的进程之间保持数据的一致性,即使某个进程退出也不会导致共享内存段的销毁
- 内核支持: Linux 内核提供了对 SystemV 共享内存的支持,包括创建、连接、分离和删除共享内存段的系统调用,例如 shmget、shmat、shmdt 和 shmctl
SYSV 机制提供了 shmget 和 shmat 等多个接口,其中 shmget 接口用于创建或获取一个共享内存的标识符,其可以指定共享内存的属性,例如可读可写等. shmat 接口用于将与其他进程共享的共享内存映射到自己的虚拟地址空间,该函数也是一个系统调用,从流程可以看出其也是获得共享内存对应的文件,然后调用 do_mmap 函数分配共享内存,其对应的文件处理接口是 shm_file_operations,该接口的 mmap 对应 hugetlbfs_file_mmap 函数,这就是分配共享内存的接口,因此 do_mmap 函数分配的内存与文件映射的 HugeTLB 共享内存一致.
当进程 A 通过 shget 接口与进程 B 共享一块共享内存,并通过 shmat 接口获得该共享内存,然后进程 A 或者进程 B 访问这段共享内存时都会发生缺页,缺页异常处理函数会找到共享内存,然后将进程的页表映射到该共享内存上. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on SYSV --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-SYSV-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-SYSV-default Source Code on Gitee
实践案例由两个应用程序组成,上图为 SERVER 端的应用程序,其在 26 行调用 shmget 函数申请一段可读可写的 HugeTLB 共享内存,并获得 IPC ID,然后调用 shmat 接口将 HugeTLB 共享内存映射到进程的地址空间,程序接着在 40 行将 HugeTLB 共享内存清零,并在 41 行向 HugeTLB 共享内存写入字符串.
上图为 CLIENT 端的应用程序,其在 26 行调用 “ipcs -m” 命令查看当前 IPC 信息,然后在 29 行调用 shmget 接口共享 SHMEM_KEY 对应的 HugeTLB 共享内存,然后在 36 行调用 shmat 行将 HugeTLB 共享内存映射到进程的地址空间,并在 43 行对共享内存发起写操作,然后在 46 行对共享内存进行读操作,49 行和 55 行两个函数的目的是进程释放 HugeTLB 共享。以上便是一个最基础的实践案例,可以知道 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:
接着在 HugeTLB 共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 5733 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-SYSV-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB SYSV PF on hugetlb_fault 0x6000000000”, 那么说明实践案例对 SYSV 的 HugeTLB 共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
缺页异常处理函数在 handle_mm_fault 函数调用 is_vm_hugetlb_page 函数判断是否为 HugeTLB 内存缺页,并调用 hugetlb_fault 函数进行 HUGETLB 缺页处理,HUGETLB 缺页与其他内存缺页流程不一样. HUGETLB 缺页流程首先检查对应的 PMD/PUD Entry 是否为空,为空说明 HUGETLB 内存首次缺页,然后调用 alloc_huge_page 从预留内存池子里分配大页,由于本场景中 SERVER 端程序已经分配过 HugeTLB 大页,因此调用 find_lock_page 时可以直接找到对应的 HugeTLB,但如果是 SERVER 端那么需要分配 HugeTLB 大页,接下来调用 page_dup_file_rmap 将 HugeTLB 添加到文件映射,以及调用 make_huge_pte 和 set_huge_pte_at 构建和设置页表.
对于基于 HugeTLBFS 分配的 HugeTLB 大页均属于文件映射,因此这类 HugeTLB 大页的逆向映射拓扑图如上,HugeTLBFS 为每个文件创建对于的 STRUCT inode,其对于的 STRUCT address_space 维护了 HugeTLBFS 文件对于的 XARRAY 和逆向映射红黑树,可以通过 i_mmap 指向的红黑树知道 HugeTLB 大页被哪些进程映射,另外 XARRAY 可以知道 HugeTLBFS 文件使用了哪些 HugeTLB 大页,每个 HugeTLB 大页的 mapping 指向了 STRUCT address_space, 以此形成封闭的逻辑.
HugeTLB Memory: POSIX 缺页场景
在 Linux 中,POSIX 共享内存是一种用于实现进程间通信(IPC)的机制,允许多个进程共享一个内存区域,从而实现数据共享。与 SystemV 共享内存不同,POSIX 共享内存是基于 POSIX 标准定义的,它具有一些与 SystemV 共享内存不同的特性和优点:
- 基于文件系统路径: 与 SystemV 共享内存使用键值标识不同,POSIX 共享内存使用文件系统路径来标识共享内存对象。这意味着进程可以通过文件路径来访问和共享数据,而不需要预先协商一个键值
- 动态创建和销毁: POSIX 共享内存对象可以在运行时动态创建和销毁,而无需在启动时预先创建, 这使得它更加灵活,不需要在多个进程之间协调共享内存的创建
- 内存映射: POSIX 共享内存通常使用 mmap 函数将共享内存对象映射到进程的地址空间中,从而使进程可以直接访问共享内存中的数据。这与 SystemV 共享内存使用 shmat 函数连接到内存的方式不同
- 文件权限控制: 由于 POSIX 共享内存对象是基于文件系统路径的,因此可以使用标准的文件权限控制机制(如文件权限位和访问控制列表)来控制对共享内存的访问
- 无需专用 IPC 函数: POSIX 共享内存不需要专门的 IPC 函数来创建、连接或删除共享内存对象. 相反,它使用标准的文件操作和内存映射函数来实现这些操作
- 跨平台性: POSIX 共享内存是跨平台的,因为它是 POSIX 标准的一部分,可以在支持 POSIX 标准的各种 UNIX 和 UNIX-like 操作系统上使用
对于 HugeTLB 内存,其只需在 HugeTLBFS 挂载的目录下创建文件,然后基于 POSIX 共享机制,便可以实现多个进程共享该文件,最终共享 HugeTLB 内存. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on POSIX --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-POSIX-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-POSIX-default Source Code on Gitee
实践案例由两个应用程序组成,上图为 SERVER 端的应用程序,其在 25 行调用 open 函数打开 “/mnt/BiscuitOS-Hugetlbfs/” 目录下的共享内存文件 “BiscuitOS.mem”,然后在 32 行调用 mmap 函数将 HugeTLB 共享内存映射到进程的地址空间,程序在 42 行对共享内存写操作,最后在释放共享内存.
上图为 CLIENT 端的应用程序,其在 24 行调用 open 函数打开 “/mnt/BiscuitOS-Hugetlbfs/” 目录下的共享内存文件 “BiscuitOS.mem”,接着在 31 行调用 mmap 函数将共享内存映射到进程的地址空间,程序在 41 行对 HugeTLB 共享内存写操作,并在 44 行对 HugeTLB 共享内存执行读操作,最后在释放共享内存. 为了可以看到内存在缺页异常里的流动,在 CLIENT 程序 41 行前后加上 BS_DEBUG 开关:
接着在 HugeTLB 共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 5733 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-POSIX-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB POSIX PF on hugetlb_fault 0x6000000000”, 那么说明实践案例对 POSIX 的 HugeTLB 共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
缺页异常处理函数在 handle_mm_fault 函数调用 is_vm_hugetlb_page 函数判断是否为 HugeTLB 内存缺页,并调用 hugetlb_fault 函数进行 HUGETLB 缺页处理,HUGETLB 缺页与其他内存缺页流程不一样. HUGETLB 缺页流程首先检查对应的 PMD/PUD Entry 是否为空,为空说明 HUGETLB 内存首次缺页,然后调用 alloc_huge_page 从预留内存池子里分配大页,由于本场景中 SERVER 端程序已经分配过 HugeTLB 大页,因此调用 find_lock_page 时可以直接找到对应的 HugeTLB,但如果是 SERVER 端那么需要分配 HugeTLB 大页,接下来调用 page_dup_file_rmap 将 HugeTLB 添加到文件映射,以及调用 make_huge_pte 和 set_huge_pte_at 构建和设置页表.
对于基于 HugeTLBFS 分配的 HugeTLB 大页均属于文件映射,因此这类 HugeTLB 大页的逆向映射拓扑图如上,HugeTLBFS 为每个文件创建对于的 STRUCT inode,其对于的 STRUCT address_space 维护了 HugeTLBFS 文件对于的 XARRAY 和逆向映射红黑树,可以通过 i_mmap 指向的红黑树知道 HugeTLB 大页被哪些进程映射,另外 XARRAY 可以知道 HugeTLBFS 文件使用了哪些 HugeTLB 大页,每个 HugeTLB 大页的 mapping 指向了 STRUCT address_space, 以此形成封闭的逻辑.
HugeTLB Memory: MEMFD 缺页场景
在 Linux 中,Unix Socket(也称为 Unix 域套接字或本地套接字) 是一种用于进程间通信(IPC)的通信机制, 其通过共享文件描述符实现多个进程之间共享内存,因此共享文件描述符称为了关键所在,那么对于采用 MAP_SHARED 与 MAP_ANONYMOUS 分配的 HugeTLB 共享内存,其如果要实现在不同进程的之间共享,那么也需要为这段共享内存创建一个共享描述符. MEMFD(内存文件描述符) 机制是一种用于创建和操作匿名内存映射区域的方法,它允许将内存块视为文件,并通过文件描述符进行访问。MEMFD 是 Linux 3.17 内核引入的新特性,它的主要目的是为了提供一种灵活、轻量级的内存共享和 IPC(进程间通信) 机制,而无需使用传统的文件或共享内存. 以下是 MEMFD 机制的一些关键特点:
- 无需文件系统路径: MEMFD 创建的内存映射区域不依赖于文件系统路径,因此不需要在文件系统中创建文件或目录。这使得它非常灵活,可以用于临时存储、临时文件、内存缓冲区等
- 文件描述符访问: MEMFD 创建的内存区域通过文件描述符进行访问,类似于普通文件。您可以使用标准的文件 I/O 函数(如 read 和 write) 来读取和写入内存区域的数据
- 轻量级: MEMFD 机制非常轻量,不需要额外的文件系统操作或共享内存管理。它适用于需要快速创建临时内存区域的情况
- 可以用于 IPC: MEMFD 的文件描述符可以用于进程间通信,允许多个进程共享内存块。这可以用于实现共享数据、消息传递等
- 内存清理: 当所有持有 MEMFD 文件描述符的进程都关闭它时,关联的内存区域将被自动释放,不会导致内存泄漏
MEMFD 机制与 UNIX-SOCKET 机制结合实现的共享内存是 POSIX 共享内存的一种应用,可以基于文件系统路径标识共享内存对象,但与普通的 UNIX-SOCKET 共享内存不同,MEMFD 创建的共享文件标识是动态随机创建的,其无需关系创建共享文件的名字和路径. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on MEMFD --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-MEMFD-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-MEMFD-default Source Code on Gitee
实践案例由两个应用程序组成,上图为 SERVER 端的应用程序,看似代码很长实则逻辑很简单,其由三个部分组成,首先是 send_fd 函数用于通过 Unix-Socket 套接字向 CLIENT 发送共享文件标识符,第二部分则由 socket_setup 函数组成,其主要目的就是构造 UNIX-Socket 通道。最后一部分就是 91-121 的逻辑,其在 91 行调用 memfd_create 函数为匿名的 HugeTLB 共享内存创建共享文件标识,并将共享文件大小修改为 MAP_SIZE, 接着在 99 行调用 mmap 函数分配 HugeTLB 共享内存,并在 108 行向 HugeTLB 共享内存写入字符串 “Bello BiscuitOS” 字符串。程序接下来进行 While 循环等待 CLIENT 的请求,在 112 行调用 accept() 函数监听请求,当有新的请求到来时,程序在 119 行调用 send_fd() 函数将共享文件标识发送给 CLIENT 端,并关闭 afd 完成该次请求
上图为 CLIENT 端的应用程序,程序代码也很长但逻辑很简单,其也是由两部分组成,首先是 sock_setup 函数用于构建 Unix-Socket 套接字通道,并向 SERVER 端发起请求,以便获得共享>内存文件标识符. 第二部分是 85-105 行的逻辑,当 85 行成功获得共享内存表示符之后,函数在 88 行调用 mmap 函数将共享 HugeTLB 内存映射到自己的地址空间,然后在 99 行对共享内存进行写操作,并在 101 行对共享内存进行读操作,最后释放内存和关闭共享文件. 以上便是最简单的实践案例,可以知道程序在 99 行对共享内存的写操作会触发缺页异常,为了可以看到内存在缺页异常里的流动,在 SERVER 程序 99 行前后加上 BS_DEBUG 开关:
接着在 HugeTLB 共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 5733 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-MEMFD-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB MEMFD PF on hugetlb_fault 0x6000000000”, 那么说明实践案例对 MEMFD 的 HugeTLB 共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
缺页异常处理函数在 handle_mm_fault 函数调用 is_vm_hugetlb_page 函数判断是否为 HugeTLB 内存缺页,并调用 hugetlb_fault 函数进行 HUGETLB 缺页处理,HUGETLB 缺页与其他内存缺页流程不一样. HUGETLB 缺页流程首先检查对应的 PMD/PUD Entry 是否为空,为空说明 HUGETLB 内存首次缺页,然后调用 alloc_huge_page 从预留内存池子里分配大页,由于本场景中 SERVER 端程序已经分配过 HugeTLB 大页,因此调用 find_lock_page 时可以直接找到对应的 HugeTLB,但如果是 SERVER 端那么需要分配 HugeTLB 大页,接下来调用 page_dup_file_rmap 将 HugeTLB 添加到文件映射,以及调用 make_huge_pte 和 set_huge_pte_at 构建和设置页表.
对于基于 HugeTLBFS 分配的 HugeTLB 大页均属于文件映射,因此这类 HugeTLB 大页的逆向映射拓扑图如上,HugeTLBFS 为每个文件创建对于的 STRUCT inode,其对于的 STRUCT address_space 维护了 HugeTLBFS 文件对于的 XARRAY 和逆向映射红黑树,可以通过 i_mmap 指向的红黑树知道 HugeTLB 大页被哪些进程映射,另外 XARRAY 可以知道 HugeTLBFS 文件使用了哪些 HugeTLB 大页,每个 HugeTLB 大页的 mapping 指向了 STRUCT address_space, 以此形成封闭的逻辑.
HugeTLB Memory: MCE(UE) 故障缺页场景
在 Linux 中,MCE 代表 Machine Check Exception(机器检查异常),它用于处理硬件错误和异常情况。MCE 可以分为 CE(Corrected Error,已纠正的错误) 和 UE(Uncorrected Error, 未纠正的错误) 两种类型。MCE 是 Linux 内核的一部分,用于监视和响应这些硬件错误,以提高系统的可靠性和稳定性。当发生 MCE 时,通常会生成系统日志(例如 /var/log/mcelog),以帮助管理员诊断和解决硬件问题。在服务器和关键应用中,监控和处理 MCE 错误非常重要,以确保系统的可靠性和可用性. CE 和 UE 的区别如下:
- CE(Corrected Error,已纠正的错误): CE 是指硬件错误,但系统可以自动纠正这些错误,而不会导致系统崩溃或严重问题。通常这些错误是由于内存或其他硬件组件的临时问题引起的,例如内存中的单个位翻转。内核会记录这些 CE 错误,但不会采取进一步的行动,因为它们被认为是暂时性的,不需要干预
- UE(Uncorrected Error,未纠正的错误): UE 是指硬件错误,但系统无法自动纠正这些错误,因此可能会导致系统崩溃或数据损坏。UE 错误通常是更严重的硬件问题,例如内存模块中的多个位错误或其他硬件组件的不可恢复故障。内核会尝试采取适当的措施来处理UE错误,例如记录错误信息并尝试停机以防止进一步损坏
在 Linux 里,当硬件检测到硬件错误或者异常,这些硬件错误可能包括内存位翻转、缓存错误、总线错误、内存单元故障等,那么硬件会触发 MCE 异常,MCE 异常处理函数能处理很多硬件错误,其中对于内存 UE 的处理逻辑如上图, 其核心是调用 memory_failure() 函数进行出来,该函数主要完成一下几个任务:
- 标记物理页: 将发生 UE 对于的物理页标记为 PG_hwpoison
- 解除映射: 通过 TTU 机制解除用户进程映射到物理页的映射
- 更新页表: 构造 UE Entry 并更新页表,以防止进程再次访问
MCE 异常处理函数构造的 UE PMD Entry 如上图,可以看到页表 Entry 的 _PAGE_PRESENT 标志位为 0,然后 MSB 5BIT 存储了 SWAP Entry 的类型,UE 场景下该类型是 SWP_HWPOIS-ON,_PAGE_PROTNONE 后面两个 BIT 的字段存储了物理页帧的反码. 因此当进程再次访问 UE 内存时,MMU 检查到对应的物理内存不存在,于是触发缺页异常,缺页异常处理函数再根据 SWP_HWPOISON 可以判断是 UE Page Fault,那么接下来就是将对应的进程杀死. 接下来通过一个实践案例进一步了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下(实践之前需要打开内核 CONFIG_MEMORY_FAILURE 宏):
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on MCE(UE) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-MCE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-MCE-default Source Code on Gitee
实践案例由一个应用程序构成,其分作三部分,首先是 21 行调用 mmap 函数结合 MAP_PRIVATE、MAP_HUGETLB 和 MAP_ANONYMOUS 分配一段可读可写的匿名 HugeTLB 内存,并在 33 行对匿名内存执行写操作, 以及在 35 行再次对匿名内存执行读操作. 第二部分是利用 madvise 注入 UE,由于 UE 是硬件行为,所有只能借助软件来模拟 UE 的发生,于是函数在 38 行调用 madvise 函数发送 MADV_HWPOISON 请求. 最后是第三部分,程序在 41 行再次对匿名内存执行写操作,此时会再次触发缺页并被识别为 UE Page Fault. 以上便是一个最基础的实践案例,可以知道 33 行读操作和 41 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 41 行前后加上 BS_DEBUG 开关:
接着在匿名 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_swap_page 函数的 3747 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-MCE-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB MCE PF on hugetlb_fault 0x6000000000”, 说明缺页异常处理函数执行过 HugeTLB UE 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
当进程再次访问 UE 的虚拟内存时,MMU 检查到物理内存不存在(因为 MCE 异常处理函数将页表的 Present 标志位清除),那么 MMU 触发缺页异常,异常处理函数首先调用 is_vm_hugetlb_page 函数判断是 HugeTLB 内存,于是调用 hugetlb_fault 函数处理缺页异常处理,其首先调用 huge_pte_offset 获得页表,此时页表内容不空,那么继续调用 is_hugetlb_entry_hwpoisoned 函数,检查页表 Entry 是否是一个 POISON 页表,此时检查到属于 POISON 页表,那么直接返回 VM_FAULT_HWPOISON_LARGE,那么最终程序会被 Kill 掉.
在 do_user_addr_fault 函数处理异常处理返回值时,VM_FAULT_HWPOISON 返回值会进入 1454 行分支,并调用 do_sigbus 函数发起信号杀死进程。以上便是 HugeTLB UE Page Fault 缺页处理的流程.
HugeTLB Memory: OOM 大页内存不足缺页场景
HugeTLB 大页机制存在一个现象,就是进程使用 mmap 映射一段虚拟内存到 HugeTLB 大页,此时这段虚拟内存并未建立任何页表,按普通内存来说只有缺页的时候才会真正分配物理内存和建立页表,而 HugeTLB 机制就不一样,它在分配虚拟内存时就将对应数量的 HugeTLB 大页进行预留,一旦预留这些 HugeTLB 就不能分配给其他进程使用,另外只有虚拟内存缺页的时候才会真正建立页表映射到预留的 HugeTLB 大页上。这个机制的存在又会带来一个问题,就是某个进程分配了一个映射 HugeTLB 大页的虚拟内存,但该进程只有很少部分在使用,因此大部分预留 HugeTLB 大页是浪费的,并且此时系统内存压力特别大,出现了旱的旱死涝的涝死. 解决这个问题的办法不是没有,其中一种解法就是分配虚拟内存的时候,不对 HugeTLB 大页内存进行预留,这样可以最大程度利用 HugeTLB 内存,但也存在一定的风险,如果这个方案缺页的时候 HugeTLB 池子里没有可用内存,而且无法动态从系统分配新的大页,那么会出现不可预期的问题。接下来通过一个实践案例深入了解这个问题,实践案例在 BiscuitOS 上的部署逻辑是:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Support Host CPU Feature Passthrough
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on OOM --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-OOM-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-OOM-default Source Code on Gitee
实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配一段 2MiB 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,因此绑定了公共 2MiB HugeTLB 大页池子了的大页, 另外使用了 MAP_NORESERVE 标志,该标志不会预留 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-OOM-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB OOM 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 大页,但该场景下由于没有预留,那么调用 alloc_buddy_huge_page_with_mpol 函数通过超发大页方式分配,但此时超发大页也无法分配,那么只能返回错误,最终返回 VM_FAULT_SIGBUS,程序异常退出.
HugeTLB Memory: Surplus 超发大页缺页场景
HugeTLB 大页机制存在一个想象,就是进程使用 mmap 映射一段虚拟内存到 HugeTLB 大页,此时这段虚拟内存并未建立任何页表,按普通内存来说只有缺页的时候才会真正分配物理内存和建立页表,而 HugeTLB 机制就不一样,它在分配虚拟内存时就将对应数量的 HugeTLB 大页进行预留,一旦预留这些 HugeTLB 就不能分配给其他进程使用,另外只有虚拟内存缺页的时候才会真正建立页表映射到预留的 HugeTLB 大页上。这个机制的存在又会带来一个问题,就是某个进程分配了一个映射 HugeTLB 大页的虚拟内存,但该进程只有很少部分在使用,因此大部分预留 HugeTLB 大页是浪费的,并且此时系统内存压力特别大,出现了旱的旱死涝的涝死. 解决这个问题的办法不是没有,其中一种解法就是分配虚拟内存的时候,不对 HugeTLB 大页内存进行预留,而当缺页是需要分配 HugeTLB 大页时才从系统进行分配,这样的大页称为Surplus HugeTLB(超发大页),这样可以最大程度利用 HugeTLB 内存,但也存在一定的风险,如果这个方案缺页的是无法动态从系统分配新的大页,那么会出现不可预期的问题。接下来通过一个实践案例深入了解这个问题,实践案例在 BiscuitOS 上的部署逻辑是:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on Surplus --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-SURPLUS-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-SURPLUS-default Source Code on Gitee
实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配一段 2MiB 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,因此绑定了公共 2MiB HugeTLB 大页池子了的大页, 另外使用了 MAP_NORESERVE 标志,该标志不会预留 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-SURPLUS-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB Surplus 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 大页,因此会调用 dequeue_huge_page_vma 函数从预留池子中分配大页,但是该场景下没有预留大页,那么 alloc_buddy_huge_page_with_mpol 函数和 alloc_surplus_huge_page 从超发池子中分配大页,其最终调用 alloc_fresh_huge_page 函数从 Buddy 分配器里分配大页. 当获得 HUGETLB 大页时判断映射是匿名映射还是文件映射,以此建立逆向映射。如果是匿名映射,则调用 hugepage_add_new_anon_rmap; 反之如果是文件映射则调用 page_dup_file_rmap 建立逆向映射. 最后就是更新页表操作和将 HUGELTB 大页更新为激活态.
HUGETLB 内存缺页之后,对于来自公共 HugeTLB 大页池子的虚拟内存均采用匿名映射,因此会构建如上的逆向映射,可以看到没有 HugeTLB 大页指向了 anon_vma, 并可以从 anon_vma 的红黑树知道哪些进程的 VMA 映射了该 HugeTLB 大页.