3. DMA-BUF 机制详解:原理、生命周期与文件描述符传递

DMA-BUF(Direct Memory Access Buffer)是 Linux 内核提供的一种跨设备、跨进程共享 DMA 缓冲区的框架。在 Camera HAL 中,它是连接 ISP、GPU、Display 和 Video Encoder 等硬件模块的核心数据载体。理解 DMA-BUF 的原理、生命周期和文件描述符传递机制,是进行 Buffer 管理调试的基础。

3.1 DMA-BUF 核心原理

DMA-BUF 本质上是一个内核对象,它代表一块物理连续或通过 IOMMU/SMMU 映射的物理内存区域。其核心设计思想是:一次分配,多处共享,且共享过程不涉及数据拷贝。

关键特性:
  • 零拷贝: 生产者(如 Camera ISP)将数据写入 DMA-BUF,消费者(如 GPU/Display)直接读取同一块物理内存。
  • 跨设备共享: 不同的硬件设备(Camera、GPU、Display)通过各自的 IOMMU 页表映射到同一块物理内存。
  • 跨进程共享: 通过文件描述符(fd)传递,实现不同用户空间进程(如 HAL、SurfaceFlinger)对同一缓冲区的访问。
  • 同步机制: 提供 dma_fence 机制,用于在硬件设备之间进行 Buffer 访问顺序的同步,防止数据竞争。
DMA-BUF 跨设备共享原理 物理内存 (DMA-BUF) Camera ISP (生产者) GPU (消费者) Display (消费者) 写入 读取 读取 dma_fence 同步信号

3.2 DMA-BUF 生命周期

DMA-BUF 的生命周期由内核中的引用计数管理。每个 DMA-BUF 对象都有一个关联的 struct dma_buf 结构体,其生命周期遵循以下阶段:

阶段 触发动作 引用计数变化 关键操作
分配 调用 dma_buf_export() 或通过 ION/DMABUF Heap 分配 refcount = 1 创建 struct dma_buf,关联 struct dma_buf_export_info
获取 fd 调用 dma_buf_fd() refcount + 1 在进程文件描述符表中创建新 fd,指向该 DMA-BUF
共享/传递 通过 UNIX Socket 或 Binder 传递 fd refcount + 1 (接收方) 接收方通过 fdget() 获取引用
映射 调用 dma_buf_map_attachment() 不变 为特定设备创建 DMA 映射,返回 sg_table
解除映射 调用 dma_buf_unmap_attachment() 不变 释放设备端的 DMA 映射
关闭 fd 调用 close(fd) refcount - 1 从进程文件描述符表中移除 fd
释放 refcount 降为 0 refcount = 0 调用 dma_buf_release(),释放物理内存
调试要点: 在 Camera HAL 调试中,常见的 Buffer 泄漏问题往往源于 DMA-BUF 引用计数未正确释放。可以通过 /sys/kernel/debug/dma_buf/bufinfo 或 /d/dma_buf/bufinfo 查看当前系统中所有 DMA-BUF 的引用计数和持有者信息。
DMA-BUF 生命周期状态机 分配 (ref=1) 活跃 (ref>1) 等待释放 (ref=1) 释放 (ref=0) dma_buf_fd() / 传递 fd 所有附加 fd 关闭 (ref--) 最后一个引用释放 新 fd 传入

3.3 文件描述符传递机制

DMA-BUF 通过文件描述符(fd)在进程间传递。这是 Linux 内核提供的一种安全、高效的共享方式。传递过程涉及以下关键步骤:

  1. 发送方: 持有 DMA-BUF 的进程调用 dma_buf_fd() 获取一个 fd,然后通过 UNIX Domain Socket 的 SCM_RIGHTS 辅助消息或 Android Binder 的 Parcel::writeFileDescriptor() 将 fd 发送给接收方。
  2. 内核处理: 内核在传递过程中,会将发送方进程文件描述符表中的 fd 复制到接收方进程的文件描述符表中,同时增加 DMA-BUF 对象的引用计数。
  3. 接收方: 接收方从消息中提取出 fd,此时该 fd 指向与发送方相同的 DMA-BUF 对象。接收方可以通过 mmap() 或 dma_buf_map_attachment() 访问缓冲区。
常见陷阱: 在 Camera HAL 中,如果 HAL 进程在传递 fd 后立即关闭了自己的 fd,但接收方(如 SurfaceFlinger)尚未完成映射,DMA-BUF 对象并不会被释放,因为内核已经为接收方增加了引用计数。但是,如果发送方在传递前就关闭了 fd,则会导致接收方收到一个无效的 fd。
DMA-BUF 文件描述符跨进程传递 进程 A (HAL) fd_table[3] → DMA-BUF dma_buf_fd() → fd=3 进程 B (SurfaceFlinger) fd_table[7] → DMA-BUF mmap() / map_attachment() 内核空间 struct dma_buf refcount=2 SCM_RIGHTS / Binder (fd=3 → fd=7)

3.4 DMA-BUF 与 ION / DMABUF Heap 的关系

ION 和 DMABUF Heap 都是 DMA-BUF 框架的具体内存分配器实现。它们之间的关系可以概括为:DMA-BUF 是接口规范,ION 和 DMABUF Heap 是具体实现。

特性 DMA-BUF 框架 ION DMABUF Heap
定位 内核标准框架 Android 传统分配器 内核主线替代方案
分配接口 dma_buf_export() ION_IOC_ALLOC ioctl DMA_HEAP_IOC_ALLOC ioctl
内存类型 不关心具体类型 系统堆、载波堆、自定义堆 系统堆、载波堆、自定义堆
内核主线状态 主线支持 已弃用 (staging) 主线支持 (推荐)
用户空间接口 通过 fd 操作 /dev/ion 字符设备 /dev/dma_heap/* 字符设备
调试接口 /d/dma_buf/bufinfo /d/ion/ /d/dma_heap/
实际应用场景:
  • 传统 Android 系统 (Android 10 及之前): 使用 ION 分配器,HAL 通过 /dev/ion 分配 Buffer,获取 fd 后传递给其他组件。
  • 新版本 Android 系统 (Android 11+): 推荐使用 DMABUF Heap,HAL 通过 /dev/dma_heap/system 或 /dev/dma_heap/reserved 分配 Buffer。
  • 无论使用哪种分配器,最终都返回一个 DMA-BUF 的 fd,上层代码可以统一使用 DMA-BUF 的 API 进行操作。
DMA-BUF、ION、DMABUF Heap 架构关系 用户空间 Camera HAL SurfaceFlinger GPU Driver 内核空间 DMA-BUF 核心框架 dma_buf.c / dma_fence.c ION 分配器 (staging) DMABUF Heap 分配器 ioctl fd 传递 ioctl

3.5 调试实战:查看 DMA-BUF 状态

在 Camera HAL 调试过程中,经常需要查看当前系统中 DMA-BUF 的分配和使用情况。以下是一些常用的调试命令和技巧:

# 查看所有 DMA-BUF 信息(需要 root 权限)
cat /sys/kernel/debug/dma_buf/bufinfo

# 输出示例:
# Dma-buf Objects:
# size      flags   mode       count   exp_name
# 8294400   0x00000 0x00000000 2       ion
# 00000000-00000000 (virt) 00000000-00000000 (phys)
# Attached devices:
# msm_camera_isp0
# Total: 1 objects, 8294400 bytes

# 查看 ION 分配器状态(如果使用 ION)
cat /d/ion/heaps/system

# 查看 DMABUF Heap 分配器状态(如果使用 DMABUF Heap)
ls /dev/dma_heap/
cat /d/dma_heap/system
调试技巧:
  • 使用 lsof | grep dma_buf 可以查看哪些进程持有 DMA-BUF 的 fd。
  • 在 HAL 代码中添加 #include <linux/dma-buf.h>,使用 dma_buf_get() 和 dma_buf_put() 手动管理引用计数。
  • 如果发现 Buffer 泄漏,可以通过 bpftrace 或 tracepoint:dma_buf:dma_buf_export 和 tracepoint:dma_buf:dma_buf_release 跟踪分配和释放事件。

3.6 总结

DMA-BUF 是 Camera HAL Buffer 管理的基石。理解其原理、生命周期和文件描述符传递机制,对于调试 Buffer 泄漏、跨进程共享问题和性能优化至关重要。ION 和 DMABUF Heap 作为具体的分配器实现,最终都统一到 DMA-BUF 框架下,使得上层代码可以以统一的方式操作缓冲区。