在 Linux 中,PMEM 指代持久内存(Persistent Memory),PMEM 既是一种存储技术,又结合了传统 RAM 的高速度和数据存储设备(如 SSD 和硬盘)的数据持久性, 简单来说就是 PMEM 既可以用作磁盘也可以用作低速 RAM。PMEM 可以在断电后数据不丢失的同时,提供类似于传统内存的性能.

  • 持久内存(PMEM): PMEM 设备提供了一个非易失性内存(NVM)的硬件平台,可以使用传统的内存访问指令进行读写,同时在系统断电时保持数据不丢失, 另由于 PMEM 在内存和存储之间提供了一个中间层,因此它改变了数据中心架构的设计和实现.
  • NVDIMM: 在硬件层面,PMEM 通常通过 NVDIMM(非易失性双内联内存模块)实现。最常见的类型是 NVDIMM-N,它在传统的 DRAM DIMM 上加入了非易失性存储,通常是 NAND 闪存,以及一个独立的电源,通常是一个超级电容器,以确保在主电源断电时可以将 DRAM 的数据刷新到闪存

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

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

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

Linux 提供了特定的文件系统,如 DAX(Direct Access),该文件系统可以绕过页面缓存,允许文件系统直接在 PMEM 上运行,这减少了延迟并提高了性能。另外,文件系统如 EXT4 和 XFS 也已经被修改以支持 DAX 操作,使它们可以直接在 PMEM 上存储数据。Linux 上启用 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
PMEM 与缺页

使用了 DAX(Direct Access) 的文件系统可以直接访问 PMEM,这意味着文件系统的数据可以直接映射到应用程序的地址空间中,而无需复制到页缓存中。因此,对于存储在 PMEM 中的数据,可能不会触发传统意义上基于磁盘的缺页异常, 而是触发 DAX 缺页. EXT4 文件系统和 XFS 文件系统都为 DAX 缺页提供了特除的处理函数,但对于应用程序,其和普通的文件映射和触发文件映射缺页的方式没有改变。那么针对不同的文件系统支持的 DAX 可能存在差异,接下来对不同的文件系统的 DAX 缺页进行详细分析:

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


XFS 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 请求读取文件内存.

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


XFS HUGE-DAX 缺页场景

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

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

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


EXT4 HUGE-DAX 缺页场景

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

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

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