Lazy loaded image
🥳嵌入式Linux开发
Linux驱动专题 - DMA 深度教程
Words 8542Read Time 22 min
2026-2-10
2026-3-8
type
Post
date
Feb 10, 2026
slug
linux_dma_technology_guide
category
🥳嵌入式Linux开发
icon
password
本教程系统梳理Linux内核DMA(Direct Memory Access)技术,覆盖核心API、调用流程、驱动开发实践及常见问题,适用于内核/驱动开发与面试复习。

一、DMA核心概念

1.1 DMA是什么

DMA允许外设绑过CPU直接访问系统内存,实现高速数据传输。

1.2 缓存一致性问题说明

DMA绕过CPU直接访问内存,而CPU通过Cache访问内存,这导致Cache与内存数据可能不一致
问题场景图示:

1.3 缓存一致性矛盾

DMA缓存一致性的核心命题
如何在 读性能(可用性)写一致性(正确性) 之间找到最优平衡点。
矛盾分析(CAP定理视角)
选择CP(一致性优先)
选择AP(可用性优先)
为什么P(分区)不可避免?
  1. 物理隔离:CPU Cache和内存是两个独立存储单元
  1. 通讯延迟:Cache同步操作需要时间(纳秒级,但非零)
  1. 异步访问:DMA控制器与CPU并行工作,访问内存时机不同

1.4 CPU Cache原子操作

CPU提供硬件级原子操作来保证Cache一致性,这是DMA能够可靠工作的基础。

Clean(清除/写回)操作

原子性保证
  • 不可中断:Cache line的Clean操作在硬件层面是原子的,不会被其他CPU核心或中断打断
  • 立即生效:操作完成后,内存中的数据立即可见于所有观察者(CPU核心、DMA控制器)
  • 顺序一致:配合内存屏障,确保操作顺序符合预期

Invalidate(失效)操作

原子性保证
  • 即时失效:Cache line被标记为无效后,CPU下次读取必定从内存获取最新数据
  • 粒度保证:以Cache line(通常32-64字节)为单位,避免部分失效
  • 一致性协议:通过MESI/MOESI等协议确保多核一致性

解决方案时序图

以下时序图对应1.2节的问题场景,展示如何通过Cache原语解决一致性问题。
场景1解决:Invalidate解决FROM_DEVICE问题
场景2解决:Clean解决TO_DEVICE问题

1.5 内存屏障配合

内存屏障(Memory Barrier)确保CPU和编译器不会重排序关键操作,是Cache原子操作正确工作的必要补充。

屏障类型

屏障类型
函数
使用场景
通用屏障
mb()
最强保证,性能开销最大
写屏障
wmb()
CPU写数据后启动DMA
读屏障
rmb()
DMA完成后CPU读取数据
DMA写屏障
dma_wmb()
更新DMA描述符字段后标记有效
DMA读屏障
dma_rmb()
读取DMA描述符状态前
编译器屏障
barrier()
仅需阻止编译器优化时使用

使用示例

屏障与Cache操作的关系


1.6 DMA缓存策略对比

两种模式的一致性保证

关键结论

  • TO_DEVICE:映射时自动Clean,确保CPU写入的数据刷到内存
  • FROM_DEVICE:传输完成后必须调用 dma_sync_for_cpu() 进行Invalidate
  • BIDIRECTIONAL:两个方向都需要同步
没有完美的缓存策略,只有适合具体场景的权衡。硬件提供的原子操作(Clean/Invalidate)是这种权衡的基础工具。

二、内核DMA核心接口

2.1 一致性DMA(Coherent DMA)

核心API

函数调用栈

关键特性

  • 返回两个地址:CPU虚拟地址(用于软件访问)+ DMA总线地址(用于设备访问)
  • 内存标记为非缓存(uncached)或使用硬件一致性机制
  • 分配开销较大,适合长期使用的缓冲区

2.2 流式DMA(Streaming DMA)

核心API

传输方向枚举

函数调用栈


2.3 Cache同步接口

流式DMA必须在正确时机同步Cache:
同步时序图

2.4 DMA内存池(DMA Pool)

适用于频繁分配小块一致性DMA内存的场景:

三、驱动开发示例

3.1 一致性DMA示例:环形缓冲区


3.2 流式DMA示例:网络驱动收发


3.3 Scatter-Gather DMA示例


四、DMA地址与内存模型

4.1 地址类型对比

地址类型
描述
获取方式
虚拟地址
CPU访问的地址
kmalloc()vmalloc()
物理地址
真实内存地址
virt_to_phys()
总线地址
设备看到的地址
dma_map_xxx()
DMA地址
通过IOMMU转换后的地址
dma_map_xxx()

4.2 IOMMU的作用

4.3 DMA内存布局与进程关系

4.3.1 64位系统完整内存空间划分

4.3.2 单个进程视角的完整布局(ARM64)

4.3.3 DMA缓冲区三种分配方式的内存视图

方式1: dma_alloc_coherent() - 一致性DMA
方式2: dma_alloc_wc() - 写合并DMA
方式3: 流式DMA (dma_map_single)

4.3.4 进程通过mmap访问DMA缓冲区

驱动实现mmap的关键代码

4.3.5 多进程场景下的隔离与共享

4.3.6 DMA溢出破坏场景的物理内存视图

本节场景基于流式DMA:驱动通过 kmalloc() 分配缓冲区,再用 dma_map_single() 映射。因此 DMA 缓冲区位于 kmalloc/slab 堆区(~0x8100_xxxx),与相邻 slab 对象物理紧邻,与 4.3.2 宏观布局一致。
破坏链路分析
典型破坏场景示例
场景1:覆盖相邻Slab对象(kmalloc区域内)
场景2:覆盖相邻DMA缓冲区(CMA区域内)

4.3.7 核心要点总结

要点
说明
内核空间共享
所有进程看到相同的内核虚拟地址空间(包括DMA区)
物理内存唯一
DMA缓冲区占用固定物理页帧,不同虚拟地址可映射到同一物理页
隔离机制
页表控制每个进程只能访问自己的用户空间
mmap桥接
允许用户进程直接访问DMA物理内存(零拷贝技术)
溢出危害
DMA控制器直接写物理内存,绕过所有保护机制,必须严格校验

4.4 DMA掩码设置


五、常见问题与解决方案

5.1 Cache一致性问题

⚠️
问题现象:DMA传输后CPU读到旧数据,或CPU写入后设备读到旧数据
根因:Cache与内存数据不一致
解决方案

5.2 DMA缓冲区溢出

⚠️
问题现象:系统崩溃、数据损坏、随机Oops
根因:DMA控制器不做边界检查,传输长度超过缓冲区大小
解决方案

5.3 DMA内存分配失败

⚠️
问题现象dma_alloc_coherent() 返回NULL
可能原因
  1. 请求内存过大
  1. 系统内存碎片化
  1. CMA区域耗尽
解决方案

5.4 DMA映射错误检测

⚠️
问题现象:使用无效DMA地址导致传输失败
解决方案

5.5 32位设备访问高端内存

⚠️
问题现象:32位PCI设备无法访问4GB以上物理内存
解决方案

5.6 中断与DMA同步问题

⚠️
问题现象:收到DMA完成中断但数据未就绪
根因:中断可能先于DMA写入完成到达(Posted Write)
解决方案

六、调试技巧

6.1 DMA调试内核配置

6.2 运行时调试

6.3 常用调试打印


七、面试高频问题

Q1:一致性DMA和流式DMA的区别?

一致性DMA:硬件保证Cache一致性,分配的内存标记为非缓存,适合长期使用的描述符/环形缓冲区,分配开销大。
流式DMA:需要软件显式同步Cache,性能更优,适合单次传输场景(网络包、块I/O),映射/解映射开销小。

Q2:什么时候调用dma_sync_for_cpu()dma_sync_for_device()

- CPU→设备dma_map_single(TO_DEVICE) 自动Clean;若重复使用缓冲区,CPU重新写入后调用 sync_for_device()
- 设备→CPU:DMA完成后、CPU读取前调用 sync_for_cpu() 进行Invalidate

Q3:为什么DMA缓冲区需要页对齐?

1. Cache一致性:Cache操作以cacheline为粒度,非对齐可能影响相邻数据
2. IOMMU要求:IOMMU页表以页为粒度管理
3. 硬件限制:部分DMA控制器要求地址对齐

Q4:如何处理设备只支持32位DMA地址?

1. 调用 dma_set_mask(&dev, DMA_BIT_MASK(32)) 告知内核
2. 内核自动使用bounce buffer:将高端内存数据复制到低端内存再传输
3. 或启用IOMMU进行地址重映射

Q5:DMA传输后CPU读到旧数据怎么办?

检查是否正确调用了 dma_sync_single_for_cpu()。该函数会Invalidate CPU Cache,确保CPU从内存而非Cache读取数据。

八、快速参考表

API
用途
Cache行为
dma_alloc_coherent()
分配一致性DMA内存
无需手动同步
dma_map_single()
映射单个缓冲区
自动sync_for_device
dma_map_sg()
映射scatter-gather列表
自动sync_for_device
dma_sync_single_for_cpu()
CPU准备读取
Invalidate
dma_sync_single_for_device()
设备准备读取
Clean
dma_pool_alloc()
从池分配小块内存
无需手动同步
 
i.MX6UL fsl-sai 驱动分析:真实 DMA vs 模拟 DMA
上一篇
Linux 驱动专题- 中断系统
下一篇
Linux 驱动专题 - ASoC 驱动子系统

Comments
Loading...
Catalog