Linux 内核学习(18) --- linux dma_buf 机制

目录

  • [Linux DMA-BUF:跨设备缓冲区共享与同步](#Linux DMA-BUF:跨设备缓冲区共享与同步)
    • [1. DMA-BUF 解决什么问题](#1. DMA-BUF 解决什么问题)
    • [2. DMA-BUF 不负责什么](#2. DMA-BUF 不负责什么)
    • [3. 三个核心原语](#3. 三个核心原语)
      • [3.1 dma_buf:共享缓冲区对象](#3.1 dma_buf:共享缓冲区对象)
      • [3.2 dma_fence:异步操作完成信号](#3.2 dma_fence:异步操作完成信号)
      • [3.3 dma_resv:与缓冲区关联的 fence 容器](#3.3 dma_resv:与缓冲区关联的 fence 容器)
    • [4. 参与者:exporter、importer 与用户空间](#4. 参与者:exporter、importer 与用户空间)
      • [4.1 Exporter](#4.1 Exporter)
      • [4.2 Importer](#4.2 Importer)
      • [4.3 用户空间](#4.3 用户空间)
    • [5. 四种地址不要混淆](#5. 四种地址不要混淆)
    • [6. sg_table 与 scatter-gather](#6. sg_table 与 scatter-gather)
    • [7. Exporter:创建与导出 DMA-BUF](#7. Exporter:创建与导出 DMA-BUF)
      • [7.1 dma_buf_ops 的职责](#7.1 dma_buf_ops 的职责)
    • [8. Importer:设备 DMA 访问流程](#8. Importer:设备 DMA 访问流程)
      • [8.1 获取与 attach](#8.1 获取与 attach)
      • [8.2 Map、提交与完成](#8.2 Map、提交与完成)
      • [8.3 DMA 方向](#8.3 DMA 方向)
      • [8.4 正确的释放顺序](#8.4 正确的释放顺序)
    • [9. 引用计数与生命周期](#9. 引用计数与生命周期)
    • [10. CPU 访问与缓存一致性](#10. CPU 访问与缓存一致性)
      • [10.1 内核空间访问](#10.1 内核空间访问)
      • [10.2 用户空间 mmap](#10.2 用户空间 mmap)
    • [11. 缓存一致性与执行同步是两件事](#11. 缓存一致性与执行同步是两件事)
    • [12. 隐式同步与显式同步](#12. 隐式同步与显式同步)
      • [12.1 隐式同步](#12.1 隐式同步)
      • [12.2 显式同步](#12.2 显式同步)
    • [13. DMA-Heap:从用户空间分配 DMA-BUF](#13. DMA-Heap:从用户空间分配 DMA-BUF)
    • [14. 图像 buffer 还需要布局元数据](#14. 图像 buffer 还需要布局元数据)
    • [15. 动态映射与内存迁移](#15. 动态映射与内存迁移)
    • [16. 锁与错误处理](#16. 锁与错误处理)
    • [17. 调试、统计与安全](#17. 调试、统计与安全)
      • [17.1 查看 fd 与 DMA-BUF 信息](#17.1 查看 fd 与 DMA-BUF 信息)
      • [17.2 常见安全检查](#17.2 常见安全检查)
    • [18. 原文中需要纠正的几个结论](#18. 原文中需要纠正的几个结论)
    • [19. 建议的验证实验](#19. 建议的验证实验)
      • [实验一:验证 fd 传递与引用生命周期](#实验一:验证 fd 传递与引用生命周期)
      • [实验二:记录 importer 的资源状态](#实验二:记录 importer 的资源状态)
      • [实验三:区分 fence 等待与缓存同步](#实验三:区分 fence 等待与缓存同步)
    • [20. 小结](#20. 小结)
    • 参考资料

Linux DMA-BUF:跨设备缓冲区共享与同步

本文面向 Linux 设备驱动开发者,接口语义以当前上游内核文档为参考。DMA-BUF 是内核内部 API,版本之间会演进;实际开发必须以目标内核的 include/linux/dma-buf.hinclude/linux/dma-resv.h 和对应驱动实现为准。

1. DMA-BUF 解决什么问题

摄像头、视频编解码器、GPU、NPU 和显示控制器经常需要依次访问同一块图像或张量数据。如果每经过一个设备都复制一次数据,会增加内存带宽、延迟和功耗。

DMA-BUF 提供了一套跨驱动、跨子系统、跨进程共享缓冲区的框架:

  • 内核使用 struct dma_buf 表示一个共享缓冲区对象;
  • 用户空间通常使用普通文件描述符(fd)持有和传递这个对象;
  • 导入驱动通过 struct dma_buf_attachment 建立缓冲区与设备的关系;
  • 设备访问时,导入驱动取得针对该设备完成 DMA 映射的 struct sg_table
  • dma_fencedma_resv 用于协调异步硬件访问;
  • CPU 访问前后需要执行相应的缓存一致性协议。

一个典型多媒体链路可以表示为:

text 复制代码
Camera / ISP -> Video Codec -> GPU -> Display
       \___________ 同一个 DMA-BUF ___________/

控制面:fd、尺寸、格式、stride、offset、modifier、fence
数据面:由 exporter 管理的实际 backing storage

Android 图形栈中,Gralloc/Allocator、SurfaceFlinger、GPU 和显示驱动经常通过包含 DMA-BUF fd 的 native handle 共享图形缓冲区。具体由哪个进程分配、使用几个 fd,以及元数据怎样传递取决于 Android 和厂商实现,因此不应把某一种 Android 流程当成 DMA-BUF 本身的定义。

2. DMA-BUF 不负责什么

理解边界比记忆接口更重要:

  1. DMA-BUF 本身不是通用内存分配器。 backing storage 由 exporter 管理;DMA-Heap、DRM GEM、V4L2 或厂商分配器可以提供 DMA-BUF。
  2. fd 不是物理地址。 fd 是进程文件描述符表中的句柄,内核通过它找到 struct filestruct dma_buf
  3. DMA-BUF 不保证物理连续。 缓冲区可能由离散页、CMA 连续内存、设备本地内存或其他存储组成。
  4. DMA-BUF 不描述像素格式。 宽高、stride、plane、offset、format 和 modifier 必须由上层 API 一起传递。
  5. 共享同一缓冲区不等于可以无序并发访问。 调用方仍需要处理硬件执行顺序、CPU 缓存一致性和业务层互斥。
  6. "零拷贝"不保证没有任何复制。 exporter 可能因设备可达性、内存迁移或 CPU 访问而移动或复制 backing storage。

3. 三个核心原语

当前 DMA-BUF 文档把体系拆成三个相互配合的原语。

3.1 dma_buf:共享缓冲区对象

struct dma_buf 表示共享缓冲区及其生命周期。用户空间看到的是 fd,内核驱动看到的是 struct dma_buf *

几个重要概念字段是:

  • size:缓冲区字节数,在该 DMA-BUF 生命周期中保持不变;
  • file:用于 fd 表示和引用计数的 struct file
  • ops:exporter 提供的回调;
  • attachments:已经附着的设备关系;
  • priv:exporter 的私有缓冲区对象;
  • resv:与缓冲区关联的 struct dma_resv

3.2 dma_fence:异步操作完成信号

struct dma_fence 表示一个异步 DMA 操作的完成点,例如:

  • GPU 渲染完成;
  • 视频解码完成;
  • 显示控制器不再读取某个 buffer;
  • DMA engine 拷贝完成。

fence 从未完成状态变为 signaled 状态。它表示"操作完成",不负责缓存刷新,也不是内存锁。

3.3 dma_resv:与缓冲区关联的 fence 容器

struct dma_resv 管理与一个资源关联的一组 fence,并为隐式同步提供基础。当前接口用 usage 区分读、写等访问语义。

粗略理解依赖关系:

text 复制代码
后续读:通常需要等待先前写完成
后续写:通常需要等待先前读和写完成

具体驱动必须遵循 enum dma_resv_usage 和所属子系统的同步规则,不能只凭"一个共享 fence、一个独占 fence"的旧模型实现新代码。

4. 参与者:exporter、importer 与用户空间

4.1 Exporter

Exporter 是拥有并管理 backing storage 的驱动或子系统,主要负责:

  • 分配、回收或迁移实际存储;
  • 实现 struct dma_buf_ops
  • 检查导入设备能否访问该存储;
  • 为每个 attachment 建立合适的 DMA 映射;
  • 配合 CPU 访问完成缓存一致性处理;
  • 在最后一个引用释放时销毁 exporter 私有对象。

exporter 描述内存对象的所有者,并不等同于数据流中的"生产者"。一个 importer 也可能通过设备向 buffer 写入数据。

4.2 Importer

Importer 是希望让某个设备访问 DMA-BUF 的驱动,主要负责:

  • 从 fd 获得 struct dma_buf 引用;
  • 将自己的 struct device attach 到缓冲区;
  • 按正确方向取得设备 DMA 映射;
  • 等待必要 fence 后向硬件提交任务;
  • 在任务完成后 signal 自己的 fence;
  • 按相反顺序解除 mapping、attachment 和引用。

struct dma_buf_attachment 表示"一个 DMA-BUF 与一个设备之间的附着关系",不是一个用户进程,也不等同于 fd。

4.3 用户空间

用户空间通常把 DMA-BUF fd 当成 opaque handle,通过以下方式传递:

  • 某个设备 UAPI 的 ioctl;
  • Unix domain socket 的 SCM_RIGHTS
  • Binder/native handle;
  • Wayland、DRM、V4L2、EGL、Vulkan 等上层协议或 API。

不同进程的 fd 数字没有全局意义。把整数 7 通过普通消息告诉另一个进程,并不能让对方访问同一个对象;必须通过能传递 struct file 引用的机制发送 fd。

5. 四种地址不要混淆

共享 DMA 缓冲区时会同时出现多种地址空间:

地址 使用者 含义
用户虚拟地址 用户进程 mmap() 返回的进程地址
内核虚拟地址 CPU 内核代码 dma_buf_vmap() 得到的映射
物理地址/页 内存管理与 exporter backing storage 的物理组成
DMA 地址或 IOVA 具体设备 设备发起 DMA 时使用的地址

存在 IOMMU 时,设备使用的 IOVA 与 CPU 物理地址可能完全不同。即使物理页离散,IOMMU 也可能提供适合设备访问的地址视图。

dma_buf_map_attachment() 返回的 sg_table 已针对 attachment->dev 完成 DMA 映射。驱动给硬件编程时应使用 DMA API 提供的 DMA 地址和长度,不能把 page_to_phys()sg_phys() 或 CPU 虚拟地址直接当成设备地址。

同一个 DMA-BUF 对不同设备可能得到不同 DMA 地址,因此不能把设备 A 的 sg_table 缓存后交给设备 B 使用。

6. sg_table 与 scatter-gather

struct sg_table 管理 scatterlist,描述缓冲区的若干内存段。它并不简单等于"按页排列的物理连续块列表"。需要区分:

  • exporter 的 backing store 可能由多段物理页组成;
  • DMA API 可以合并相邻段;
  • orig_nents 与 DMA 映射后的 nents 可能不同;
  • DMA 地址受设备 DMA mask、IOMMU、segment size 和边界约束影响;
  • 映射结果只在对应 attachment 和 mapping 生命周期内有效。

遍历已映射的 scatterlist 时,应采用目标内核为 DMA 映射提供的遍历宏,并读取 DMA address/length 字段。具体宏和语义应依据目标内核版本核对。

7. Exporter:创建与导出 DMA-BUF

Exporter 先创建自己的私有 buffer 对象,然后用 DMA-BUF 包装它。简化骨架如下:

c 复制代码
#include <linux/dma-buf.h>
#include <linux/err.h>
#include <linux/fcntl.h>

static int my_export_buffer(struct my_buffer *buffer)
{
    DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
    struct dma_buf *dmabuf;
    int fd;

    exp_info.ops = &my_dma_buf_ops;
    exp_info.size = buffer->size;
    exp_info.flags = O_RDWR;
    exp_info.priv = buffer;

    dmabuf = dma_buf_export(&exp_info);
    if (IS_ERR(dmabuf))
        return PTR_ERR(dmabuf);

    fd = dma_buf_fd(dmabuf, O_CLOEXEC);
    if (fd < 0) {
        dma_buf_put(dmabuf);
        return fd;
    }

    /* 成功后,fd 持有这份引用;不要再对同一引用调用 dma_buf_put()。 */
    return fd;
}

DEFINE_DMA_BUF_EXPORT_INFO() 会初始化结构并设置 exporter/module 信息。dma_buf_export() 创建 struct dma_buf 和关联的匿名文件对象,但是否已经分配实际 backing storage 由 exporter 决定。有些 exporter 可以延迟到 attach 或 map 时再选择内存位置。

dma_buf_fd() 把 DMA-BUF 安装到当前进程的 fd 表。创建 fd 时必须原子设置 O_CLOEXEC,避免多线程进程在 exec 竞态中把敏感 buffer fd 泄漏给新程序。

7.1 dma_buf_ops 的职责

当前内核中需要重点理解的回调包括:

回调 必选性 主要职责
attach / detach 可选 检查设备约束并管理每个 attachment 的私有状态
map_dma_buf / unmap_dma_buf 必选 创建和释放面向导入设备的 DMA 映射
release 必选 最后一个 DMA-BUF 引用消失后释放 exporter 私有对象
begin_cpu_access / end_cpu_access 可选 准备和结束 CPU 访问,处理缓存或存储迁移
mmap 可选 支持用户空间映射
vmap / vunmap 可选 支持整个 buffer 的内核虚拟映射
pin / unpin 动态映射相关 控制 backing store 是否允许迁移

Exportermap_dma_buf() 通常需要完成以下工作:

  1. 确保 backing storage 已分配且满足设备约束;
  2. 必要时固定或迁移存储;
  3. 为当前 attachment 构造独立的 sg_table
  4. 通过 DMA API 映射到 attachment->dev 的 DMA 地址空间;
  5. 在失败时按逆序释放已经取得的资源。

每个 attachment 应获得自己可释放的映射对象。Exporter 不能随意把同一份已 DMA-map 的 sg_table 返回给不同设备。

8. Importer:设备 DMA 访问流程

8.1 获取与 attach

Importer 从用户传入的 fd 取得引用:

c 复制代码
struct dma_buf *dmabuf;
struct dma_buf_attachment *attach;

dmabuf = dma_buf_get(fd);
if (IS_ERR(dmabuf))
    return PTR_ERR(dmabuf);

attach = dma_buf_attach(dmabuf, dev);
if (IS_ERR(attach)) {
    int ret = PTR_ERR(attach);

    dma_buf_put(dmabuf);
    return ret;
}

dma_buf_get() 使用底层 struct file 的引用计数增加一份内核引用。这个引用计数包含 fd 和其他内核持有者,不等于"消费者数量"。

Attach 阶段 exporter 可以检查:

  • 设备 DMA mask;
  • 最大 segment 大小和边界;
  • backing storage 是否能被该设备访问;
  • 是否需要移动到 system memory;
  • peer-to-peer 访问能力。

因此 dma_buf_attach() 可以失败,importer 必须处理错误。

8.2 Map、提交与完成

当前内核同时提供普通 helper 和带 _unlocked 后缀的变体。按照上游 locking convention,普通 map/unmap helper 要求 importer 已经持有 DMA-BUF 的 reservation 锁;_unlocked 变体要求调用时未持有该锁,并由 helper 处理需要的加锁。不能只按后缀的字面含义推断锁条件。未自行管理 reservation 锁的 importer 可采用下面的简化流程:

text 复制代码
dir = DMA_FROM_DEVICE
sgt = dma_buf_map_attachment_unlocked(attach, dir)
if sgt 返回错误:
    dma_buf_detach(dmabuf, attach)
    dma_buf_put(dmabuf)
    返回错误

等待本次访问依赖的 fence
使用 sgt 中面向 attach->dev 的 DMA 地址配置硬件
创建并发布代表本次任务的 dma_fence
提交硬件任务

硬件中断或完成 worker:
    确认设备不再访问该映射
    signal fence
    dma_buf_unmap_attachment_unlocked(attach, sgt, dir)
    dma_buf_detach(dmabuf, attach)
    dma_buf_put(dmabuf)

这段代码只展示资源顺序,不是完整的异步驱动实现。真实驱动不能在提交硬件后立即执行 unmap;mapping 和 attachment 必须至少存活到硬件任务完成。

8.3 DMA 方向

DMA 方向从内存与设备之间的数据流理解:

方向 含义 示例
DMA_TO_DEVICE 内存中的数据被设备读取 显示扫描、编码器读取输入
DMA_FROM_DEVICE 设备向内存写入数据 摄像头采集、解码器写输出
DMA_BIDIRECTIONAL 设备可能读写 确实无法使用更精确方向时

方向必须与 map、硬件访问和 unmap 保持一致。错误方向在 DMA-coherent 平台上可能暂时看不出问题,但在 non-coherent 平台上可能产生陈旧数据或额外同步开销。

8.4 正确的释放顺序

text 复制代码
确认硬件访问完成
    -> signal/处理 fence
    -> dma_buf_unmap_attachment_unlocked()
    -> dma_buf_detach()
    -> dma_buf_put()

dma_buf_unmap_attachment() 的含义是释放 DMA mapping,并不负责等待硬件完成,也不等价于"通知 exporter DMA 已完成"。硬件完成应由驱动中断、完成队列和 fence 等机制确认。

9. 引用计数与生命周期

DMA-BUF 的生命期主要由关联 struct file 的引用计数管理:

  • dma_buf_get(fd) 获得一份引用,必须由 dma_buf_put() 配对;
  • get_dma_buf(dmabuf) 增加已有对象的额外内核引用,也必须配对 dma_buf_put()
  • dup()fork()SCM_RIGHTS 和 Binder fd 传递都可能增加或共享文件引用;
  • 用户空间 close(fd) 只释放该 fd 持有的引用;
  • 所有 fd 与内核引用都释放后,才会调用 exporter 的 release()

fd、attachment 和 mapping 是三个不同生命周期:

text 复制代码
fd / dma_buf reference:保证 dma_buf 对象存在
attachment:保证设备与 buffer 的关系存在
mapping:保证当前设备 DMA 地址映射有效,通常也约束 backing store 迁移

Importer 从 fd 调用 dma_buf_get() 后,用户进程关闭原 fd 不会立刻使 importer 手中的指针失效,因为 importer 已有独立引用。

10. CPU 访问与缓存一致性

CPU 访问 DMA-BUF 有两种常见情况:内核通过 vmap 访问,或用户空间通过 mmap 访问。二者都必须遵循 begin/end 协议。

10.1 内核空间访问

当前接口使用 struct iosys_map 表示映射,因为 backing storage 可能是普通系统内存,也可能是 I/O memory:

c 复制代码
#include <linux/dma-buf.h>
#include <linux/errno.h>
#include <linux/iosys-map.h>

static int process_buffer_on_cpu(struct dma_buf *dmabuf)
{
    struct iosys_map map = { };
    int ret;

    ret = dma_buf_begin_cpu_access(dmabuf, DMA_BIDIRECTIONAL);
    if (ret)
        return ret;

    ret = dma_buf_vmap_unlocked(dmabuf, &map);
    if (ret)
        goto out_end_access;

    /* 使用 iosys_map helper 访问内容,不要假定 map 一定是普通 void *。 */

    dma_buf_vunmap_unlocked(dmabuf, &map);

out_end_access:
    if (dma_buf_end_cpu_access(dmabuf, DMA_BIDIRECTIONAL) && !ret)
        ret = -EIO;

    return ret;
}

需要注意:

  • begin_cpu_access() 可以失败;
  • vmap() 也可以因 exporter 不支持或虚拟地址空间不足而失败;
  • begin_cpu_access() 成功后,即使后续处理失败也必须调用 end_cpu_access()
  • vmap/vunmapbegin/end_cpu_access 解决不同问题,不能互相替代;
  • 大 buffer 的长期 vmap 会消耗内核虚拟地址空间。

旧文中的 dma_buf_kmap()dma_buf_kmap_atomic() 已不适合作为当前通用接口讲解。高端内存和局部页访问应根据目标内核提供的现代内存映射 API 重新设计。

10.2 用户空间 mmap

如果 exporter 支持 mmap,用户空间可以对 DMA-BUF fd 调用普通 mmap()。每一次 CPU 读写都应由 DMA_BUF_IOCTL_SYNC 包围:

c 复制代码
#include <errno.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/dma-buf.h>

static int dmabuf_sync(int fd, uint64_t flags)
{
    struct dma_buf_sync sync = { .flags = flags };
    int ret;

    do {
        ret = ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
    } while (ret < 0 && (errno == EINTR || errno == EAGAIN));

    return ret;
}

static int write_dmabuf(int fd, size_t size)
{
    void *addr;
    int ret = -1;

    addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
        return -1;

    if (dmabuf_sync(fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE) < 0)
        goto out_unmap;

    /* 在 [addr, addr + size) 范围内写入数据。 */

    if (dmabuf_sync(fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE) < 0)
        goto out_unmap;

    ret = 0;

out_unmap:
    munmap(addr, size);
    return ret;
}

生产代码必须保证 START 成功后,即使 CPU 处理失败也尽力执行匹配的 END;上面为突出主线省略了该状态管理。

DMA_BUF_IOCTL_SYNC 只提供 CPU 映射所需的缓存一致性机会,并不阻止其他进程或设备同时访问。CPU 开始访问前必须先等待 GPU/其他设备的相关工作完成,END 之后才能提交依赖 CPU 写入结果的新设备任务。

11. 缓存一致性与执行同步是两件事

这是 DMA-BUF 最容易混淆的部分:

text 复制代码
执行同步:某个设备是否已经完成读写?
    工具:dma_fence、dma_resv、sync_file、poll、子系统同步对象

缓存一致性:CPU 与设备看到的数据是否一致?
    工具:begin/end_cpu_access、DMA_BUF_IOCTL_SYNC、DMA API

即使 fence 已经 signaled,CPU 在 non-coherent 平台上仍可能需要 invalidate cache 才能看到设备写入。反过来,即使做了 cache flush,也不表示仍在运行的 GPU 已停止访问内存。

CPU 访问的一般顺序是:

text 复制代码
等待先前设备 fence
    -> begin CPU access / SYNC_START
    -> CPU 读写
    -> end CPU access / SYNC_END
    -> 提交后续设备任务并建立 fence

12. 隐式同步与显式同步

12.1 隐式同步

隐式同步把 fence 关联到 DMA-BUF 的 dma_resv。支持该模型的驱动在提交任务时:

  1. 根据读写方向取得并等待所需依赖 fence;
  2. 创建代表本次异步任务的 fence;
  3. 以正确 usage 加入 reservation object;
  4. 在硬件真正完成后 signal fence。

用户空间可以对 DMA-BUF fd 使用 poll()/epoll() 查询隐式 fence:

  • POLLIN/EPOLLIN 用于判断读取所需的先前写是否完成;
  • POLLOUT/EPOLLOUT 用于判断写入所需的先前读写是否完成。

poll() 就绪只表示相应 fence 完成;CPU 访问前仍需执行 DMA_BUF_IOCTL_SYNC

12.2 显式同步

显式同步把 dma_fence 包装为独立的 sync_file fd,由用户空间明确随任务传递。它常用于 Vulkan 等显式同步 API,也用于跨子系统交换完成点。

DMA-BUF UAPI 提供:

  • DMA_BUF_IOCTL_EXPORT_SYNC_FILE:把 DMA-BUF 当前相关 fence 的快照导出为 sync_file
  • DMA_BUF_IOCTL_IMPORT_SYNC_FILE:把一个 sync_file fence 加入 DMA-BUF 的隐式同步状态。

这两个 ioctl 可以桥接显式同步和隐式同步,但"导出依赖→提交任务→导入完成 fence"不是原子操作。多个线程或上下文同时操作时,用户空间还需要额外锁定或协议保证这组步骤之间没有竞态。

13. DMA-Heap:从用户空间分配 DMA-BUF

DMA-Heap 为用户空间提供分配 DMA-BUF 的统一 UAPI。每个 /dev/dma_heap/<name> 节点表示一种分配策略或内存池。例如上游内核常见的 system heap 提供 cacheable、虚拟连续的 buffer;配置 CMA 后还可能出现连续内存 heap。实际节点由内核配置和平台决定。

简化的用户空间分配代码:

c 复制代码
#include <fcntl.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/dma-heap.h>

static int alloc_from_system_heap(uint64_t size)
{
    struct dma_heap_allocation_data data = {
        .len = size,
        .fd_flags = O_RDWR | O_CLOEXEC,
        .heap_flags = 0,
    };
    int heap_fd;
    int ret;

    heap_fd = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
    if (heap_fd < 0)
        return -1;

    ret = ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data);
    close(heap_fd);
    if (ret < 0)
        return -1;

    return data.fd;
}

DMA-Heap 只解决"从哪个 heap 分配并取得 DMA-BUF fd"。它不提供图像宽高、格式、stride,也不替调用方完成设备任务同步。

14. 图像 buffer 还需要布局元数据

一个 fd 和一个 size 不足以解释图像内容。跨 DRM、V4L2、EGL、Wayland 或 Vulkan 交换图像时,通常还需要:

  • width、height;
  • pixel format;
  • plane 数量;
  • 每个 plane 的 fd、offset 和 stride;
  • format modifier;
  • color space、range、transfer function 等颜色信息;
  • 与读写完成相关的 fence。

多 plane 图像可能所有 plane 共用一个 DMA-BUF,也可能每个 plane 使用独立 fd。Importer 必须验证 offset、stride 和尺寸计算不会越过 buffer 边界;不能仅信任用户空间元数据。

Modifier 描述线性、tiling、压缩等实际内存布局。两个设备都认识同一个四字符格式,不代表它们一定能以相同 modifier 访问该 buffer。

15. 动态映射与内存迁移

简单 importer 通常在 attachment 或 mapping 存活期间固定 backing storage。对于 GPU VRAM 等需要迁移的内存,这会限制内存管理器回收和移动资源。

动态 importer 可以使用 dma_buf_dynamic_attach() 和 importer attach ops,在 exporter 准备移动 backing storage 时接收 mapping 失效通知,然后销毁旧 mapping,并在下一次设备访问前重新 map。

动态路径需要严格遵守:

  • dma_resv 锁规则;
  • fence 等待与发布规则;
  • mapping 失效回调的时限;
  • pin/unpin 的适用范围;
  • peer-to-peer memory 是否有 struct page

这不是普通 importer 的必选优化。除非驱动确实需要可迁移内存并理解所有同步约束,否则先实现静态、生命周期清晰的 attachment/mapping 更容易保证正确性。

16. 锁与错误处理

DMA-BUF、DMA reservation 和各子系统锁会形成跨驱动锁依赖。实现时必须阅读目标内核文档中的 DMA-BUF locking convention。

关键原则:

  • 不要在 core 已持有 dma_resv 锁调用的 exporter callback 中再次获取同一锁;
  • 普通 map/unmapvmap/vunmap helper 要求 importer 已持有 reservation 锁;对应的 _unlocked helper 要求调用时未持锁,并由 helper 处理需要的加锁;
  • attachmapbegin_cpu_accessvmap 和用户态 ioctl 都可能失败;
  • dma_buf_get()dma_buf_attach()dma_buf_map_attachment() 返回错误指针时使用 IS_ERR()/PTR_ERR()
  • 清理路径必须按获取资源的逆序执行;
  • 不能在原子上下文调用可能 sleep 的回调;map_dma_buf() 可能因分配或迁移而睡眠。

推荐把 importer 状态明确拆成:

text 复制代码
EMPTY -> GOT_DMABUF -> ATTACHED -> MAPPED -> IN_FLIGHT
  ^          |            |          |           |
  +----------+------------+----------+-----------+
            每次错误都按逆序回退

不要用一个布尔变量同时表示"已经 attach""已经 map"和"硬件已经完成"。

17. 调试、统计与安全

17.1 查看 fd 与 DMA-BUF 信息

对于持有 DMA-BUF fd 的进程,可以先检查:

bash 复制代码
ls -l /proc/<pid>/fd
cat /proc/<pid>/fdinfo/<fd>

DMA-BUF 的 fdinfo 通常包含 size、引用相关计数和 exp_name 等字段,具体内容随内核和 exporter 变化。

启用 debugfs 的开发系统还可能提供:

bash 复制代码
cat /sys/kernel/debug/dma_buf/bufinfo

debugfs 不应作为稳定生产 ABI,也不应仅为采集数据就在生产环境放宽挂载权限。某些内核在启用相应配置时还提供 DMA-BUF sysfs statistics;路径和字段应以目标内核的 ABI 文档为准。

Exporter 和用户空间可以为 DMA-BUF 设置有意义的名称,便于定位泄漏和统计,但名称只用于调试,不能作为权限或身份判断依据。

17.2 常见安全检查

  • 创建或接收 fd 时使用 O_CLOEXEC/MSG_CMSG_CLOEXEC
  • 把 DMA-BUF fd 当作访问内存的能力句柄,只传给确实需要的进程;
  • 分配给用户空间或其他安全域前清除旧内容,防止信息泄漏;
  • 验证 buffer size、plane offset、stride、长度加法和整数溢出;
  • 验证设备 DMA mask、IOMMU domain 和访问权限;
  • 不要信任用户提供的 fd 一定是 DMA-BUF,dma_buf_get() 失败必须正常返回;
  • 确保异常、进程退出和设备 reset 路径最终都能释放引用并 signal 或终止相关 fence。

18. 原文中需要纠正的几个结论

原表述 更准确的理解
DMA-BUF 在物理内存分配后导出 backing storage 由 exporter 管理,可以提前分配,也可能延迟分配、迁移或位于设备内存
dma_buf 绑定文件描述符 dma_buf 关联 struct file;fd 是每个进程 fd 表中的句柄,可以有多个
引用计数表示消费者数量 引用计数统计文件和内核引用,不等于 attachment 或业务消费者数量
attachment 是消费者对象 attachment 是 DMA-BUF 与某个 struct device 的关系
scatterlist 是按页的连续物理块 map 后的 sg_table 是面向具体设备的 DMA 段描述,可能受 IOMMU 和 DMA API 合并影响
unmap 通知 DMA 结束 驱动必须先确认硬件结束;unmap 只释放 mapping
begin/end 负责所有同步 begin/end 主要解决 CPU 可见性与缓存一致性,硬件执行顺序仍需 fence 或子系统同步机制
kmap/kmap_atomic 是当前通用访问接口 当前 DMA-BUF 整体内核映射使用 vmap/vunmapiosys_map;旧接口不应直接套用

19. 建议的验证实验

实验一:验证 fd 传递与引用生命周期

使用 DMA-Heap 分配一块 DMA-BUF,进程 A 通过 SCM_RIGHTS 发送给进程 B:

  1. A 分配并设置 DMA-BUF 名称;
  2. A 通过 mmap + DMA_BUF_IOCTL_SYNC 写入固定模式;
  3. A 用 Unix domain socket 发送 fd;
  4. B 映射并校验内容;
  5. A 关闭自己的 fd,B 再次读取;
  6. 分别观察两个进程的 /proc/<pid>/fdinfo/<fd>

预期结果:B 收到的 fd 数字不一定与 A 相同;A 关闭 fd 后,只要 B 仍持有引用,buffer 就不会释放。

实验二:记录 importer 的资源状态

在一个可控的测试驱动中为 get -> attach -> map -> submit -> complete -> unmap -> detach -> put 每一步记录 tracepoint 或动态调试日志,并注入 attach 失败、map 失败和设备超时。

检查每个失败点是否都按逆序清理,重点确认:

  • map 失败时没有调用 unmap;
  • 已 map 但未提交时仍会 unmap;
  • 已提交任务必须等硬件停止访问后才能 unmap;
  • 任意路径最终都恰好执行一次 detach 和 put。

实验三:区分 fence 等待与缓存同步

在具有真实 non-coherent DMA 设备的平台上,分别记录:

  1. 设备任务 fence 何时 signaled;
  2. CPU 的 begin/end access 何时执行;
  3. 数据校验何时进行。

正确顺序应是先等待设备完成,再开始 CPU access。不要在生产设备上故意省略缓存同步来"证明会出错",因为 coherent 平台可能掩盖问题,non-coherent 平台上的结果也可能是非确定的数据损坏。

20. 小结

DMA-BUF 的主线可以归纳为:exporter 管理存储并导出对象,用户空间传递 fd,importer 为具体设备建立 attachment 和 DMA mapping,fence/reservation 管理异步访问顺序,begin/end 协议管理 CPU 缓存一致性,最后所有资源按相反顺序释放。

排查问题时,应始终分别回答四个问题:

  1. 谁持有 buffer 的引用,生命周期是否正确?
  2. 当前地址属于 CPU 虚拟、物理还是设备 DMA/IOVA 空间?
  3. 上一次异步读写是否已经通过 fence 确认完成?
  4. CPU 与设备之间是否完成了正确方向的缓存同步?

把这四点分开,DMA-BUF 中大多数"偶尔花屏""某平台正常、另一平台错误""buffer 无法释放"和 IOMMU fault 问题都会更容易定位。

参考资料

相关推荐
叶子野格1 小时前
《Verilog学习:数字系统设计基础》
学习·fpga开发
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day36|阿里云 OSS 文件上传、Spring 事务、全局异常思维导图复盘
java·学习·阿里云
zhengqweasd1 小时前
网站卡死、接口超时的隐性根源
运维·服务器·网络
Dream Cosmos1 小时前
自旋锁:从忙等待、原子操作到线程同步
linux
kaixin_啊啊1 小时前
PandaWiki 本地 AI 知识库实战:文档导入、智能问答与远程访问
linux·服务器·人工智能·windows·ai
handler012 小时前
【Linux】进程退出、等待与程序替换
linux·运维·exec·进程·进程等待·进程退出·程序替换
Linux-lucky2 小时前
33-Linux学习之旅之MySQL用户管理
java·linux·运维·学习·mysql·ubuntu
亮工硬件2 小时前
1-8 简易串口通讯协议(UART + 自定义帧协议)
笔记·stm32·单片机·嵌入式硬件·学习
像风一样的男人@2 小时前
linux --安装openGL,EGL/OSMesa
linux·运维·服务器