Linux DMA 详解:原理、子系统与实践指南

在计算机系统中,数据传输是核心操作之一。传统模式下,CPU 需要全程参与外设与内存之间的数据搬运,这会占用大量 CPU 资源,导致系统效率低下。直接内存访问(DMA,Direct Memory Access) 技术应运而生:它允许外设通过 DMA 控制器直接访问系统内存,无需 CPU 干预,从而显著提升数据传输效率和系统吞吐量。

Linux 作为多任务操作系统,需要高效管理 DMA 资源以支持各类硬件设备(如网卡、磁盘控制器、GPU 等)。Linux 内核提供了一套完善的 DMA 子系统,包括统一的 API、地址空间管理、缓存一致性处理等机制,简化了驱动开发并保证了跨硬件平台的兼容性。

本文将深入解析 Linux DMA 的原理、子系统架构、编程接口、最佳实践,并通过示例演示如何在 kernel 模块中使用 DMA。

目录#

  1. 简介
  2. DMA 基础原理
  3. Linux 中的 DMA 子系统
  4. Linux DMA 类型
  5. Linux DMA 编程接口
  6. 最佳实践与常见问题
  7. 示例:Linux 内核模块中的 DMA 使用
  8. 总结
  9. 参考资料

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 的硬件组件,负责协调外设与内存的数据传输。典型工作流程如下:

  1. 请求阶段:外设(如网卡)需传输数据时,向 DMA 控制器发送 DMA 请求(DREQ)。
  2. 授权阶段:DMA 控制器向 CPU 发送总线请求(HRQ),CPU 响应并释放总线控制权(HLDA)。
  3. 传输阶段:DMA 控制器接管总线,直接在内存与外设间传输数据(通过指定内存地址、外设地址、传输长度和方向)。
  4. 完成阶段:传输结束后,DMA 控制器向 CPU 发送中断,通知传输完成,CPU 恢复总线控制权。

DMA 工作流程
(注:实际硬件架构可能有差异,例如集成在 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 核心组件#

  1. DMA API 层
    内核提供的用户接口(如 dma_alloc_coherentdma_map_single),屏蔽底层硬件细节,支持跨架构移植。

  2. DMA 控制器驱动
    针对具体 DMA 硬件的驱动(如 drivers/dma/ 目录下的驱动),实现 DMA 传输的底层控制逻辑。

  3. DMA 引擎框架
    统一管理不同厂商的 DMA 控制器,提供标准化的 DMA 操作(如启动/停止传输、链式操作)。

  4. 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_DEVICEDMA_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 最佳实践#

  1. 设置正确的 DMA 掩码
    调用 dma_set_mask(dev, DMA_BIT_MASK(32)) 告知内核设备支持的地址位数,避免分配设备不可访问的内存。

  2. 对齐缓冲区
    DMA 传输通常要求地址和长度对齐(如按 cache line 或页对齐),可通过 dma_get_cache_alignment() 获取对齐要求。

  3. 及时解除映射
    流式 DMA 映射后需在传输完成后立即解除(dma_unmap_*),避免内存泄漏或缓存不一致。

  4. 优先使用散射-聚集
    对于非连续内存,使用 sg_table 避免内存拷贝,提升性能(如网络协议栈中的 skb 缓冲区)。

  5. 错误处理
    始终检查 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");

编译与测试

  1. 编写 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
  2. 编译后加载模块:insmod dma_demo.ko
  3. 查看日志:dmesg | grep DMA,可看到 CPU 写入的数据和 DMA 地址。

8. 总结#

Linux DMA 子系统是内核硬件支持的核心组件,通过抽象硬件差异和提供统一 API,极大简化了驱动开发。本文从原理、子系统架构、编程接口到最佳实践进行了全面解析,关键结论如下:

  • DMA 类型:根据场景选择一致性(长期缓冲区)、流式(短期传输)或散射-聚集(非连续内存)DMA。
  • 接口使用:优先使用内核提供的 dma_alloc_*dma_map_* 接口,避免直接操作硬件。
  • 最佳实践:关注缓存一致性、地址对齐、错误处理,优先使用散射-聚集提升性能。

掌握 Linux DMA 对于开发高性能设备驱动至关重要,建议结合内核文档(如 Documentation/DMA-API.txt)和实际硬件手册深入学习。

9. 参考资料#

  1. Linux 内核文档:DMA-API.txt
  2. Linux 内核文档:DMA-API-HOWTO.txt
  3. 《Linux Device Drivers, 3rd Edition》(Jonathan Corbet 等著)第 15 章
  4. Linux 内核源码:include/linux/dma-mapping.hdrivers/dma/
  5. ARM 架构文档:ARM® Cortex®-A Series Programmer’s Guide(缓存一致性章节)

作者:Linux 驱动开发者
日期:2023 年 10 月
许可:本文采用 CC BY-SA 4.0 协议共享。