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控制器驱动提供通道,客户端驱动提交描述符,适合内存到内存、内存到设备的大块搬运和链式传输。
二、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、多页缓冲 |
| dmaengine | dma_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()再读取状态。
最后给出一个选择清单:控制块长期共享选一致性映射;一次性数据流选流式映射;多页数据选SG映射;需要独立控制器搬运选dmaengine;任何映射都要配对解映射,任何方向变化都要重新同步。掌握这四条,DMA子系统的API选择就不会迷路。