驱动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 并发能力 | 差(必须等待传输完成) | 强(传输期间可执行其他任务) |
1.2 DMA 工作原理
DMA 传输的基本流程包含三个阶段:请求与仲裁、数据传输、传输完成通知。整个过程中 CPU 仅在初始化阶段设置传输参数,并在完成阶段处理中断。
1.2.1 标准 DMA 传输流程
- CPU 配置 DMA 控制器:设置源地址、目标地址、传输长度、传输方向(内存→设备 / 设备→内存 / 内存→内存)、突发大小等参数。
- 设备发起 DMA 请求:当 I/O 设备准备好数据或需要数据时,通过硬件信号线(DREQ)向 DMA 控制器发出请求。
- 总线仲裁:DMA 控制器向 CPU 请求总线控制权(HRQ),CPU 在完成当前总线周期后让出总线(HLDA)。
- 数据传输:DMA 控制器直接驱动地址总线和控制总线,在源与目标之间搬运数据,每次传输后地址递增、计数递减。
- 传输完成:当计数归零时,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 |
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 内核内存映射优化的前提。