深入 Linux DMA-BUF:手机流畅背后的零拷贝秘密
从架构原理到内核源码,一文讲透 dma-buf 框架
基于内核源码分析与实际调试经验,涵盖 Linux DMA-BUF 框架原理、生命周期、sysfs 接口、内存统计以及用户态分析工具。最后更新:2026-04-21,基于 Android GKI kernel(common 仓库)
你知道拍一张照片,数据是怎么从摄像头流向屏幕的吗?
摄像头采集 → GPU 处理滤镜 → 屏幕显示,看起来不过三步,但在这背后,数据需要在多个硬件之间流转共享。如果每一步都要 CPU 亲自把内存搬来搬去,手机早就因频繁拷贝导致带宽打满、功耗剧增、帧率崩溃而卡死发烫了。
让这条链路高效运转的幕后英雄,叫做 dma-buf。它是 Linux 3.3 正式引入的一个零拷贝内存共享框架,不拷贝数据、不搬运数据,专职打通各硬件子系统之间的内存隔阂,是现代 Android 图形和多媒体体验的底层基石。本文从架构到源码,带你彻底搞懂它。
一、为什么需要 dma-buf?
在 dma-buf 出现之前(Linux 3.3 之前),各个硬件子系统是完全孤立的:
- V4L2 (Video for Linux 2):负责摄像头视频采集,管自己的内存
- DRM/KMS (Direct Rendering Manager):负责图形渲染和屏幕显示,管自己的内存
结果,一个简单的「摄像头采集 → GPU 滤镜 → 屏幕显示」流程,数据需要这样被搬运:
【没有 dma-buf 的年代】
摄像头内存(物理内存 A)
↓ CPU 拷贝 ★
用户空间缓冲区(物理内存 B)
↓ CPU 拷贝 ★
GPU 驱动内存(物理内存 C)
↓ CPU 拷贝 ★
显示控制器内存(物理内存 D)
★ = CPU 全程参与搬运,带宽 × 3,功耗剧增
后果:频繁内存拷贝 → CPU 资源耗尽 → 内存带宽打满 → 功耗剧增、发热严重、延迟飙升。对于手机和车机这类嵌入式设备,这是不可接受的。
引入 dma-buf 之后,所有硬件直接共享同一块物理内存,CPU 完全不需要参与搬运:
【有了 dma-buf】
同一块物理内存(Physical Memory)
├── 摄像头 DMA 直写 ✅
├── GPU DMA 直读 ✅
└── 显示控制器 DMA 直读 ✅
零拷贝,CPU 全程解放
二、dma-buf 是什么?架构全景图
一句话定义:dma-buf 是 Linux 内核中纯软件的「物流管理中心」。它不生产数据、不搬运数据,专门负责建立秩序——让不同硬件安全高效地共享同一块物理内存。
要理解 dma-buf,先要搞懂 CPU 和 DMA 访问内存的本质区别:
|
CPU 的世界 |
DMA 的世界 |
| 使用者 |
应用程序 / 内核驱动 |
硬件外设(GPU / Camera / NPU) |
| 地址类型 |
VA(CPU 虚拟地址) |
IOVA(I/O 虚拟地址) |
| 翻译单元 |
MMU → 物理地址 |
IOMMU → 物理地址 |
| 访问方式 |
可选映射到 VMA |
通过 sg_table(IOVA 路线图) |
| 最终目标 |
同一块 Physical Memory |
同一块 Physical Memory |
关键认知:「搬到物理内存」和「映射到 VMA」是两回事。你可以把数据存进物理内存(DMA 搬运),但 CPU 可以选择不映射 VMA、完全不参与——这就是零拷贝的核心。
物流类比
🏭 物理内存 = 真实仓库(货物只存在一处)
🚛 DMA 控制器 = 专业搬运工(直接存取仓库,不经过门卫 CPU)
🗺️ IOMMU / IOVA = 导航系统(给搬运工提供路线图)
📋 dma-buf = 物流公司管理系统(开运单、协调多方路线、指挥交通、最终销账)
架构示意:
┌─────────────────────────────────────────────────────┐
│ 用户空间 │
│ Camera App GPU App Display App │
│ │ │ │ │
│ └────────────┴───────────┘ │
│ │ fd(提货单) │
├────────────────────┼────────────────────────────────┤
│ 内核空间 │
│ ┌──────────▼──────────┐ │
│ │ dma-buf 框架 │ ← 管理中心 │
│ │ (file + dma_buf) │ │
│ └──┬──────────────┬───┘ │
│ attach │ │ attach │
│ ┌───────▼───┐ ┌─────▼──────┐ │
│ │ GPU Driver│ │ Camera Drv │ │
│ │ sg_table │ │ sg_table │ │
│ └───────────┘ └────────────┘ │
│ │ │ │
│ └────────┬───────┘ │
│ │ DMA(无 CPU 参与) │
│ ┌──────────▼──────────┐ │
│ │ 物理内存(共享) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────┘
三、dma-buf 的四大职责
| 职责 |
类比 |
解决的问题 |
核心机制 |
| 跨进程通行证 |
物流运单 |
不同驱动如何共享同一块内存? |
fd 文件描述符 |
| IOVA 翻译协调员 |
导航系统 |
硬件 DMA 只认 IOVA,不认物理地址 |
dma_buf_map_attachment + sg_table |
| 红绿灯交警 |
交通信号灯 |
多硬件并发访问同一块内存会花屏 |
Fence + dma_resv 同步 |
| 仓库管理员 |
库存台账 |
还在用的内存被提前释放会崩溃 |
f_count 引用计数 |
1. 跨进程通行证 — 物流运单(fd 机制)
问题:摄像头驱动和 GPU 驱动是两个独立的世界,它们怎么共享同一块内存?
dma-buf 的解法:摄像头驱动把物理内存打包成 dma-buf 对象,生成一个文件描述符(fd)。这个 fd 就是「提货单」——持有它,就能凭它找到那块内存。
Camera Driver 用户态 GPU Driver
│ │ │
│ dma_buf_export() │ │
│ dma_buf_fd() → fd=5 │ │
│──── 传递 fd=5 ─────────►│ │
│ │──── 传递 fd=5 ──────►│
│ │ │ dma_buf_get(fd=5)
│ │ │ → 拿到 dma_buf 指针
│ │ │ → 同一块物理内存 ✅
用户态程序把 fd 传给 GPU 驱动(通过 Binder 或 SCM_RIGHTS),GPU 驱动拿 fd 找内核兑换,精准找到那块内存。底层物理地址完全隐藏,共享简单且安全。
2. IOVA 翻译协调员 — dma_buf_map_attachment
问题:GPU 拿到了物理内存的引用,但 GPU 的 DMA 控制器只认识 IOVA,不认识物理地址。
dma-buf 的解法:GPU 驱动调用 dma_buf_map_attachment(),dma-buf 框架在底层与 IOMMU 沟通,为该 GPU 专门生成一份合法的 IOVA 路线图(sg_table)。如果 NPU 也来了,会单独为它再生成一份——每个设备的 IOVA 空间互相隔离,互不干扰。
3. 红绿灯交警 — Fence 同步机制
问题:摄像头 DMA 还没写完,GPU DMA 就猴急去读 → 画面撕裂(花屏)。
dma-buf 的解法:内置 dma_resv(Reservation Object)和 Fence(栅栏) 机制。
Camera DMA 写入: ████████████░░░░ (写进行中,Fence 亮红灯 🔴)
↓ 写完,Signal Fence 变绿灯 🟢
GPU DMA 读取: ░░░░░░░░░░░░████ (等绿灯才启动,保证数据完整)
摄像头写数据 → 亮红灯(挂 Write Fence)→ 写完变绿灯(Signal Fence)→ GPU DMA 启动。
4. 仓库管理员 — 生命周期与引用计数
问题:GPU 还在处理,摄像头就把内存释放了 → 野指针 → 系统崩溃。
dma-buf 的解法:通过 f_count(文件引用计数)严格记录有几个设备、几个进程在使用这块内存。只有所有人都 dma_buf_put() 或 close(fd),f_count 降为 0,才真正释放物理内存。
四、完整生命周期与关键 API
dma-buf 的完整生命周期调用链:
dma_buf_export() ← Exporter 分配内存,创建 dma_buf(f_count = 1)
│
├─ [可选] dma_buf_fd() ← 将 file 安装到进程 fd 表(f_count 保持为 1)
│ └─ fd 传递给用户态 / 其他进程(Binder / SCM_RIGHTS)
│ └─ dma_buf_get(fd) ← Importer 获取指针(f_count++)
│
├─ [可选] dma_buf_attach(dev) ← 创建 attachment(f_count 不变)
│ └─ dma_buf_map_attachment() ← 建立 DMA 映射,获取 sg_table
│ │
│ │ ← 设备通过 DMA 直接访问,CPU 不参与搬运
│ │
│ dma_buf_unmap_attachment() ← 解除 DMA 映射
│ dma_buf_detach() ← 销毁 attachment(f_count 不变)
│
dma_buf_put() / close(fd) ← f_count--
│
└─ f_count == 0 → 触发 release → Exporter 真正释放物理内存
关键 API 一览
| API |
作用 |
引用计数变化 |
dma_buf_export() |
分配 dma_buf,初始化 f_count |
f_count = 1 |
dma_buf_fd() |
安装 fd,fd_install 引入 fd 表 |
不变(fd_install 不增引用) |
dma_buf_get(fd) |
通过 fd 获取 dma_buf,fget() 增引用 |
f_count++ |
get_dma_buf() |
内核侧增加引用,等价 get_file() |
f_count++ |
dma_buf_attach() |
创建 attachment,建立设备关联 |
不变 |
dma_buf_detach() |
销毁 attachment |
不变 |
dma_buf_put() |
减少引用,等价 fput() |
f_count-- |
close(fd) |
用户态关闭 fd,触发 .flush |
f_count-- |
⚠️ 重要:dma_buf_attach/detach 和 map/unmap_attachment 不影响 f_count,只有 get_file/fput 系列才会改变引用计数。这是非常容易误解的地方。
典型生命周期变体对比
| 场景 |
典型 f_count |
说明 |
| 纯内核 buffer(从未暴露用户态) |
1 |
驱动内部工作 buffer,只有 exporter 自身持有 |
| 用户态通过 fd 持有 |
≥2 |
exporter 初始引用 + 用户进程 fd(dma_buf_get / dup / fork) |
| 多个 Importer attach |
≥2 |
attach 本身不增加引用,f_count 由 get_dma_buf 决定 |
| 用户态 fd 全部关闭、内核仍持有 |
1 |
close(fd) 后 exporter 内核侧仍有引用,buffer 未释放 |
五、f_count 引用计数深度解析
f_count 是 struct file 的文件引用计数(atomic_long_t f_count),本质是对那个匿名文件对象的引用。因为 dma_buf 的生命周期完全绑定在这个文件上,所以间接就是 buffer 的引用计数。
f_count 的构成公式:
f_count = 1 (export 初始值)
+ N (用户态持有的 fd 数,每个 open/dup/fork 各 +1)
+ M (内核侧 dma_buf_get/get_dma_buf 各 +1)
引用计数变化场景示例
操作 f_count 说明
─────────────────────────────────────────────────────
dma_buf_export() 1 创建文件,初始引用
dma_buf_fd() 1 fd_install 不加引用
─── Importer A ───
dma_buf_get(fd) 2 fget → get_file
dma_buf_attach() 2 attach 不改变 f_count
─── Importer B ───
get_dma_buf(dmabuf) 3 内核侧手动增引用
dma_buf_attach() 3 attach 不改变 f_count
─── 开始释放 ───
dma_buf_detach() × 2 3 detach 不改变 f_count
dma_buf_put() × 2 1 fput × 2
close(fd) 0 → 触发 release,物理内存释放 🗑️
f_count = 0 时触发的完整清理链
fput() → f_count-- == 0
│
├─ VFS → file->f_op->release()
│ └─ dma_buf_file_release()
│ └─ 从 db_list 链表中摘除 dmabuf
│
└─ VFS → dentry->d_op->d_release()
└─ dma_buf_release()
├─ dma_buf_stats_teardown() ← 删除 sysfs 节点
├─ dmabuf->ops->release() ← Exporter 释放物理内存
├─ dma_resv_fini() ← 清理 reservation
└─ kfree(dmabuf) ← 释放 dma_buf 结构体本身
引用计数增减时机汇总
+1 时机:
| 操作 |
调用路径 |
说明 |
dma_buf_export() |
内部 alloc_file_pseudo() |
创建文件,初始 f_count = 1 |
dma_buf_get(fd) |
fget(fd) → get_file() |
内核通过 fd 获取 dma_buf 指针时 |
get_dma_buf(dmabuf) |
get_file(dmabuf->file) |
纯内核态手动增加引用 |
进程 dup(fd) / fork() |
VFS 文件机制 |
复制 fd 时 |
-1 时机:
| 操作 |
调用路径 |
说明 |
dma_buf_put(dmabuf) |
fput(dmabuf->file) |
内核态主动减引用 |
进程 close(fd) |
VFS fput() |
用户态关闭 fd |
| 进程退出 |
VFS 自动关闭所有 fd |
— |
六、核心数据结构(源码级)
struct dma_buf(buffer 本体)
/* include/linux/dma-buf.h (Linux mainline / AOSP GKI) */
struct dma_buf {
size_t size; // buffer 字节大小,生命周期内不变
struct file *file; // 关联文件,file->f_count 即引用计数
struct list_head attachments; // dma_buf_attachment 链表
const struct dma_buf_ops *ops; // exporter 提供的操作回调
const char *exp_name; // exporter 驱动名称(如 "system-heap")
const char *name; // 用户态可设置的名称(见 DMA_BUF_SET_NAME ioctl)
struct dma_resv *resv; // reservation 对象,用于 Fence 同步
/* ... */
};
struct dma_buf_attachment(attach 记录)
/* include/linux/dma-buf.h (Linux mainline / AOSP GKI) */
struct dma_buf_attachment {
struct dma_buf *dmabuf;
struct device *dev; // 被 attach 的设备
struct list_head node; // 挂在 dmabuf->attachments 上
struct sg_table *sgt; // 缓存的 DMA scatter-gather 表
enum dma_data_direction dir; // DMA 方向(读/写/双向)
bool peer2peer; // 是否允许 peer-to-peer DMA
/* ... */
};
两者关系示意:
dma_buf
├── file (f_count 在这里)
├── attachments 链表
│ ├── dma_buf_attachment [GPU]
│ │ └── sgt (GPU 的 IOVA 路线图)
│ └── dma_buf_attachment [NPU]
│ └── sgt (NPU 的 IOVA 路线图)
├── ops (exporter 回调)
└── resv (Fence 同步锁)
七、sysfs 调试接口与内存统计
sysfs 路径
需内核配置 CONFIG_DMABUF_SYSFS_STATS=y,路径:
/sys/kernel/dmabuf/buffers/<inode>/
├── size # buffer 字节大小
├── exporter_name # exporter 驱动名
└── details # 详细调试信息(内容因驱动而异)
内存分类统计
标准 AOSP 内核通过 /sys/kernel/dmabuf/buffers/ 提供两类基础统计:
| 分类 |
统计方式 |
说明 |
| 所有 buffer 总大小 |
遍历 */size 求和 |
系统当前所有 dma-buf 占用的物理内存 |
| 按 exporter 分类 |
按 exporter_name 分组统计 |
各驱动(system-heap、ion 等)分配量 |
| 单个 buffer 详情 |
读取 size + exporter_name |
定位大内存来源 |
读取示例:
# 查看某个 buffer 的基本信息
cat /sys/kernel/dmabuf/buffers/12345/size # 输出:8388608 (8MB)
cat /sys/kernel/dmabuf/buffers/12345/exporter_name # 输出:system-heap
# 统计所有 dma-buf 总大小(字节)
awk '{sum += $1} END {print sum}' /sys/kernel/dmabuf/buffers/*/size
# 按 exporter 分类汇总
for d in /sys/kernel/dmabuf/buffers/*/; do
name=$(cat "$d/exporter_name")
size=$(cat "$d/size")
echo "$name $size"
done | sort | awk '{a[$1]+=$2} END {for(k in a) print k, a[k]}'
注意:details 节点的具体内容取决于内核版本和 exporter 驱动实现,不同平台输出格式不同,以实际内核源码为准。
八、常见问题与陷阱
| 问题 |
根本原因 |
正确理解 |
| f_count 远大于持有进程数 |
内核侧 get_dma_buf() 不体现在 /proc |
f_count = 初始1 + 用户态N + 内核侧M |
| dma_buf_fd() 多次调用 |
每次都是新 fd,各自独立计数 |
多个 fd 各自 close 各自减 f_count |
| fork 后 f_count 比预期大 |
fork 复制 fd 表走 get_file,不走 dma_buf_fd |
通过 /proc/[pid]/fd 扫描确认实际持有 |
| 以为 attach 会增加 f_count |
attach 只建立设备关联,不持有 file 引用 |
只有 get_file/fput 系列改变 f_count |
Q1:为什么 f_count 会大于实际持有进程数 + 1?
f_count 的构成:exporter 初始引用(+1)+ 每个用户态 fd(dup/fork 也各 +1)+ 内核侧 get_dma_buf() 调用。因此 f_count 通常大于通过 /proc/[pid]/fd 扫描到的用户态持有数量,因为内核侧引用(驱动内 get_dma_buf())不体现在进程 fd 表中。
Q2:dma_buf_fd() 调用多次会怎样?
每次调用都会新分配一个 fd 号,调用 fd_install() 将 file 引入进程 fd 表(f_count++)。多次调用会产生多个独立的 fd,各自 close 时 f_count 各减 1。
Q3:fork() 之后子进程持有父进程的 dmabuf fd 怎么算?
fork 时内核复制 fd 表,调用 get_file() 使 f_count 增加。如果只通过 dma_buf_fd() 的调用次数来统计 fd 数,会低估实际 fd 总数(因为 fork/dup 不走 dma_buf_fd())。可以通过遍历 /proc/[pid]/fd 并检查 fd 指向的文件是否为 dmabuf 来确认实际持有情况。
Q4:如何判断一个 buffer 是否可以安全回收?
满足以下两个条件可考虑回收(驱动层面决策,框架不主动回收):
f_count == 1(只剩 exporter 自身持有,无用户态 fd、无内核侧额外引用)attachments 链表为空(没有设备 attach)
可通过遍历 /proc/[pid]/fd 确认无用户态进程持有该 buffer 对应的 fd。
Q5:/proc/[pid]/smaps 中如何识别 dmabuf 段?
7f1234560000-7f1234570000 r--s 00000000 00:01 12345 /dmabuf:qcom_hgsl
行末路径包含 dmabuf,通过 Pss: 字段累加即为该进程 mmap 的 dmabuf PSS。
总结
读完本文,你应该能回答这些问题:
- ✅ dma-buf 为什么被引入?它解决了什么核心痛点?
- ✅ dma-buf 的四大职责是什么?各自解决什么问题?
- ✅ dma_buf_attach/detach 会不会影响 f_count?
- ✅ f_count 的构成是什么?何时 +1 何时 -1?
- ✅ 如何通过 sysfs 分析线上 dmabuf 内存占用?
核心结论:dma-buf 的出现是 Linux 图形和多媒体架构走向成熟的标志。它打破了各个硬件驱动之间的壁垒,是现代 Android 系统(ION/DMA-BUF Heaps 机制)、Wayland 显示服务器、以及各种复杂音视频处理流水线高效运行的底层基石。没有 dma-buf,现代智能手机和车机的流畅图形体验是无法实现的。
附录:相关内核源码文件
| 文件 |
职责 |
include/linux/dma-buf.h |
核心数据结构定义、API 声明 |
drivers/dma-buf/dma-buf.c |
API 实现、file_operations |
drivers/dma-buf/dma-buf-sysfs-stats.c |
sysfs 统计接口实现 |
drivers/dma-buf/dma-buf-selftest.c |
自测代码 |
Documentation/driver-api/dma-buf.rst |
官方文档 |