【Linux驱动开发】Linux DMA 技术详解与驱动开发实战

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 子系统提供了一层屏蔽硬件差异的抽象层:

  1. DMA Engine API:用于通用的 Slave DMA(如嵌入式 SoC 中的 SPI/UART DMA)。
  2. 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 延迟。
  • APIpci_p2pdma_map_sg(), NVMe 和 RDMA 网卡已广泛支持。

5. 参考文献

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao3 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠4 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush44 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5204 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩4 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言