驱动DMA缓冲区管理与Android内核内存映射 - 第1节

1.1 DMA 定义与核心概念

直接存储器访问(Direct Memory Access, DMA) 是一种允许硬件设备(如磁盘控制器、网卡、GPU、音频接口)直接读写系统内存,而无需 CPU 参与每次数据传输的机制。在 DMA 出现之前,所有 I/O 操作都必须经过 CPU 寄存器中转(PIO 模式),导致 CPU 在大量数据搬运中被完全占用,无法执行其他计算任务。

核心思想:将数据搬运工作从 CPU 卸载给专用的 DMA 控制器(DMAC)或支持总线主控的设备,使 CPU 可以并行执行指令流水线、进程调度或中断处理,从而大幅提升系统吞吐量。
特性 PIO(Programmed I/O) DMA(Direct Memory Access)
数据搬运主体 CPU 逐字搬运 DMA 控制器或设备总线主控
CPU 占用率 高(传输期间完全占用) 低(仅初始化与完成时介入)
传输速率 受限于 CPU 寄存器宽度与频率 接近总线带宽(如 PCIe Gen4 x4 ~8 GB/s)
适用场景 少量控制寄存器读写 大数据块(网络包、磁盘块、帧缓冲)
CPU 并发能力 差(必须等待传输完成) 强(传输期间可执行其他任务)
DMA 与 PIO 对比示意图 CPU I/O 设备 系统内存 PIO: CPU 逐字搬运 CPU I/O 设备 系统内存 DMA 控制器 仅初始化 DMA: 控制器直接搬运

1.2 DMA 工作原理

DMA 传输的基本流程包含三个阶段:请求与仲裁、数据传输、传输完成通知。整个过程中 CPU 仅在初始化阶段设置传输参数,并在完成阶段处理中断。

1.2.1 标准 DMA 传输流程

  1. CPU 配置 DMA 控制器:设置源地址、目标地址、传输长度、传输方向(内存→设备 / 设备→内存 / 内存→内存)、突发大小等参数。
  2. 设备发起 DMA 请求:当 I/O 设备准备好数据或需要数据时,通过硬件信号线(DREQ)向 DMA 控制器发出请求。
  3. 总线仲裁:DMA 控制器向 CPU 请求总线控制权(HRQ),CPU 在完成当前总线周期后让出总线(HLDA)。
  4. 数据传输:DMA 控制器直接驱动地址总线和控制总线,在源与目标之间搬运数据,每次传输后地址递增、计数递减。
  5. 传输完成:当计数归零时,DMA 控制器释放总线,并通过中断信号(IRQ)通知 CPU 传输结束。
DMA 传输完整流程 ① CPU 配置 DMA 参数 ② 设备发起 DREQ 请求 ③ 总线仲裁(HRQ/HLDA) ④ DMA 数据传输 ⑤ 计数归零 → 释放总线 ⑥ IRQ 通知 CPU 完成

1.2.2 DMA 传输模式

模式 描述 典型场景
单次传输(Single Transfer) 每次 DMA 请求只传输一个字节/字,然后释放总线 低速设备、共享总线环境
块传输(Block Transfer) 一次请求传输整个数据块,期间持续占用总线 磁盘控制器、高速网卡
请求传输(Demand Transfer) 设备通过 DREQ 信号控制传输节奏,信号有效则持续传输 流式设备(音频、视频)
链式传输(Scatter-Gather) 使用描述符链表,一次 DMA 操作可传输多个不连续内存区域 Linux 内核 SG DMA、高级网卡
关键理解:链式传输(Scatter-Gather)是现代操作系统 DMA 的核心。它允许 DMA 控制器直接操作物理上不连续的内存页,避免了传统 DMA 对连续物理内存的苛刻要求,是 Android 内核内存映射优化的重要基础。

1.3 DMA 与 CPU 的交互模式

DMA 与 CPU 的交互主要围绕总线控制权移交和同步机制展开。根据系统架构的不同,存在三种主流交互模式:

1.3.1 三种总线交互模式

模式 总线控制方式 CPU 参与度 适用架构
周期窃取(Cycle Stealing) DMA 控制器在 CPU 不访问总线的时钟周期内插入传输 极低(几乎无感知) 早期 ISA 总线、嵌入式 MCU
突发模式(Burst Mode) DMA 控制器获得总线后连续传输整个块,CPU 被阻塞 高(传输期间 CPU 等待) PCI/PCIe 传统设备
总线主控(Bus Mastering) I/O 设备自身具备总线控制能力,直接发起 DMA 传输 最低(仅初始化与中断) PCIe 设备、NVMe SSD、GPU
DMA 与 CPU 总线交互模式 周期窃取(Cycle Stealing) DMA 利用 CPU 空闲总线周期 单次传输 1 字节后释放总线 突发模式(Burst Mode) DMA 持续占用总线直到块传输完成 CPU 在此期间被阻塞 总线主控(Bus Mastering) 设备自身作为总线主控 直接发起 DMA 传输 CPU 占用率递减 → 系统吞吐量递增 →

1.3.2 同步机制:中断与轮询

DMA 传输完成后,CPU 需要通过某种机制获知结果:

  • 中断驱动(Interrupt-Driven):DMA 控制器在传输完成时触发硬件中断,CPU 响应中断并处理数据。这是最常用的方式,适合非实时场景。
  • 轮询(Polling):CPU 循环检查 DMA 控制器的状态寄存器或完成标志位。适用于极低延迟要求或中断不可用的场景(如早期系统或某些 RTOS)。
  • 事件通知(Event/Callback):在 Linux 内核中,DMA 完成回调通过 dma_async_issue_pending() 和完成回调函数实现,属于中断机制的软件抽象。
Android 内核注意点:在 Android 设备中,DMA 中断处理必须遵循 top-half / bottom-half 模型。top-half 仅做最小必要操作(如清除中断标志、唤醒等待队列),耗时的数据解析工作推迟到 bottom-half(tasklet 或 workqueue)中执行,以避免阻塞高优先级任务或导致音频/视频帧丢失。

1.3.3 Linux 内核 DMA API 交互示例

在 Linux 内核驱动中,DMA 的初始化与同步通过标准 API 完成。以下是一个典型的 DMA 发送流程(内存→设备):

/* 1. 分配 DMA 缓冲区 */
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

/* 2. 准备数据到 cpu_addr 指向的缓冲区 */

/* 3. 启动 DMA 传输(假设使用 dmaengine 框架) */
struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_slave_sg(dma_chan, &sg, 1, DMA_MEM_TO_DEV,
                             DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
tx->callback = my_dma_callback;  /* 注册完成回调 */
dmaengine_submit(tx);
dma_async_issue_pending(dma_chan);

/* 4. 回调函数中处理完成事件 */
static void my_dma_callback(void *param) {
    /* 此时数据已传输到设备,可释放或重用缓冲区 */
    complete(&dma_done);
}
交互要点总结:
  • CPU 通过 dma_alloc_coherent 分配一致性内存(同时返回 CPU 虚拟地址和 DMA 总线地址)。
  • DMA 控制器使用总线地址访问内存,CPU 使用虚拟地址访问同一物理内存。
  • 传输完成后,中断回调在原子上下文中执行,不能调用可能睡眠的函数。
  • Android 内核中,DMA 缓冲区通常需要与 ION 或 dmabuf 框架结合,以实现跨设备、跨进程的内存共享。

1.4 本章小结

DMA 是现代计算机系统 I/O 性能的基石。通过将数据搬运任务从 CPU 卸载到专用控制器或总线主控设备,DMA 实现了 CPU 计算与数据传输的高度并行。理解 DMA 的工作原理、传输模式以及与 CPU 的三种总线交互方式,是后续学习 DMA 缓冲区管理、Cache 一致性、以及 Android 内核内存映射优化的前提。