Linux 下 DMA 内存映射浅析
序
系统 I/O 设备驱动程序通常调用其特定子系统的接口为 DMA 分配内存,但最终会调到 DMA 子系统的dma_alloc_coherent()/dma_alloc_attrs() 等接口。
关于 dma_alloc_coherent
接口详细的代码讲解、调用流程,可以参考这篇文章,我觉得写的非常好!
深入浅出 Linux 中的 ARM IOMMU SMMU III
注:本篇文章代码,均节选自 Linux 6.53 版本
1、DMA 映射类型
Linux系统支持两种 DMA 映射类型:一致性 DMA 映射(Consistent DMA Mapping)和流式 DMA映射(Streaming DMA Mapping)。二者的核心差异在于:
- 一致性 DMA 映射情况下,无论是 CPU 还是设备,在访问 DMA 内存时,全程都不使用 Cache,从而避免数据一致性问题;
- 流式 DMA 映射则只在 DMA 传输数据时,通过软件操作来处理 Cache 的同步,以减轻关闭 Cache 对性能的影响。
2、一致性 DMA
一致性 DMA 映射本质上是利用了硬件的支持,禁用了 DMA 内存缓存区的 Cache 功能。 CPU 和 DMA controller 在发起对 DMA 缓冲区的并行访问的时候不需要考虑 cache 的影响,也就是说不需要软件进行 Cache 操作,CPU 和 DMA controller 都可以看到对方对 DMA 缓冲区的更新。
dma_alloc_coherent
DMA 一致性映射使用 dma_alloc_coherent
接口来分配 DMA 内存,实现流程如下:
dma_alloc_coherent(dma_alloc_attrs)--> dma_alloc_from_dev_coherent (如果 Device 绑定了Reserved Memory)--> dma_direct_alloc (如果 Device 没有绑定 Reserved Memory )--> dma_ops->alloc (如果 Device 绑定了 IOMMU,使用 IOMMU 申请接口)
2.1 Device 绑定了 Reserved Memory
dma_alloc_from_dev_coherent
函数从设备的 coherent 内存池中分配内存。coherent 内存池由设备驱动程序通过dma_declare_coherent_memory
接口创建,这个接口实现 (位于 kernel/dma/coherent.c 文件中) 如下:
static struct dma_coherent_mem *dma_init_coherent_memory(phys_addr_t phys_addr,dma_addr_t device_addr, size_t size, bool use_dma_pfn_offset)
{struct dma_coherent_mem *dma_mem;int pages = size >> PAGE_SHIFT;void *mem_base;if (!size)return ERR_PTR(-EINVAL);mem_base = memremap(phys_addr, size, MEMREMAP_WC);if (!mem_base)return ERR_PTR(-EINVAL);......
}void *memremap(resource_size_t offset, size_t size, unsigned long flags)
{......if (!addr && (flags & MEMREMAP_WT))addr = ioremap_wt(offset, size);if (!addr && (flags & MEMREMAP_WC))addr = ioremap_wc(offset, size);......
}
从 mem_base = memremap(phys_addr, size, MEMREMAP_WC);
这行代码不难看出,默认情况下,该接口分配的内存类型是 WC(Write Combine),最终调用到的是 ioremap_wc
接口。对于 ARM64 架构而言,这里分配的地址类型是 PROT_NORMAL_NC
。
2.2 Device 没有绑定 Reserved Memory
void *dma_direct_alloc(struct device *dev, size_t size,dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{....../* For non-coherent device */if (!dev_is_dma_coherent(dev)) {/* 首先尝试调用 arch 目录下的 dma_alloc */if (!IS_ENABLED(CONFIG_ARCH_HAS_DMA_SET_UNCACHED) &&!IS_ENABLED(CONFIG_DMA_DIRECT_REMAP) &&!dev_is_dma_coherent(dev))return arch_dma_alloc(dev, size, dma_handle, gfp, attrs);/** If there is a global pool, always allocate from it for* non-coherent devices.*/if (IS_ENABLED(CONFIG_DMA_GLOBAL_POOL))return dma_alloc_from_global_coherent(dev, size,dma_handle);/** Otherwise remap if the architecture is asking for it. But* given that remapping memory is a blocking operation we'll* instead have to dip into the atomic pools.*/remap = IS_ENABLED(CONFIG_DMA_DIRECT_REMAP);if (remap) {if (dma_direct_use_pool(dev, gfp))return dma_direct_alloc_from_pool(dev, size,dma_handle, gfp);} else {if (!IS_ENABLED(CONFIG_ARCH_HAS_DMA_SET_UNCACHED))return NULL;set_uncached = true;}}....../* we always manually zero the memory once we are done *//* 从软件管理的 page 内存池中获取对应大小的 page */page = __dma_direct_alloc_pages(dev, size, gfp & ~__GFP_ZERO, true);if (!page)return NULL;/** dma_alloc_contiguous can return highmem pages depending on a* combination the cma= arguments and per-arch setup. These need to be* remapped to return a kernel virtual address.*/if (PageHighMem(page)) {remap = true;set_uncached = false;}/* For coherent device */if (remap) {/* 设备页表属性(内存属性) */pgprot_t prot = dma_pgprot(dev, PAGE_KERNEL, attrs);if (force_dma_unencrypted(dev))prot = pgprot_decrypted(prot);/* remove any dirty cache lines on the kernel alias */arch_dma_prep_coherent(page, size);/* create a coherent mapping *//* 对刚刚申请的 page,重新 remap,建立页表属性 */ret = dma_common_contiguous_remap(page, size, prot,__builtin_return_address(0));if (!ret)goto out_free_pages;} else {ret = page_address(page);if (dma_set_decrypted(dev, ret, size))goto out_free_pages;}......
}
2.2.1 dma-coherent
关于设备是否是 dma-coherent(注意,dma-coherent 表示的是硬件维护一致性!),即 if (!dev_is_dma_coherent(dev))
,由 dts 传入 dma-coherent 标志决定。例如,rk3568 dts 中的 rga 节点:
rga: rga@ff680000 {compatible = "rockchip,rga2";dev_mode = <1>;reg = <0x0 0xff680000 0x0 0x1000>;interrupts = <GIC_SPI 55 IRQ_TYPE_LEVEL_HIGH 0>;clocks = <&cru ACLK_RGA>, <&cru HCLK_RGA>, <&cru SCLK_RGA_CORE>;clock-names = "aclk_rga", "hclk_rga", "clk_rga";power-domains = <&power RK3399_PD_RGA>;dma-coherent;status = "okay";};
2.2.2 dma_alloc_from_global_coherent
当设备不支持硬件一致性时,若其支持 global dma pool,则将从 global dma pool 中分配 dma 内存。其中若要支持 global dma pool,需要在 dts 的 reserved-memory 中保留 compatible 值为 shared-dma-pool。以 rk3568 为例:
reserved-memory {#address-cells = <2>;#size-cells = <2>;ranges;/* Reserve 128MB memory for hdmirx-controller@fdee0000 */cma {compatible = "shared-dma-pool";reusable;reg = <0x0 (256 * 0x100000) 0x0 (128 * 0x100000)>;linux,cma-default;};};
当 dts 包含以上节点时,则在 linux 内核初始化时,会通过以下接口为其初始化 global dma pool,并将相应的保留内存设置到该内存池中:
dma_init_reserved_memory --> dma_init_global_coherent--> dma_init_coherent_memory (该函数 2.1 章节有讲解)
2.2.3 dma_pgprot
dma_direct_alloc
接口中调用的 dma_pgprot
接口,默认情况下,调用的是 pgprot_dmacoherent
接口
/** Return the page attributes used for mapping dma_alloc_* memory, either in* kernel space if remapping is needed, or to userspace through dma_mmap_*.*/
pgprot_t dma_pgprot(struct device *dev, pgprot_t prot, unsigned long attrs)
{if (dev_is_dma_coherent(dev))return prot;
#ifdef CONFIG_ARCH_HAS_DMA_WRITE_COMBINEif (attrs & DMA_ATTR_WRITE_COMBINE)return pgprot_writecombine(prot);
#endifreturn pgprot_dmacoherent(prot);
}
在 arch/arm64/include/asm/pgtable.h 文件中:
/** DMA allocations for non-coherent devices use what the Arm architecture calls* "Normal non-cacheable" memory, which permits speculation, unaligned accesses* and merging of writes. This is different from "Device-nGnR[nE]" memory which* is intended for MMIO and thus forbids speculation, preserves access size,* requires strict alignment and can also force write responses to come from the* endpoint.*/
#define pgprot_dmacoherent(prot) \__pgprot_modify(prot, PTE_ATTRINDX_MASK, \PTE_ATTRINDX(MT_NORMAL_NC) | PTE_PXN | PTE_UXN)
到这可以看到,使用 Direct 申请接口,默认情况下分配的 DMA 内存,是 MT_NORMAL_NC
类型的,和 2.1 章节一样。
2.3 使用 iommu
通过已注册的 dev 设备内存分配操作函数分配(ops->alloc)
当设备注册了 dma_ops,则该设备需要通过其 ops 对应的接口分配 dma 内存。以 armv8 的 iommu 为例,该接口的注册流程如下:
of_dma_configure(由设备驱动调用)--> of_dma_configure_id (这一步会去解析设备树节点,看是否有 “iommus” 属性)--> arch_setup_dma_ops-->iommu_setup_dma_ops
arch/arm64/mm/dma-mapping.c
void arch_setup_dma_ops(struct device *dev, u64 dma_base, u64 size,const struct iommu_ops *iommu, bool coherent)
{int cls = cache_line_size_of_cpu();WARN_TAINT(!coherent && cls > ARCH_DMA_MINALIGN,TAINT_CPU_OUT_OF_SPEC,"%s %s: ARCH_DMA_MINALIGN smaller than CTR_EL0.CWG (%d < %d)",dev_driver_string(dev), dev_name(dev),ARCH_DMA_MINALIGN, cls);dev->dma_coherent = coherent;if (iommu)iommu_setup_dma_ops(dev, dma_base, dma_base + size - 1);xen_setup_dma_ops(dev);
}
其中 iommu 对应的 dma_ops 定义如下:
const struct dma_map_ops iommu_dma_ops = {.flags = DMA_F_PCI_P2PDMA_SUPPORTED,.alloc = iommu_dma_alloc,.free = iommu_dma_free,.alloc_pages = dma_common_alloc_pages,.free_pages = dma_common_free_pages,.alloc_noncontiguous = iommu_dma_alloc_noncontiguous,.free_noncontiguous = iommu_dma_free_noncontiguous,.mmap = iommu_dma_mmap,.get_sgtable = iommu_dma_get_sgtable,.map_page = iommu_dma_map_page,.unmap_page = iommu_dma_unmap_page,.map_sg = iommu_dma_map_sg,.unmap_sg = iommu_dma_unmap_sg,.sync_single_for_cpu = iommu_dma_sync_single_for_cpu,.sync_single_for_device = iommu_dma_sync_single_for_device,.sync_sg_for_cpu = iommu_dma_sync_sg_for_cpu,.sync_sg_for_device = iommu_dma_sync_sg_for_device,.map_resource = iommu_dma_map_resource,.unmap_resource = iommu_dma_unmap_resource,.get_merge_boundary = iommu_dma_get_merge_boundary,.opt_mapping_size = iommu_dma_opt_mapping_size,
};
3、流式 DMA
与一致性映射接口不同,流式 DMA 映射接口不会分配内存,而是将输入参数中传入的内存映射为 DMA 地址。由于在 CPU 视角下,它也可能被映射成了 cache 类型的内存,故其对应的 cache 中可能含有 dirty 数据。为了确保 DMA 和 CPU 都能获取到正确的数据,则在 DMA 操作流程中,软件需要维护 cache 与主存数据的一致性。
流式 DMA 最大的一个特点就是带 cache,这会带来 cache 和内存的一致性问题。如果处理不好,会触发各种问题,使用的时候必须小心!!!
关于 cache 一致性问题,我前面有篇文章已经做了总结:ARM 架构下 cache 一致性问题整理
流式 DMA 包括以下两类接口:
3.1 内存映射接口
#define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)
#define dma_unmap_single(d, a, s, r) dma_unmap_single_attrs(d, a, s, r, 0)
#define dma_map_sg(d, s, n, r) dma_map_sg_attrs(d, s, n, r, 0)
#define dma_unmap_sg(d, s, n, r) dma_unmap_sg_attrs(d, s, n, r, 0)
#define dma_map_page(d, p, o, s, r) dma_map_page_attrs(d, p, o, s, r, 0)
#define dma_unmap_page(d, a, s, r) dma_unmap_page_attrs(d, a, s, r, 0)
这里以 dma_map_single 接口说明 DMA 内存的映射流程:
dma_map_single--> dma_map_single_attrs--> dma_map_page_attrs--> dma_map_direct (直接映射方式,使用SWIOTLB机制)--> get_dma_ops-->map_page (设备支持IOMMU,通过IOMMU提供的操作接口映射内存)
3.2 cache 维护接口
inline void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr,size_t size, enum dma_data_direction dir)
inline void dma_sync_single_for_device(struct device *dev,dma_addr_t addr, size_t size, enum dma_data_direction dir)
inline void dma_sync_sg_for_cpu(struct device *dev,struct scatterlist *sg, int nelems, enum dma_data_direction dir)
inline void dma_sync_sg_for_device(struct device *dev,struct scatterlist *sg, int nelems, enum dma_data_direction dir)
dma_direct_sync_single_for_cpu
如果你需要多次访问同一个流式映射DMA缓冲区,并且在DMA传输之间读写DMA缓冲区上的数据,这时候你需要使用dma_sync_single_for_cpu进行DMA缓冲区的sync操作,以便CPU和设备可以看到最新的、正确的数据。
dma_direct_sync_single_for_cpu
接口,最终调用的 arch_sync_dma_for_cpu
。以 ARM64 架构为例,其实最终就是刷 cache。
arch/arm64/mm/dma-mapping.c
void arch_sync_dma_for_device(phys_addr_t paddr, size_t size,enum dma_data_direction dir)
{unsigned long start = (unsigned long)phys_to_virt(paddr);dcache_clean_poc(start, start + size);
}void arch_sync_dma_for_cpu(phys_addr_t paddr, size_t size,enum dma_data_direction dir)
{unsigned long start = (unsigned long)phys_to_virt(paddr);if (dir == DMA_TO_DEVICE)return;dcache_inval_poc(start, start + size);
}void arch_dma_prep_coherent(struct page *page, size_t size)
{unsigned long start = (unsigned long)page_address(page);dcache_clean_poc(start, start + size);
}
dma_direct_sync_single_for_device
如果CPU操作了DMA缓冲区的数据,然后你又想让硬件设备访问DMA缓冲区,这时候,在真正让硬件设备去访问DMA缓冲区之前,你需要调用dma_direct_sync_single_for_device接口以便让硬件设备可以看到cpu更新后的数据。
dma_direct_sync_single_for_device
接口,最终调用的就是 arch_sync_dma_for_device
。
总结:
流式 DMA 映射根据数据方向对 cache 进行 “flush/invalid”,既保证了数据一致性,也避免了完全关闭 cache 带来的性能影响。