Linux 内核动态 DMA 映射完全指南:从地址模型到驱动实践(DMA API HOWTO)
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
本文基于 Linux 内核源码树中的 Documentation/core-api/dma-api-howto.rst 编写,是面向设备驱动开发者的动态 DMA 映射实战指南。文章系统讲解 CPU 虚拟地址、物理地址与总线地址的区别,覆盖 DMA mask 设置、一致性映射(coherent)与流式映射(streaming)两大 API 族,并给出完整的错误处理与性能优化范式。读完本文,你将掌握在 Linux 驱动中安全、高效地使用dma_alloc_coherent()、dma_map_single()、dma_map_sg()等核心接口的完整方法论,并理解其背后的 IOMMU 与缓存一致性原理。
地址模型:为什么驱动不能直接把指针交给设备
DMA API 使用中最大的认知障碍在于地址空间的多样性。Linux 内核涉及三种相互独立、需要通过映射关系联系的地址:
- CPU 虚拟地址(Virtual Address):
kmalloc()、vmalloc()等接口返回的地址,可安全保存在void *指针中,由 TLB、页表等虚拟内存系统翻译。 - CPU 物理地址(Physical Address):以
phys_addr_t或resource_size_t存储,出现在/proc/iomem中。驱动不能直接使用物理地址访问设备资源,必须先通过ioremap()得到虚拟地址。 - 总线地址(Bus Address):I/O 设备进行 MMIO 访问或 DMA 读写时使用的地址。在部分系统上与 CPU 物理地址相同,但一般而言并不相同——IOMMU 和 host bridge 可以在物理地址与总线地址之间建立任意映射。从设备视角看,DMA 使用总线地址空间,且往往被限制在该空间的某个子集(例如系统支持 64 位地址,但通过 IOMMU 只让设备使用 32 位 DMA 地址)。
原文档用一张经典示意图展示了三者关系:CPU 通过虚拟映射访问 MMIO 空间(C→B)与 RAM 缓冲区(X→Y),而设备通过 host bridge 的偏移与 IOMMU 的映射访问总线地址(A、Z)。当设备支持 DMA 时,驱动用kmalloc()等接口分配缓冲区得到虚拟地址 X,虚拟内存系统将 X 映射到 RAM 中的物理地址 Y;设备不能直接使用 X,因为 DMA 不经过 CPU 虚拟内存系统。在存在 IOMMU 的系统中,IOMMU 把设备使用的总线地址 Z 翻译为物理地址 Y——这正是dma_map_single()等接口存在的意义:驱动把虚拟地址 X 交给它,它建立所需的 IOMMU 映射并返回 DMA 地址 Z,驱动再让设备对 Z 发起 DMA。
需要特别强调的是,动态 DMA 映射要求驱动只在 DMA 传输实际进行期间保持映射,传输结束后必须解除映射。这套 API 在完全没有此类硬件的平台上同样可用。
此外,DMA API 与具体总线无关,应优先使用通用的dma_map_*()接口,而不是pci_map_*()等总线专用接口。在使用任何 DMA 接口前,驱动必须包含头文件:
#include <linux/dma-mapping.h>该头文件定义了dma_addr_t类型,它能容纳平台上任何合法的 DMA 地址,凡是保存 DMA 映射函数返回值的地方都应使用该类型。
什么内存可以做 DMA
并非所有内核内存都能用于 DMA,这是驱动开发者必须牢记的第一条规则:
- 可以 DMA 的内存:通过页分配器(
__get_free_page*())或通用内存分配器(kmalloc()、kmem_cache_alloc())获得的内存。 - 不可以 DMA 的内存:
vmalloc()返回的内存(虽然可以对 vmalloc 区域底层映射的物理页做 DMA,但需要遍历页表取物理地址,再用__va()之类的手段转回内核地址,复杂度极高);- 内核镜像地址(data/text/bss 段)、模块镜像地址、栈地址。这些地址可能被映射到与其余物理内存完全不同的位置,即便物理上可行,也必须保证 I/O 缓冲区按 cache line 对齐,否则在 DMA 不一致缓存的 CPU 上会因 cache line 共享导致数据损坏(CPU 写一个 word,DMA 写同一 cache line 的另一个 word,其中一方被覆盖);
kmap()的返回值,理由与vmalloc()类似。
- 由子系统保证:块 I/O 与网络子系统会确保其使用的缓冲区对 DMA 是合法的,驱动可以直接使用。
结构体内的 DMA 缓冲区隔离:__dma_from_device_group_begin/end
当一个结构体既包含DMA_FROM_DEVICE/DMA_BIDIRECTIONAL缓冲区(设备写入内存),又包含 CPU 写入的字段时,缓存一致性较弱的平台上会因两者共享 cache line 而出现数据损坏。为此内核提供了一对标注宏__dma_from_device_group_begin(GROUP)/__dma_from_device_group_end(GROUP),通过保证对齐来隔离缓冲区,保护其头尾两端不与相邻字段共享 cache line:
struct my_device { spinlock_t lock1; __dma_from_device_group_begin(); char dma_buffer1[16]; char dma_buffer2[16]; __dma_from_device_group_end(); spinlock_t lock2; };GROUP是可选标识符,用于在同一结构体内区分多个 DMA 缓冲区组:
struct my_device { spinlock_t lock1; __dma_from_device_group_begin(buffer1); char dma_buffer1[16]; __dma_from_device_group_end(buffer1); spinlock_t lock2; __dma_from_device_group_begin(buffer2); char dma_buffer2[16]; __dma_from_device_group_end(buffer2); };在缓存一致的平台上,这些宏展开为零长度数组标记;在非一致平台上,它们还会保证最小的 DMA 对齐(最大可达 128 字节)。文档同时提醒:组内可以包含并非用于设备 DMA 的额外字段以紧凑打包结构体,但这较脆弱——仅当组内任何字段被映射为DMA_FROM_DEVICE或DMA_BIDIRECTIONAL期间 CPU 不写这些字段时才是安全的。
DMA 寻址能力:正确设置 DMA mask
内核默认假设设备只能寻址 32 位 DMA 地址。64 位能力的设备需要调大,受限设备则需要调小。正确设置 DMA mask 是设备正常工作的前提。
最常用的是同时设置流式与一致性两种 mask 的接口:
int dma_set_mask_and_coherent(struct device *dev, u64 mask);特殊需求下也可分开调用:
int dma_set_mask(struct device *dev, u64 mask); // 流式映射 int dma_set_coherent_mask(struct device *dev, u64 mask); // 一致性分配其中dev是设备的struct device *,通常取自总线设备结构体(PCI 设备即&pdev->dev);mask描述设备支持的地址位宽。调用成功返回 0;若 mask 过小、平台无法支持,则返回非零——此时不得对该设备使用 DMA,否则属于未定义行为。失败时的两条出路:改用非 DMA 数据传输模式,或忽略该设备不予初始化。建议失败时打印KERN_WARNING级别告警,便于用户回报内核日志定位问题。
原文档给出了几类典型设备的写法。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_set_mask_and_coherent(dev, DMA_BIT_MASK(64))值得注意:dma_set_mask_and_coherent()在 mask 大于 32 位时几乎不会失败,因此不要写出"失败后回退到 32 位"的错误代码:
/* Wrong code */ if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));推荐写法是直接按设备能力分支:
/* Recommended code */ if (support_64bit) dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); else dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));对于描述符的一致性分配只支持 32 位、流式映射支持完整 64 位的设备,应只调用dma_set_mask();只有驱动只用一致性分配时,才需要检查dma_set_coherent_mask()的返回值(一致性 mask 恒可设为与流式 mask 相同或更小)。
dma_set_mask()/dma_set_mask_and_coherent()成功后,内核会保存该 mask,并在后续 DMA 映射时使用。在 include/linux/dma-mapping.h 中可以看到dma_set_mask_and_coherent()的实现:先调用dma_set_mask(),成功后再调用dma_set_coherent_mask()——这也从源码层面印证了"一致性 mask 可以设为与流式 mask 相同或更小"的保证。DMA_BIT_MASK(n)在 同一头文件 定义为GENMASK_ULL((n) - 1, 0),即低 n 位全 1 的掩码。
多功能设备的分级探测
若设备支持多个功能(如声卡的播放与录音),且各功能 DMA 寻址能力不同,应分别探测各 mask,只提供当前机器能支持的功能。最后一次dma_set_mask()调用必须对应最严格的 mask。原文档给出的声卡伪代码:
#define PLAYBACK_ADDRESS_BITS DMA_BIT_MASK(32) #define RECORD_ADDRESS_BITS DMA_BIT_MASK(24) struct my_sound_card *card; struct device *dev; ... if (!dma_set_mask(dev, PLAYBACK_ADDRESS_BITS)) { card->playback_enabled = 1; } else { card->playback_enabled = 0; dev_warn(dev, "%s: Playback disabled due to DMA limitations\n", card->name); } if (!dma_set_mask(dev, RECORD_ADDRESS_BITS)) { card->record_enabled = 1; } else { card->record_enabled = 0; dev_warn(dev, "%s: Record disabled due to DMA limitations\n", card->name); }这类声卡在历史上很常见——ISA 芯片加 PCI 前端,因而保留了 ISA 时代 16MB(24 位)的 DMA 寻址限制。
两类 DMA 映射:一致性与流式
DMA 映射分为两类,理解其差异是正确选型的前提。
一致性映射(Coherent DMA mappings):通常在驱动初始化时建立、结束时解除,硬件保证设备与 CPU 可以并行访问数据、无需软件显式刷新即可看到对方的更新。可以理解为"同步"(synchronous)。典型用途:
- 网卡 DMA 环形描述符;
- SCSI 适配器 mailbox 命令数据结构;
- 从主存执行固件微码的设备。
这些场景的共同不变量是"CPU 的任何存储立即可见,反之亦然"。默认返回低 32 位 DMA 空间的内存,但为兼容未来平台,即使默认值够用也建议显式设置 coherent mask。注意:一致性 DMA 内存并不免除内存屏障——CPU 对一致性内存的存储重排与普通内存无异。例如设备必须先看到描述符第一个 word 再看到第二个,就必须写:
desc->word0 = address; wmb(); desc->word1 = DESC_VALID;部分平台上驱动可能还需要像刷新 PCI 桥写缓冲那样(写完寄存器后再读一次)刷新 CPU 写缓冲。
流式映射(Streaming DMA mappings):通常为单次 DMA 传输建立、传输结束后立即解除(除非使用dma_sync_*系列),硬件可以针对顺序访问优化。可以理解为"异步"或"处于一致性域之外"。典型用途:
- 设备收发网络缓冲区;
- SCSI 设备读写文件系统缓冲区。
使用流式映射时必须显式声明期望的行为,实现才能做硬件允许的性能优化。两类映射都没有来自底层总线的对齐限制(个别设备可能有),缓存不一致的系统在缓冲区不与其他数据共享 cache line 时工作得更好。
使用一致性 DMA 映射
分配并映射大块(约一个 PAGE_SIZE 或更大)一致性区域:
dma_addr_t dma_handle; cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);其中dev是struct device *,size是要分配区域的字节数。该例程的行为类似__get_free_pages()(按 size 而非 page order),可在中断上下文以GFP_ATOMIC调用。如果驱动需要小于一页的区域,优先使用下面介绍的dma_pool接口。
dma_alloc_coherent()返回两个值:CPU 访问用的虚拟地址,以及交给设备的dma_handle。CPU 虚拟地址与 DMA 地址都保证按"不小于请求 size 的最小 PAGE_SIZE 阶"对齐——例如分配不超过 64KB 的块,得到的缓冲区不会跨越 64K 边界。在 include/linux/dma-mapping.h 中可以看到它是对dma_alloc_attrs()的封装。
解除映射并释放:
dma_free_coherent(dev, size, cpu_addr, dma_handle);参数与分配时对应,此函数不可在中断上下文调用。
dma_pool:大批量小对象的分配
需要大量小内存区域时,可以自己细分dma_alloc_coherent()返回的页,也可以使用dma_pool。dma_pool类似kmem_cache,但底层用dma_alloc_coherent()而非__get_free_pages(),并且理解常见的硬件对齐约束(如队列头需要按 N 字节边界对齐)。其接口声明在 include/linux/dmapool.h。
创建:
struct dma_pool *pool; pool = dma_pool_create(name, dev, size, align, boundary);name:诊断用名称(类似 kmem_cache 名称);dev、size:同上;align:硬件对本类数据的对齐要求,单位字节,必须是 2 的幂;boundary:无边界跨越限制时传 0;传 4096 表示池中内存不得跨越 4K 边界(此时或许直接用dma_alloc_coherent()更合适)。
分配:
cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);flags在允许阻塞时(不在中断中、不持 SMP 锁)为GFP_KERNEL,否则为GFP_ATOMIC。同样返回cpu_addr与dma_handle两个值。
释放与销毁:
dma_pool_free(pool, cpu_addr, dma_handle); // 可在中断上下文调用 dma_pool_destroy(pool); // 不可在中断上下文调用销毁池之前必须释放池中分配的所有内存。
DMA 方向
流式映射接口需要方向参数,取值为以下整数(定义于 include/linux/dma-direction.h):
DMA_BIDIRECTIONAL 双向 DMA_TO_DEVICE 从主存到设备 DMA_FROM_DEVICE 从设备到主存 DMA_NONE 调试用方向指 DMA 传输中数据移动的方向。应尽可能精确指定:
- 精确指定能带来平台相关的优化,也便于调试——部分平台为 DMA 映射提供写权限位(类似用户地址空间的页保护),DMA 控制器检测到违反权限设置时会在内核日志中报告错误;
- 无法确定方向时用
DMA_BIDIRECTIONAL,平台保证其合法可用,但可能牺牲性能; DMA_NONE用于在确定方向之前占位,帮助捕获方向跟踪逻辑未正确初始化的 bug。
只有流式映射需要指定方向,一致性映射隐式具有DMA_BIDIRECTIONAL属性。子系统会告知方向:SCSI 子系统在命令的sc_data_direction成员中给出;网络驱动发送包用DMA_TO_DEVICE、接收包用DMA_FROM_DEVICE。
使用流式 DMA 映射
流式映射例程可在中断上下文调用。每组 map/unmap 都有单区域与 scatterlist 两个版本。
单区域映射:dma_map_single / dma_map_page
struct device *dev = &my_dev->dev; dma_addr_t dma_handle; void *addr = buffer->ptr; size_t size = buffer->len; dma_handle = dma_map_single(dev, addr, size, direction); if (dma_mapping_error(dev, dma_handle)) { /* * reduce current DMA mapping usage, * delay and try again later or * reset driver. */ goto map_error_handling; }解除映射:
dma_unmap_single(dev, dma_handle, size, direction);必须调用dma_mapping_error()检查返回值——dma_map_single()可能失败并返回错误值。不检查就使用返回地址,轻则 panic,重则静默数据损坏。dma_map_page()同理。应在 DMA 活动结束后(例如收到 DMA 完成中断时)调用dma_unmap_single()。
dma_map_single()的问题在于无法引用 HIGHMEM 内存,因此提供了以 page/offset 对替代 CPU 指针的接口:
struct device *dev = &my_dev->dev; dma_addr_t dma_handle; struct page *page = buffer->page; unsigned long offset = buffer->offset; size_t size = buffer->len; dma_handle = dma_map_page(dev, page, offset, size, direction); if (dma_mapping_error(dev, dma_handle)) { /* * reduce current DMA mapping usage, * delay and try again later or * reset driver. */ goto map_error_handling; } ... dma_unmap_page(dev, dma_handle, size, direction);这里offset是页内字节偏移。dma_unmap_page()同样在 DMA 活动结束后调用。在 include/linux/dma-mapping.h 中可以看到dma_map_single(d, a, s, r)与dma_map_page(d, p, o, s, r)分别是dma_map_single_attrs()/dma_map_page_attrs()带attrs=0的宏封装,而dma_map_single_attrs()的实质是把 CPU 指针换算为 page/offset 后委托给dma_map_page_attrs(),这也解释了单区域接口与 page 接口在实现上的统一。
scatterlist 映射:dma_map_sg
将多个区域聚合映射:
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 的条目数。实现有权把若干连续的 sg 条目合并为一个(例如以 PAGE_SIZE 粒度映射时,前一条目结尾与后一条目起始都在页边界上即可合并——这对不能做 scatter-gather 或条目数受限的网卡是巨大优势),并返回实际映射到的条目数;失败返回 0。因此必须按返回的count循环(可能小于nents),用sg_dma_address()与sg_dma_len()宏取值——这两个宏在 include/linux/scatterlist.h 中定义,分别返回条目的 DMA 地址与映射后长度。
解除 scatterlist 映射:
dma_unmap_sg(dev, sglist, nents, direction);务必等 DMA 活动结束后调用,且传给dma_unmap_sg()的nents必须与传给dma_map_sg()的相同,而不是它返回的count。同样,每个dma_map_{single,sg}()都必须有对应的dma_unmap_{single,sg}()——DMA 地址空间是共享资源,耗尽所有 DMA 地址会让机器不可用。
dma_sync_*:重复使用流式缓冲区
需要多次使用同一流式 DMA 区域、且传输之间 CPU 要访问数据时,缓冲区必须正确同步。流程是:先dma_map_{single,sg}()建立映射;每次 DMA 传输后调用:
dma_sync_single_for_cpu(dev, dma_handle, size, direction); // 或 dma_sync_sg_for_cpu(dev, sglist, nents, direction);CPU 访问结束后、把缓冲区再次交给硬件前调用:
dma_sync_single_for_device(dev, dma_handle, size, direction); // 或 dma_sync_sg_for_device(dev, sglist, nents, direction);注意:dma_sync_sg_for_cpu()/dma_sync_sg_for_device()的nents参数同样必须是传给dma_map_sg()的那个值,而非其返回值。最后一次传输结束后调用dma_unmap_{single,sg}();如果从首次dma_map_*()到dma_unmap_*()期间 CPU 从不触碰数据,则完全不需要dma_sync_*()。
原文档给出的收包伪代码完整展示了这一范式:设置接收缓冲区时用dma_map_single(..., DMA_FROM_DEVICE)建立映射;中断处理器中收到RX_BUF_TRANSFERRED后先dma_sync_single_for_cpu()再检查头部;接受数据则dma_unmap_single()并上交上层、重新设置接收缓冲区,拒绝则直接把缓冲区交还网卡(对DMA_FROM_DEVICE映射区域 CPU 不应写入,故无需dma_sync_single_for_device();DMA_BIDIRECTIONAL映射若内存被修改则需要)。
错误处理
DMA 地址空间在部分架构上有限,分配失败可通过以下途径检测:
dma_alloc_coherent()返回 NULL,或dma_map_sg()返回 0;- 用
dma_mapping_error()检查dma_map_single()/dma_map_page()返回的dma_addr_t:
dma_addr_t dma_handle; dma_handle = dma_map_single(dev, addr, size, direction); if (dma_mapping_error(dev, dma_handle)) { /* * reduce current DMA mapping usage, * delay and try again later or * reset driver. */ goto map_error_handling; }多页映射中途失败时,必须解除此前已建立的映射。示例 1(两个独立映射):
dma_addr_t dma_handle1; dma_addr_t dma_handle2; dma_handle1 = dma_map_single(dev, addr, size, direction); if (dma_mapping_error(dev, dma_handle1)) { goto map_error_handling1; } dma_handle2 = dma_map_single(dev, addr, size, direction); if (dma_mapping_error(dev, dma_handle2)) { goto map_error_handling2; } ... map_error_handling2: dma_unmap_single(dma_handle1); map_error_handling1:示例 2(循环分配中途失败,回滚所有已映射缓冲区):
dma_addr_t dma_addr; dma_addr_t array[DMA_BUFFERS]; int save_index = 0; for (i = 0; i < DMA_BUFFERS; i++) { ... dma_addr = dma_map_single(dev, addr, size, direction); if (dma_mapping_error(dev, dma_addr)) { goto map_error_handling; } array[i].dma_addr = dma_addr; save_index++; } ... map_error_handling: for (i = 0; i < save_index; i++) { ... dma_unmap_single(array[i].dma_addr); }子系统还有各自的失败约定:网络驱动在发送钩子(ndo_start_xmit)中 DMA 映射失败时,必须调用dev_kfree_skb()释放 socket buffer 并返回NETDEV_TX_OK(即失败时直接丢弃该包);SCSI 驱动在queuecommand钩子中映射失败时返回SCSI_MLQUEUE_HOST_BUSY,SCSI 子系统稍后会重新提交该命令。
优化 Unmap 状态的空间占用
许多平台上dma_unmap_{single,page}()只是空操作,保存映射地址与长度会浪费状态空间。内核提供了宏族,避免用 ifdef 打补丁破坏 API 可移植性:
- 在状态保存结构体中使用
DEFINE_DMA_UNMAP_ADDR/DEFINE_DMA_UNMAP_LEN:
struct ring_state { struct sk_buff *skb; DEFINE_DMA_UNMAP_ADDR(mapping); DEFINE_DMA_UNMAP_LEN(len); };- 用
dma_unmap_addr_set()/dma_unmap_len_set()写入:
dma_unmap_addr_set(ringp, mapping, FOO); dma_unmap_len_set(ringp, len, BAR);- 用
dma_unmap_addr()/dma_unmap_len()读取:
dma_unmap_single(dev, dma_unmap_addr(ringp, mapping), dma_unmap_len(ringp, len), DMA_FROM_DEVICE);ADDR 与 LEN 分开处理,是因为某些实现只需地址即可完成 unmap。
平台移植注意事项
面向架构移植者的两点要求(普通驱动作者可跳过):
CONFIG_NEED_SG_DMA_LENGTH:架构支持 IOMMU(含软件 IOMMU)时,需要开启该配置以满足struct scatterlist的要求。ARCH_DMA_MINALIGN:架构必须保证kmalloc的缓冲区对 DMA 安全,驱动与子系统依赖这一点。若架构并非完全 DMA 一致(硬件不能保证 CPU 缓存与主存数据一致),必须设置ARCH_DMA_MINALIGN,使内存分配器保证kmalloc缓冲区不与其它数据共享 cache line。参考实现见 arch/arm/include/asm/cache.h。注意它只关乎 DMA 内存对齐约束,与架构的数据对齐约束(如 64 位对象的对齐)无关。
小结
动态 DMA 映射是 Linux 设备驱动与硬件交互的基石。掌握三条主线即可写出正确驱动:一是区分虚拟地址、物理地址与总线地址,理解 IOMMU 的翻译作用;二是通过dma_set_mask_and_coherent()正确声明设备寻址能力;三是严格区分一致性映射(dma_alloc_coherent()/dma_pool,面向描述符等长生命周期结构)与流式映射(dma_map_single()/dma_map_page()/dma_map_sg()+dma_sync_*,面向单次传输的数据缓冲),并始终遵循"映射必查错、用完必解除、nents保持一致"的纪律。更简洁的 API 速查可参考 Documentation/core-api/dma-api.rst。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考