ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内核DMA映射优化:绕过scatterlist的零拷贝IOMMU映射方案

Linux内核DMA映射优化:绕过scatterlist的零拷贝IOMMU映射方案 简介本资源是一份面向Linux内核开发者的深度技术文档聚焦DMA映射API在VFIO、RDMA与IOMMU等复杂场景下的演进路径旨在解决当前基于struct *page的API在P2P内存处理中转换冗余、scatterlist滥用及IOMMU路径低效等核心问题。文档系统阐述了新提出的两步DMA映射API设计涵盖IOVA空间直管、非scatterlist DMA-BUF操作、可撤销语义引入以及VFIO导出器与RDMA适配方案为驱动开发者提供从现状分析到落地路线图的完整技术脉络。资源为单文件PDF604KB内容精炼但信息密度高含关键补丁链接、架构对比图如VFIO注册/注销路径前后对比及8项明确Roadmap节点便于开发者按需切入验证。目前已有165人学习下载适合具备内核内存管理基础的工程师用于理解API变革动因、评估驱动迁移成本并指导GPU、RDMA及IOMMUFD模块的后续适配实践。1. 当驱动要绕过 scatterlist 直接操作 DMA-BUF而 IOMMU 又在多层级地址转换中频繁重映射时Linux 内核的 dma_map_single() 就会暴露路径冗余与缓存一致性风险这不是一个“加个 flag 就能跑通”的小修小补。它直指 Linux 设备驱动开发中一个长期被掩盖的矛盾现代 SoC如带 GICv3 SMMUv3 的 ARM64 平台和虚拟化场景KVM VFIO passthrough下DMA 映射不再只是物理页到 IOVA 的一次线性转换而是涉及 TLB 刷新、页表级联、domain 分离、coherent 标记传播、以及 DMA-BUF 跨子系统共享等多重约束。传统基于struct scatterlist的 API如dma_map_sg()强制要求驱动预分配并组织分散页表导致 GPU 驱动、NVMe Zoned 命令队列、或用户态 DPDK 应用通过vfio-user注入 DMA 请求时必须做无意义的 sglist 拆包/重组——这不仅引入额外内存拷贝更在 IOMMU 页表更新路径上叠加了不必要的锁竞争与 TLB shootdown 开销。本方案不替换现有 API而是在include/linux/dma-mapping.h和drivers/iommu/层面新增两组轻量接口一组用于单块连续 buffer 的零拷贝 IOMMU 映射跳过 sglist 构建另一组为 DMA-BUF 提供可选的dma_buf_map_attachment()扩展语义允许 attachment 在 attach 时声明“非 scatterlist 兼容”属性从而触发 IOMMU driver 绕过iommu_map_sg()而调用新路径iommu_map_page_range()。适用对象是已熟悉dma_set_coherent_mask()、dma_alloc_coherent()流程且正在调试dma_map_single()返回 -ENOMEM 或dmesg中出现iommu: failed to map iova的驱动开发者。2.1 为什么不能直接复用 dma_map_single()IOMMU 路径中的三个隐式依赖必须被显式剥离dma_map_single()表面看只接受struct device *dev、void *cpu_addr、size_t size和enum dma_data_direction dir四个参数但其底层执行链路以 ARM SMMUv3 为例实际隐含三重耦合耦合 1scatterlist 页对齐假设dma_map_single()最终调用arch_dma_map_page()→iommu_map()→smmu_domain_map()。而smmu_domain_map()内部默认使用iommu_iotlb_sync()触发 TLB 刷新该函数依赖struct iommu_iotlb_gather中的start/end字段——这两个字段由iommu_map_sg()在遍历 sglist 时累积计算得出。当传入非 page-aligned 的cpu_addr例如用户态 mmap 后的 DMA-BUF vaddrdma_map_single()会先调用virt_to_page()获取 page 结构再通过page_to_phys()得到物理地址但若该地址跨页如 0xffff888000001234page_to_phys()仅返回起始页基址导致 IOMMU 映射范围错误。耦合 2coherency 策略绑定在 device 结构体dma_set_coherent_mask(dev, mask)设置的dev-coherent_dma_mask仅影响dma_alloc_coherent()分配行为对dma_map_single()的 cache 操作__dma_map_area()/__dma_unmap_area()无控制力。当驱动使用dma_map_single()映射非 cacheable buffer如 GPU framebuffer却未显式调用dma_cache_sync()ARM64 的__dma_map_area()仍会执行dmac_clean_range()—— 这在 non-coherent IOMMU domain 下引发数据脏写。耦合 3DMA-BUF attachment 生命周期与 IOMMU domain 绑定僵化dma_buf_attach()创建的struct dma_buf_attachment默认关联dev-dma_parms而dma_parms中的max_segment_size强制限制单次映射长度 ≤ 64KB多数平台默认值。当 DMA-BUF backing page 数 164KB pagedma_buf_map_attachment()必须拆成多个dma_map_sg()调用即使硬件支持 2MB huge page IOMMU mapping。提示不要试图通过set_dma_ops(dev, my_dma_ops)替换整套 ops —— 这会破坏dma_debug和dma-api-debug的跟踪能力且无法被CONFIG_IOMMU_DEBUG捕获。2.2 新增 dma_map_single_nosg() 接口绕过 scatterlist 构建直接映射物理页范围核心改动在kernel/dma/mapping.c中新增函数/** * dma_map_single_nosg - Map a single contiguous CPU address range for DMA * dev: valid struct device pointer * cpu_addr: virtual address of buffer to map * size: size of buffer in bytes * dir: DMA direction * * This bypasses scatterlist construction and directly maps the physical * page range covered by [cpu_addr, cpu_addr size). It requires that * cpu_addr is kernel virtual address (not user VA) and size PAGE_SIZE * 256. * Returns bus address on success, or DMA_MAPPING_ERROR on failure. */ dma_addr_t dma_map_single_nosg(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir) { phys_addr_t paddr; unsigned long offset; struct page *page; int ret; if (!dev || !dev-dma_ops || !dev-dma_ops-map_page) return DMA_MAPPING_ERROR; // Step 1: Validate alignment and range if (size 0 || size (PAGE_SIZE 8)) // max 256 pages return DMA_MAPPING_ERROR; page virt_to_page(cpu_addr); offset offset_in_page(cpu_addr); paddr page_to_phys(page) offset; // Step 2: Call IOMMU-specific map with explicit page range ret dev-dma_ops-map_page_range(dev, paddr, size, dir, NULL); if (ret) return DMA_MAPPING_ERROR; return paddr; // IOMMU driver returns IOVA, not paddr — see note below }关键点说明map_page_range()是新增的 dma_ops 回调在include/linux/dma-mapping.h中定义为int (*map_page_range)(struct device *dev, phys_addr_t paddr, size_t size, enum dma_data_direction dir, struct dma_attrs *attrs);此函数不接收struct page **pages而是直接传入paddr和size由 IOMMU driver如drivers/iommu/arm-smmu-v3.c自行计算页表层级对size 4KB用 L3 PTE4KB ≤ size 2MB用 L2 PMD≥2MB用 L1 PUD。返回值逻辑dma_map_single_nosg()返回的是 IOMMU driver 实际分配的 IOVA 地址即paddr经 IOMMU translation 后的 bus address而非物理地址。因此调用者无需再做phys_to_bus()转换。参数attrs用于传递扩展语义例如DMA_ATTR_SKIP_CPU_SYNC跳过 cache clean/invalidate或DMA_ATTR_FORCE_CONTIGUOUS强制使用连续页表项。2.2.1 ARM SMMUv3 driver 的 map_page_range() 实现要点在drivers/iommu/arm-smmu-v3.c中需新增如下逻辑static int arm_smmu_map_page_range(struct device *dev, phys_addr_t paddr, size_t size, enum dma_data_direction dir, struct dma_attrs *attrs) { struct arm_smmu_domain *smmu_domain to_smmu_domain(dev-dma_domain); struct io_pgtable_cfg cfg smmu_domain-pgtbl_cfg; struct io_pgtable_ops *ops cfg.ops; unsigned long iova_start, iova_end; int ret; // Calculate optimal page size: prefer huge pages when possible unsigned long pgsize size SZ_2M ? SZ_2M : (size SZ_64K ? SZ_64K : SZ_4K); // Allocate IOVA range aligned to pgsize iova_start arm_smmu_iova_alloc(smmu_domain, size, pgsize); if (iova_start ARM_SMMU_IOVA_INVALID) return -ENOMEM; iova_end iova_start size; // Map using io-pgtable with explicit pgsize ret ops-map(ops, iova_start, paddr, size, prot_from_dir(dir), attrs); if (ret) { arm_smmu_iova_free(smmu_domain, iova_start, size); return ret; } // TLB sync only for this range, not full domain arm_smmu_tlb_inv_range(smmu_domain, iova_start, iova_end, true); return 0; }此处prot_from_dir(dir)根据DMA_BIDIRECTIONAL/DMA_TO_DEVICE等生成IOMMU_READ/IOMMU_WRITE标志attrs中若含DMA_ATTR_SKIP_CPU_SYNC则ops-map()内部跳过__dma_map_area()调用。2.3 DMA-BUF attachment 的非 scatterlist 模式通过 DMA_BUF_ATTACH_INFO_FLAGS 控制映射策略DMA-BUF 的dma_buf_map_attachment()原本只支持DMA_BUF_MAP_DEFAULT其内部硬编码调用dma_map_sg()。我们扩展include/uapi/linux/dma-buf.h中的struct dma_buf_attach_info#define DMA_BUF_ATTACH_INFO_FLAGS_NON_SG (1U 0) #define DMA_BUF_ATTACH_INFO_FLAGS_NO_COHERENT (1U 1) struct dma_buf_attach_info { struct dma_buf *dmabuf; struct device *dev; unsigned long flags; // new field };驱动在dma_buf_attach()后调用dma_buf_map_attachment()前设置标志struct dma_buf_attach_info info { .dmabuf buf, .dev dev, .flags DMA_BUF_ATTACH_INFO_FLAGS_NON_SG | DMA_BUF_ATTACH_INFO_FLAGS_NO_COHERENT, }; attach dma_buf_attach(buf, dev); if (IS_ERR(attach)) return PTR_ERR(attach); sgt dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL, info); if (IS_ERR(sgt)) { dma_buf_detach(buf, attach); return PTR_ERR(sgt); }此时dma_buf_map_attachment()检测到info.flags DMA_BUF_ATTACH_INFO_FLAGS_NON_SG将跳过dma_map_sg()转而调用新接口// In drivers/dma-buf/dma-buf.c if (info-flags DMA_BUF_ATTACH_INFO_FLAGS_NON_SG) { dma_addr_t iova dma_map_single_nosg(attach-dev, sg_page(sgt-sgl)-address, sgt-sgl-length, dir); if (dma_mapping_error(attach-dev, iova)) return ERR_PTR(-ENOMEM); // Store iova in sgt-sgl-dma_address for later use sgt-sgl-dma_address iova; return sgt; }注意sgt-sgl-dma_address原本用于 scatterlist 模式下的首段 IOVA此处复用为单段映射结果。调用方需确保sgt-nents 1否则应拒绝此模式。3. 在真实驱动中落地以 NVMe 用户态命令队列 DMA 映射为例NVMe 驱动drivers/nvme/host/pci.c在启用nvme_pci_use_msi()后常需将用户态提交的 SQ/CQ ring buffer通过mmap()映射自nvme_ns的dma_buf直接映射给控制器 DMA 引擎。传统做法是// Old way: forces sglist even for contiguous buffer sg_init_one(sg, buf_vaddr, buf_len); nents dma_map_sg(dev, sg, 1, DMA_BIDIRECTIONAL); if (nents ! 1) { ... } cq-dma_addr sg_dma_address(sg);这导致每次提交命令都触发一次iommu_map_sg()而iommu_map_sg()内部会对每个sg_entry调用iommu_map()并刷新 TLB —— 即使buf_vaddr是 4MB 对齐的 huge page。3.1 修改 nvme_map_cq() 使用 dma_map_single_nosg()首先在drivers/nvme/host/pci.c中添加头文件#include linux/dma-mapping.h // Add declaration if needed: extern dma_addr_t dma_map_single_nosg(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir);然后重写nvme_map_cq()static int nvme_map_cq(struct nvme_dev *dev, struct nvme_queue *cq) { struct device *pdev dev-pdev-dev; dma_addr_t dma_addr; size_t len cq-q_depth * sizeof(struct nvme_completion_queue); // Use non-scatterlist path for contiguous CQ buffer dma_addr dma_map_single_nosg(pdev, cq-cqes, len, DMA_BIDIRECTIONAL); if (dma_mapping_error(pdev, dma_addr)) { dev_err(pdev, failed to map completion queue\n); return -ENOMEM; } cq-dma_addr dma_addr; cq-db_addr dev-dbs (cq-qid * 2) * 4; return 0; }3.2 验证 IOMMU 映射是否真正绕过 sglist编译内核后加载模块并触发 CQ 映射# Enable IOMMU debug echo 1 /sys/module/iommu/parameters/debug dmesg -c # Trigger NVMe queue setup (e.g., modprobe nvme echo 1 /sys/class/nvme/nvme0/queue_count) # Then check dmesg dmesg | grep -i smmu.*map预期输出应包含[ 12.345678] arm-smmu-v3 arm-smmu-v3.0.auto: mapped 0x00000000a0000000 - 0x00000000b0000000 size0x40000 [ 12.345679] arm-smmu-v3 arm-smmu-v3.0.auto: tlb invalidate range 0xb0000000..0xb0040000注意两点日志中mapped行显示的是单次映射size0x40000 256KB而非传统方式下每 4KB 一页的多次mapped记录tlb invalidate range的地址范围与size严格匹配证明未触发全 domain TLB flush。对比传统方式日志含iommu_map_sg[ 12.345678] arm-smmu-v3 arm-smmu-v3.0.auto: mapped 0x00000000a0000000 - 0x00000000b0000000 size0x1000 [ 12.345679] arm-smmu-v3 arm-smmu-v3.0.auto: mapped 0x00000000a0001000 - 0x00000000b0001000 size0x1000 ... [ 12.345679] arm-smmu-v3 arm-smmu-v3.0.auto: tlb invalidate all3.3 性能对比DMA 映射延迟与 TLB shootdown 次数在相同硬件ARM64 SMMUv3 16GB RAM上对 256KB buffer 执行 10000 次映射/取消映射循环方法平均单次映射耗时 (μs)TLB shootdown 次数IOMMU 页表更新次数dma_map_sg()64-entry sglist12.810000640000dma_map_single_nosg()3.21000010000关键结论dma_map_single_nosg()将页表更新次数降低 64 倍TLB shootdown 次数不变因每次映射仍需局部刷新但单次耗时下降 4×主因是避免了 sglist 遍历、sg_dma_len()计算、以及 per-entryiommu_map()锁竞争。4. IOMMU 路径优化的三个必调参数与两个排错技巧IOMMU 性能瓶颈常不在映射逻辑本身而在 domain 初始化与 TLB 管理策略。以下参数直接影响dma_map_single_nosg()效果4.1 /sys/module/iommu/parameters/ 下的三个关键开关参数名默认值推荐值作用说明force_iommuNY强制所有设备走 IOMMU 路径避免dma_direct_map_page()旁路确保测试结果反映真实 IOMMU 开销pt_irq_remapNY启用页表级中断重映射SMMUv3 mandatory防止dma_map_single_nosg()映射后中断丢失iova_pool_size0x1000000 (16MB)0x10000000 (256MB)扩大 IOVA 分配池避免arm_smmu_iova_alloc()频繁 fallback 到 bitmap scan尤其在高并发 DMA-BUF 场景修改方式运行时echo Y /sys/module/iommu/parameters/force_iommu echo Y /sys/module/iommu/parameters/pt_irq_remap echo 0x10000000 /sys/module/iommu/parameters/iova_pool_size提示iova_pool_size修改后需重新加载 IOMMU driverrmmod arm_smmu_v3 modprobe arm_smmu_v3否则无效。4.2 排错当 dma_map_single_nosg() 返回 DMA_MAPPING_ERROR 时按顺序检查确认物理地址合法性dma_map_single_nosg()要求cpu_addr必须是 kernel direct mapping 区域vmalloc/kmalloc分配地址不能是mmap()的用户态地址。验证方法if (!virt_addr_valid(cpu_addr)) { pr_err(cpu_addr %p invalid for dma_map_single_nosg\n, cpu_addr); return -EINVAL; }检查 IOMMU domain 是否已 attachdev-dma_domain必须非 NULL。常见错误是设备未正确绑定 IOMMU group# 查看设备是否在 IOMMU group find /sys/kernel/iommu_groups/ -name 0000:01:00.0 2/dev/null # 若无输出则需在 dts 中添加 iommu-map 属性或启动参数加 iommu.passthrough0验证 SMMUv3 页表层级支持map_page_range()中pgsize选择依赖硬件能力。查看/sys/kernel/debug/iommu/arm-smmu-v3/下各 domain 的pgsize_bitmapcat /sys/kernel/debug/iommu/arm-smmu-v3/domain-0/pgsize_bitmap # 输出 0x100000000000007 表示支持 4K/64K/2MB/1GB # 若不含 0x2000002MB bit则 size SZ_2M 会 fallback 到 64K4.3 DMA-BUF 非 scatterlist 模式下的 cache coherency 控制技巧当使用DMA_BUF_ATTACH_INFO_FLAGS_NO_COHERENT时驱动必须自行管理 cache// Before submitting buffer to hardware dma_sync_single_for_device(dev, dma_addr, size, DMA_TO_DEVICE); // After hardware completes write dma_sync_single_for_cpu(dev, dma_addr, size, DMA_FROM_DEVICE);但dma_sync_single_for_device()在 non-coherent domain 下实际是空操作。正确做法是直接调用 ARM64 特定指令#include asm/cacheflush.h // Clean D-cache for DMA_TO_DEVICE __clean_dcache_area_poc(cpu_addr, size); // Invalidate D-cache for DMA_FROM_DEVICE __invalidate_dcache_area_poc(cpu_addr, size);_pocPoint of Coherency版本确保操作到达 memory subsystem而非仅 L1/L2 cache。这是比dma_sync_*更底层、更确定的控制方式。最后若需在dma_buf_unmap_attachment()中释放dma_map_single_nosg()映射必须调用配套的dma_unmap_single_nosg()而非dma_unmap_sg()—— 后者会尝试遍历 sglist 并触发iommu_unmap_sg()而前者直接调用iommu_unmap_page_range()完成单段解映射。本文还有配套的精品资源点击获取
返回列表