news 2026/9/15 2:40:22

Linux 内核 DMA API 完全指南:从动态映射到非一致性内存与调试(dma-api)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 内核 DMA API 完全指南:从动态映射到非一致性内存与调试(dma-api)

Linux 内核 DMA API 完全指南:从动态映射到非一致性内存与调试(dma-api)

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

本篇指南以 Linux 内核源码树中的 Documentation/core-api/dma-api.rst 为骨架,系统讲解通用设备动态 DMA 映射 API(Dynamic DMA mapping using the generic device)的完整面貌:大型/小型一致性(coherent)缓冲区分配、DMA 寻址掩码设置、流式(streaming)映射、IOVA 映射、非一致性内存分配以及 DMA API 调试设施。读完本文,你将掌握每个核心接口的签名、语义、调用约束与源码级实现细节,并能在自己的驱动中正确选择与使用这些 API。面向初学者的渐进式讲解与实际伪代码示例,请参见姊妹篇 Documentation/core-api/dma-api-howto.rst;本文则是一份"词典式"的完整 API 参考。

概览:API 的两大部分

DMA API 被划分为两部分:

  • Part I:基本 API。涵盖一致性(coherent)缓冲区分配、DMA 寻址限制设置、流式(streaming)映射等绝大多数驱动所需的接口。除非你的驱动必须支持非一致性内存平台(通常只有遗留平台如此),否则只应使用 Part I 的接口。
  • Part II:非一致性内存机器的扩展。这些接口返回的页面保证可被设备 DMA 寻址,但需要显式管理内核与设备之间的内存所有权(即需要手动缓存同步)。

要使用 DMA API,驱动必须包含头文件:

#include <linux/dma-mapping.h>

该头文件提供dma_addr_t类型以及下文描述的全部接口。dma_addr_t可以保存平台上任何合法的 DMA 地址,它被交给设备作为 DMA 源或目标。CPU 不能直接解引用dma_addr_t,因为 CPU 物理地址空间与 DMA(总线)地址空间之间可能存在地址转换(如 IOMMU 或主机桥)。关于 CPU 虚拟地址、物理地址与总线地址三者之间的关系,dma-api-howto.rst 中有详细的图解说明。

Part Ia:使用大型 DMA 一致性缓冲区

dma_alloc_coherent()

void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag)

一致性(coherent)内存是指:设备或处理器任一方写入后,另一方可以立即读取,而无需担心缓存效应。(不过你仍可能需要先刷新处理器的写缓冲区,再告知设备读取该内存。)

  • 该例程分配size字节的一致性内存区域。
  • 返回指向该区域的指针(处理器虚拟地址空间内),分配失败则返回NULL
  • 同时通过dma_handle返回 DMA 地址,它可被强制转换为与总线同宽的无符号整数,作为该区域的 DMA 地址基址交给设备。

注意:一致性内存在某些平台上代价昂贵,且最小分配长度可能高达一个页面,因此应尽可能合并对一致性内存的请求。最简单的做法是使用dma_pool接口(见 Part Ib)。flag参数允许调用者指定GFP_标志(参见kmalloc());实现可能会忽略影响返回内存位置的标志(如GFP_DMA)。

从源码看,dma_alloc_coherent()在 include/linux/dma-mapping.h 中是dma_alloc_attrs()的内联封装:当gfp中带__GFP_NOWARN时,会自动附加DMA_ATTR_NO_WARN属性以抑制分配失败告警。同理,dma_alloc_wc()(write-combine 变体)则通过DMA_ATTR_WRITE_COMBINE实现。

dma_free_coherent()

void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle)

释放之前分配的一致性内存区域。devsizedma_handle必须与传入dma_alloc_coherent()的完全相同,cpu_addr必须是dma_alloc_coherent()返回的虚拟地址。

注意:与分配例程不同,该例程只能在 IRQ 使能的情况下调用(不能在中断上下文调用)。此外内核还提供设备资源管理(devres)版本dmam_alloc_coherent()/dmam_free_coherent()(见 include/linux/dma-mapping.h),可随设备解绑自动释放。

Part Ib:使用小型 DMA 一致性缓冲区(DMA Pool)

大量驱动需要许多小型 DMA 一致性内存区域用于 DMA 描述符或 I/O 缓冲区。与其用dma_alloc_coherent()以页面为单位分配,不如使用DMA pools。它们的工作方式类似struct kmem_cache,但底层使用 DMA 一致性分配器而非__get_free_pages()。同时它们理解常见硬件对齐约束,例如队列头(queue head)需要按 N 字节边界对齐。

使用该部分 API 需包含头文件:

#include <linux/dmapool.h>

struct dma_pool的核心接口(声明见 include/linux/dmapool.h):

struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t boundary); void dma_pool_destroy(struct dma_pool *pool); void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags, dma_addr_t *handle); void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);

另有便捷封装dma_pool_zalloc()(带__GFP_ZERO清零)以及 devres 托管版本dmam_pool_create()/dmam_pool_destroy()(驱动卸载时自动销毁,见 include/linux/dmapool.h)。

从实现看(mm/dmapool.c):

  • dma_pool_create()是对dma_pool_create_node()的封装(nodeNUMA_NO_NODE,见 include/linux/dmapool.h)。
  • align必须是 2 的幂,size不能为 0 且不能超过INT_MAX;对象实际大小会被对齐补齐(mm/dmapool.c)。
  • boundary若非 0,则从池中分配的对象不会跨越该边界;例如传入 4096 表示分配的内存不得跨越 4KB 边界(此时直接使用dma_alloc_coherent()可能更合适)。实现中boundary会与分配单元取较小者,并在pool_initialise_page()中按边界切分页面(mm/dmapool.c)。
  • 池每次通过dma_alloc_coherent()获取整页,再分割成所需大小的块;空闲块以单链表形式跟踪。调试构建(CONFIG_SLUB_DEBUG_ON)下会对块做毒化(poison)检查,帮助发现越界写或双重释放。
  • 池的统计信息可通过 sysfs 的pools属性查看(poolinfo - 0.1格式,列出每个池的名称、活跃块数、总块数、块大小与页数)。

Part Ic:DMA 寻址限制(DMA Mask)

DMA mask是设备可寻址区域的位掩码。换句话说:如果将 DMA mask(按位与操作)作用于某内存区域的 DMA 地址而不清除该地址中的任何位,则设备可以对这片内存执行 DMA。下面所有设置 DMA mask 的函数,在请求的 mask 无法用于该设备、或设备不具备 DMA 能力时都可能失败。

函数作用返回值
int dma_set_mask_and_coherent(struct device *dev, u64 mask)同时更新流式与一致性 DMA mask成功返回 0,失败返回负错误码
int dma_set_mask(struct device *dev, u64 mask)仅更新流式 DMA mask成功返回 0,失败返回负错误码
int dma_set_coherent_mask(struct device *dev, u64 mask)仅更新一致性 DMA mask成功返回 0,失败返回负错误码
u64 dma_get_required_mask(struct device *dev)返回平台高效运行所需的 mask(通常为覆盖全部内存所需的最小 mask)mask 值
size_t dma_max_mapping_size(struct device *dev)返回设备单次映射的最大尺寸,dma_map_single()/dma_map_page()等的size参数不应超过该值字节数
size_t dma_opt_mapping_size(struct device *dev)返回设备映射的最优尺寸上限;对于高频率、短生命周期的流式映射,映射耗时可能占请求生命周期可观比例,若拆分大请求无显著性能损失,建议将流式映射总长度限制在该值内字节数
bool dma_need_sync(struct device *dev, dma_addr_t dma_addr)返回true表示必须调用dma_sync_single_for_{device,cpu}()来转移内存所有权;返回false表示可跳过布尔值
unsigned long dma_get_merge_boundary(struct device *dev)返回 DMA 合并边界;若设备无法合并任何 DMA 地址段则返回 0边界值

源码细节:

  • DMA_BIT_MASK(n)定义为GENMASK_ULL((n) - 1, 0)(include/linux/dma-mapping.h),用于构造 n 位掩码。
  • dma_set_mask_and_coherent()内部先调用dma_set_mask(),成功后再调用dma_set_coherent_mask();由于 API 保证一致性 mask 可以设为与流式 mask 相同或更小,因此实现不检查第二个返回值(include/linux/dma-mapping.h)。
  • 未设置 mask 时,内核默认假设设备只能寻址 32 位 DMA 地址(dma_get_mask()dev->dma_mask为空时返回DMA_BIT_MASK(32),见 include/linux/dma-mapping.h)。
  • 通过dma_get_required_mask()查询所需 mask 并不会改变当前 mask;若想利用该结果,需要自行调用dma_set_mask()设置为返回值。

驱动编写建议(源自 dma-api-howto.rst):

/* 24 位寻址设备 */ if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(24))) { dev_warn(dev, "mydev: No suitable DMA available\n"); goto ignore_this_device; } /* 标准 64 位设备:DMA_BIT_MASK(64) 时 dma_set_mask_and_coherent() 永不失败 */ dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); /* 若设备描述符仅支持 32 位一致性寻址、但流式映射支持完整 64 位 */ if (dma_set_mask(dev, DMA_BIT_MASK(64))) { dev_warn(dev, "mydev: No suitable DMA available\n"); goto ignore_this_device; }

错误写法警示dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))在掩码大于 32 位时不会返回失败,因此如下回退逻辑是错误的:

/* 错误代码:64 位失败回退 32 位 —— 该回退永远不会执行 */ if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)); /* 推荐代码:按能力分支 */ if (support_64bit) dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); else dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));

如果驱动只有一致性分配需求,则必须检查dma_set_coherent_mask()的返回值。若设备的多个功能具有不同的 DMA 寻址限制(例如声卡的播放/录音功能),应逐个探测 mask,并确保最后一次dma_set_mask()调用使用最具体的 mask(完整伪代码见 dma-api-howto.rst)。

Part Id:流式 DMA 映射(Streaming DMA Mappings)

流式 DMA 允许将已有缓冲区映射用于 DMA 传输,完成后解除映射。映射函数不保证成功,返回值必须检查

注意:映射可能因内存不在设备可寻址范围内而失败,例如超出设备 DMA mask 和/或连接的总线桥范围。流式 DMA 函数会尝试克服此类寻址限制:要么使用 IOMMU(将 I/O DMA 地址映射到物理内存地址的设备),要么在内核配置了 SWIOTLB 时通过反弹缓冲区(bounce buffer)拷贝数据。但这些手段并非总是可用,即便可用也可能因多种原因失败。简言之,驱动需要警惕缓冲区在物理内存中的位置,尤其是 DMA mask 小于 32 位时。

DMA 方向枚举

DMA API 对方向使用强类型枚举(定义见 include/linux/dma-direction.h):

枚举值含义
DMA_NONE无方向(用于调试)
DMA_TO_DEVICE数据从内存发往设备
DMA_FROM_DEVICE数据从设备发往内存
DMA_BIDIRECTIONAL方向未知

源码中这四个枚举的实际取值为DMA_BIDIRECTIONAL = 0DMA_TO_DEVICE = 1DMA_FROM_DEVICE = 2DMA_NONE = 3valid_dma_direction()用于校验方向合法性。

单缓冲区映射:dma_map_single() / dma_unmap_single()

dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction direction) void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction)

dma_map_single()将一段处理器虚拟内存映射为设备可访问的内存,并返回其 DMA 地址。解除映射时所有参数必须与dma_map_single()传入(及返回)的完全一致。

注意:连续的虚拟内核空间在物理内存上可能并不连续。由于本 API 不提供 scatter/gather 能力,若尝试映射物理上不连续的内存会失败。因此,本 API 映射的内存应来自保证物理连续性的分配源(如kmalloc())。从源码看,dma_map_single()展开为dma_map_single_attrs(dev, ptr, size, dir, 0)(include/linux/dma-mapping.h),其内联实现会拒绝映射 vmalloc 内存并打印 WARN(dev_WARN_ONCE(dev, is_vmalloc_addr(ptr), "rejecting DMA map of vmalloc memory\n")),随后转化为dma_map_page_attrs()完成实际映射(include/linux/dma-mapping.h)。

警告(缓存行对齐):内存一致性以缓存行宽度为粒度。为使本 API 映射的内存正确工作,映射区域必须恰好从缓存行边界开始并在缓存行边界结束(防止两个独立映射的区域共享一条缓存行)。由于缓存行大小在编译期可能未知,API 不强制该要求。因此,未在运行时专门探测缓存行大小的驱动作者,建议只映射以页边界开始和结束的虚拟区域(页边界保证也是缓存行边界)。

方向同步约束:

  • DMA_TO_DEVICE:必须在软件最后一次修改内存区域之后、移交给设备之前完成同步。一旦使用该原语,设备应将该内存视为只读。若设备可能随时写入,应使用DMA_BIDIRECTIONAL
  • DMA_FROM_DEVICE:必须在驱动访问可能被设备修改的数据之前完成同步。驱动应将该内存视为只读。若驱动可能随时写入,应使用DMA_BIDIRECTIONAL
  • DMA_BIDIRECTIONAL需要特殊处理:驱动不确定内存移交给设备前是否被修改,也不确定设备是否会修改它。因此必须同步两次:一次在内存移交给设备之前(确保所有内存修改从处理器刷新出去),一次在设备使用后访问数据之前(确保处理器缓存行更新为设备可能修改后的数据)。

页面映射:dma_map_page() / dma_unmap_page()

dma_addr_t dma_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction direction) void dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction)

用于映射/解除映射页面。其他映射 API 的所有注意事项与警告同样适用。虽然提供了offsetsize参数用于部分页面映射,但除非你确实了解缓存宽度,否则不建议使用。

MMIO 资源映射:dma_map_resource() / dma_unmap_resource()

dma_addr_t dma_map_resource(struct device *dev, phys_addr_t phys_addr, size_t size, enum dma_data_direction dir, unsigned long attrs) void dma_unmap_resource(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir, unsigned long attrs)

用于映射/解除映射 MMIO 资源。所有其他映射 API 的注意事项与警告同样适用。该 API 只应用于映射设备 MMIO 资源,不允许映射 RAM

错误检查:dma_mapping_error()

int dma_mapping_error(struct device *dev, dma_addr_t dma_addr)

某些情况下dma_map_single()dma_map_page()dma_map_resource()会无法创建映射。驱动可以通过dma_mapping_error()测试返回的 DMA 地址来检查错误:非零返回值表示映射未创建成功,驱动应采取适当措施(如减少当前 DMA 映射用量、延迟稍后重试)。源码中映射失败哨兵值为DMA_MAPPING_ERROR (~(dma_addr_t)0)dma_mapping_error()直接比较该值(include/linux/dma-mapping.h),并在CONFIG_DMA_API_DEBUG使能时调用debug_dma_mapping_error()记录调试信息。

散列表映射:dma_map_sg() / dma_unmap_sg()

int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction) void dma_unmap_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)

将 scatter/gather 列表映射用于 DMA。返回映射得到的 DMA 地址段数,若多个连续的 sglist 条目被合并(例如借助 IOMMU,或某些相邻段恰好物理连续),返回值可能小于传入的nents

  • 已映射过的 sg 不能再次映射,映射过程允许破坏 sg 中的信息。
  • 与其他映射接口一样,dma_map_sg()可能失败,失败时返回 0,驱动必须采取适当措施。关键在于驱动必须有所行动——对块设备驱动而言,中止请求甚至 oops 都好过什么都不做而损坏文件系统。

使用结果映射的标准模式:

int i, count = dma_map_sg(dev, sglist, nents, direction); struct scatterlist *sg; for_each_sg(sglist, sg, count, i) { hw_address[i] = sg_dma_address(sg); hw_len[i] = sg_dma_len(sg); }

其中nents是 sglist 的条目数。实现可自由将多个连续条目合并;返回的count是实际映射的 sg 条目数。之后应循环count次(注意可能小于nents),使用sg_dma_address()sg_dma_len()宏替代原先直接访问sg->addresssg->length的方式。dma_unmap_sg()的所有参数必须与映射调用相同,nents必须是传入映射 API 的那个值,而不是返回的 DMA 地址段数。相关的struct scatterlistsg_table细节参见 include/linux/scatterlist.h 与 lib/scatterlist.c。

同步:dma_sync_*()

void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction direction) void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction direction) void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction) void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)

同步单个连续或 scatter/gather 映射供 CPU 或设备使用。sync_sg 变体的所有参数必须与 sg 映射 API 传入的一致;sync_single 变体允许使用与单映射调用不完全相同的dma_handlesize参数进行部分同步

必须执行的同步时机

  • 在读取设备通过 DMA 写入的值之前(使用DMA_FROM_DEVICE方向);
  • 在写入将被设备通过 DMA 读取的值之后(使用DMA_TO_DEVICE方向);
  • 内存为DMA_BIDIRECTIONAL时,在移交给设备之前和之后都要同步。

从源码看,dma_sync_*内联函数在 include/linux/dma-mapping.h:当CONFIG_DMA_NEED_SYNC使能时,先通过dma_dev_need_sync()判断是否需要同步(跳过dev_dma_skip_sync()标记的设备,但CONFIG_DMA_API_DEBUG使能时始终同步),再调用__dma_sync_*底层实现。若设备无需同步(如完全一致的平台),这些调用会被编译为空操作。

带属性变体:dma_*_attrs()

dma_addr_t dma_map_single_attrs(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir, unsigned long attrs) void dma_unmap_single_attrs(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction dir, unsigned long attrs) int dma_map_sg_attrs(struct device *dev, struct scatterlist *sgl, int nents, enum dma_data_direction dir, unsigned long attrs) void dma_unmap_sg_attrs(struct device *dev, struct scatterlist *sgl, int nents, enum dma_data_direction dir, unsigned long attrs)

这四个函数与不带_attrs后缀的对应函数完全一样,只是额外传递一个可选的dma_attrs。DMA 属性的解释是架构相关的,每个属性都在 Documentation/core-api/dma-attributes.rst 中说明。若dma_attrs为 0,这些函数的语义与无后缀版本完全一致,因此dma_map_single_attrs()通常可以替代dma_map_single()等。

使用示例(以假设的属性DMA_ATTR_FOO为例):

#include <linux/dma-mapping.h> /* DMA_ATTR_FOO 应定义在 linux/dma-mapping.h 中,并在 * Documentation/core-api/dma-attributes.rst 中说明 */ ... unsigned long attr; attr |= DMA_ATTR_FOO; .... n = dma_map_sg_attrs(dev, sg, nents, DMA_TO_DEVICE, attr); ....

关注该属性的架构会在其映射/解除映射例程实现中检查属性位,例如:

void whizco_dma_map_sg_attrs(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction dir, unsigned long attrs) { .... if (attrs & DMA_ATTR_FOO) /* twizzle the frobnozzle */ .... }

内核当前定义的主要属性(完整语义见 Documentation/core-api/dma-attributes.rst):

属性语义
DMA_ATTR_WEAK_ORDERING允许对映射的读写弱排序(读写可以互相越过)
DMA_ATTR_WRITE_COMBINE允许对映射的写操作缓冲以提升性能
DMA_ATTR_NO_KERNEL_MAPPING让平台避免为分配的缓冲区创建内核虚拟映射,缓冲区只能通过dma_mmap_attrs()交给用户空间
DMA_ATTR_SKIP_CPU_SYNC跳过 CPU 缓存同步,假设缓冲区已转入设备域(多设备共享缓冲区时避免重复同步)
DMA_ATTR_FORCE_CONTIGUOUS强制分配在物理内存中连续的缓冲区
DMA_ATTR_ALLOC_SINGLE_PAGES提示分配子系统不必追求大页 TLB 效率
DMA_ATTR_NO_WARN抑制分配失败报告(类似__GFP_NOWARN
DMA_ATTR_PRIVILEGED缓冲区在特权级完全可访问
DMA_ATTR_MMIO指示映射的是 MMIO 区域而非普通内存(P2P 场景),不得缓存
DMA_ATTR_DEBUGGING_IGNORE_CACHELINES允许缓存行重叠(供 DMA 调试时抑制告警)
DMA_ATTR_REQUIRE_COHERENT要求 DMA 一致性,无法与 SWIOTLB/缓存管理共存(用于 RDMA、DRM 等 uAPI)
DMA_ATTR_CC_SHARED机密计算(confidential computing)客户机中的共享(解密)映射
__DMA_ATTR_ALLOC_CC_SHARED内部属性,由分配路径使用,驱动不得传入

Part Ie:基于 IOVA 的 DMA 映射

当使用 IOMMU 时,以下 API 允许非常高效的映射。它们是可选路径,需要额外代码,仅推荐给 DMA 映射性能、或存储 DMA 地址的空间占用至关重要的驱动。上一节的所有注意事项在此同样适用。

bool dma_iova_try_alloc(struct device *dev, struct dma_iova_state *state, phys_addr_t phys, size_t size);

用于尝试为映射操作分配 IOVA 空间。若返回false,说明该 API 无法用于给定设备,应使用常规流式 DMA 映射 API。struct dma_iova_state由驱动分配,且必须保留到解除映射时。该结构体在 include/linux/dma-mapping.h 中定义(含addr__size字段,__size的高位DMA_IOVA_USE_SWIOTLB用于标记是否使用了 swiotlb)。

static inline bool dma_use_iova(struct dma_iova_state *state)

驱动可用它检查在调用dma_iova_try_alloc()后是否实际使用了基于 IOVA 的 API(对解除映射路径很有用)。实现上即检查state->__size != 0(include/linux/dma-mapping.h)。

int dma_iova_link(struct device *dev, struct dma_iova_state *state, phys_addr_t phys, size_t offset, size_t size, enum dma_data_direction dir, unsigned long attrs);

用于将范围链接到之前分配的 IOVA 上。对给定state,除第一次外的所有dma_iova_link()调用,其起始位置必须按dma_get_merge_boundary()返回的 DMA 合并边界对齐;除最后一个范围外,所有范围的尺寸也必须按该边界对齐。

int dma_iova_sync(struct device *dev, struct dma_iova_state *state, size_t offset, size_t size);

必须被调用以同步一个或多个dma_iova_link()调用所映射的 IOVA 范围的 IOMMU 页表。

对于使用一次性(one-shot)映射的驱动,可通过以下调用解除所有范围并释放 IOVA:

void dma_iova_destroy(struct device *dev, struct dma_iova_state *state, size_t mapped_len, enum dma_data_direction dir, unsigned long attrs);

或者,驱动可以动态管理 IOVA 空间,逐个解除映射/映射单个区域。此时使用:

void dma_iova_unlink(struct device *dev, struct dma_iova_state *state, size_t offset, size_t size, enum dma_data_direction dir, unsigned long attrs);

解除之前映射的范围,以及:

void dma_iova_free(struct device *dev, struct dma_iova_state *state);

释放 IOVA 空间。在调用dma_iova_free()之前,必须先用dma_iova_unlink()解除所有范围的映射。这些接口仅在CONFIG_IOMMU_DMA使能时可用,否则退化为返回false/-EOPNOTSUPP的桩实现(见 include/linux/dma-mapping.h)。

Part II:非一致性 DMA 分配

以下 API 允许分配保证可被传入设备 DMA 寻址的页面,但内核与设备之间需要显式管理内存所有权。如果你不理解处理器与 I/O 设备之间缓存行一致性的工作原理,就不应使用这部分 API。

dma_alloc_pages() / dma_free_pages()

struct page * dma_alloc_pages(struct device *dev, size_t size, dma_addr_t *dma_handle, enum dma_data_direction dir, gfp_t gfp) void dma_free_pages(struct device *dev, size_t size, struct page *page, dma_addr_t dma_handle, enum dma_data_direction dir)

分配size字节非一致性内存,返回区域第一个struct page的指针,失败返回NULL。得到的struct page可用于一切struct page适用的场景。同时通过dma_handle返回 DMA 地址。dir参数指明设备是读和/或写数据(细节见dma_map_single())。gfp参数允许指定GFP_标志,但拒绝用于指定内存区域的标志(如GFP_DMAGFP_HIGHMEM)。

在将内存交给设备前,需要调用dma_sync_single_for_device();在读取设备写入的内存前,需要调用dma_sync_single_for_cpu()——与复用的流式 DMA 映射完全一致。释放时devsizedma_handledir必须与传入dma_alloc_pages()的完全相同,page必须是其返回的指针。

int dma_mmap_pages(struct device *dev, struct vm_area_struct *vma, size_t size, struct page *page)

dma_alloc_pages()返回的分配映射到用户地址空间。devsize必须与传入dma_alloc_pages()的相同,page必须是其返回的指针。

dma_alloc_noncoherent() / dma_free_noncoherent()

void * dma_alloc_noncoherent(struct device *dev, size_t size, dma_addr_t *dma_handle, enum dma_data_direction dir, gfp_t gfp) void dma_free_noncoherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle, enum dma_data_direction dir)

dma_alloc_noncoherent()是围绕dma_alloc_pages()的便捷封装,返回分配内存的内核虚拟地址而非 page 结构(源码见 include/linux/dma-mapping.h)。释放时各参数须与分配调用一致。

dma_alloc_noncontiguous() 及其配套接口

struct sg_table * dma_alloc_noncontiguous(struct device *dev, size_t size, enum dma_data_direction dir, gfp_t gfp, unsigned long attrs);

分配size字节非一致性且可能非连续的内存,返回描述已分配并已 DMA 映射内存的struct sg_table指针,失败返回NULL。返回的 sg_table 保证只有 1 个 DMA 映射段(sgt->nents),但 CPU 侧可能有多个段(sgt->orig_nents)。dirgfp语义同上(同样拒绝区域标志)。attrs必须为 0 或DMA_ATTR_ALLOC_SINGLE_PAGES

在将内存交给设备前,需要调用dma_sync_sgtable_for_device();在读取设备写入的数据前,需要调用dma_sync_sgtable_for_cpu()——与复用的流式映射一致。

配套接口:

void dma_free_noncontiguous(struct device *dev, size_t size, struct sg_table *sgt, enum dma_data_direction dir) void * dma_vmap_noncontiguous(struct device *dev, size_t size, struct sg_table *sgt) void dma_vunmap_noncontiguous(struct device *dev, void *vaddr) int dma_mmap_noncontiguous(struct device *dev, struct vm_area_struct *vma, size_t size, struct sg_table *sgt)
  • dma_free_noncontiguous():释放之前分配的内存,参数须与分配调用一致。
  • dma_vmap_noncontiguous():为非连续分配返回连续的(vmap)内核映射。一旦用此函数映射非连续分配,必须使用flush_kernel_vmap_range()invalidate_kernel_vmap_range()API 管理内核映射、设备与用户空间映射(如有)之间的一致性。
  • dma_vunmap_noncontiguous():解除由dma_vmap_noncontiguous()返回的内核映射。
  • dma_mmap_noncontiguous():将分配映射到用户地址空间。

dma_get_cache_alignment()

int dma_get_cache_alignment(void)

返回处理器缓存对齐值。这是映射内存或做部分刷新时必须遵守的绝对最小对齐与宽度

注意:该 API 可能返回比实际缓存行更大的数,但它保证返回的宽度内能恰好容纳一条或多条缓存行,且始终是 2 的幂,便于对齐运算。其默认实现见 include/linux/dma-mapping.h:在定义了ARCH_HAS_DMA_MINALIGN的架构上返回ARCH_DMA_MINALIGN,否则返回 1。

Part III:调试驱动对 DMA API 的使用

上述 DMA API 存在一些约束,例如 DMA 地址必须用对应的函数、以相同的尺寸释放。随着硬件 IOMMU 的普及,驱动违反这些约束的后果越来越严重:最坏情况可能导致数据损坏甚至文件系统被毁。

为了调试驱动、找出 DMA API 使用中的错误,可以将检查代码编译进内核。如果你的架构支持,可以在内核配置中选择"Enable debugging of DMA API usage"(即CONFIG_DMA_API_DEBUG)。启用该选项会有性能影响,不要在生产内核中启用

启用后,内核会包含一段记账代码,记录每个设备分配了哪些 DMA 内存。一旦检测到错误,它会在内核日志中打印包含细节的警告消息。示例警告如下:

WARNING: at /data2/repos/linux-2.6-iommu/lib/dma-debug.c:448 check_unmap+0x203/0x490() Hardware name: forcedeth 0000:00:08.0: DMA-API: device driver frees DMA memory with wrong function [device address=0x00000000640444be] [size=66 bytes] [mapped as single] [unmapped as page] Modules linked in: nfsd exportfs bridge stp llc r8169 Pid: 0, comm: swapper Tainted: G W 2.6.28-dmatest-09289-g8bb99c0 #1 Call Trace: <IRQ> [<ffffffff80240b22>] warn_slowpath+0xf2/0x130 [<ffffffff80647b70>] _spin_unlock+0x10/0x30 ...

驱动开发者可以从中找到出错的驱动、设备以及导致警告的 DMA API 调用栈。

默认情况下,只有第一个错误会产生警告消息,其余错误只被静默计数。此限制是为了防止刷屏内核日志。要支持调试某个驱动,可以通过 debugfs 关闭该限制,详见下面的 debugfs 接口说明。

debugfs 接口:dma-api/

DMA API 调试代码的 debugfs 目录名为dma-api/,其中当前可用的文件如下:

文件说明
dma-api/all_errors包含数值。若该值不为 0,调试代码将对发现的每个错误在内核日志中打印警告。慎用此选项,它很容易刷屏日志
dma-api/disabled只读。若调试代码被禁用(内存耗尽或启动时被禁用)则包含字符Y
dma-api/dump只读,包含当前 DMA 映射
dma-api/error_count只读,显示发现的错误总数
dma-api/num_errors显示在停止前会向内核日志打印多少条警告。系统启动时初始化为 1,可向该文件写入新值修改
dma-api/min_free_entries只读,分配器曾见过的最小空闲dma_debug_entries数量。若该值降到 0,代码会尝试增大nr_total_entries以补偿
dma-api/num_free_entries分配器中当前空闲的dma_debug_entries数量
dma-api/nr_total_entries分配器中dma_debug_entries总数(含空闲与使用中)
dma-api/driver_filter向该文件写入驱动名,可将调试输出限制为该驱动的请求;写入空字符串可禁用过滤器,恢复查看所有错误

启动参数

  • 若内核编译了该调试代码,它默认启用。若希望不带记账代码启动,可提供启动参数dma_debug=off禁用 DMA API 调试。注意:运行时无法重新启用,必须重启。
  • 若只想查看某个特殊设备驱动的调试消息,可指定启动参数dma_debug_driver=<drivername>,它会在启动时启用驱动过滤器,此后调试代码只打印该驱动的错误。过滤器之后可通过 debugfs 禁用或更改。
  • 当代码在运行时自行禁用,很可能是dma_debug_entries耗尽且无法按需分配更多。启动时预分配65536个条目;若对你来说太低,可用dma_debug_entries=<your_desired_number>覆盖默认值。注意代码按批次分配条目,因此实际预分配数可能大于请求数。每当动态分配数量达到初始预分配量时,代码会向内核日志打印一次提示,表示可能需要更大的预分配值;若持续发生,则说明某驱动可能在泄漏映射。

debug_dma_mapping_error()

void debug_dma_mapping_error(struct device *dev, dma_addr_t dma_addr);

这是 dma-debug 接口,用于调试未检查dma_map_single()dma_map_page()返回地址映射错误的驱动。该接口清除由debug_dma_map_phys()设置的标志,以表明驱动已调用dma_mapping_error()。当驱动解除映射时,debug_dma_unmap()检查该标志;若仍被设置,则打印包含直至 unmap 调用栈的警告消息。该接口可从dma_mapping_error()例程中调用,以启用 DMA 映射错误检查调试(dma_mapping_error()内联实现中确实调用了它,见 include/linux/dma-mapping.h)。

相关文档与进一步阅读

  • Documentation/core-api/dma-api-howto.rst:面向驱动开发者的渐进式入门指南,含 CPU/DMA 地址空间图解、dma_set_mask完整示例、coherent 与 streaming 映射的选择依据及完整伪代码。
  • Documentation/core-api/dma-attributes.rst:每个 DMA 属性的详细语义定义。
  • Documentation/core-api/swiotlb.rst:软件 IOMMU / 反弹缓冲区机制说明。
  • 头文件 include/linux/dma-mapping.h 与 include/linux/dmapool.h:全部接口声明、内联实现与宏定义。
  • 实现源码 mm/dmapool.c:DMA pool 分配器实现。
  • 数据结构 include/linux/scatterlist.h 与 lib/scatterlist.c:scatterlist / sg_table 相关的sg_dma_address()sg_dma_len()等宏与辅助函数。

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 2:40:16

EditPlus便携版高效配置:语法高亮、FTP与正则实战

简介&#xff1a;EditPlus是一款在程序员与Web开发者中广泛使用的轻量级文本编辑器&#xff0c;安装包直接解压即可运行&#xff0c;适合需要高效编写HTML、CSS、JS、Python、Java等代码的初学者与进阶用户。压缩包共51个文件&#xff0c;大小仅1.68MB&#xff0c;内含主程序ex…

作者头像 李华
网站建设 2026/9/15 2:39:28

MAX98357A实战:I2S音频功放从接线到排坑全攻略

做桌面小主机时想给它加个外置功放&#xff0c;做毕业设计想给语音识别模块接个喇叭&#xff0c;或者单纯想把树莓派变成网络电台……只要搜过“I2S音频功放”&#xff0c;MAX98357A这颗芯片基本都会出现在你的搜索结果前列。我用它做过两个小项目&#xff0c;第一次通电没声&a…

作者头像 李华
网站建设 2026/9/15 2:39:24

系统宕机别甩锅黑客:IT基础管理才是真命门

先讲个我经历过好几次的场景&#xff1a;周五下午四点半&#xff0c;业务群突然炸了&#xff0c;财务说报表打不开&#xff0c;仓库说 WMS 系统登不进去&#xff0c;销售在客户面前盯着大屏幕转圈。老板冲过来第一句话就是&#xff1a;“是不是被攻击了&#xff1f;赶紧找安全厂…

作者头像 李华
网站建设 2026/9/15 2:36:43

Unity 3D游戏设计与实现:从场景搭建到完整交付的实战指南

做Unity 3D游戏开发这些年&#xff0c;我接过最多的项目类型&#xff0c;不是那种大型商业游戏&#xff0c;反而是“基于Unity 3D的游戏设计与实现”这类带完整交付物的作品级项目。客户通常不只要一个能跑的工程&#xff0c;还要设计源文件、万字设计报告、讲解演示&#xff0…

作者头像 李华
网站建设 2026/9/15 2:36:24

东莞做网站首选企业铭哪家好?3个方案帮你看清报价猫腻

东莞做网站首选企业铭哪家好?3个方案帮你看清报价猫腻 域名备案卡壳、服务器选型迷茫,这种“域名服务器搞不懂”的焦虑,是不是让你在看建站方案时心里没底?很多老板在东莞找建站公司,最怕的不是价格贵,而是报价单上一堆看不懂的术语,比如“高防IP”、“CDN加速”、“独立部署”,问客服就告诉你“这个必须配”…

作者头像 李华