4、流式DMA映射:dma_map_single / dma_unmap_single、dma_map_sg / dma_unmap_sg、dma_sync_single_for_cpu / device
流式DMA映射是Linux内核中用于设备与内存之间进行高效数据传输的核心机制。与一致性DMA映射(coherent mapping)不同,流式映射不保证缓存一致性,因此需要开发者显式管理缓存同步操作。本章将深入剖析三种最常用的流式DMA映射API:单缓冲区映射、分散/聚集映射以及缓存同步操作。
4.1 流式DMA映射基础概念
流式DMA映射的核心思想是“所有权转移”:在DMA传输开始前,CPU将缓冲区所有权交给设备;传输完成后,设备将所有权交还给CPU。这种所有权转移通过缓存同步操作实现,确保CPU和设备看到一致的数据视图。
| 特性 | 一致性DMA映射 | 流式DMA映射 |
|---|---|---|
| 缓存一致性 | 硬件保证(如IOMMU/缓存一致性总线) | 软件显式管理 |
| 映射开销 | 较高(可能涉及页表修改) | 较低(仅缓存操作) |
| 适用场景 | 长期映射、小数据量控制结构 | 大数据量、短时传输(如视频帧) |
| API函数 | dma_alloc_coherent / dma_free_coherent | dma_map_single / dma_map_sg 等 |
| 缓存同步 | 不需要 | 必须调用 dma_sync_* 系列函数 |
4.2 单缓冲区映射:dma_map_single / dma_unmap_single
dma_map_single 是最基础的流式DMA映射函数,用于将一段连续的虚拟地址空间映射到DMA总线地址。其原型如下:
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
参数说明:
- dev:DMA设备指针,用于获取设备所属的DMA域
- cpu_addr:CPU虚拟地址,必须指向已分配的缓冲区(kmalloc / vmalloc / 页分配)
- size:映射的字节数
- dir:数据传输方向(DMA_TO_DEVICE / DMA_FROM_DEVICE / DMA_BIDIRECTIONAL)
返回值是DMA总线地址,设备使用该地址进行数据传输。映射失败时返回DMA_MAPPING_ERROR。
对应的解除映射函数:
void dma_unmap_single(struct device *dev, dma_addr_t dma_handle,
size_t size, enum dma_data_direction dir);
以下是一个典型的单缓冲区映射使用流程:
/* 分配缓冲区 */
buf = kmalloc(buf_size, GFP_KERNEL);
if (!buf)
return -ENOMEM;
/* 映射到DMA地址 */
dma_addr = dma_map_single(dev, buf, buf_size, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
kfree(buf);
return -EIO;
}
/* 启动DMA传输(设备使用dma_addr) */
start_dma_transfer(dma_addr, buf_size);
/* 等待传输完成 */
wait_for_dma_completion();
/* 同步缓存:将设备数据刷新到CPU可见 */
dma_sync_single_for_cpu(dev, dma_addr, buf_size, DMA_FROM_DEVICE);
/* 现在CPU可以安全访问buf */
process_data(buf);
/* 解除映射 */
dma_unmap_single(dev, dma_addr, buf_size, DMA_FROM_DEVICE);
kfree(buf);
4.3 分散/聚集映射:dma_map_sg / dma_unmap_sg
在实际的多媒体驱动中,视频帧缓冲区往往由多个不连续的物理页组成(例如从vmalloc或用户空间分配的缓冲区)。dma_map_sg 专门用于处理这种分散/聚集(SG)缓冲区,它可以将一个scatterlist数组映射为设备可用的DMA地址列表。
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
关键数据结构:
struct scatterlist {
unsigned long page_link; /* 指向物理页或链表 */
unsigned int offset; /* 页内偏移 */
unsigned int length; /* 该段长度 */
dma_addr_t dma_address; /* 映射后的DMA地址(由dma_map_sg填充) */
unsigned int dma_length; /* DMA传输长度 */
};
dma_map_sg 的返回值是成功映射的DMA段数(可能少于nents,因为硬件可能合并相邻段)。映射后,每个scatterlist的dma_address字段被填充为对应的DMA总线地址。
解除映射函数:
void dma_unmap_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
SG映射的典型使用模式:
struct scatterlist sglist[NR_SG];
int i, mapped_nents;
/* 初始化scatterlist */
sg_init_table(sglist, NR_SG);
for (i = 0; i < NR_SG; i++) {
struct page *page = alloc_page(GFP_KERNEL);
sg_set_page(&sglist[i], page, PAGE_SIZE, 0);
}
/* 执行DMA映射 */
mapped_nents = dma_map_sg(dev, sglist, NR_SG, DMA_FROM_DEVICE);
if (mapped_nents == 0) {
/* 映射失败处理 */
goto err_unmap;
}
/* 遍历映射后的DMA地址 */
for_each_sg(sglist, sg, mapped_nents, i) {
dma_addr_t dma_addr = sg_dma_address(sg);
unsigned int len = sg_dma_len(sg);
/* 将dma_addr和len传递给设备DMA描述符 */
}
/* 启动DMA传输 */
start_sg_dma(sglist, mapped_nents);
/* 传输完成后同步 */
dma_sync_sg_for_cpu(dev, sglist, mapped_nents, DMA_FROM_DEVICE);
/* 解除映射 */
dma_unmap_sg(dev, sglist, mapped_nents, DMA_FROM_DEVICE);
4.4 缓存同步操作:dma_sync_single_for_cpu / device
缓存同步是流式DMA映射的核心操作。由于CPU和DMA设备各自拥有独立的缓存视图,在所有权转移时必须执行缓存刷新或无效化操作。
| 函数 | 调用时机 | 操作含义 |
|---|---|---|
| dma_sync_single_for_cpu | DMA传输完成后,CPU访问前 | 无效化CPU缓存(DMA_FROM_DEVICE)或刷新回写(DMA_TO_DEVICE) |
| dma_sync_single_for_device | CPU写入数据后,启动DMA前 | 刷新CPU缓存到内存(DMA_TO_DEVICE)或无效化设备端缓存 |
| dma_sync_sg_for_cpu | SG传输完成后,CPU访问前 | 对scatterlist中所有段执行for_cpu同步 |
| dma_sync_sg_for_device | CPU写入SG数据后,启动DMA前 | 对scatterlist中所有段执行for_device同步 |
方向参数决定缓存操作的具体行为:
- DMA_TO_DEVICE(CPU→设备):同步时刷新CPU缓存到内存,确保设备看到最新数据
- DMA_FROM_DEVICE(设备→CPU):同步时无效化CPU缓存,强制从内存重新读取
- DMA_BIDIRECTIONAL:同时执行刷新和无效化(性能较差,尽量避免)
4.5 videobuf2中的流式DMA映射实践
在videobuf2框架中,流式DMA映射通过vb2_dma_contig或vb2_dma_sg内存分配器实现。以下是vb2_dma_sg分配器中流式映射的核心流程:
/* vb2_dma_sg_alloc - 分配并映射SG缓冲区 */
static void *vb2_dma_sg_alloc(struct device *dev, unsigned long size)
{
struct vb2_dma_sg_buf *buf;
struct sg_table *sgt;
int ret;
buf = kzalloc(sizeof(*buf), GFP_KERNEL);
if (!buf)
return ERR_PTR(-ENOMEM);
buf->dev = dev;
buf->size = size;
/* 分配物理页并构建scatterlist */
sgt = vb2_dma_sg_alloc_sgt(dev, size);
if (IS_ERR(sgt)) {
kfree(buf);
return ERR_PTR(PTR_ERR(sgt));
}
/* 执行DMA映射 */
ret = dma_map_sg(dev, sgt->sgl, sgt->nents, DMA_BIDIRECTIONAL);
if (ret == 0) {
vb2_dma_sg_free_sgt(sgt);
kfree(buf);
return ERR_PTR(-EIO);
}
buf->sgt = sgt;
buf->dma_dir = DMA_BIDIRECTIONAL;
return buf;
}
/* vb2_dma_sg_put - 解除映射并释放 */
static void vb2_dma_sg_put(void *buf_priv)
{
struct vb2_dma_sg_buf *buf = buf_priv;
dma_unmap_sg(buf->dev, buf->sgt->sgl,
buf->sgt->nents, buf->dma_dir);
vb2_dma_sg_free_sgt(buf->sgt);
kfree(buf);
}
/* 在队列操作中执行缓存同步 */
static void vb2_dma_sg_prepare(void *buf_priv)
{
struct vb2_dma_sg_buf *buf = buf_priv;
/* 启动DMA前:将CPU数据刷新到设备 */
dma_sync_sg_for_device(buf->dev, buf->sgt->sgl,
buf->sgt->nents, buf->dma_dir);
}
static void vb2_dma_sg_finish(void *buf_priv)
{
struct vb2_dma_sg_buf *buf = buf_priv;
/* DMA完成后:将设备数据同步到CPU */
dma_sync_sg_for_cpu(buf->dev, buf->sgt->sgl,
buf->sgt->nents, buf->dma_dir);
}
4.6 性能对比与选择指南
| 映射类型 | 映射开销 | 适用缓冲区 | 缓存同步 | 典型场景 |
|---|---|---|---|---|
| dma_map_single | 低(仅缓存操作) | 连续物理内存(kmalloc) | dma_sync_single_* | 控制结构、小数据包 |
| dma_map_sg | 中(遍历scatterlist) | 不连续物理页(vmalloc/用户空间) | dma_sync_sg_* | 视频帧、网络大包 |
| dma_alloc_coherent | 高(页表/IOMMU操作) | 连续物理内存 | 不需要 | 描述符环、寄存器 |
选择建议:
- 对于视频采集驱动,优先使用 dma_map_sg + dma_sync_sg_* 组合,因为视频帧缓冲区通常来自用户空间分配,物理页不连续
- 对于硬件编解码器的控制结构(如描述符链表),使用 dma_alloc_coherent 避免缓存同步开销
- 避免使用 DMA_BIDIRECTIONAL 方向,除非缓冲区确实需要双向传输;单向方向允许内核优化缓存操作 <