3. Linux DMA子系统概览

在Linux内核中,DMA子系统并不是单一模块,而是由DMA引擎框架、DMA映射接口、一致性DMA与流式DMA两条分配路径共同组成的协作体系。理解这套体系的关键,是分清“谁在搬数据”和“CPU与设备如何看到同一块内存”这两个问题。前者由DMA控制器和DMA引擎驱动负责,后者由DMA映射API和缓存一致性机制负责。

一、DMA引擎:把“搬运”抽象成通道与描述符

DMA引擎框架位于drivers/dma/,核心思想是把不同厂商的DMA控制器统一抽象为DMA channel和DMA descriptor。外设驱动不再直接操作寄存器,而是通过dmaengineAPI提交传输请求。典型流程如下:

struct dma_chan *chan;
struct dma_slave_config cfg;
struct dma_async_tx_descriptor *tx;

chan = dma_request_chan(dev, "rx");
dmaengine_slave_config(chan, &cfg);
tx = dmaengine_prep_slave_sg(chan, sgl, nents,
                             DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
tx->callback = my_dma_callback;
dmaengine_submit(tx);
dma_async_issue_pending(chan);

其中dma_request_chan()获取通道,dmaengine_slave_config()配置方向、地址宽度和突发长度,dmaengine_prep_slave_sg()把散列表转换成硬件描述符,dma_async_issue_pending()真正启动传输。DMA引擎的价值在于:同一套API可以驱动不同SoC的DMA控制器,驱动开发者只需关注通道、描述符和回调,而不必为每颗芯片重写搬运逻辑。

二、DMA映射接口:CPU地址与设备地址的翻译层

CPU访问内存用的是虚拟地址,设备访问内存用的是DMA地址(总线地址)。两者之间需要映射。Linux提供两类映射接口:

  • 一致性DMA映射:dma_alloc_coherent() / dma_free_coherent(),分配一块CPU和设备都能直接访问、且缓存属性一致的内存。
  • 流式DMA映射:dma_map_single()、dma_map_page()、dma_map_sg(),把已经存在的内存临时映射给设备,传输结束后用dma_unmap_*()解除。

映射接口还负责处理IOMMU、SWIOTLB和缓存一致性。在无IOMMU的平台上,DMA地址通常等于物理地址;在启用IOMMU后,DMA地址是IOVA,由IOMMU完成二次翻译。因此,驱动不能假设virt_to_phys()的结果就是设备能用的地址,必须走DMA API。

三、一致性DMA与流式DMA的核心区别

两者的本质差异在于缓存一致性由谁保证以及生命周期如何管理。一致性DMA在分配时就保证CPU和设备看到相同数据,驱动无需手动刷缓存;流式DMA则要求驱动在设备读取前执行dma_sync_single_for_device(),在设备写完后执行dma_sync_single_for_cpu(),否则可能读到旧数据或覆盖设备写入。

对比项一致性DMA流式DMA
典型APIdma_alloc_coherentdma_map_single / dma_map_sg
缓存一致性硬件或架构保证驱动显式sync
生命周期长期持有,直到free单次传输,map/unmap成对
性能分配慢,访问可能非缓存映射快,可复用已有内存
适用场景描述符环、控制块、长期共享缓冲网络收包、块设备IO、音频流

一句话记忆:一致性DMA是“长期共享、自动一致”,流式DMA是“临时借用、手动同步”。

四、典型场景与选择方法

以网卡收包为例,驱动通常为每个RX描述符分配流式DMA缓冲区:设备把包写入内存后,驱动调用dma_unmap_single()并同步缓存,再把数据交给协议栈。而网卡的描述符环本身是CPU和设备长期共享的控制结构,适合用dma_alloc_coherent()分配。再如音频播放,周期性的PCM缓冲区适合流式DMA,因为每次传输后需要重新映射;而DMA控制器的链表节点则适合一致性DMA。

选择时可按以下顺序判断:

  1. 这块内存是否长期被CPU和设备同时读写?是则优先一致性DMA。
  2. 是否只是单次或周期性传输、且内存已存在?是则用流式DMA。
  3. 是否位于中断上下文或原子上下文?流式DMA的map/unmap通常可原子执行,但一致性DMA分配可能睡眠。
  4. 平台是否有IOMMU?有则注意DMA地址与物理地址分离,必须使用DMA API。

五、常见误区与复盘

误区1:把kmalloc()返回的虚拟地址直接写入DMA寄存器。正确做法是dma_map_single()后使用返回的DMA地址。

误区2:流式DMA映射后忘记unmap,导致IOMMU映射泄漏或SWIOTLB耗尽。

误区3:在设备写内存后没有调用dma_sync_single_for_cpu(),CPU读到缓存中的旧数据。

误区4:认为一致性DMA一定比流式DMA快。实际上一致性DMA可能使用非缓存内存,CPU访问反而更慢,应只在必要时使用。

下面这张图概括了Linux DMA子系统的整体结构:上层是DMA引擎框架,中间是DMA映射接口,下层分别落到一致性DMA和流式DMA两条路径,最终由设备通过DMA地址访问内存。

Linux DMA 子系统结构 DMA 引擎框架(dmaengine) dma_request_chan · dmaengine_prep_slave_sg · dma_async_issue_pending DMA 映射接口(DMA API) dma_alloc_coherent · dma_map_single · dma_map_sg · dma_sync_* 一致性 DMA dma_alloc_coherent() 长期共享 · 自动一致 描述符环 · 控制块 无需手动 sync 流式 DMA dma_map_single() / dma_map_sg() 单次传输 · 手动同步 网络收包 · 块设备 IO map / unmap 成对 设备通过 DMA 地址访问内存

掌握这张结构图后,再回头看驱动代码,就能快速定位:dma_request_chan()属于引擎层,dma_map_single()属于映射层,而dma_alloc_coherent()与dma_map_sg()分别对应两条不同的内存路径。只有把“引擎—映射—一致性/流式”三层关系理清,后续的缓存一致性调试和性能优化才有稳固的起点。