3. Linux 内核 DMA API 概览
在 PCIe 驱动开发中,DMA(直接内存访问)是设备与主机内存之间进行高速数据传输的核心机制。Linux 内核提供了一套标准化的 DMA API,用于管理设备与 CPU 之间的内存一致性、地址映射以及数据传输。本章将深入剖析三个最核心的 DMA API:dma_alloc_coherent、dma_map_single 和 dma_map_sg,帮助您理解其工作原理、适用场景及常见故障点。
3.1 DMA 地址映射基础概念
在深入 API 之前,必须理解两个关键地址空间:CPU 虚拟地址(VA)和设备总线地址(DMA 地址)。PCIe 设备无法直接访问 CPU 的虚拟地址或物理地址,它只能通过其 BAR 空间或 ATS(地址转换服务)访问总线地址。DMA API 的核心职责就是在这两个地址空间之间建立正确的映射关系。
3.2 dma_alloc_coherent:一致性 DMA 缓冲区
dma_alloc_coherent 用于分配一个物理连续且cache 一致性的内存区域。该 API 会同时返回 CPU 可以访问的虚拟地址和设备可以使用的 DMA 地址。由于硬件保证了 cache 一致性,驱动无需手动执行 cache 刷新操作,适用于描述符环、控制寄存器等对数据一致性要求极高的场景。
函数原型与参数
#include <linux/dma-mapping.h>
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
| 参数 | 说明 | 常见值 |
|---|---|---|
dev |
指向 PCIe 设备的 device 结构体指针 | &pdev->dev |
size |
请求分配的字节数(必须为页大小的整数倍) | PAGE_SIZE 或 PAGE_SIZE * n |
dma_handle |
输出参数,返回设备可用的 DMA 总线地址 | 传递给设备寄存器 |
flag |
内存分配标志(GFP_KERNEL / GFP_ATOMIC) | 中断上下文用 GFP_ATOMIC |
| 返回值 | CPU 可访问的虚拟地址(失败返回 NULL) | 用于 memcpy / memset |
dma_free_coherent 释放。避免在数据路径中频繁分配/释放,以减少内存碎片和延迟。
典型使用场景:描述符环
struct my_dev {
struct pci_dev *pdev;
void *desc_ring_cpu; /* CPU 虚拟地址 */
dma_addr_t desc_ring_dma; /* DMA 总线地址 */
};
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->pdev = pdev;
/* 分配 4 页大小的描述符环 */
dev->desc_ring_cpu = dma_alloc_coherent(&pdev->dev,
4 * PAGE_SIZE,
&dev->desc_ring_dma,
GFP_KERNEL);
if (!dev->desc_ring_cpu)
return -ENOMEM;
/* 将 DMA 地址写入设备 BAR 寄存器 */
pci_write_config_dword(pdev, PCI_BASE_ADDRESS_0,
lower_32_bits(dev->desc_ring_dma));
pci_write_config_dword(pdev, PCI_BASE_ADDRESS_0 + 4,
upper_32_bits(dev->desc_ring_dma));
pci_set_drvdata(pdev, dev);
return 0;
}
static void my_remove(struct pci_dev *pdev)
{
struct my_dev *dev = pci_get_drvdata(pdev);
if (dev->desc_ring_cpu)
dma_free_coherent(&pdev->dev, 4 * PAGE_SIZE,
dev->desc_ring_cpu, dev->desc_ring_dma);
}
3.3 dma_map_single:流式 DMA 映射
dma_map_single 用于将已有的内核缓冲区(通常由 kmalloc、__get_free_pages 或静态数组分配)映射为 DMA 地址。与一致性分配不同,流式映射不保证 cache 一致性,驱动必须在适当的时机调用 dma_sync_single_for_device 和 dma_sync_single_for_cpu 来管理 cache。
函数原型与配对 API
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);
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir);
void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir);
| 方向枚举 | 含义 | cache 同步策略 |
|---|---|---|
DMA_TO_DEVICE |
CPU 写入,设备读取(如发送网络包) | 映射前 flush cache;设备完成后无需操作 |
DMA_FROM_DEVICE |
设备写入,CPU 读取(如接收网络包) | 映射前 invalidate cache;设备完成后 invalidate |
DMA_BIDIRECTIONAL |
双向数据传输 | 映射前 flush+invalidate;每次切换方向都需同步 |
DMA_NONE |
调试用途,无实际数据传输 | 无操作 |
dma_unmap_single 会导致 IOMMU 页表泄漏,最终耗尽 DMA 地址空间。在中断处理函数中完成数据传输后,务必及时解映射或同步 cache。
典型使用场景:网络数据包收发
/* 发送数据包 */
static int my_tx_packet(struct my_dev *dev, void *data, size_t len)
{
dma_addr_t dma_addr;
/* 将数据缓冲区映射为 DMA 地址 */
dma_addr = dma_map_single(&dev->pdev->dev, data, len,
DMA_TO_DEVICE);
if (dma_mapping_error(&dev->pdev->dev, dma_addr))
return -ENOMEM;
/* 将 DMA 地址写入设备发送描述符 */
writel(lower_32_bits(dma_addr), dev->tx_reg + TX_DESC_ADDR_LO);
writel(upper_32_bits(dma_addr), dev->tx_reg + TX_DESC_ADDR_HI);
writel(len, dev->tx_reg + TX_DESC_LEN);
writel(1, dev->tx_reg + TX_DESC_GO); /* 触发发送 */
return 0;
}
/* 发送完成中断处理 */
static irqreturn_t my_tx_done_irq(int irq, void *data)
{
struct my_dev *dev = data;
dma_addr_t dma_addr;
size_t len;
/* 从完成队列获取 DMA 地址和长度 */
dma_addr = dev->tx_complete_dma;
len = dev->tx_complete_len;
/* 解映射,释放 DMA 地址空间 */
dma_unmap_single(&dev->pdev->dev, dma_addr, len, DMA_TO_DEVICE);
/* 通知上层协议栈数据已发送完成 */
if (dev->tx_complete_cb)
dev->tx_complete_cb(dev->tx_complete_ctx);
return IRQ_HANDLED;
}
3.4 dma_map_sg:分散/聚集 DMA 映射
对于高性能设备(如 NVMe SSD、万兆网卡),一次 DMA 传输可能涉及多个非连续的物理内存片段。dma_map_sg 可以将一个 struct scatterlist 数组映射为设备可用的 DMA 地址列表,并返回合并后的有效片段数。
函数原型与 scatterlist 操作
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);
/* scatterlist 遍历宏 */
struct scatterlist *sg;
int i;
for_each_sg(sglist, sg, nents, i) {
dma_addr_t dma_addr = sg_dma_address(sg);
unsigned int len = sg_dma_len(sg);
/* 将 dma_addr 和 len 写入设备描述符 */
}
| 参数 | 说明 |
|---|---|
sg |
指向 scatterlist 数组的指针 |
nents |
输入时的 scatterlist 条目数 |
| 返回值 | 映射后的有效 DMA 片段数(可能小于 nents,因为 IOMMU 可能合并相邻页) |
dma_map_sg 而非多次调用 dma_map_single 可以减少 IOMMU 页表操作次数,并允许硬件通过 scatter-gather 列表执行一次 DMA 传输多个片段,显著降低中断和 CPU 开销。
典型使用场景:NVMe PRP 列表
static int my_nvme_submit(struct my_dev *dev, struct scatterlist *sglist,
int nents, u64 slba, u16 nlb)
{
struct scatterlist *sg;
int i, mapped_nents;
/* 映射 scatterlist */
mapped_nents = dma_map_sg(&dev->pdev->dev, sglist, nents,
DMA_FROM_DEVICE);
if (mapped_nents == 0)
return -ENOMEM;
/* 构建 PRP(物理区域页)列表 */
i = 0;
for_each_sg(sglist, sg, mapped_nents, i) {
dma_addr_t dma_addr = sg_dma_address(sg);
u32 len = sg_dma_len(sg);
if (i == 0) {
/* 第一个 PRP 写入命令的 PRP1 字段 */
dev->cmd.prp1 = cpu_to_le64(dma_addr);
} else if (i == 1) {
/* 第二个 PRP 写入命令的 PRP2 字段 */
dev->cmd.prp2 = cpu_to_le64(dma_addr);
} else {
/* 更多 PRP 写入 PRP 列表页 */
dev->prp_list[i - 2] = cpu_to_le64(dma_addr);
}
}
/* 提交命令到 SQ(提交队列) */
return my_nvme_submit_cmd(dev, &dev->cmd);
}
/* 命令完成后的解映射 */
static void my_nvme_complete(struct my_dev *dev, struct scatterlist *sglist,
int nents)
{
dma_unmap_sg(&dev->pdev->dev, sglist, nents, DMA_FROM_DEVICE);
}
3.5 常见故障模式与排查要点
- DMA 地址未对齐: 某些 PCIe 设备要求 DMA 缓冲区对齐到特定边界(如 64 字节、页大小)。使用
dma_alloc_coherent时默认页对齐;使用dma_map_single时需确保原始缓冲区对齐。 - 忘记检查 dma_mapping_error:
dma_map_single和dma_map_sg可能因 IOMMU 地址空间耗尽而失败。必须检查返回值,否则设备将使用无效地址。 - 方向错误导致数据损坏: 使用
DMA_FROM_DEVICE时,如果 CPU 在设备写入前读取 cache 中的旧数据,会导致数据不一致。务必在设备写入后调用dma_sync_single_for_cpu。 - 中断上下文中使用 GFP_KERNEL: 在中断处理函数或持有自旋锁时,必须使用
GFP_ATOMIC,否则会导致内核调度错误。 - 64 位 DMA 地址截断: 在 32 位系统或未启用 64 位 DMA 支持的设备上,高 32 位地址会被忽略。使用
dma_set_mask_and_coherent设置正确的 DMA 掩码。
/sys/kernel/debug/dma-api/ 下可以查看 DMA API 的使用统计,包括分配次数、泄漏检测和错误计数。启用 CONFIG_DMA_API_DEBUG 内核选项可以在运行时检测常见的 DMA API 误用。