Linux DMA 详解:原理、子系统与实践指南
在计算机系统中,数据传输是核心操作之一。传统模式下,CPU 需要全程参与外设与内存之间的数据搬运,这会占用大量 CPU 资源,导致系统效率低下。直接内存访问(DMA,Direct Memory Access) 技术应运而生:它允许外设通过 DMA 控制器直接访问系统内存,无需 CPU 干预,从而显著提升数据传输效率和系统吞吐量。
Linux 作为多任务操作系统,需要高效管理 DMA 资源以支持各类硬件设备(如网卡、磁盘控制器、GPU 等)。Linux 内核提供了一套完善的 DMA 子系统,包括统一的 API、地址空间管理、缓存一致性处理等机制,简化了驱动开发并保证了跨硬件平台的兼容性。
本文将深入解析 Linux DMA 的原理、子系统架构、编程接口、最佳实践,并通过示例演示如何在 kernel 模块中使用 DMA。
目录#
- 简介
- DMA 基础原理
- 2.1 DMA 的工作流程
- 2.2 DMA 的优势与应用场景
- Linux 中的 DMA 子系统
- Linux DMA 类型
- Linux DMA 编程接口
- 5.1 一致性 DMA 接口
- 5.2 流式 DMA 接口
- 5.3 散射-聚集 DMA 接口
- 最佳实践与常见问题
- 示例:Linux 内核模块中的 DMA 使用
- 总结
- 参考资料
1. 简介#
在计算机系统中,数据传输是核心操作之一。传统模式下,CPU 需要全程参与外设与内存之间的数据搬运,这会占用大量 CPU 资源,导致系统效率低下。直接内存访问(DMA,Direct Memory Access) 技术应运而生:它允许外设通过 DMA 控制器直接访问系统内存,无需 CPU 干预,从而显著提升数据传输效率和系统吞吐量。
Linux 作为多任务操作系统,需要高效管理 DMA 资源以支持各类硬件设备(如网卡、磁盘控制器、GPU 等)。Linux 内核提供了一套完善的 DMA 子系统,包括统一的 API、地址空间管理、缓存一致性处理等机制,简化了驱动开发并保证了跨硬件平台的兼容性。
本文将深入解析 Linux DMA 的原理、子系统架构、编程接口、最佳实践,并通过示例演示如何在 kernel 模块中使用 DMA。
2. DMA 基础原理#
2.1 DMA 的工作流程#
DMA 传输的核心是 DMA 控制器(DMA Controller),它是一个独立于 CPU 的硬件组件,负责协调外设与内存的数据传输。典型工作流程如下:
- 请求阶段:外设(如网卡)需传输数据时,向 DMA 控制器发送 DMA 请求(DREQ)。
- 授权阶段:DMA 控制器向 CPU 发送总线请求(HRQ),CPU 响应并释放总线控制权(HLDA)。
- 传输阶段:DMA 控制器接管总线,直接在内存与外设间传输数据(通过指定内存地址、外设地址、传输长度和方向)。
- 完成阶段:传输结束后,DMA 控制器向 CPU 发送中断,通知传输完成,CPU 恢复总线控制权。

(注:实际硬件架构可能有差异,例如集成在 SoC 中的 DMA 控制器可能省略总线仲裁步骤)
2.2 DMA 的优势与应用场景#
优势:
- 降低 CPU 负载:CPU 无需参与数据搬运,可专注于计算任务。
- 提升吞吐量:DMA 传输速度通常高于 CPU 逐字节搬运,尤其适用于大数据量场景。
- 减少延迟:避免 CPU 中断处理的上下文切换开销。
典型应用场景:
- 存储设备(SATA/SCSI 硬盘、NVMe SSD):通过 DMA 读写磁盘数据。
- 网络设备(以太网控制器):通过 DMA 接收/发送网络数据包。
- 图形设备(GPU):通过 DMA 访问显存与系统内存。
- 音频设备(声卡):通过 DMA 传输音频流数据。
3. Linux 中的 DMA 子系统#
Linux 内核 DMA 子系统的目标是 抽象硬件差异,为驱动开发者提供统一的 DMA 操作接口,并处理复杂的内存管理(如虚拟内存、缓存一致性、地址转换等)。
3.1 核心组件#
-
DMA API 层
内核提供的用户接口(如dma_alloc_coherent、dma_map_single),屏蔽底层硬件细节,支持跨架构移植。 -
DMA 控制器驱动
针对具体 DMA 硬件的驱动(如drivers/dma/目录下的驱动),实现 DMA 传输的底层控制逻辑。 -
DMA 引擎框架
统一管理不同厂商的 DMA 控制器,提供标准化的 DMA 操作(如启动/停止传输、链式操作)。 -
IOMMU(输入输出内存管理单元)
可选组件,负责 DMA 地址到物理地址的转换,支持:- 地址扩展:允许 32 位设备访问 64 位系统内存。
- 隔离保护:防止设备越权访问内存。
- 散射-聚集优化:支持非连续内存的 DMA 传输。
3.2 DMA 地址空间#
Linux 中存在多种地址类型,需明确区分:
| 地址类型 | 定义 | 使用者 |
|---|---|---|
| 虚拟地址(VA) | CPU 视角的内存地址,需 MMU 转换 | 内核/用户态 |
| 物理地址(PA) | 内存硬件的实际地址 | DMA 控制器 |
| DMA 地址(DA) | 设备视角的地址(可能等于 PA 或经 IOMMU 转换) | 外设 |
关键问题:部分设备仅支持有限的地址空间(如 32 位设备只能访问 ≤4GB 的内存)。Linux 通过以下机制解决:
dma_set_mask(dev, mask):设置设备支持的 DMA 地址掩码(如DMA_BIT_MASK(32)表示 32 位地址)。dma_alloc_coherent等接口会自动从设备可访问的内存区域分配内存。
4. Linux DMA 类型#
根据使用场景,Linux 内核将 DMA 分为三类:
4.1 一致性 DMA(Coherent DMA)#
定义:CPU 与 DMA 控制器对内存的访问始终保持一致(无缓存不一致问题)。
特点:
- 内存区域在分配时即保证物理连续,且对 CPU 和 DMA 可见。
- 通常用于 长期存在的缓冲区(如设备固件、环形缓冲区)。
- 实现方式:要么禁用缓存(低速),要么依赖硬件缓存一致性(如 ARM 的CCI 或 x86 的 MESI 协议)。
适用场景:设备需要持续访问的缓冲区(如网卡的接收环形缓冲区)。
4.2 流式 DMA(Streaming DMA)#
定义:临时用于单次数据传输的 DMA,传输前后需显式同步 CPU 缓存与内存。
特点:
- 内存区域可临时映射给 DMA 使用,传输完成后解除映射。
- 需指定传输方向(如
DMA_TO_DEVICE、DMA_FROM_DEVICE),内核自动处理缓存刷新/失效。 - 适用于 短期、单次的数据传输(如磁盘读写单个扇区)。
传输方向:
DMA_TO_DEVICE:数据从内存传输到设备(CPU 先写内存,DMA 读内存)。DMA_FROM_DEVICE:数据从设备传输到内存(DMA 写内存,CPU 后读内存)。DMA_BIDIRECTIONAL:双向传输(如 USB 批量传输)。
4.3 散射-聚集 DMA(Scatter-Gather DMA)#
定义:支持 DMA 控制器直接访问 非连续内存区域 的传输方式。
特点:
- 通过
sg_table结构描述多个分散的内存段(物理页),DMA 控制器按顺序传输。 - 避免内存拷贝(无需将分散数据合并为连续缓冲区),提升效率。
- 适用于 大数据量、非连续内存场景(如网络协议栈中的skb 缓冲区、文件系统的分散读/写)。
5. Linux DMA 编程接口#
Linux 内核提供了丰富的 DMA 操作接口,以下是常用函数及示例:
5.1 一致性 DMA 接口#
分配/释放一致性 DMA 缓冲区#
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t gfp);
void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle);dev:设备结构体指针(需提前设置dma_mask)。size:缓冲区大小(字节)。dma_handle:输出参数,返回 DMA 地址(设备视角)。cpu_addr:返回 CPU 虚拟地址(内核视角)。gfp:内存分配标志(如GFP_KERNEL)。
示例:
struct device *dev = ...; // 设备指针
dma_addr_t dma_addr;
char *buf = dma_alloc_coherent(dev, 4096, &dma_addr, GFP_KERNEL);
if (!buf) {
dev_err(dev, "dma_alloc_coherent failed\n");
return -ENOMEM;
}
// 使用 buf(CPU 虚拟地址)和 dma_addr(DMA 地址)...
dma_free_coherent(dev, 4096, buf, dma_addr); // 释放5.2 流式 DMA 接口#
映射/解除映射单个缓冲区#
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir);dir:传输方向(DMA_TO_DEVICE/DMA_FROM_DEVICE/DMA_BIDIRECTIONAL)。
示例:向设备传输数据(DMA_TO_DEVICE)
char *buf = kmalloc(4096, GFP_KERNEL); // 分配内核缓冲区
dma_addr_t dma_addr = dma_map_single(dev, buf, 4096, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) { // 检查映射是否失败
kfree(buf);
return -ENOMEM;
}
// 告知设备 DMA 地址 dma_addr,启动传输...
dma_unmap_single(dev, dma_addr, 4096, DMA_TO_DEVICE); // 传输完成后解除映射
kfree(buf);5.3 散射-聚集 DMA 接口#
分配/映射散射-聚集表#
struct sg_table *sg_alloc_table(int nents, gfp_t gfp_mask);
void sg_free_table(struct sg_table *sgt);
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
void dma_unmap_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);sg_table:散射-聚集表,包含多个scatterlist条目(每个条目描述一个物理页)。nents:条目数量。
示例:
struct sg_table sgt;
int nents = 2; // 2 个分散内存段
char *buf1 = kmalloc(PAGE_SIZE, GFP_KERNEL);
char *buf2 = kmalloc(PAGE_SIZE, GFP_KERNEL);
// 分配 sg_table
if (sg_alloc_table(&sgt, nents, GFP_KERNEL))
return -ENOMEM;
// 填充 scatterlist(将内核虚拟地址转换为物理页)
sg_init_table(sgt.sgl, nents);
sg_set_buf(&sgt.sgl[0], buf1, PAGE_SIZE);
sg_set_buf(&sgt.sgl[1], buf2, PAGE_SIZE);
// 映射 DMA 地址
int mapped = dma_map_sg(dev, sgt.sgl, nents, DMA_FROM_DEVICE);
if (!mapped) {
sg_free_table(&sgt);
return -ENOMEM;
}
// 遍历映射后的 DMA 地址
struct scatterlist *sg;
for_each_sg(sgt.sgl, sg, mapped, i) {
dma_addr_t dma_addr = sg_dma_address(sg); // 获取 DMA 地址
size_t len = sg_dma_len(sg); // 获取长度
dev_info(dev, "DMA segment %d: 0x%lx, len %zu\n", i, dma_addr, len);
}
// 传输完成后解除映射并释放
dma_unmap_sg(dev, sgt.sgl, nents, DMA_FROM_DEVICE);
sg_free_table(&sgt);
kfree(buf1);
kfree(buf2);6. 最佳实践与常见问题#
6.1 最佳实践#
-
设置正确的 DMA 掩码
调用dma_set_mask(dev, DMA_BIT_MASK(32))告知内核设备支持的地址位数,避免分配设备不可访问的内存。 -
对齐缓冲区
DMA 传输通常要求地址和长度对齐(如按 cache line 或页对齐),可通过dma_get_cache_alignment()获取对齐要求。 -
及时解除映射
流式 DMA 映射后需在传输完成后立即解除(dma_unmap_*),避免内存泄漏或缓存不一致。 -
优先使用散射-聚集
对于非连续内存,使用sg_table避免内存拷贝,提升性能(如网络协议栈中的skb缓冲区)。 -
错误处理
始终检查dma_alloc_*和dma_map_*的返回值(如dma_mapping_error()),避免空指针或无效地址。
6.2 常见问题与解决方案#
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缓存不一致 | CPU 缓存未刷新,DMA 读取旧数据 | 流式 DMA 中使用 dma_map_* 自动处理缓存同步;一致性 DMA 无需手动同步。 |
| DMA 地址无效 | 设备不支持高地址(如 32 位设备访问 64 位内存) | 使用 dma_set_mask 限制地址范围,或启用 IOMMU。 |
| 物理内存分配失败 | 大连续内存难以分配(尤其系统运行一段时间后) | 使用散射-聚集 DMA 或 dma_alloc_coherent(内核优先从低端内存分配)。 |
| 性能低下 | 频繁映射/解除映射导致开销 | 优先使用一致性 DMA 或预分配缓冲区池。 |
7. 示例:Linux 内核模块中的 DMA 使用#
以下是一个简化的内核模块示例,演示如何使用 一致性 DMA 分配缓冲区并模拟数据传输:
#include <linux/module.h>
#include <linux/device.h>
#include <linux/dma-mapping.h>
static struct device *dma_dev;
static void *cpu_buf; // CPU 虚拟地址
static dma_addr_t dma_buf; // DMA 地址
static size_t buf_size = 4096;
static int __init dma_demo_init(void) {
int ret;
// 创建设备(实际驱动中通常使用平台设备或 PCI 设备)
dma_dev = root_device_register("dma_demo");
if (IS_ERR(dma_dev)) {
ret = PTR_ERR(dma_dev);
pr_err("Failed to register device: %d\n", ret);
return ret;
}
// 设置 DMA 地址掩码(假设设备支持 32 位地址)
if (!dma_set_mask(dma_dev, DMA_BIT_MASK(32))) {
pr_warn("Device does not support 32-bit DMA addresses\n");
root_device_unregister(dma_dev);
return -EINVAL;
}
// 分配一致性 DMA 缓冲区
cpu_buf = dma_alloc_coherent(dma_dev, buf_size, &dma_buf, GFP_KERNEL);
if (!cpu_buf) {
pr_err("dma_alloc_coherent failed\n");
root_device_unregister(dma_dev);
return -ENOMEM;
}
// 模拟 CPU 写入数据
snprintf(cpu_buf, buf_size, "Hello, DMA World!");
pr_info("CPU wrote: %s\n", cpu_buf);
pr_info("DMA address: 0x%lx\n", dma_buf);
// 模拟 DMA 传输完成(实际设备会通过中断通知)
pr_info("DMA transfer completed. Data read from DMA buffer: %s\n", cpu_buf);
return 0;
}
static void __exit dma_demo_exit(void) {
// 释放一致性 DMA 缓冲区
dma_free_coherent(dma_dev, buf_size, cpu_buf, dma_buf);
root_device_unregister(dma_dev);
pr_info("DMA demo module unloaded\n");
}
module_init(dma_demo_init);
module_exit(dma_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Linux DMA Demo Module");编译与测试:
- 编写 Makefile:
obj-m += dma_demo.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean - 编译后加载模块:
insmod dma_demo.ko - 查看日志:
dmesg | grep DMA,可看到 CPU 写入的数据和 DMA 地址。
8. 总结#
Linux DMA 子系统是内核硬件支持的核心组件,通过抽象硬件差异和提供统一 API,极大简化了驱动开发。本文从原理、子系统架构、编程接口到最佳实践进行了全面解析,关键结论如下:
- DMA 类型:根据场景选择一致性(长期缓冲区)、流式(短期传输)或散射-聚集(非连续内存)DMA。
- 接口使用:优先使用内核提供的
dma_alloc_*和dma_map_*接口,避免直接操作硬件。 - 最佳实践:关注缓存一致性、地址对齐、错误处理,优先使用散射-聚集提升性能。
掌握 Linux DMA 对于开发高性能设备驱动至关重要,建议结合内核文档(如 Documentation/DMA-API.txt)和实际硬件手册深入学习。
9. 参考资料#
- Linux 内核文档:DMA-API.txt
- Linux 内核文档:DMA-API-HOWTO.txt
- 《Linux Device Drivers, 3rd Edition》(Jonathan Corbet 等著)第 15 章
- Linux 内核源码:
include/linux/dma-mapping.h、drivers/dma/ - ARM 架构文档:ARM® Cortex®-A Series Programmer’s Guide(缓存一致性章节)
作者:Linux 驱动开发者
日期:2023 年 10 月
许可:本文采用 CC BY-SA 4.0 协议共享。