4. 一致性DMA映射(Coherent DMA):dma_alloc_coherent()函数详解
一致性DMA映射(Coherent DMA Mapping)是Linux内核为DMA操作提供的最直接、最安全的缓冲区分配方式。它保证在CPU和DMA控制器访问缓冲区时,不需要任何额外的缓存一致性维护操作(如手动flush或invalidate cache)。这种“一致性”是通过硬件和内核的协同配置实现的,其核心在于分配时对内存页表属性的特殊设置。
核心概念: 一致性DMA映射的“一致性”并非指数据在CPU和Device之间实时同步,而是指硬件保证任何时刻,CPU通过虚拟地址看到的缓存内容与DMA设备通过物理地址看到的内存内容是一致的。这通常通过将内存区域标记为非缓存(Non-Cacheable)或非缓冲(Non-Bufferable)来实现。
4.1 dma_alloc_coherent() 函数原型与参数
该函数是分配一致性DMA缓冲区的主要接口,定义在 <linux/dma-mapping.h> 中。其原型如下:
/**
* dma_alloc_coherent - 分配一致性DMA缓冲区
* @dev: 指向设备结构的指针,用于标识DMA设备
* @size: 请求分配的缓冲区大小(字节)
* @dma_handle: 输出参数,返回DMA总线地址(物理地址)
* @gfp: GFP内核内存分配标志(GFP_KERNEL / GFP_ATOMIC 等)
*
* 返回值: 内核虚拟地址(CPU可访问),失败返回 NULL
*/
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t gfp);
| 参数 | 类型 | 说明 |
|---|---|---|
| dev | struct device * |
执行DMA操作的设备。不能为NULL,内核需要它来获取IOMMU或总线信息。 |
| size | size_t |
请求的字节数。内核会按页对齐分配,实际分配大小可能大于请求值。 |
| dma_handle | dma_addr_t * |
输出参数。函数返回后,此指针指向DMA设备可直接使用的总线地址(物理地址)。 |
| gfp | gfp_t |
内存分配标志。在进程上下文中通常使用 GFP_KERNEL;在中断上下文中必须使用 GFP_ATOMIC。 |
4.2 返回的虚拟地址与物理地址
dma_alloc_coherent() 成功返回后,会提供两个关键的地址值:
- 返回值(虚拟地址): 这是CPU可以直接解引用访问的内核虚拟地址(属于内核线性映射区或vmalloc区)。驱动通过此地址读写缓冲区数据。
dma_handle(DMA地址): 这是DMA设备用来寻址的总线地址。在大多数平台上(无IOMMU),它等同于物理地址。驱动将此地址写入DMA控制器的源/目的地址寄存器。
关键区别: 虚拟地址用于CPU编程,DMA地址用于设备DMA。两者指向同一块物理内存,但CPU访问时经过MMU和Cache,而设备访问时直接通过总线。一致性映射保证了这两种访问路径看到的数据一致。
下面的SVG图展示了CPU和DMA设备通过一致性映射访问同一块物理内存的路径:
4.3 内存属性:非缓存(Non-Cacheable)与非缓冲(Non-Bufferable)
一致性DMA映射的本质在于修改了页表项中的内存属性。当内核分配一致性缓冲区时,它会将对应的物理页框映射到内核虚拟地址空间,并清除该映射的 C(Cacheable) 和 B(Bufferable) 位。这导致CPU对该区域的访问完全绕过L1/L2 Cache和写缓冲。
| 内存属性 | ARMv8 页表描述符 | x86 PAT/MTRR | 行为描述 |
|---|---|---|---|
| 非缓存(Non-Cacheable) | AttrIndx 指向 MAIR 中 Normal Non-Cacheable 条目 | PAT 设置为 WC(Write Combining)或 UC(Uncacheable) | CPU读/写直接访问内存,不经过Cache。读操作不会填充Cache,写操作直接写入内存。 |
| 非缓冲(Non-Bufferable) | 在 Non-Cacheable 基础上,设置 Shareability 为 Non-Shareable | UC(Uncacheable)模式隐含非缓冲 | CPU写操作必须等待写事务在总线上完成才返回。不会使用写缓冲合并写操作。 |
性能代价: 一致性DMA映射虽然简化了驱动开发,但代价是显著的性能损失。CPU对一致性缓冲区的每次读写都是慢速的总线事务,而不是快速的Cache命中。对于高频小数据量的控制/状态寄存器通信,这种代价可以接受;但对于大数据块传输(如网络数据包、视频帧),应使用流式DMA映射(Streaming DMA Mapping)配合手动Cache同步以获得更好性能。
4.4 分配流程与内部机制
内核在实现 dma_alloc_coherent() 时,经历以下关键步骤:
- 大小对齐: 将请求的
size向上对齐到页大小(PAGE_SIZE)。 - 物理页分配: 调用底层内存分配器(如
__get_free_pages()或alloc_pages())获取连续的物理页框。 - 页表属性设置: 在建立内核虚拟地址到物理页框的映射时,将页表项中的 Cacheable 和 Bufferable 位清零。在ARM平台上,这通过设置MAIR(Memory Attribute Indirection Register)索引实现;在x86上,通过设置PAT(Page Attribute Table)条目实现。
- DMA地址转换: 如果系统有IOMMU/SMMU,内核会将物理地址映射到设备可见的I/O虚拟地址空间,并返回该I/O虚拟地址作为
dma_handle。如果没有IOMMU,则直接返回物理地址。 - 清零初始化: 内核使用
memset()将分配的缓冲区清零,确保设备不会读到残留的敏感数据。
下面的SVG流程图展示了从驱动调用到内存属性设置的完整分配流程:
4.5 使用示例与释放
以下是一个典型的驱动代码片段,展示如何分配和使用一致性DMA缓冲区:
#include <linux/dma-mapping.h>
#include <linux/slab.h>
struct my_device {
struct device *dev;
void *cpu_addr; // CPU虚拟地址
dma_addr_t dma_addr; // DMA总线地址
size_t buf_size;
};
int my_driver_probe(struct my_device *mydev)
{
struct device *dev = mydev->dev;
size_t size = 4096; // 4KB缓冲区
// 分配一致性DMA缓冲区
mydev->cpu_addr = dma_alloc_coherent(dev, size,
&mydev->dma_addr,
GFP_KERNEL);
if (!mydev->cpu_addr) {
dev_err(dev, "Failed to allocate coherent DMA buffer\n");
return -ENOMEM;
}
mydev->buf_size = size;
// CPU写入数据(直接写,无需Cache同步)
memset(mydev->cpu_addr, 0, size);
strcpy((char *)mydev->cpu_addr, "DMA transfer data");
// 将dma_addr写入DMA控制器的地址寄存器
// writel(mydev->dma_addr, dma_reg_base + DMA_SRC_ADDR_REG);
dev_info(dev, "Coherent DMA buffer: cpu=%px dma=%pad size=%zu\n",
mydev->cpu_addr, &mydev->dma_addr, size);
return 0;
}
void my_driver_remove(struct my_device *mydev)
{
if (mydev->cpu_addr) {
// 释放一致性DMA缓冲区
dma_free_coherent(mydev->dev, mydev->buf_size,
mydev->cpu_addr, mydev->dma_addr);
mydev->cpu_addr = NULL;
mydev->dma_addr = 0;
mydev->buf_size = 0;
}
}
释放注意事项: 调用
dma_free_coherent() 时,必须传入与分配时完全相同的 dev、size、cpu_addr 和 dma_handle。任何参数不匹配都会导致内核崩溃或内存泄漏。建议在驱动移除路径中始终检查 cpu_addr 是否为NULL后再释放。
4.6 一致性DMA映射的适用场景
根据一致性DMA映射的特性,以下场景最适合使用:
- 控制/状态寄存器通信: 如网卡的描述符环(Descriptor Ring)、USB的DMA描述符、音频设备的DMA控制块。这些数据结构通常较小(几十到几百字节),且CPU和设备需要频繁交换状态信息。
- 小数据量周期性传输: 如I2S音频数据、低速ADC采样数据。一致性映射避免了每次传输都做Cache同步的开销。
- 不需要高吞吐的场景: 当数据传输速率较低(< 10 MB/s)时,非Cache访问的性能损失可以接受。
- 驱动开发初期: 一致性映射是最简单的DMA方式,适合在驱动原型阶段快速验证功能,后续再优化为流式映射。
| 场景 | 推荐映射类型 | 原因 |
|---|---|---|
| 网卡描述符环 | 一致性映射 | CPU和设备频繁更新描述符状态,一致性保证避免Cache同步 |
| 大块网络数据包(> 1KB) | 流式映射 | 数据量大,流式映射配合Cache同步可获得更高吞吐 |
| 音频DMA缓冲区 | 一致性映射 | 周期性小数据块,实时性要求高,避免同步延迟 |
| 视频帧缓冲区 | 流式映射 | 数据量大,一致性映射的Non-Cache访问会成为性能瓶颈 |
经验法则: 如果DMA缓冲区的大小小于一个Cache Line(通常64字节),或者CPU和设备对该缓冲区的访问频率超过每秒1000次,优先考虑一致性DMA映射。否则,流式DMA映射通常是更好的选择。