3. Linux内核DMA子系统概览

在Linux内核中,DMA并不是一个单一接口,而是由一致性DMA映射API、流式DMA映射API、dmaengine异步传输框架以及DMA属性/方向/掩码等配置接口共同组成的分层子系统。理解这些API的边界,是写出可移植、可维护驱动的前提。很多驱动问题并非出在硬件,而是把“谁拥有缓冲区、谁负责同步缓存、谁触发传输”这三件事混在了一起。

一、核心原理:三类DMA使用模型

从内核视角看,DMA使用方式可以归纳为三种模型。第一种是一致性映射,适用于驱动与设备长期共享的控制块、描述符环、状态寄存器镜像等。这类内存由dma_alloc_coherent()分配,CPU和设备看到的内容天然一致,不需要显式同步,但分配成本较高、数量有限。第二种是流式映射,适用于一次性的数据缓冲区,例如网络收包、块设备读写、音频采样块。它通过dma_map_single()或dma_map_sg()建立临时映射,使用前后必须用dma_sync_single_for_cpu/device()维护缓存一致性。第三种是dmaengine,由DMA控制器驱动提供通道,客户端驱动提交描述符,适合内存到内存、内存到设备的大块搬运和链式传输。

关键判断:如果缓冲区生命周期跨越多次传输且被CPU频繁读写,优先考虑一致性映射;如果是一次性、批量、可同步的数据流,优先使用流式映射;如果需要由独立DMA控制器完成搬运并支持回调,使用dmaengine。

二、DMA API分类与典型调用链

内核DMA API大致可分为四组:分配/释放、映射/解映射、同步、方向与属性配置。下面是一段典型的流式映射代码:

struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
void *buf = kmalloc(BUF_SIZE, GFP_KERNEL);

dma_handle = dma_map_single(dev, buf, BUF_SIZE, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_handle)) {
    kfree(buf);
    return -ENOMEM;
}

/* 把 dma_handle 写入设备寄存器,启动传输 */
writel(dma_handle, base + REG_DMA_SRC);

/* 传输完成后,CPU 要读回数据 */
dma_sync_single_for_cpu(dev, dma_handle, BUF_SIZE, DMA_FROM_DEVICE);
/* 读取 buf ... */
dma_sync_single_for_device(dev, dma_handle, BUF_SIZE, DMA_FROM_DEVICE);

dma_unmap_single(dev, dma_handle, BUF_SIZE, DMA_TO_DEVICE);
kfree(buf);

对于分散/聚集场景,应使用dma_map_sg(),它返回实际映射的段数,驱动需要按返回的sg_dma_address()和sg_dma_len()填写描述符。注意:dma_map_sg()可能合并相邻页,不能假设段数等于原始sg数量。

三、dmaengine框架:通道、描述符与回调

dmaengine是内核为DMA控制器抽象出的通用框架。客户端驱动通过dma_request_chan()申请通道,用dmaengine_prep_slave_sg()或dmaengine_prep_dma_memcpy()准备描述符,再通过dmaengine_submit()提交,最后dma_async_issue_pending()触发执行。传输完成由dma_cookie_t跟踪,回调在tasklet或中断上下文中运行。

chan = dma_request_chan(dev, "rx");
desc = dmaengine_prep_slave_sg(chan, sgl, nents,
                               DMA_DEV_TO_MEM,
                               DMA_PREP_INTERRUPT);
desc->callback = rx_done;
desc->callback_param = priv;
cookie = dmaengine_submit(desc);
dma_async_issue_pending(chan);

dmaengine的价值在于把“搬运”从CPU卸载到独立控制器,特别适合音频、串口、SPI、内存拷贝等场景。但它不负责缓存一致性,客户端仍需根据方向调用同步接口。

四、映射类型对比与使用场景

映射类型典型API缓存一致性适用场景
一致性映射dma_alloc_coherent硬件保证一致描述符环、控制块、状态区
流式单缓冲区dma_map_single需显式同步单次收发、寄存器数据块
流式分散聚集dma_map_sg需显式同步网络包、块IO、多页缓冲
dmaenginedma_request_chan等由客户端同步音频、SPI、memcpy、链式传输

五、案例与误区复盘

某平台音频驱动在播放时出现偶发杂音。排查发现,驱动用dma_map_single()映射播放缓冲区后,在DMA仍在搬运时CPU又写入了下一段数据,且没有调用dma_sync_single_for_device()。在非一致性架构上,CPU写回缓存的时间不确定,DMA可能读到旧数据。修复方式是在每次提交DMA前执行dma_sync_single_for_device(),并在传输完成后执行dma_sync_single_for_cpu()再读取状态。

常见误区:1)认为dma_map_single会自动处理缓存同步;2)在DMA传输进行中释放或复用缓冲区;3)忽略dma_mapping_error检查;4)把dmaengine当成缓存一致性方案;5)在原子上下文调用可能睡眠的映射API。

最后给出一个选择清单:控制块长期共享选一致性映射;一次性数据流选流式映射;多页数据选SG映射;需要独立控制器搬运选dmaengine;任何映射都要配对解映射,任何方向变化都要重新同步。掌握这四条,DMA子系统的API选择就不会迷路。

Linux DMA 子系统 API 分层与数据流 客户端驱动层 网络/块/音频驱动 dma_request_chan dma_map_single/sg dma_alloc_coherent API 调用 内核 DMA 核心层 一致性映射 API dma_alloc_coherent 流式映射 API map/sync/unmap dmaengine 框架 prep/submit/issue 属性与方向 DMA_TO_DEVICE 等 硬件操作 硬件与内存层 DMA 控制器 设备寄存器 系统内存 / 缓存 IOMMU / SMMU 缓存一致性同步路径 CPU 写 → dma_sync_single_for_device → DMA 读 → 传输完成 → dma_sync_single_for_cpu → CPU 读 一致性映射无需显式同步;流式映射必须按方向配对同步