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_* 系列函数
实战建议:在videobuf2框架中,视频帧缓冲区通常使用流式DMA映射。因为视频数据量大、传输频繁,流式映射的低开销特性非常适合帧级传输。一致性映射则多用于描述符环、控制寄存器等小数据量且需要长期访问的场景。

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。

关键约束:dma_map_single 要求传入的CPU地址必须来自 同一物理页 或 连续物理内存。如果缓冲区跨越多个不连续的物理页,必须使用dma_map_sg。此外,映射后的缓冲区在dma_unmap_single之前,CPU不能访问该缓冲区(除非先调用dma_sync_*)。

对应的解除映射函数:

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);
dma_map_single 生命周期流程图 分配缓冲区 (kmalloc) dma_map_single() 启动DMA传输 等待传输完成 dma_sync_single_for_cpu() dma_unmap_single() + kfree() CPU可读写缓冲区 设备不可访问 所有权转移给设备 CPU不可访问 所有权归还给CPU 缓存刷新完成

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总线地址。

SG映射的核心优势:硬件DMA引擎通常支持SG列表传输,设备可以直接遍历多个DMA地址完成一次传输,无需CPU参与数据搬运。这在视频采集、网络包处理等场景中大幅降低CPU负载。

解除映射函数:

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);
dma_map_sg 分散/聚集映射结构图 CPU视角(不连续物理页) 页0 页1 页2 页3 dma_map_sg DMA视角(连续地址空间) DMA地址区间(合并后) scatterlist 数组结构 sg[0] page=页0, len=4096 sg[1] page=页1, len=4096 sg[2] page=页2, len=4096 sg[3] page=页3, len=4096 映射后:dma_address 字段填充 dma_addr=0x1000 dma_addr=0x2000 dma_addr=0x3000 dma_addr=0x4000 注意:dma_map_sg可能合并相邻物理页,返回的mapped_nents ≤ nents

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:同时执行刷新和无效化(性能较差,尽量避免)
常见错误:忘记调用dma_sync_*是流式DMA映射中最常见的bug。在DMA传输完成后,如果CPU直接访问缓冲区而不先调用dma_sync_single_for_cpu,CPU可能读到过时的缓存数据(对于DMA_FROM_DEVICE)或设备看到不完整的数据(对于DMA_TO_DEVICE)。这种bug极难调试,因为缓存行为具有不确定性。
缓存同步操作时序图(DMA_FROM_DEVICE) CPU缓存 可能包含旧数据 (脏缓存行) 物理内存 DMA设备写入数据 (最新数据) DMA设备 完成数据传输 (数据已写入内存) ① DMA写入 ② 缓存未同步 dma_sync_single_for_cpu ③ 无效化CPU缓存 CPU缓存(已同步) 缓存行无效,下次读取从内存加载 物理内存 数据保持不变 ④ CPU读取(从内存) 关键:dma_sync_single_for_cpu 确保CPU看到的是设备写入的最新数据,而不是缓存中的陈旧副本 对于 DMA_TO_DEVICE,则使用 dma_sync_single_for_device 将CPU写入的数据刷新到内存

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);
}
videobuf2最佳实践:在vb2_ops的queue_setup中分配缓冲区并调用dma_map_sg,在buf_prepare中调用dma_sync_sg_for_device,在buf_finish中调用dma_sync_sg_for_cpu,在buf_cleanup中调用dma_unmap_sg。这种分层设计确保了缓存同步操作只在必要时执行,避免不必要的性能开销。

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 方向,除非缓冲区确实需要双向传输;单向方向允许内核优化缓存操作
  • <