1 - kernel docs
https://docs.kernel.org/filesystems/dax.html
Direct Access for files
动机
页面缓存(page cache)通常用于缓冲对文件的读写操作。它还用于通过调用 mmap 映射到用户空间的页面。
对于类内存的块设备,页缓存页面(page cache pages)将是原始存储的不必要副本。DAX 代码通过直接对存储设备执行读写操作来消除这个额外副本。对于文件映射,存储设备会直接映射到用户空间。
用法
如果你有一个支持 DAX 的块设备,可以像往常一样在其上创建文件系统。目前 DAX 代码仅支持块大小等于内核 PAGE_SIZE 的文件,因此在创建文件系统时可能需要指定块大小。
目前有 5 个文件系统支持 DAX:ext2、ext4、xfs、virtiofs 和 erofs。在这些文件系统上启用 DAX 的方式各不相同。
在 ext2 和 erofs 上启用 DAX
挂载文件系统时,在命令行上使用 -o dax 选项或在 /etc/fstab 的选项中添加 ‘dax’。这可以启用文件系统中所有文件的 DAX 功能。它等同于下面的 -o dax=always 行为。
在 xfs 和 ext4 上启用 DAX
摘要
-
存在一个内核文件访问模式标志 S_DAX,对应于 statx 标志 STATX_ATTR_DAX。有关此访问模式的详细信息,请参阅 statx(2) 的手册页。
-
存在一个持久的标志 FS_XFLAG_DAX,它可以应用于常规文件和目录。这个建议标志可以在任何时候设置或清除,但这样做不会立即影响 S_DAX 状态。
-
如果在目录上设置了持久的 FS_XFLAG_DAX 标志,那么随后在此目录中创建的所有常规文件和子目录都将继承此标志。在父目录上设置或清除此标志时已存在的文件和子目录不会受到此父目录修改的影响。
-
存在一些 dax 挂载选项可以覆盖 FS_XFLAG_DAX 在 S_DAX 标志设置中的值。假设底层存储支持 DAX,则以下情况成立:
- -o dax=inode 表示"遵循 FS_XFLAG_DAX",这是默认设置。
- -o dax=never 表示"永不设置 S_DAX,忽略 FS_XFLAG_DAX。"
- -o dax=always 表示"始终设置 S_DAX 忽略 FS_XFLAG_DAX。"
- -o dax 是一个传统选项,它是 dax=always 的别名。
警告: 选项 -o dax 可能会在未来被移除,因此 -o dax=always 是指定此行为的推荐方法。
注意: 即使文件系统以 dax 选项挂载,FS_XFLAG_DAX 的修改和继承行为也保持不变。但是,直到文件系统以 dax=inode 重新挂载并且 inode 从内核内存中驱逐之前,内存中的 inode 状态(S_DAX)将被覆盖。
-
可以通过以下方式更改 S_DAX 策略:
-
在创建文件前根据需要设置父目录的 FS_XFLAG_DAX 标志
-
设置适当的 dax=“foo” 挂载选项
-
更改现有常规文件和目录上的 FS_XFLAG_DAX 标志。这具有下文 6) 中描述的运行时限制和约束。
-
-
通过切换持久化的 FS_XFLAG_DAX 标志来更改 S_DAX 策略时,对现有常规文件的更改只有在所有进程关闭这些文件后才会生效。
详情
每个文件有 2 个 dax 标志。一个是持久的 inode 设置(FS_XFLAG_DAX),另一个是表示该功能活动状态的易失性标志(S_DAX)。
FS_XFLAG_DAX 在文件系统中被保留。这个持久性配置设置可以使用 FS_IOC_FS[GS]`ETXATTR ioctl(参见 ioctl_xfs_fsgetxattr(2))或诸如 ‘xfs_io’ 之类的工具进行设置、清除和/或查询。
新文件和目录在创建时会自动从其父目录继承 FS_XFLAG_DAX。因此,在创建目录时设置 FS_XFLAG_DAX 可以为整个子树设置默认行为。
为阐明继承关系,以下是 3 个示例:
Example A: 示例 A:
mkdir -p a/b/c
xfs_io -c 'chattr +x' a
mkdir a/b/c/d
mkdir a/e
------[outcome]------
dax: a,e
no dax: b,c,d
Example B: 示例 B:
mkdir a
xfs_io -c 'chattr +x' a
mkdir -p a/b/c/d
------[outcome]------
dax: a,b,c,d
no dax:
Example C: 示例 C:
mkdir -p a/b/c
xfs_io -c 'chattr +x' c
mkdir a/b/c/d
------[outcome]------
dax: c,d
no dax: a,b
当前启用状态(S_DAX)是在内核将文件 inode 实例化到内存时设置的。该状态基于底层媒体支持、FS_XFLAG_DAX 的值以及文件系统的 dax 挂载选项进行设置。
statx 可用于查询 S_DAX。
注意
只有常规文件才会设置 S_DAX 标志,因此 statx 绝不会指示目录上设置了 S_DAX。即使底层介质不支持 dax 和/或文件系统被挂载选项覆盖,设置 FS_XFLAG_DAX 标志(无论是直接设置还是通过继承)仍然会发生。
在 virtiofs 上启用 DAX
virtiofs 上 DAX 的语义基本上与 ext4 和 xfs 上的语义相同,只是当指定了’-o dax=inode’时,virtiofs 客户端通过 FUSE 协议从 virtiofs 服务器获取是否启用 DAX 的提示,而不是持久的 FS_XFLAG_DAX 标志。也就是说,是否启用 DAX 完全由 virtiofs 服务器决定,而 virtiofs 服务器本身可能部署各种算法来做出此决定,例如取决于主机上的持久 FS_XFLAG_DAX 标志。
在客户机内部设置或清除持久性 FS_XFLAG_DAX 标志仍然受支持,但不能保证相应文件会启用或禁用 DAX。客户机内的用户仍需调用 statx(2) 并检查 statx 标志 STATX_ATTR_DAX,以查看是否为此文件启用了 DAX。
块驱动程序编写者的实现技巧
要在块驱动程序中支持 DAX,请实现’direct_access’块设备操作。该操作用于将扇区号(以 512 字节扇区为单位)转换为标识内存物理页面的页框号(pfn)。它还会返回一个可用于访问内存的内核虚拟地址。
direct_access 方法接受一个 ‘size’ 参数,表示请求的字节数。该函数应返回在该偏移量处可以连续访问的字节数。如果发生错误,它也可以返回一个负的 errno。
为了支持此方法,存储必须始终能够被 CPU 字节访问。如果您的设备使用分页技术通过较小的窗口暴露大量内存,那么您就无法实现 direct_access。同样,如果您的设备偶尔会使 CPU 长时间停滞,您也不应尝试实现 direct_access。
这些块设备可作为参考: - pmem:NVDIMM 持久内存驱动
文件系统编写者的实现技巧
文件系统支持包括:
- 通过在 i_flags 中设置 S_DAX 标志来添加将 inode 标记为 DAX 的支持
- 实现 ->read_iter 和 ->write_iter 操作,当 inode 设置了 S_DAX 标志时使用
dax_iomap_rw() - 为 DAX 文件实现一个 mmap 文件操作,该操作在 VMA 上设置 VM_MIXEDMAP 和 VM_HUGEPAGE 标志,并将 vm_ops 设置为包含 fault、pmd_fault、page_mkwrite 和 pfn_mkwrite 的处理程序。这些处理程序可能应该调用
dax_iomap_fault(),并传递适当的故障大小和 iomap 操作。 - 调用
iomap_zero_range()为 DAX 文件传递适当的 iomap 操作,而不是block_truncate_page() - 确保在读取、写入、截断和缺页之间有足够的锁机制
用于分配块的 iomap 处理程序必须确保分配的块在返回前已清零并转换为已写入范围,以避免通过 mmap 暴露未初始化的数据。
处理介质错误
libnvdimm 子系统为每个 pmem 块设备(在 gendisk->badblocks 中)存储已知介质错误位置的记录。如果我们在此类位置或存在尚未发现的潜在错误的位置发生故障,应用程序可以预期会收到 SIGBUS。libnvdimm 还允许通过简单写入受影响的扇区来清除这些错误(通过 pmem 驱动,并且如果底层 NVDIMM 支持 ACPI 定义的 clear_poison DSM)。
由于 DAX IO 通常不会经过 driver/bio 路径,应用程序或系统管理员可以通过以下方式从先前的 backup/inbuilt 冗余中恢复丢失的数据:
- 删除受影响的文件,并从备份中恢复(系统管理员方式):这将释放文件占用的文件系统块,下次分配这些块时,它们将被先清零,这个过程通过驱动程序完成,可以清除坏扇区。
- 截断或打孔处理文件中含有坏块的部分(至少需要打孔处理整个对齐的扇区,但不一定是整个文件系统块)。
这是两条基本路径,它们允许 DAX 文件系统在媒体错误存在的情况下继续运行。未来可以在此基础上构建更强大的错误恢复机制,例如,通过 DM 在块层提供的冗余/镜像,或者额外地在文件系统级别提供。这些机制必须依赖于上述两个基本原则:错误清除可以通过驱动程序发送 IO 来完成,或者通过清零(同样通过驱动程序)来完成。
不足之处
即使内核或其模块存储在支持 DAX 的文件系统上,该文件系统位于支持 DAX 的块设备上,它们仍然会被复制到 RAM 中。
DAX 代码在具有虚拟映射缓存的架构(如 ARM、MIPS 和 SPARC)上无法正常工作。
对从 DAX 文件 mmapped 的用户内存范围调用 get_user_pages() ,当没有 struct page 来描述这些页面时会失败。这个问题已经在一些设备驱动程序中得到解决,通过添加可选的 struct page 支持来支持驱动程序控制的页面(参见 drivers/nvdimm 中的 CONFIG_NVDIMM_PFN 示例,了解如何实现这一点)。在非 struct page 情况下,从非 DAX 文件对这些内存范围进行 O_DIRECT 读取/写入将会失败
注意
对 DAX 文件的 O_DIRECT 读取/写入是有效的,关键在于所访问的内存)。在非struct page 情况下,其他无法实现的功能包括 RDMA、 sendfile() 和 splice() 。
2 - aistudio.google
https://aistudio.google.com/prompts/1xw2bDNLkdRaZq5u8OqOhAuIZm5Nwox1j
输入:
我在设计 bms 机器上启动 cloud hypervisor 作为 microvm 运行 ai agent 的方案。现在我想实现高密部署,其中有一个优化策略是降低每个 microvm 实例的内存使用。我想深入了解一下 Virtio-fs + DAX (Direct Access) 在这方面的作用,另外详细介绍一下这个技术的原理。
这是一个非常专业且切中要害的问题。在基于 BMS(裸金属服务器)+ Cloud Hypervisor + MicroVM 的架构下,Virtio-fs + DAX 是实现高密度部署(High Density)、尤其是 AI Agent 这种依赖大文件(模型、Python 库)场景的核心优化技术。
简单来说,它的核心价值在于消除了 Host 和 Guest 之间的“双重缓存”(Double Caching),实现了内存的真正共享。
以下是关于 Virtio-fs + DAX 的深入原理解析及其在 AI Agent 场景下的作用。
1. 核心痛点:传统的“双重缓存”问题
在没有 DAX 的传统虚拟化文件系统(如 virtio-9p 或未开启 DAX 的 virtio-fs)中,当 MicroVM 需要读取一个文件(例如 500MB 的 PyTorch 库或 2GB 的 LLM 模型)时,数据流向如下:
- Host Page Cache: 宿主机内核从磁盘读取文件,缓存在宿主机的 Page Cache 中。
- Copy Operation: 数据通过 virtio 通道从宿主机内存拷贝到 Guest 的内存中。
- Guest Page Cache: Guest 内核将这份数据再次缓存在自己的 Page Cache 中。
后果:
- 内存浪费: 同一份数据在物理内存中存在了两份(一份在 Host,一份在 Guest)。
- 无法复用: 如果你启动了 100 个 MicroVM,每个 VM 都要加载 PyTorch,那么内存中就会有 100 + 1 份 PyTorch 的拷贝。这是高密部署的噩梦。
2. Virtio-fs + DAX 的解决方案:共享 Page Cache
DAX (Direct Access) 技术允许 Guest VM 的文件系统绕过自己的 Page Cache,直接映射宿主机的文件缓存。
原理机制(Technical Deep Dive)
Virtio-fs + DAX 的工作流程利用了 PCI BAR (Base Address Register) 和 内存映射(mmap)技术。
-
PCI BAR 窗口(DAX Window):
- Cloud Hypervisor 在启动时,会为 Virtio-fs 设备分配一块巨大的内存区域(例如 4GB 或更大),这块区域在 Guest 看来是 PCI 设备的一块内存空间(PCI BAR)。
- 在 Host 侧,这块区域实际上并没有立即分配物理 RAM,而是作为一个虚拟地址窗口。
-
文件映射(The Mapping):
- 当 Guest 应用(如 AI Agent 的 Python 进程)发起
read请求读取pytorch.so时。 - Guest 内核检测到该文件系统支持 DAX,它不会发起传统的 I/O 请求将数据复制进来。
- 相反,Guest 内核发送一个 FUSE 请求给 Host 端的
virtiofsd(后端守护进程),请求建立映射(FUSE_SETUPMAPPING)。
- 当 Guest 应用(如 AI Agent 的 Python 进程)发起
-
建立页表关联(The Magic):
- Host 端的
virtiofsd收到请求后,会把宿主机上该文件对应的 Page Cache(Host 物理内存)mmap到 Cloud Hypervisor 的 DAX Window 中。 - KVM 更新 EPT(Extended Page Tables,扩展页表)。
- 结果: Guest 的物理地址空间(GPA)直接指向了 Host 的物理地址空间(HPA)中存放文件内容的页面。
- Host 端的
-
直接访问:
- Guest 应用程序读取内存时,实际上是直接读取了 Host 的 Page Cache。
3. 图解对比
传统模式 (Without DAX):
Host Disk -> Host RAM (Page Cache) -> (Copy) -> Guest RAM (Page Cache) -> Guest App
100 个 VM = 100 份 Guest Cache + 1 份 Host Cache
Virtio-fs + DAX 模式:
Host Disk -> Host RAM (Page Cache) <-- (Map/Ptr) -- Guest App (直接读取)
100 个 VM = 0 份 Guest Cache + 1 份 Host Cache
4. 在 AI Agent 高密部署中的具体作用
对于 AI Agent 场景,Virtio-fs + DAX 的收益是巨大的,主要体现在以下三个方面:
A. 共享 Python 环境与依赖库 (Huge Win)
AI Agent 通常运行在 Python 环境中,依赖 torch, numpy, tensorflow 等巨型库。
- 场景:
torch库解压后可能占用 500MB~1GB 空间。 - 无 DAX: 启动 50 个 Agent,仅加载 torch 就需要消耗 50GB 内存。
- 有 DAX: Host 加载一次 torch 占用 1GB 内存。50 个 Agent 全部通过 DAX 映射读取这同一份物理内存。内存开销接近于 0 的增长。
B. 共享 AI 模型权重 (Massive Win)
如果你的 Agent 是同质化的(例如都调用同一个本地 Embedding 模型或小型 LLM)。
- 场景: 模型文件大小 2GB。
- 有 DAX: 所有 Agent 共享这 2GB 的物理内存。你可以轻松在 64GB 内存的服务器上跑几十个加载了 2GB 模型的 MicroVM,只要 CPU 撑得住。
C. 极速启动 (Fast Boot)
- 由于不需要将数据从 Host 拷贝到 Guest 内存,也不需要 Guest 内核进行磁盘解压或加载操作,应用启动速度(Cold Start)会显著加快。MicroVM 启动时,并没有真正加载文件,只是建立了内存映射指针,通过“缺页异常”(Page Fault)按需加载,速度极快。
5. Cloud Hypervisor 配置指南
在 Cloud Hypervisor 中启用 Virtio-fs DAX 需要配合 virtiofsd 使用。
1. 启动 virtiofsd (Host 端): 你需要先运行 virtio-fs 的后端守护进程。
# --cache=auto 或 always 配合 DAX 效果最好
# --socket-path 指定通信 socket
# --shared-dir 指定宿主机共享目录(存放 rootfs, models, python envs)
./virtiofsd --socket-path=/tmp/vhost-user.sock --shared-dir=/path/to/shared/agents --cache=auto
2. 配置 Cloud Hypervisor (JSON 或 CLI): 在启动 Cloud Hypervisor 时,需要指定 DAX 缓存窗口的大小。这个大小限制了 Guest 可以同时映射多少文件内容,建议设置得大一些(覆盖你的模型+库的大小)。
{
"fs": [
{
"tag": "myfs",
"socket": "/tmp/vhost-user.sock",
"num_request_queues": 1,
"queue_size": 1024,
"cache_size": "4G" <-- 这里开启 DAX,设置 DAX Window 大小为 4GB
}
],
"kernel": "...",
"cpus": "...",
"memory": "..."
}
或者 CLI 参数:
./cloud-hypervisor \
... \
--fs tag=myfs,socket=/tmp/vhost-user.sock,num_request_queues=1,queue_size=1024,cache_size=4G
3. Guest 内部挂载:
Guest 启动后,挂载文件系统需要指定 dax 选项:
mount -t virtiofs -o dax myfs /mnt/shared
6. 总结与建议
Virtio-fs + DAX 是 AI Agent MicroVM 高密部署的基石。
- 原理: 利用 Host Page Cache 作为唯一的真实数据源,Guest 通过 PCI BAR 直接映射访问,消除数据拷贝。
- 收益: 将 O(N) 的内存复杂度降低为 O(1)(针对只读数据)。
- 策略建议:
- 构建统一的 Base Image/Rootfs: 将 Python 环境、CUDA 库、常用模型文件放在 Host 的共享目录中。
- 只读挂载: 尽量以只读方式挂载代码和模型目录,这样能最大化利用共享页缓存。
- 计算 DAX Window:
cache_size不要太小,至少要大于“常用热点文件总和”(如 OS 基础库 + Python 解释器 + PyTorch + 模型权重)。
这一方案能让你在有限的物理内存上,将 MicroVM 的密度提高 3-10 倍(取决于 AI 负载中静态资源的占比)。
3 - arm64 支持
https://aistudio.google.com/prompts/1xw2bDNLkdRaZq5u8OqOhAuIZm5Nwox1j
输入:
有一个关键问题,Virtio-fs + DAX 在 arm64 平台上可以使用吗?
这是一个非常关键且实际的问题。因为 ARM64(AArch64)架构在云原生和 AI 推理场景(如 AWS Graviton, Huawei Kunpeng, Ampere Altra)中越来越普及,但很多虚拟化特性最初都是围绕 x86 开发的。
结论是:可以使用。Virtio-fs + DAX 目前已经支持 ARM64 平台,但有严格的版本要求和配置限制(尤其是页大小对齐)。
以下是针对 ARM64 平台上使用 Virtio-fs + DAX 的详细现状、限制和注意事项:
1. 支持现状与版本要求
Virtio-fs 的 DAX 支持引入 ARM64 架构比 x86 晚一些,主要涉及到 ARM 架构下的内存管理(MMU)、缓存一致性(Cache Coherency)和 PCI BAR 映射机制的差异。
要使其正常工作,你需要满足以下软件栈要求:
- Host Kernel(宿主机内核): 建议 Linux 5.13+,最好是 5.15 LTS 或更高。
- 早期的内核在 ARM64 上处理 virtio-fs 的 DAX 映射时存在 bug,或者不支持通过 PCI BAR 进行大内存窗口映射。
- Guest Kernel(MicroVM 内核): 同样建议 Linux 5.13+。
- Guest 内核必须编译开启
CONFIG_DAX,CONFIG_FS_DAX,CONFIG_VIRTIO_FS。
- Guest 内核必须编译开启
- Cloud Hypervisor: 建议使用 v26.0 或更高版本(越新越好)。
- Cloud Hypervisor 社区对 ARM64 的支持非常积极,它是目前在 ARM64 上运行 MicroVM 的首选 VMM 之一。
2. 核心大坑:页大小(Page Size)对齐问题
这是在 ARM64 上使用 DAX 最容易踩的坑,也是与 x86 最大的不同点。
- x86: 几乎所有通用 Linux 发行版默认 Page Size 都是 4KB。Host 和 Guest 天然一致。
- ARM64: Linux 内核支持 4KB 和 64KB 页大小。
- 很多 ARM 服务器发行版(如某些版本的 CentOS/RHEL for ARM)默认可能开启 64KB 页以提升性能。
- Android 或某些嵌入式环境通常是 4KB。
DAX 的硬性限制:Host 和 Guest 的 Page Size 必须一致。
因为 DAX 机制是通过 mmap 将 Host 的物理页直接映射到 Guest 的物理地址空间。如果页大小不一致(例如 Host 4KB,Guest 64KB),Guest 内核在建立页表映射时会因为对齐(Alignment)问题而失败,或者回退到无 DAX 模式。
避坑指南:
- 检查 Host: 执行
getconf PAGESIZE,确认是否为 4096 (4KB)。 - 检查 Guest Kernel Config: 确保 MicroVM 的内核编译选项
CONFIG_ARM64_4K_PAGES=y。 - 强制一致: 强烈建议 Host 和 Guest 都统一使用 4KB 页大小。虽然 64KB/64KB 理论上可行,但在虚拟化大页分配和内存碎片上可能会遇到边缘 Case。
3. PCI BAR 窗口大小限制
在 ARM64 架构上,PCI 设备(这里指 Virtio-fs 设备)映射的 MMIO 空间(DAX Window)大小可能会受到物理地址布局的限制。
- x86: 我们可以比较随意地给
cache_size设置 8G、16G。 - ARM64: 取决于 Cloud Hypervisor 定义的 Guest 物理内存布局(Memory Layout)。在 ARM64 虚拟化中,用于 PCI MMIO 的地址空间是有限的(通常在 highmem 区域)。
- 如果你设置了非常大的 DAX Window(例如 32GB),可能会导致 Cloud Hypervisor 启动失败,提示无法分配足够的 PCI 资源或地址空间冲突。
- 建议: 开始时不要设得过大,例如先从 2GB 或 4GB 开始测试,够用即可。
4. 缓存一致性 (Cache Coherency)
ARM64 是弱内存序(Weakly Ordered)架构,且不同 SoC 对 DMA 一致性的处理不同。
- 在 Virtio-fs DAX 模式下,CPU 直接访问 Host 内存。
- Cloud Hypervisor 和 KVM 在 ARM64 上已经处理了大部分一致性问题(通过将映射区域标记为合适的内存属性)。
- 注意: 确保你的 ARM 服务器(BMS)是标准的服务器级芯片(如 Kunpeng 920, Yitian 710, Graviton),它们通常有硬件支持的 IO Coherency。如果在一些嵌入式 ARM 板卡(如 Jetson, Raspberry Pi)上尝试,可能会遇到数据损坏问题,但在 BMS 场景下通常不需要担心。
5. 验证命令
在 ARM64 机器上部署后,务必在 Guest 内部验证 DAX 是否真正生效:
-
启动 Guest 后检查挂载信息:
# 在 Guest 内部 mount | grep virtiofs # 输出应该包含 "dax" 关键字 # myfs on /mnt type virtiofs (rw,relatime,dax) -
查看内核日志(dmesg): 如果 DAX 失败,内核日志通常会非常诚实地报错:
dmesg | grep -i "virtio-fs" # 如果看到类似 "DAX unsupported" 或 "alignment error" 的警告,说明降级到了传统模式。
总结方案
在 ARM64 BMS 上设计高密 AI Agent:
- 可行性: 完全可行,且效果显著。
- Host OS: 使用 Ubuntu 22.04 LTS (Kernel 5.15) 或更高,确认为 4KB Page Size。
- Guest Kernel: 编译专门的 MicroVM Kernel(推荐 5.15 或 6.1),务必开启 4KB Page Size。
- 配置: Cloud Hypervisor 启动参数开启
cache_size,初始设置为 4GB。 - 收益: 与 x86 一样,可以实现 Python 库和 AI 模型内存的零拷贝共享。
4 - DAX机制的功能和实现
https://www.cnblogs.com/dream397/p/13900936.html
https://blog.csdn.net/maokelong95/article/details/107195192
DAX 简述
直接访问(Direct Access,DAX) 机制是一种支持用户态软件直接访问存储于持久内存(Persistent Memory,PM) 的文件的机制,用户态软件无需先将文件数据拷贝到页高速缓存(Page Cache)。
上述描述对应到下面这张图(Typical NVDIMM Software Architecture)中,就是说(File)和(Memory)这两条 IO 路径都能绕过页高速缓存。
- 其中 File 路径(下称普通文件路径)表示,用户态软件通过标准文件接口(Standard File API)访问持久内存文件系统。
- 其中 Memory 路径(下称映射文件路径)表示,用户态软件通过映射文件(Memory-mapped File)直接访问 PM。

DAX 的原理
以下将结合 Linux v5.8-rc1 中的 XFS 为例进行介绍。
普通文件路径如何旁路页缓存
以文件写路径为例,其由 xfs_file_write_iter 提供,该函数部分代码如下所示:
STATIC ssize_t
xfs_file_write_iter(
struct kiocb *iocb,
struct iov_iter *from)
{
struct file *file = iocb->ki_filp;
struct address_space *mapping = file->f_mapping;
struct inode *inode = mapping->host;
ssize_t ret;
if (IS_DAX(inode))
return xfs_file_dax_write(iocb, from);
if (iocb->ki_flags & IOCB_DIRECT) {
ret = xfs_file_dio_aio_write(iocb, from);
if (ret != -EREMCHG)
return ret;
}
return xfs_file_buffered_aio_write(iocb, from);
}
这段代码在执行写操作的时候,将分别处理三种情况:
- DAX
- 在将文件系统挂载到 PM 设备时,若设置 DAX 标识(
mount -o dax),则持久内存文件系统将为所有写操作采用该路径; - 该路径主要调用 dax_iomap_rw。该函数在通过 dax_direct_access 获取目标物理内存的地址后,通过 dax_copy_from_iter 调用 NVDIMM 驱动直接把数据拷贝到目标物理内存中,并冲洗(Flush)相应缓存行(Cache Line)。
- 在将文件系统挂载到 PM 设备时,若设置 DAX 标识(
- DIO(Direct IO):
- 在打开文件时,若设置直接 IO 标识(
O_DIRECT) ,则文件操作将采用该路径; - 该路径主要调用 iomap_dio_rw。该函数仍通过传统存储栈(Storage Stack)访问设备,即通过构造 bio,将请求传递到块设备层(Block Device Layer),再由块设备层调用驱动从而访问设备。
- 在打开文件时,若设置直接 IO 标识(
- 正常 IO:
- 常规的、使用页缓存的 IO 方式。
- 该路径主要调用 iomap_file_buffered_write。该函数首先通过 pagecache_get_page 获取页缓存(有关页缓存机制的最新设计,可阅读345),接着通过 iomap_read_page_sync 封装 bio,以请求块设备层调用驱动,将设备上的数据读取到页缓存中。在准备好页缓存之后,调用 iov_iter_copy_from_user_atomic 将用户态软件请求写入的数据拷贝到页缓存中。一切完成之后,通过 iomap_set_page_dirty 将页缓存设置为脏页。如此迭代,直至用户所有数据都写入页缓存,最后通过 balance_dirty_pages_ratelimited 酌情使用后台进程将脏页回写到块设备中。
映射文件路径如何旁路页缓存
调用 mmap 时,文件系统仅仅在进程的 mm_struct 中注册了一段使用虚拟内存区域(Virtual Memory Area,VMA)描述的虚拟地址。后续当用户态软件首次访问映射文件时,内存管理单元(Memory Managment Unit)发现页表项(Page Table Entry,PTE)为空,于是触发 14 号故障,即页故障(Page Fault),使得操作系统开始执行请求调页(Demand Paging)。此时,由虚拟内存管理器(Virtual Memory Manager)和文件系统共同管理页表,以建立虚拟内存到物理内存之间的映射关系。注意,以上为同步过程,而非异步过程,因为页故障是一个异常(Exception)而非软/硬件中断(Software/Hardware Interrupt)。
调用 mmap 时发生了什么
在执行 mmap 系统调用时,主要执行 do_mmap 中的 mmap_region,其根据用户态软件的请求,返回一个用于描述一段可用进程虚拟地址空间的 VMA,接着通过 call_mmap 执行文件系统注册的 mmap 实现,最后将该段 VMA 之添加在进程的 mm_struct 中。
XFS 中 mmap 由 xfs_file_mmap 实现,其中主要语句就一个:vma->vm_ops = &xfs_file_vm_ops;。它告诉异常处理例程(Exception Handler)应该调用 xfs_file_vm_ops 中相应的函数处理页错误。
请求调页时发生了什么
请求调页主要执行 __xfs_filemap_fault,其代码如下所示:
static vm_fault_t
__xfs_filemap_fault(
struct vm_fault *vmf,
enum page_entry_size pe_size,
bool write_fault)
{
struct inode *inode = file_inode(vmf->vma->vm_file);
struct xfs_inode *ip = XFS_I(inode);
vm_fault_t ret;
trace_xfs_filemap_fault(ip, pe_size, write_fault);
if (write_fault) {
sb_start_pagefault(inode->i_sb);
file_update_time(vmf->vma->vm_file);
}
xfs_ilock(XFS_I(inode), XFS_MMAPLOCK_SHARED);
if (IS_DAX(inode)) {
pfn_t pfn;
ret = dax_iomap_fault(vmf, pe_size, &pfn, NULL,
(write_fault && !vmf->cow_page) ?
&xfs_direct_write_iomap_ops :
&xfs_read_iomap_ops);
if (ret & VM_FAULT_NEEDDSYNC)
ret = dax_finish_sync_fault(vmf, pe_size, pfn);
} else {
if (write_fault)
ret = iomap_page_mkwrite(vmf,
&xfs_buffered_write_iomap_ops);
else
ret = filemap_fault(vmf);
}
xfs_iunlock(XFS_I(inode), XFS_MMAPLOCK_SHARED);
if (write_fault)
sb_end_pagefault(inode->i_sb);
return ret;
}
显然,这段代码有两条分支:
-
DAX: 该路径主要调用 dax_iomap_fault。该函数首先通过 grab_mapping_entry 获取页缓存中的 DAX Exception Entry(详见3),接着通过 xfs_bmbt_to_iomap 准备一个名为 struct iomap 的数据结构。
struct iomap { u64 addr; /* disk offset of mapping, bytes */ loff_t offset; /* file offset of mapping, bytes */ u64 length; /* length of mapping, bytes */ u16 type; /* type of mapping */ u16 flags; /* flags for mapping */ struct block_device *bdev; /* block device for I/O */ struct dax_device *dax_dev; /* dax_dev for dax operations */ void *inline_data; void *private; /* filesystem private */ const struct iomap_page_ops *page_ops; };准备好 struct iomap 之后,通过 dax_iomap_pfn ,结合 struct iomap 所提供的信息,获取目标 PM 页的物理页号(Physical Page Number,pfn)。之后由 dax_insert_entry 将与该页相关联的 DAX Exception Entry 添加到用于维护页缓存的数据结构 XArray 中。最后调用 vmf_insert_mixed_mkwrite 将从 DAX Exception Entry 中缓存的 pfn 填写到对应虚拟页的 PTE 中。
-
正常请求调页
该路径主要调用 filemap_fault。该函数首先通过 do_sync_mmap_readahead 试图同步地预读文件数据(预读行为可受 madvise 系统调用的影响,因此也可能完全不读取),接着通过 pagecache_get_page 分配页缓存,再通过 xfs_vm_readpage 将读取块设备数据的请求发送到块设备层,从而将文件数据读取到页缓存中。在将文件数据拷贝到页缓存之后,取决于映射文件的类型(MAP_SHARED、MAP_PRIVATE)执行不同的分支。最后返回的页保存在 vmf->page 中。
-
当是 MAP_SHARED,主要调用 do_shared_fault 函数,该函数:
-
调用 xfs_file_vm_ops 中注册的 xfs_filemap_page_mkwrite,从而调用 iomap_page_create 在 vmf->page 对应的 struct page 的 private 字段中塞进去一个 struct iomap_page。
/* * Structure allocated for each page when block size < PAGE_SIZE to track * sub-page uptodate status and I/O completions. */ struct iomap_page { atomic_t read_count; atomic_t write_count; spinlock_t uptodate_lock; DECLARE_BITMAP(uptodate, PAGE_SIZE / 512); };
finish_fault
alloc_set_pte
信息以支持空闲页回收。
-
-
当是 MAP_PRIVATE,调用 do_cow_fault。该函数首先通过 alloc_page_vma 为 VMA 分配一个页,该页保存在 vmf->cow_page 中。接着通过 copy_user_highpage 将 vmf->page 中的数据拷贝到 vmf->cow_page 中。最后通过 finish_fault 将 vmf->cow_page 映射到虚拟页上。
-
附录 1:术语表
-
持久内存(Persistent Memory,PM):指能通过访存指令(区别于系统调用)访问、可按字节寻址的(区别于块)非易失存储器(Non-volatile Memory,NVM)。其中可按字节寻址(Byte-addressable)表示每个寻址单位对应一个 PM 单元(而非字或块)。
-
持久内存系统(PM-aware File System):指支持 DAX 机制的文件系统。该词的常见表述也包括「Persistent Memory File System」、「DAX Enabled File System」。
-
映射文件(Memory-mapped File):是一段虚内存逐字节对应于一个文件或类文件的资源,使得应用程序处理映射部分如同访问主内存。进程地址空间由映射文件和匿名内存(Anonymous Memory)组成。
附录 2:DAX 历史沿革
- 2015,Carsten Otte 在 Linux v2.6 中引入 XIP(Execute-in-place)机制。 XIP 原本用于嵌入式系统,它摒弃了存储栈中的通用块层及驱动层,并旁路了页高速缓存,使得进程可以直接访问只读存储器或基于 Flash 的内存。
- 2014 年,Subramanya R Dulloor 等人在 PMFS 中基于 XIP 机制管理 PM。
- 同年,Matthew Wilcox 改进了 XIP 并提出了名为 DAX 的子系统。
他在尝试将 XIP 集成到 Ext4 文件系统时,发现 XIP 无法很好地应对竞争条件(Race Conditions)。当多个线程需要同时访问共享资源,且结果依赖于它们执行的相对速度时,便出现了竞争条件。他所做出的最主要的变动,就是使用文件系统的
get_block路径替代struct address_space_operations中的get_xip_mem操作。