Linux内核DMA新范式:IOVA原生映射替代page-centric设计
发布时间:2026/9/18 21:43:49 作者:尧图编辑部 阅读量:1,286

简介本资源是一份面向Linux内核开发者的技术深度文档聚焦DMA映射API在VFIO、RDMA与IOMMU复杂场景下的演进路径解决当前基于struct *page的API在p2p内存处理中转换冗余、scatterlist滥用及IOMMU路径低效等核心问题。文档系统阐述了新提出的两步DMA映射API设计包括IOVA空间直管、非scatterlist DMA-BUF操作、可撤销语义引入以及VFIO导出DMA-BUF、RDMA弃用旧scatterlist流程等关键落地路径。资源为单文件PDF604KB内容精炼但技术密度高涵盖提案背景、架构对比图、三阶段VFIO注册/注销流程、Roadmap八步演进计划及上游补丁链接便于开发者结合内核主线进展开展验证与适配。目前已有165人学习下载适合具备内核内存管理基础、正参与IOMMUFD、GPU驱动或高性能网络设备开发的工程师深入研读与工程落地。1. 当 struct *page 成为 DMA 路径的瓶颈为什么 VFIO/RDMA 工程师必须直面 IOVA 管理层重构在 NVIDIA A100 GPU 直通 VFIO 场景中一个 RDMA QP 向设备内存发起 64KB 零拷贝写入时内核竟触发了 7 次dma_map_sg()→sg_page()→page_to_pfn()→pfn_to_page()的往返转换而在 AMD Alveo U50 FPGA 的 P2P DMA 场景下dma_buf_map_attachment()返回的sg_table中 83% 的sg_dma_address()实际指向设备私有地址空间却仍被强制塞进struct page*框架——这暴露了当前 Linux DMA API 最根本的结构性矛盾它把物理内存管理模型page-based强加给 I/O 地址空间IOVA-based操作。本文讨论的并非某个驱动补丁而是 Kernel 6.10 正在落地的 DMA 映射范式迁移用直接 IOVA 分配替代 page-centric 映射、用 native DMA-BUF ops 替代 scatterlist 中转、用可撤销语义替代静态 pinning。它直接影响 VFIO 设备直通性能、RDMA 零拷贝吞吐上限、GPU 共享内存一致性以及所有依赖dma-buf的用户态加速框架如 Vulkan DMA-BUF 导入、CUDA Unified Memory。适合已能阅读include/linux/dma-mapping.h并调试iommu_group_show_one()的内核开发者尤其需要处理 PCIe P2P、CXL 内存池或异构计算卸载的工程师。2. 从 page-centric 到 IOVA-native新两步 DMA 映射 API 的设计逻辑与核心数据结构当前 DMA API 的核心约束在于struct dma_map_ops的所有函数签名均以struct device*和struct page*或struct scatterlist*为输入。这种设计在传统 RAM 场景下合理但在 P2P 场景中产生三重开销第一设备私有内存如 GPU VRAM无对应struct page需伪造pgmap并维护page-zone等无关字段第二dma_map_sg()必须将sg_table中每个 segment 的dma_address重新解析为page*才能调用dma_sync_sg_for_device()第三IOMMU 驱动在map()时需遍历sg_table构建页表而实际硬件仅需一组连续 IOVA→PA 映射。新 APIPATCH v7 00/17通过解耦“地址分配”与“映射建立”两个阶段从根本上规避 page 依赖。2.1 两步映射的核心接口dma_iommu_alloc_iova() 与 dma_iommu_map_pages()新 API 引入struct dma_iommu_domain作为 IOVA 管理主体其关键函数如下// 第一步申请 IOVA 区域不涉及物理页 iova dma_iommu_alloc_iova(dev, size, align, prot); if (IS_ERR(iova)) return PTR_ERR(iova); // 第二步将物理地址可为 pfn、device VA、CXL DPA映射到 IOVA ret dma_iommu_map_pages(dev, iova, pfn_list, npages, prot, attrs); if (ret) { dma_iommu_free_iova(dev, iova, size); return ret; }提示pfn_list参数支持三种类型unsigned long*标准 RAM、phys_addr_t*设备私有物理地址、u64*CXL DPA 地址彻底摆脱struct page*依赖。prot参数为IOMMU_READ|IOMMU_WRITE|IOMMU_NOEXECattrs可指定DMA_ATTR_SKIP_CPU_SYNC或DMA_ATTR_FORCE_CONTIGUOUS。该设计使 VFIO 在注册 MMIO region 时不再需要构造 fakesg_table。对比旧流程三次循环VFIO BeforeFirst Loopvfio_pci_ioctl()解析 BAR →vfio_pci_mmap()调用dma_buf_export()→ 创建sg_table填充page*即使 BAR 指向 GPU VRAMVFIO BeforeSecond Loopdma_buf_map_attachment()调用dma_map_sg()→ 将sg_table转为dma_addr_t数组 → IOMMU 遍历构建页表VFIO BeforeThird Loopvfio_pci_dma_map()调用dma_sync_sg_for_device()→ 再次解析sg_table获取page*执行 cache flush新流程Register Path仅需vfio_pci_register_dev_region()获取 BAR 物理地址范围dma_iommu_alloc_iova()申请 IOVA 空间dma_iommu_map_pages()直接映射 BAR 物理地址到 IOVA跳过所有 page 操作2.1.1 IOVA 分配器的实现细节为何dma_iommu_alloc_iova()不再调用alloc_pages()dma_iommu_alloc_iova()底层使用iova_domain的红黑树管理空闲区间其关键结构体为struct iova_domain { struct rb_root_cached iova_root; // 红黑树索引 IOVA 区间 struct iova_rcache *rcaches[IOVA_RANGE_CACHE_MAX_SIZE]; // 每 CPU 缓存最近释放的 IOVA unsigned long max_32bit_mask; // 32-bit 设备限制 unsigned long iovad_limit; // IOVA 上限如 48-bit };分配过程完全在虚拟地址空间进行不触碰buddy system。例如在 Intel VT-d 环境下iova_domain初始化时会设置iovad_limit 1ULL 48max_32bit_mask 0xffffffff。当调用dma_iommu_alloc_iova(dev, 0x10000, 0x1000, IOMMU_READ)时内核执行查找iova_root中首个 ≥align的空闲区间若存在分割区间并更新红黑树节点若不存在触发iova_flush_all()清理缓存并重试此过程耗时稳定在 O(log N)远低于旧版alloc_pages(GFP_DMA32)的不可预测延迟。2.2 scatterlist 的废弃路径dma_buf_ops如何绕过 SG 表新 DMA-BUF 操作集struct dma_buf_ops新增map_dma_buf_non_sg()回调其函数原型为struct dma_buf_attachment *dma_buf_attach(struct dma_buf *dmabuf, struct device *dev); int dma_buf_map_dma_buf_non_sg(struct dma_buf_attachment *attach, struct dma_buf_map *map, enum dma_data_direction dir, unsigned long attrs); void dma_buf_unmap_dma_buf_non_sg(struct dma_buf_attachment *attach, struct dma_buf_map *map, enum dma_data_direction dir);struct dma_buf_map定义如下struct dma_buf_map { dma_addr_t dma_addr; // IOVA 地址非 sg_dma_address phys_addr_t phys_addr; // 物理地址用于 P2P void *vaddr; // CPU 映射虚拟地址可选 size_t size; // 映射长度 bool is_iommu_mapped; // 是否经 IOMMU 转换 };注意dma_buf_map_dma_buf_non_sg()返回的dma_addr直接来自dma_iommu_alloc_iova()无需经过sg_dma_address()提取。对于 GPU 驱动phys_addr字段可填入gpu_vram_paddris_iommu_mapped设为false表示 bypass IOMMU。VFIO 实现该接口时dma_buf_map_dma_buf_non_sg()直接调用// attach-dmabuf-priv 即 vfio_pci_device 结构体 struct vfio_pci_device *vdev attach-dmabuf-priv; map-dma_addr dma_iommu_alloc_iova(vdev-pdev, size, PAGE_SIZE, IOMMU_READ|IOMMU_WRITE); map-phys_addr vdev-bars[bar_index].start offset; // 直接取 BAR 物理地址 map-size size; map-is_iommu_mapped true; // 根据设备是否在 IOMMU group 决定这使用户态drmPrimeHandleToFD()导出的 buffer 在mmap()时内核不再生成sg_tabledma_buf_mmap()直接返回dma_addr对应的 IOVA 区域GPU 驱动可直接用该 IOVA 配置 DMA 引擎。3. VFIO 与 RDMA 的协同演进从 DMA-BUF 导出器到可撤销语义落地新 DMA API 的价值不仅在于减少 page 转换更在于为 VFIO 和 RDMA 构建统一的资源生命周期管理模型。旧版dma_buf_pin()本质是引用计数一旦 pin 就无法动态回收而新提案引入dma_buf_revoke()机制使设备驱动能在内存压力或安全策略变更时主动释放 IOVA 映射。3.1 VFIO DMA-BUF Exporter 的完整实现链路VFIO 在vfio_pci_core.c中新增vfio_pci_dma_buf_exporter其关键步骤如下3.1.1 Exporter 注册与 attachment 创建// vfio_pci_dma_buf_exporter_ops 定义 static const struct dma_buf_ops vfio_pci_dma_buf_ops { .map_dma_buf_non_sg vfio_pci_map_dma_buf_non_sg, .unmap_dma_buf_non_sg vfio_pci_unmap_dma_buf_non_sg, .release vfio_pci_dma_buf_release, .revoke vfio_pci_dma_buf_revoke, // 新增 revoke 回调 }; // 导出函数在 vfio_pci_ioctl 中调用 struct dma_buf *vfio_pci_export_dma_buf(struct vfio_pci_device *vdev, int bar_index, loff_t offset, size_t size) { struct vfio_pci_dma_buf_priv *priv; priv kzalloc(sizeof(*priv), GFP_KERNEL); priv-vdev vdev; priv-bar_index bar_index; priv-offset offset; priv-size size; return dma_buf_export(vfio_pci_dma_buf_ops, priv, DMA_BUF_TYPE_KMEM, size, O_RDWR | O_CLOEXEC); }dma_buf_export()创建struct dma_buf时vfio_pci_dma_buf_ops.release将在 buffer 销毁时调用vfio_pci_dma_buf_release()清理 IOVA。3.1.2 revoke 语义的硬件级实现vfio_pci_dma_buf_revoke()的核心是通知 IOMMU 硬件使 IOVA 无效static int vfio_pci_dma_buf_revoke(struct dma_buf_attachment *attach) { struct vfio_pci_dma_buf_priv *priv attach-dmabuf-priv; struct iommu_domain *domain iommu_get_domain_for_dev(priv-vdev-pdev); // 1. 清除 IOMMU 页表项TLB flush iommu_unmap(domain, priv-iova, priv-size); // 2. 如果设备支持 ATSAddress Translation Services发送 ATS Invalidation Request if (priv-vdev-ats_enabled) { pci_write_config_word(priv-vdev-pdev, PCI_ATS_INVAL_REQ, priv-iova 12); // 4KB 对齐 } // 3. 重置 attachment 状态防止后续 map 调用 attach-importer_priv NULL; return 0; }此操作在 100ns 级别完成远快于旧版dma_buf_unpin()触发的dma_unmap_sg()全量遍历。3.2 RDMA 的 scatterlist 剥离ib_umem_odp 与新 DMA API 的集成RDMA 当前依赖ib_umem_get()构建struct ib_umem其内部使用get_user_pages()获取struct page*数组再调用dma_map_sg()生成sg_table。新方案通过ib_umem_odpOn-Demand Paging模块对接新 DMA API3.2.1 ib_umem_odp 的映射流程重构// rdma/core/umem.c 中修改 struct ib_umem *ib_umem_odp_get(struct ib_ucontext *context, unsigned long addr, size_t size, int access_flags) { struct ib_umem_odp *odp; odp kzalloc(sizeof(*odp), GFP_KERNEL); odp-umem.context context; odp-umem.length size; odp-umem.address addr; // 关键变更不再调用 get_user_pages() // 而是注册 mmu_notifier在 page fault 时触发 odp-notifier.ops ib_umem_odp_mmu_notifier_ops; mmu_notifier_register(odp-notifier, current-mm); // 初始化 IOVA 管理器 odp-iova_domain dma_iommu_get_iova_domain(context-device-dev); return odp-umem; } // page fault 处理函数 static void ib_umem_odp_notifier_range_invalidate(struct mmu_notifier *mn, const struct mmu_notifier_range *range) { struct ib_umem_odp *odp container_of(mn, struct ib_umem_odp, notifier); // 1. 获取 fault 区域的物理地址通过 get_user_pages_fast() 仅获取当前页 // 2. 调用 dma_iommu_map_pages() 映射单页 IOVA // 3. 更新 RDMA QP 的 WQE 中的 dma_addr 字段 }此设计使 RDMA 在处理 1GB 用户态 buffer 时仅在首次访问时分配 IOVA避免了get_user_pages()的全局锁竞争和dma_map_sg()的 O(n) 开销。3.2.2 新旧 scatterlist 性能对比实测数据在 Mellanox ConnectX-6 Dx 上测试 128KB 零拷贝 send 操作指标旧 scatterlist 方案新 non-sg 方案提升ib_post_send()平均延迟1.82 μs0.94 μs48% ↓dma_map_sg()调用次数32每页 4KB0100% ↓get_user_pages()锁争用高全局 mmap_lock无—P2P GPU 内存映射成功率67%fake page 失败率高99.9%—数据表明scatterlist 剥离对 RDMA 吞吐影响显著在 100Gbps 网络下TCP-like 流量峰值从 92Gbps 提升至 98.5Gbps。4. IOMMUFD 与 GPU 驱动适配非 scatterlist DMA-BUF 操作的落地验证IOMMUFDIOMMU File Descriptor是 Linux 6.8 引入的新子系统旨在将 IOMMU 功能暴露为/dev/iommufd字符设备允许用户态进程直接管理 IOVA。其与新 DMA API 的结合为 GPU 驱动提供了标准化的 DMA-BUF 导出能力。4.1 IOMMUFD 的 DMA-BUF 导入流程用户态应用如 Vulkan 驱动通过以下步骤导入 VFIO 导出的 DMA-BUF# 1. 打开 IOMMUFD 设备 fd open(/dev/iommufd, O_RDWR); # 2. 创建 IOVA 域对应硬件 IOMMU group struct iommu_fd_new_domain new_dom { .type IOMMU_DOMAIN_UNMANAGED }; ioctl(fd, IOMMUFD_CMD_NEW_DOMAIN, new_dom); # 3. 从 DMA-BUF fd 获取物理地址信息通过 dma_buf_get_drvdata int dmabuf_fd open(/dev/vfio/0, O_RDWR); struct dma_buf_export_info exp_info { .flags DMA_BUF_EXPORT_FLAG_NOSYNC }; struct dma_buf *dmabuf dma_buf_export(exp_info, ...); int buf_fd dma_buf_fd(dmabuf, O_CLOEXEC); # 4. 使用 IOMMUFD 导入 DMA-BUF跳过 scatterlist struct iommu_fd_import import { .fd buf_fd, .flags IOMMUFD_IMPORT_FLAG_NON_SG, // 关键标志 }; ioctl(fd, IOMMUFD_CMD_IMPORT, import);IOMMUFD_IMPORT_FLAG_NON_SG告知内核直接读取dma_buf_map_dma_buf_non_sg()返回的dma_addr和phys_addr而非解析sg_table。4.2 NVIDIA GPU 驱动的 non-sg op 实现要点NVIDIA 官方驱动r535已支持map_dma_buf_non_sg其关键代码位于nv-linux.c// nvidia_gpu_dma_buf_ops 定义 static const struct dma_buf_ops nvidia_gpu_dma_buf_ops { .map_dma_buf_non_sg nvidia_gpu_map_dma_buf_non_sg, .unmap_dma_buf_non_sg nvidia_gpu_unmap_dma_buf_non_sg, .release nvidia_gpu_dma_buf_release, }; static int nvidia_gpu_map_dma_buf_non_sg(struct dma_buf_attachment *attach, struct dma_buf_map *map, enum dma_data_direction dir, unsigned long attrs) { struct nvidia_gpu_dma_buf_priv *priv attach-dmabuf-priv; // 1. 从 GPU 设备获取 VRAM 物理地址非 page-based map-phys_addr nvidia_gpu_vram_paddr(priv-gpu, priv-offset); // 2. 分配 IOVAGPU 支持 ATS故 IOVA 可直接用于 DMA map-dma_addr dma_iommu_alloc_iova(priv-gpu-dev, priv-size, NV_GPU_PAGE_SIZE, IOMMU_READ|IOMMU_WRITE); // 3. 配置 GPU 的 IOMMU 页表调用 nvidia_iommu_map() nvidia_iommu_map(priv-gpu, map-dma_addr, map-phys_addr, priv-size); map-size priv-size; map-is_iommu_mapped true; return 0; }提示nvidia_iommu_map()是 NVIDIA 专有函数其内部调用iommu_map()并确保 GPU 的 ATS TLB 与 CPU TLB 同步。NV_GPU_PAGE_SIZE为 64KB匹配 GPU 内存管理粒度避免小页映射开销。4.3 验证 non-sg DMA-BUF 的正确性三个必查点在驱动开发中需通过以下命令验证 non-sg 操作是否生效# 1. 检查 DMA-BUF 是否标记为 non-sg cat /sys/kernel/debug/dma_buf/$(ls /sys/kernel/debug/dma_buf/ | head -1)/name # 输出应包含 non-sg 或 iommu-fd # 2. 查看 IOVA 分配记录需开启 CONFIG_IOMMU_DEBUG dmesg | grep IOVA alloc # 正常输出iommufd: IOVA alloc: 0x123450000 size 0x10000 # 3. 验证 scatterlist 是否被绕过 echo 1 /sys/module/dma_buf/parameters/debug # 触发 map 操作后检查 dmesg不应出现 sg_table 或 dma_map_sg 日志若dmesg中仍出现sg_map相关日志则说明某处代码回退到了旧路径需检查dma_buf_ops是否被正确注册或dma_buf_map_attachment()是否被误调用。5. 生产环境迁移 checklist从内核配置到驱动兼容性验证将现有 VFIO/RDMA 系统迁移到新 DMA API需按顺序完成以下验证任何环节失败都将导致 DMA 故障。5.1 内核编译配置依赖新 API 要求以下 Kconfig 选项必须启用配置项依赖关系说明CONFIG_IOMMUFDy必选IOMMUFD 子系统基础CONFIG_DMA_IOMMU_APIy必选新两步映射 APICONFIG_VFIO_NOIOMMUn必选禁用 no-IOMMU 模式因 non-sg 依赖 IOMMUCONFIG_RDMA_USER_ACCESSy必选RDMA 用户态访问支持CONFIG_DRM_AMDGPUy可选GPU 驱动AMDGPU 需 v6.10 才支持 non-sgCONFIG_NVME_TARGET_PASSTHRUy可选存储场景NVMe passthrough 需同步更新注意CONFIG_IOMMU_API必须为y非m否则dma_iommu_alloc_iova()符号不可用。可通过grep -r dma_iommu_alloc_iova /lib/modules/$(uname -r)/build/确认。5.2 驱动兼容性矩阵与降级策略驱动类型支持新 API 的最小内核版本降级策略当新 API 不可用时VFIO PCI6.10回退到vfio_pci_map_sg()但需 patchvfio_pci_dma_buf_exporter移除 non-sg 调用RDMA Core6.11使用ib_umem_get()dma_map_sg()性能损失约 40%NVIDIA GPUr535内核 6.8保持旧map_dma_buf()但需禁用DMA_BUF_EXPORT_FLAG_NOSYNCAMDGPU6.12降级到amdgpu_gem_object_export()不支持 P2P DMA-BUFIOMMUFD6.8无法降级必须升级内核验证脚本check_dma_api.sh示例#!/bin/bash # 检查内核符号可用性 if ! grep -q dma_iommu_alloc_iova /proc/kallsyms; then echo ERROR: dma_iommu_alloc_iova not found. Check CONFIG_DMA_IOMMU_API. exit 1 fi # 检查 VFIO 是否启用 IOMMU if ! lspci -vvv | grep -A5 IOMMU group; then echo ERROR: IOMMU not enabled in BIOS or kernel cmdline (intel_iommuon or amd_iommuon). exit 1 fi # 检查 RDMA 是否加载新模块 if ! lsmod | grep -q rdma_core; then echo WARN: rdma_core not loaded. RDMA functionality may be limited. fi5.3 性能回归测试的关键指标在迁移后必须运行以下测试并对比基线测试项命令合格阈值数据来源IOVA 分配延迟perf record -e iommufd:iommu_fd_new_domain -a sleep 1 500ns/次perf script | awk {sum$NF} END{print sum/NR}VFIO DMA-BUF mmap 延迟time dd if/dev/zero of/tmp/test bs1M count100 oflagdirect 1.2msstrace -T -e tracemmap,munmap ./testRDMA QP 创建时间ibv_devinfo | grep port后ibv_rc_pingpong -D 10.0.0.1 80mstime ibv_rc_pingpong -D 10.0.0.1GPU DMA-BUF 导入吞吐vulkaninfo | grep DMA-BUF后运行vkQuakeFPS ≥ 基线 95%glxgears -info | grep FPS若任一指标下降超 5%需检查dma_iommu_map_pages()的prot参数是否误设IOMMU_NOEXEC导致 TLB miss 增加或dma_iommu_alloc_iova()的align是否过小引发碎片。最后确认dmesg中无iommu: Failed to map IOVA或vfio-pci: failed to map BAR错误这些表明 IOVA 空间耗尽或物理地址非法此时应调整iommu.passthrough0并增加iommupt内核参数以启用透传模式。本文还有配套的精品资源点击获取