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)释放之前分配的一致性内存区域。dev、size和dma_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()的封装(node取NUMA_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 = 0、DMA_TO_DEVICE = 1、DMA_FROM_DEVICE = 2、DMA_NONE = 3;valid_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 的所有注意事项与警告同样适用。虽然提供了offset和size参数用于部分页面映射,但除非你确实了解缓存宽度,否则不建议使用。
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->address和sg->length的方式。dma_unmap_sg()的所有参数必须与映射调用相同,且nents必须是传入映射 API 的那个值,而不是返回的 DMA 地址段数。相关的struct scatterlist与sg_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_handle和size参数进行部分同步。
必须执行的同步时机:
- 在读取设备通过 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_DMA或GFP_HIGHMEM)。
在将内存交给设备前,需要调用dma_sync_single_for_device();在读取设备写入的内存前,需要调用dma_sync_single_for_cpu()——与复用的流式 DMA 映射完全一致。释放时dev、size、dma_handle和dir必须与传入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()返回的分配映射到用户地址空间。dev、size必须与传入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)。dir、gfp语义同上(同样拒绝区域标志)。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),仅供参考