3. DMA 基础概念:工作原理、类型、描述符与传输方向
DMA(Direct Memory Access,直接存储器访问)是硬件子系统在不依赖 CPU 逐字节搬运的情况下,直接在外设与系统内存之间传输数据的机制。在 UMDF 驱动中,理解 DMA 的底层原理是正确配置 DMA 适配器、编写传输回调和处理 PnP 资源重新平衡的前提。本节从四个维度建立 DMA 基础概念体系。
3.1 DMA 工作原理
DMA 传输的核心思想是:将数据搬运任务从 CPU 卸载给专用的 DMA 控制器(DMAC)或支持总线主控的外设。一次典型的 DMA 传输包含以下阶段:
- 请求阶段:外设(如网卡、磁盘控制器)通过硬件信号线向 DMA 控制器发出传输请求(DREQ)。
- 仲裁阶段:DMA 控制器检查总线优先级,获得总线控制权后,向 CPU 发出 HOLD 信号,CPU 释放总线(或进入周期窃取模式)。
- 传输阶段:DMA 控制器在源地址与目标地址之间直接搬运数据,每次传输后递增地址、递减计数器。
- 完成阶段:当计数器归零或外设发出终止信号时,DMA 控制器释放总线,并向 CPU 发送中断(或通过 UMDF 完成回调通知驱动)。
关键指标:DMA 传输效率取决于总线宽度(32/64 位)、传输模式(单次/突发)、描述符链深度和缓存一致性策略。UMDF 驱动通过 WDF DMA 对象封装这些硬件细节。
3.2 系统 DMA 与总线主控 DMA
Windows 支持两种 DMA 模型,它们在架构、性能和使用场景上有本质区别。UMDF 驱动在创建 DMA 适配器时必须明确选择模型。
| 对比维度 | 系统 DMA(System DMA) | 总线主控 DMA(Bus-Master DMA) |
|---|---|---|
| 控制器位置 | 主板芯片组内置 DMA 控制器(如 8237) | 外设自身集成 DMA 引擎 |
| 总线控制权 | 由系统 DMAC 持有,外设被动 | 外设主动申请并持有总线 |
| 传输性能 | 较低(受限于 DMAC 速度,通常 < 20 MB/s) | 高(直接利用 PCIe 带宽,可达数 GB/s) |
| 描述符管理 | 驱动填充系统 DMAC 寄存器 | 驱动构建描述符链表,外设自行读取 |
| UMDF 支持 | 仅适用于传统 ISA/LPC 设备 | 主流 PCIe/PCI 设备首选 |
| 资源分配 | PnP 管理器分配 DMA 通道号 | PnP 管理器分配总线主控资源(BAR 空间) |
UMDF 实践建议:对于现代 PCIe 设备,始终使用总线主控 DMA。系统 DMA 在 Windows 10/11 上已被标记为遗留模式,新驱动不应依赖。
3.3 DMA 描述符
DMA 描述符(Descriptor)是驱动与 DMA 引擎之间的契约数据结构,它告诉 DMA 控制器“从哪里搬、搬到哪里、搬多少、搬完后做什么”。在总线主控 DMA 中,描述符通常以链表(Scatter/Gather List)形式组织。
3.3.1 描述符核心字段
| 字段 | 说明 | 典型宽度 |
|---|---|---|
| 源地址 | 数据读取的起始物理地址(外设 FIFO 或内存缓冲区) | 32/64 位 |
| 目标地址 | 数据写入的起始物理地址 | 32/64 位 |
| 传输长度 | 本次描述符搬运的字节数(通常为 1~4 MB) | 16/32 位 |
| 控制标志 | 中断使能、链结束、方向、突发大小等 | 16/32 位 |
| 下一描述符指针 | 指向链表中下一个描述符的物理地址(NULL 表示链尾) | 32/64 位 |
3.3.2 描述符链工作模式
UMDF 驱动通过 WDF 框架构建散聚列表(SCATTER_GATHER_LIST),框架自动将虚拟地址转换为物理地址并填充描述符。典型流程如下:
// 伪代码:UMDF 驱动构建描述符链
WDF_DMA_TRANSFER_CONFIG config;
WDF_DMA_TRANSFER dmaTransfer;
SCATTER_GATHER_LIST sgList;
WDF_DMA_TRANSFER_CONFIG_INIT(&config, EvtDmaTransferComplete);
WdfDmaTransactionCreate(Adapter, &config, &dmaTransfer);
// 框架自动将 MDL 链转换为物理地址描述符
WdfDmaTransactionInitialize(
dmaTransfer,
EvtProgramDma, // 回调:将描述符写入外设
WdfDmaDirectionReadFromDevice,
pMdl,
Length
);
// 在 EvtProgramDma 中,驱动将 sgList 写入外设描述符 FIFO
WdfDmaTransactionGetTransferInfo(dmaTransfer, &sgList, NULL);
for (i = 0; i < sgList.NumberOfElements; i++) {
WriteToDeviceFifo(sgList.Elements[i].Address.PhysicalAddress,
sgList.Elements[i].Length);
}
调试陷阱:描述符中的地址必须是物理地址(PA),而非虚拟地址(VA)。UMDF 驱动运行在用户态,但 WDF DMA 框架通过内核态 DMA 库自动完成转换。如果驱动手动构造描述符,必须使用
MmGetPhysicalAddress 或 WDF 提供的辅助函数。
3.4 DMA 传输方向
DMA 传输方向由外设与内存之间的数据流决定。UMDF 驱动通过 WDF_DMA_DIRECTION 枚举指定方向,框架据此配置缓存一致性(Cache Coherency)和总线事务类型。
| 方向枚举值 | 数据流 | 典型场景 | 缓存操作 |
|---|---|---|---|
WdfDmaDirectionReadFromDevice |
外设 → 内存 | 网卡接收数据、磁盘读取 | DMA 写入内存后,CPU 缓存行失效(Invalidate) |
WdfDmaDirectionWriteToDevice |
内存 → 外设 | 网卡发送数据、磁盘写入 | DMA 读取内存前,CPU 缓存行刷回(Flush) |
3.4.1 方向与缓存一致性
在 x64 系统上,CPU 缓存与 DMA 引擎之间可能存在数据不一致问题。UMDF 框架通过以下机制保证一致性:
- 读方向(外设→内存):DMA 控制器将数据直接写入内存,CPU 在访问同一地址前必须执行缓存行失效指令(如
CLFLUSH或INVD)。WDF 框架在传输完成回调中自动插入内存屏障。 - 写方向(内存→外设):CPU 可能已将数据写入缓存但尚未写回内存。DMA 控制器读取内存前,必须将相关缓存行刷回到系统内存。WDF 框架在初始化传输时调用
KeFlushIoBuffers。
调试检查点:如果 UMDF 驱动在 DMA 完成后读到陈旧数据,首先检查方向枚举是否正确。常见错误是读/写方向颠倒,导致框架执行了错误的缓存维护操作。
3.5 本节核心概念速查表
| 概念 | 一句话定义 | UMDF 相关 API/对象 |
|---|---|---|
| DMA 传输 | 外设与内存之间不经 CPU 的数据搬运 | WdfDmaTransaction |
| 系统 DMA | 使用主板 DMAC,外设被动 | WdfDmaEnabler + 系统 DMA 通道 |
| 总线主控 DMA | 外设自带 DMA 引擎,主动控制总线 | WdfDmaEnabler + 散聚列表 |
| DMA 描述符 | 描述一次传输的源、目标、长度、控制信息的结构 | SCATTER_GATHER_LIST |
| 读方向 | 外设 → 内存,需缓存失效 | WdfDmaDirectionReadFromDevice |
| 写方向 | 内存 → 外设,需缓存刷回 | WdfDmaDirectionWriteToDevice |