Linux DMA 技术详解与驱动开发实战
摘要 :
本文档全面解析 Linux 内核中的直接内存访问 (DMA) 技术。从硬件工作原理出发,深入分析 Linux DMA 子系统的架构设计,详细阐述一致性 DMA 与流式 DMA 的 API 使用规范。结合字符设备驱动实战,演示 Scatter-Gather DMA 的实现细节,并探讨 IOMMU、CMA 及 PCIe P2P DMA 等高级主题,旨在为嵌入式及驱动工程师提供一份权威的技术指南。
关键词:Linux Kernel, DMA, Scatter-Gather, dma_map_single, IOMMU, PCIe P2P
1. 技术原理
1.1 DMA 基本概念
直接内存访问 (Direct Memory Access) 是一种允许硬件子系统直接读写系统主内存的技术,而无需中央处理器 (CPU) 介入数据的每次传输。
在没有 DMA 的系统中,CPU 必须执行指令来从 I/O 设备读取数据到寄存器,再写入内存(PIO 模式)。这会导致 CPU 在大量数据传输期间处于忙碌状态,无法处理其他任务。
1.2 DMA 控制器的作用
DMA 控制器 (DMAC) 是系统总线上的一个专用处理器。
- 总线主控 (Bus Master):DMAC 可以接管总线控制权。
- 地址生成:自动生成源地址和目的地址。
- 中断触发:传输完成后触发中断通知 CPU。
1.3 有无 DMA 的对比

- PIO 模式 (Programmed I/O) :
- 数据路径:Device <-> CPU <-> Memory
- 缺点:CPU 占用率高,传输速度受限于 CPU 指令周期。
- DMA 模式 :
- 数据路径:Device <-> Memory (由 DMAC 控制)
- 优点:CPU 仅需发送配置指令(源/目地址、长度),随后即可释放处理其他中断或进程。
2. Linux 内核实现
2.1 DMA 子系统架构
Linux DMA 子系统提供了一层屏蔽硬件差异的抽象层:
- DMA Engine API:用于通用的 Slave DMA(如嵌入式 SoC 中的 SPI/UART DMA)。
- DMA Mapping API:用于 PCI/PCIe 等具备总线主控能力的设备,处理虚拟地址到物理地址(总线地址)的映射,以及缓存一致性问题。
2.2 核心 API 详解
2.2.1 一致性 DMA (Coherent DMA)
用于分配常驻内存(如描述符环、控制块),CPU 和设备都能看到最新数据,无需手动同步 Cache。
c
/* 分配 */
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
/* 释放 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
- 参数 :
dev(struct device*),size(字节数),dma_handle(返回的总线地址). - 特点:开销较大,通常在驱动初始化时分配。
2.2.2 流式 DMA (Streaming DMA)
用于单次数据传输(如网络包、磁盘块),通常使用已有的内核缓冲区(如 kmalloc 申请的内存)。
c
/* 映射 (CPU -> Device) */
dma_addr_t dma_handle = dma_map_single(dev, ptr, size, DMA_TO_DEVICE);
/* 检查错误 */
if (dma_mapping_error(dev, dma_handle)) {
/* 错误处理 */
}
/* ... 启动 DMA 传输 ... */
/* 取消映射 (传输完成后) */
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);
- 方向 :
DMA_TO_DEVICE,DMA_FROM_DEVICE,DMA_BIDIRECTIONAL。 - 注意 :在
unmap之前,CPU 不应触碰这块内存,否则可能导致数据损坏(Cache 一致性问题)。
2.3 Scatter-Gather (分散-聚集) DMA
当需要传输的数据在虚拟内存中连续,但在物理内存中不连续时(例如巨大的视频帧),使用 SG DMA 可以避免内存拷贝。

数据结构 :struct scatterlist
c
struct scatterlist {
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address; // 映射后的总线地址
unsigned int dma_length; // 映射后的长度
};
API 使用:
c
int nents = dma_map_sg(dev, sg_list, n_pages, DMA_FROM_DEVICE);
/* 遍历映射后的段 */
for_each_sg(sg_list, sg, nents, i) {
dma_addr_t addr = sg_dma_address(sg);
unsigned int len = sg_dma_len(sg);
/* 配置硬件描述符 */
}
dma_unmap_sg(dev, sg_list, n_pages, DMA_FROM_DEVICE);
3. 驱动开发实践
本节演示一个模拟的字符设备 DMA 驱动。
3.1 设备树配置 (Device Tree)
dts
my_dma_device: my-dma@40000000 {
compatible = "my,dma-device";
reg = <0x40000000 0x1000>;
interrupts = <0 20 4>;
dmas = <&dma0 1 0>; /* 引用系统 DMA 控制器 */
dma-names = "rx";
};
3.2 驱动代码示例
c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/dma-mapping.h>
#include <linux/cdev.h>
#include <linux/platform_device.h>
struct my_dma_dev {
struct device *dev;
struct cdev cdev;
void *dma_buf_virt;
dma_addr_t dma_buf_phys;
size_t buf_size;
};
#define BUF_SIZE (4 * 1024) // 4KB
static int my_open(struct inode *inode, struct file *file)
{
struct my_dma_dev *mydev = container_of(inode->i_cdev, struct my_dma_dev, cdev);
/* 1. 分配一致性 DMA 缓冲区 */
mydev->buf_size = BUF_SIZE;
mydev->dma_buf_virt = dma_alloc_coherent(mydev->dev, mydev->buf_size,
&mydev->dma_buf_phys, GFP_KERNEL);
if (!mydev->dma_buf_virt)
return -ENOMEM;
file->private_data = mydev;
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct my_dma_dev *mydev = file->private_data;
/* 2. 模拟启动 DMA 传输 (硬件寄存器操作省略) */
/* writel(mydev->dma_buf_phys, REG_SRC_ADDR); */
/* writel(count, REG_LEN); */
/* writel(START_BIT, REG_CTRL); */
/* 3. 等待 DMA 完成 (wait_event_interruptible...) */
/* 4. 将数据拷贝给用户 (实际场景中可能使用 mmap 零拷贝) */
if (copy_to_user(buf, mydev->dma_buf_virt, count))
return -EFAULT;
return count;
}
static int my_release(struct inode *inode, struct file *file)
{
struct my_dma_dev *mydev = file->private_data;
/* 5. 释放缓冲区 */
if (mydev->dma_buf_virt)
dma_free_coherent(mydev->dev, mydev->buf_size,
mydev->dma_buf_virt, mydev->dma_buf_phys);
return 0;
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.release = my_release,
};
/* Platform Driver Probe */
static int my_dma_probe(struct platform_device *pdev)
{
struct my_dma_dev *mydev;
/* ... cdev 初始化 ... */
/* 设置 DMA 掩码 (如 32位或64位) */
if (dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32))) {
dev_err(&pdev->dev, "No suitable DMA available\n");
return -EIO;
}
return 0;
}
3.3 性能优化与排查
- 合并传输:尽量使用 SG DMA 合并小的物理块,减少中断频率。
- 对齐:确保缓冲区地址和长度按照 Cache Line 对齐(通常 64 字节),避免 Cache Bouncing。
- 调试 :开启
CONFIG_DMA_API_DEBUG,内核会检查dma_map/unmap配对错误和越界访问。
4. 高级主题
4.1 IOMMU (Input-Output MMU)
IOMMU 位于设备和系统总线之间,类似于 CPU 的 MMU。
- 功能:将设备可见的虚拟地址 (IOVA) 映射到物理地址。
- 优势 :
- 内存保护:防止设备非法访问其他内存。
- 分散聚集:让物理不连续的页面在设备看来是连续的(简化 DMA 硬件设计)。
- Linux 支持:DMA Mapping API 自动处理 IOMMU,驱动通常无感。
4.2 CMA (Contiguous Memory Allocator)
在没有 IOMMU 的嵌入式系统中,硬件往往需要大块连续物理内存(如 1080p 帧缓冲)。
- 原理:在启动时预留一大块内存区域(CMA Heap)。
- 作用 :
dma_alloc_coherent在申请大内存时,底层会自动从 CMA 分配器中获取。
4.3 PCIe P2P DMA (Peer-to-Peer)
传统 DMA 数据必须经过系统内存(Device A -> Memory -> Device B)。
P2P DMA 允许 PCIe 设备直接交换数据(Device A -> Device B)。
- 优势:大幅降低系统内存带宽压力和 CPU 延迟。
- API :
pci_p2pdma_map_sg(), NVMe 和 RDMA 网卡已广泛支持。
5. 参考文献
- Linux Kernel DMA API Documentation
- Linux Device Drivers, 3rd Edition, Chapter 15.
- Dynamic DMA Mapping Guide