3. DMA 基础回顾:DMA 控制器工作原理、传输模式与地址映射
在深入缓存一致性协议之前,必须扎实掌握 DMA 控制器的底层行为。DMA(Direct Memory Access)允许外设与内存直接交换数据,无需 CPU 逐字节搬运,从而大幅提升系统吞吐量。然而,DMA 的“旁路”特性也正是缓存一致性问题的根源——DMA 直接读写物理内存,而 CPU 可能还在 Cache 中持有旧副本。
3.1 DMA 控制器工作原理
DMA 控制器本质上是一个专用的总线主控引擎。它从 CPU 接管总线控制权,在源地址与目的地址之间搬运数据,搬运完成后通过中断通知 CPU。典型的工作流程包含以下步骤:
- CPU 配置 DMA 寄存器:设置源地址、目的地址、传输长度、传输模式、触发源等参数。
- CPU 启动 DMA:写 DMA 使能位或触发软件请求。
- DMA 请求总线:DMA 控制器向总线仲裁器发出总线请求(Bus Request)。
- 总线授权:仲裁器在 CPU 空闲或总线空闲时授予总线控制权。
- DMA 执行传输:按配置的传输模式(Burst/Single)在源与目的之间搬运数据。
- 传输完成:DMA 释放总线,向 CPU 发送中断信号。
关键认知: DMA 传输过程中,CPU 可以继续执行指令(如果指令和数据都在 Cache 中命中),但 CPU 无法访问被 DMA 占用的总线。DMA 操作的是物理地址,完全绕过 CPU 的 Cache 层次。
排查提示: 在调试 DMA 相关问题时,首先确认 DMA 寄存器配置是否正确,特别是地址对齐和传输长度。错误的地址对齐可能导致 Burst 传输退化为 Single 传输,性能骤降。
3.2 传输模式:Burst 与 Single
DMA 传输模式直接决定总线占用时长和传输效率。两种模式的核心差异在于:一次总线仲裁后连续传输的数据量。
| 特性 | Burst(突发)模式 | Single(单次)模式 |
|---|---|---|
| 一次仲裁传输量 | 连续传输多个数据单元(如 4/8/16 次) | 每次仲裁只传输 1 个数据单元 |
| 总线占用时间 | 长,持续占用总线直到 Burst 完成 | 短,每次传输后释放总线 |
| 传输效率 | 高(减少仲裁开销) | 低(频繁仲裁) |
| 对 CPU 影响 | CPU 可能长时间无法访问总线 | CPU 有更多机会抢占总线 |
| 典型应用 | 大块数据搬运(如帧缓冲、文件系统) | 小数据量、实时性要求高的场景 |
| 缓存一致性风险 | 高(连续修改内存,Cache 行易失效) | 低(单次修改,CPU 可及时同步) |
实战避坑: Burst 传输中,如果 DMA 修改的内存区域恰好与 CPU Cache 行重叠,且 Cache 策略为写回(Write-Back),则 DMA 完成后 CPU 读到的可能是 Cache 中的脏数据。这是缓存一致性问题的典型触发场景。
3.3 地址映射:DMA 眼中的内存世界
DMA 控制器直接操作物理地址空间,不经过 MMU 的虚拟地址转换。这意味着:
- DMA 地址 = 物理地址:配置 DMA 时填入的地址必须是物理地址(PA),而非 CPU 看到的虚拟地址(VA)。
- 连续物理内存:DMA 要求传输缓冲区在物理地址上连续。如果使用不连续的物理页,需要借助 SG(Scatter-Gather)链表。
- IOMMU/SMMU 介入:在虚拟化或安全系统中,IOMMU 可以为 DMA 提供设备虚拟地址(DVA)到物理地址的映射,但底层仍然是物理地址访问。
地址映射核心原则: CPU 通过 Cache 访问内存(虚拟地址 → 物理地址),DMA 直接访问物理内存。两者看到的“内存视图”不同,这是缓存一致性问题的根本原因。
| 地址类型 | 使用者 | 转换方式 | 是否经过 Cache |
|---|---|---|---|
| 虚拟地址(VA) | CPU 指令/数据访问 | MMU 页表转换 → PA | 是(Cache 命中时) |
| 物理地址(PA) | DMA 控制器、Cache 标签 | 直接使用 | 否(DMA 直接访问内存控制器) |
| 设备虚拟地址(DVA) | 支持 IOMMU 的 DMA | IOMMU 页表转换 → PA | 否(DMA 路径) |
3.4 地址映射实战:Scatter-Gather 与一致性
当 DMA 缓冲区需要跨越多个不连续的物理页时,必须使用 Scatter-Gather(SG)链表。SG 链表由多个描述符组成,每个描述符包含一个物理地址段和长度。DMA 控制器自动遍历链表,完成整块数据的搬运。
SG 与一致性: 使用 SG 时,每个描述符指向的物理页可能被 CPU 缓存。如果 DMA 写入了某个页,而 CPU 的 Cache 中恰好有该页的脏数据,则一致性被破坏。因此,在启动 DMA 前,必须对涉及的所有物理页执行 Cache 刷新(Clean)或失效(Invalidate)操作。
/* 伪代码:DMA SG 描述符初始化与 Cache 同步 */
struct sg_descriptor {
uint32_t phys_addr; /* 物理地址 */
uint32_t length; /* 传输长度 */
uint32_t flags; /* 控制标志 */
uint32_t next; /* 下一个描述符指针 */
};
void dma_prepare_sg(struct sg_descriptor *sg_list, int num_entries) {
for (int i = 0; i < num_entries; i++) {
/* 1. 对每个物理页执行 Cache 刷新(写方向)或失效(读方向) */
cache_flush_invalidate_page(sg_list[i].phys_addr, sg_list[i].length);
/* 2. 确保描述符本身也在一致性内存中 */
dma_sync_single_for_device(sg_list[i].phys_addr, sg_list[i].length, DMA_TO_DEVICE);
}
/* 3. 写 DMA 寄存器,启动传输 */
dma_start(sg_list);
}
常见错误: 工程师容易忘记在 DMA 完成后再次执行 Cache 失效操作。DMA 写入内存后,CPU 必须失效对应 Cache 行,才能读到最新数据。否则,CPU 可能读到 Cache 中的旧值,导致数据错误。