深入 Linux DMA-BUF(五):内存的两本账——dmabuf/buffers 与 total_pools_kb 到底统计什么?
你是否遇到过这样的困惑:相机 App 关闭后,MemFree 几乎纹丝不动;或者把 /sys/kernel/dmabuf/buffers 和 /sys/kernel/dma_heap/total_pools_kb 两个节点的数值加起来,发现远超预期,开始怀疑内存是否被重复计算?
这背后的答案,藏在 Linux DMA-Heap 框架的两级内存账本设计里。这两个节点统计的是同一批物理内存在不同生命周期阶段的状态快照——一个记录「正在出租的车辆」,另一个记录「停在库里的备用车」。理解它们的边界,是做准确 Android 内存分析的必修课。本文将从底层物理内存的流转路径出发,结合 Android GKI (android14-6.1) 源码,彻底厘清这两者的关系。
本文是《深入 Linux DMA-BUF》系列第五篇。前四篇分别讲了整体架构与生命周期、CPU 访问与 Cache 一致性、Fence 同步与 DMA-BUF Heaps、以及 20 道精讲精问。本篇专注于内存统计的视角:当你观测系统时,那两个 sysfs 节点究竟在看什么?
一、先给结论:它们互斥,统计的是同一批内存的不同状态
核心结论:/sys/kernel/dmabuf/buffers 统计「正在被使用的 DMA 内存」,total_pools_kb 统计「被释放后缓存在内核池子里的备用 DMA 内存」。同一块物理内存,在同一时刻,只能存在于其中一侧。
用一句话类比:dmabuf/buffers 是「已出租的车辆清单」,total_pools_kb 是「停车场里备用的闲置车辆数」。
| 节点 |
统计对象 |
内存状态 |
有无 DMA-BUF 对象 |
/sys/kernel/dmabuf/buffers |
所有活跃的 DMA-BUF 对象 |
正在被进程使用 |
有(已 export) |
/sys/kernel/dma_heap/total_pools_kb |
DMA-Heap 池中的物理页 |
空闲,等待复用 |
无(对象已销毁) |
二、/sys/kernel/dmabuf/buffers:活跃对象的花名册
什么时候出现,什么时候消失?
回顾前几篇讲过的生命周期:dma_buf_export() 把一块物理内存封装成内核对象,同时内部调用 dma_buf_stats_setup() 将其注册到 /sys/kernel/dmabuf/buffers/ 下,生成一个带 fd 的可共享对象。进程关闭所有 fd、内核侧引用计数(f_count)归零后,dma_buf_stats_teardown() 将其从 sysfs 移除。
dma_buf_export()
│
├─ 分配物理页
├─ 调用 dma_buf_stats_setup()
│ → /sys/kernel/dmabuf/buffers/<id>/ ✅ 出现
└─ 返回 fd 给用户空间
f_count 归零(所有 fd 关闭 + 内核侧 put)
│
└─ 调用 dma_buf_stats_teardown()
→ /sys/kernel/dmabuf/buffers/<id>/ ❌ 消失
源码路径:dma_buf_export() 定义在 drivers/dma-buf/dma-buf.c,其中调用 dma_buf_stats_setup() 将 buffer 注册到 /sys/kernel/dmabuf/buffers/ 下,引用计数归零时调用 dma_buf_stats_teardown() 将其移除。
哪些内存会出现在这里?
只要内核驱动调用了 dma_buf_export() 将内存封装成 DMA-BUF,无论这块内存是否已经分发 fd 给用户空间,也无论是纯内核驱动间共享还是跨进程共享,都会出现在这里。
| 场景 |
是否出现在 dmabuf/buffers |
| App 通过 DMA-Heap 申请并获得 fd |
✅ 出现 |
| GPU 驱动申请并 export 给用户空间 |
✅ 出现 |
| 内核驱动间互传(无用户态 fd)的 DMA-BUF |
✅ 出现(只要 export 了) |
驱动用 dma_alloc_coherent() 私有申请 |
❌ 不出现 |
| NPU/DSP 驱动内部 CMA 申请,未 export |
❌ 不出现 |
与第一篇的 f_count 知识点结合:f_count 不是 0 时,buffer 就在这里。dma_buf_fd() 和 dma_buf_attach() 不改变 f_count,但 fork/dup 会,这决定了 sysfs 统计的精确性。
三、total_pools_kb:DMA-Heap 的「停车场」
为什么要有 Pool?
每次向 Buddy System 申请和归还物理页,开销不小(需要清零、TLB 刷新等)。对于相机、视频编解码这类高频申请/释放 DMA 内存的场景,内核引入了 Pool 机制:释放内存时,先不还给 Buddy System,而是缓存起来,等下次申请时直接复用,省去初始化开销。
System Heap 维护三档 Pool,覆盖不同粒度的内存请求:
| Order |
每页大小 |
典型用途 |
| Order 8 |
1 MB |
大块视频帧、GPU 纹理 |
| Order 4 |
64 KB |
中等缓冲区 |
| Order 0 |
4 KB |
小块内存碎片回填 |
源码路径:drivers/dma-buf/heaps/system_heap.c 的 system_heap_create() 为每个 Order 调用 dmabuf_page_pool_create() 建立对应的 Pool;system_get_pool_size() 通过遍历三个 Pool 累加字节数,提供给 total_pools_kb 的 show 回调。
释放时发生了什么?
这是理解两本账「互斥」关系的关键:
进程 close(fd)
│
▼
DMA-BUF 对象引用计数归零
│
├─ [立刻] 销毁 DMA-BUF 对象本身
│ → 从 /sys/kernel/dmabuf/buffers 消失 ❌
│
└─ [同时] 底层物理页不归还 Buddy System
① system_heap_zero_buffer()(清零安全擦除)
② 按页的 compound_order 找对应 Pool
③ dmabuf_page_pool_free() 放入缓存
→ /sys/kernel/dma_heap/total_pools_kb 增加 ✅
⚠️ 关键细节:页面进入 Pool 之前会先被清零(system_heap_zero_buffer()),这是 Android 的安全设计——确保下一个使用者拿到干净内存,防止数据泄漏。这也是为什么从 Pool 复用内存比直接从 Buddy System 申请更快:Pool hit 跳过了清零步骤。
四、并非所有 DMA-BUF 释放都进 Pool——来源决定去向

第三篇讲过 DMA-BUF Heaps 的架构:系统里有多种 Heap(System Heap、CMA Heap、vendor heap……),total_pools_kb 只是 System Heap 的私有 Pool 统计,不代表全系统。
DMA-BUF 来源
│
├─ DMA-Heap System Heap ──── 释放 ──→ total_pools_kb(池化)
│
├─ CMA 区域(视频解码器等)── 释放 ──→ CmaFree 增加,MemFree 增加
│
├─ GPU 驱动(KGSL/Mali)──── 释放 ──→ GPU 私有缓存池 或 MemFree
│
└─ ION 框架(Android 11-)── 释放 ──→ ION 自己的池子(非 dma_heap)
| DMA-BUF 来源 |
释放后去向 |
是否进 total_pools_kb |
| DMA-Heap System Heap |
DMA-Heap Pool(三档 Order) |
✅ 进入 |
| CMA(连续内存分配器) |
CmaFree / MemFree |
❌ 不进入 |
| GPU 驱动(高通 KGSL) |
GPU 私有缓存 / MemFree |
❌ 不进入 |
| ION 框架(旧版) |
ION 自身池子(/sys/kernel/ion/total_pools_kb) |
❌ 不进入 |
结论:total_pools_kb 仅是 DMA-Heap System Heap 的「部门小金库」,不代表全系统所有 DMA 内存的回收站。分析时切忌把它当成全部 DMA 内存的池化统计。
五、物理页的一生:以相机拍照为例

以相机 App 拍一张照片为例,追踪这块内存的完整生命周期——这也是把前四篇知识点串联起来的最好场景:
① App 申请内存
─────────────────────────────────────────────────────────
Pool 有缓存?
├─ Pool hit → 从 Pool 取出(total_pools_kb 减少)
│ 跳过清零,直接封装 ← 这是 Pool 最大的价值
└─ Pool miss → 向 Buddy System 申请新物理页
先清零再封装(保证安全)
dma_buf_export() → DMA-BUF 对象诞生
→ /sys/kernel/dmabuf/buffers/<id>/ ✅ 出现
→ 返回 fd 给 Camera App
② 多硬件共享使用(零拷贝)
─────────────────────────────────────────────────────────
Camera 驱动调用 dma_buf_attach() → map_attachment → 建 IOVA(不改 f_count)
GPU 调用 dma_buf_attach() → map_attachment → 建独立 IOVA
Fence 协调时序:Camera 写完 → dma_fence_signal → GPU 开始读
③ App 释放 fd
─────────────────────────────────────────────────────────
close(fd) → fput → f_count--
f_count 归零
→ DMA-BUF 对象销毁
→ /sys/kernel/dmabuf/buffers/<id>/ ❌ 消失
→ system_heap_zero_buffer() 清零
→ dmabuf_page_pool_free() 进 Pool
→ /sys/kernel/dma_heap/total_pools_kb ✅ 增加
→ MemFree 不变(物理页未还 Buddy)← 这就是 MemFree 不涨的原因
整个过程清晰地表明:同一时刻,一块物理内存只能处于「在用」(dmabuf/buffers)或「缓存」(total_pools_kb)两种状态之一。
六、内核如何回收 Pool?Shrinker 机制(Android GKI 专属)
Pool 里缓存的物理页并非永远占用——内核 shrinker 机制会在内存压力下主动回收它们。
内存压力触发(kswapd / lmkd 触发 shrinker 扫描)
│
▼
内核 shrinker 框架
│ 调用 dmabuf_page_pool_shrink_count() ← 统计可回收页数
│ 调用 dmabuf_page_pool_shrink_scan() ← 执行实际回收
▼
dmabuf_page_pool_do_shrink()
│ 从 Pool 中取出物理页
│ free_pages() 归还给 Buddy System
▼
total_pools_kb 减少,MemFree 增加
| Shrinker 回调 |
作用 |
dmabuf_page_pool_shrink_count |
向内核报告可回收的页面数量 |
dmabuf_page_pool_shrink_scan |
实际扫描并释放指定数量的页面 |
dmabuf_page_pool_do_shrink |
底层单次释放操作 |
源码路径:Android GKI drivers/dma-buf/heaps/page_pool.c 中,dmabuf_page_pool_init_shrinker() 注册 shrinker,shrink 入口为 dmabuf_page_pool_shrink_count / dmabuf_page_pool_shrink_scan,两者内部均调用 dmabuf_page_pool_shrink()。该实现仅存在于 Android GKI,Linux mainline 中无对应。
七、实战:如何用这两个节点分析内存问题
场景 1:相机关闭后 MemFree 纹丝不动
dmabuf/buffers 减少 → 对象已销毁 ✅
total_pools_kb 增加 → 物理页进 Pool(已清零)✅
MemFree 不变 → 物理页未还 Buddy ⚠️ 这是正常行为
结论:Pool 机制「截留」了物理页。这不是泄漏,是内核的性能优化。下次相机再打开,Pool hit 直接复用,省去清零开销。
场景 2:total_pools_kb 异常偏高(如 > 500 MB)
Pool 过大说明 DMA-Heap 缓存了大量物理页。此时:
- 是否有大量相机/视频解码场景刚结束,Pool 尚未被系统回收
- 在内存压力下,内核会主动收缩 Pool(shrinker:
dmabuf_page_pool_shrink_scan),正常情况下不必担心 - 如果 Pool 长期高居不下,可检查 shrinker 注册是否正常(源码:
drivers/dma-buf/heaps/page_pool.c)
场景 3:dmabuf/buffers 持续增长且 App 已退出
说明有 DMA-BUF 对象泄漏——有进程持有 fd 但未关闭,或内核侧驱动持有 get_dma_buf() 引用未释放(回忆第一篇:这类引用不在任何进程的 /proc/pid/fd 里,只能从 f_count 减去已知引用来反推)。
# 快速定位:哪个 exporter 泄漏最多?
for d in /sys/kernel/dmabuf/buffers/*/; do
name=$(cat "$d/exporter_name")
size=$(cat "$d/size")
echo "$name $size"
done | awk '{a[$1]+=$2} END {for(k in a) print a[k], k}' \
| sort -rn | head -10
需要详细持有者信息时,配合 /sys/kernel/debug/dma_buf/bufinfo。
场景 4:两值之和远超预期
可能混入了 ION / GPU 私有缓存统计。total_pools_kb 只是 System Heap 的池,不是全系统 DMA 内存的池。
| 现象 |
dmabuf/buffers |
total_pools_kb |
结论 |
| 相机刚释放,MemFree 未涨 |
减少 |
增加 |
正常,Pool 截留(已清零) |
| 内存压力,系统收缩 Pool |
不变 |
减少 |
Pool shrink,释放给 Buddy |
| DMA-BUF 泄漏 |
持续增加 |
无明显变化 |
对象未释放,需排查持有者 |
| 两值之和超出预期 |
- |
- |
检查是否混入了 ION/GPU 的统计 |
八、总结
两个节点的关系,用一张表格概括:
| 维度 |
dmabuf/buffers |
total_pools_kb |
| 统计内容 |
活跃 DMA-BUF 对象大小之和 |
DMA-Heap Pool 中缓存的物理页 |
| 内存状态 |
正在使用中 |
空闲且已清零,等待复用 |
| 有无 DMA-BUF 对象 |
有(f_count > 0) |
无(对象已销毁) |
| 覆盖来源 |
所有 export 过的 DMA-BUF |
仅 DMA-Heap System Heap |
| Pool Order 分层 |
不涉及 |
1MB / 64KB / 4KB 三档 |
| 互斥关系 |
✅ 同一块内存不会同时出现在两边 |
✅ 同一块内存不会同时出现在两边 |
记住这一句话:释放 DMA-BUF 时,「壳」(对象)立刻消失,「肉」(物理页)先清零再进 Pool。dmabuf/buffers 统计「壳」,total_pools_kb 统计「肉」。分析内存时,把两者加在一起,才是 DMA-Heap System Heap 框架真实占用的物理内存总量。
附录:本文涉及的关键内核接口
以下接口均来自 Android GKI android14-6.1 分支;标注 ⚠️ 的接口在 Linux mainline 中不存在,为 Android 专有扩展。
| 接口 / 节点 |
所在文件 |
说明 |
dma_buf_export() |
drivers/dma-buf/dma-buf.c |
将物理内存封装为 DMA-BUF 对象,并创建匿名 fd |
dma_buf_stats_setup() |
drivers/dma-buf/dma-buf.c |
export 时注册 sysfs 节点到 /sys/kernel/dmabuf/buffers/ |
dma_buf_stats_teardown() |
drivers/dma-buf/dma-buf.c |
引用计数归零时移除对应 sysfs 节点 |
total_pools_kb_show() ⚠️ |
drivers/dma-buf/dma-heap.c |
sysfs 读回调,遍历所有 heap 调用 get_pool_size 累加后以 KB 输出 |
system_heap_zero_buffer() ⚠️ |
drivers/dma-buf/heaps/system_heap.c |
释放时安全清零物理页,防止数据泄漏 |
system_get_pool_size() ⚠️ |
drivers/dma-buf/heaps/system_heap.c |
遍历三档 Pool(1MB/64KB/4KB)累加字节数 |
dmabuf_page_pool_create() ⚠️ |
drivers/dma-buf/heaps/page_pool.c |
创建指定 Order 的 Pool 实例 |
dmabuf_page_pool_alloc() ⚠️ |
drivers/dma-buf/heaps/page_pool.c |
从 Pool 取页(Pool hit 时直接返回,跳过清零) |
dmabuf_page_pool_free() ⚠️ |
drivers/dma-buf/heaps/page_pool.c |
将物理页放回 Pool 缓存 |
dmabuf_page_pool_init_shrinker() ⚠️ |
drivers/dma-buf/heaps/page_pool.c |
注册 shrinker,内存压力时触发 Pool 回收 |
dmabuf_page_pool_shrink_count() ⚠️ |
drivers/dma-buf/heaps/page_pool.c |
shrinker 回调:向内核报告可回收页数量 |
dmabuf_page_pool_shrink_scan() ⚠️ |
drivers/dma-buf/heaps/page_pool.c |
shrinker 回调:实际扫描并释放指定数量的页 |
/sys/kernel/dmabuf/buffers/ |
内核 sysfs |
活跃 DMA-BUF 对象目录,每个 buffer 一个子目录 |
/sys/kernel/dma_heap/total_pools_kb ⚠️ |
内核 sysfs |
DMA-Heap 所有 System Heap Pool 的缓存总量(KB) |
/sys/kernel/debug/dma_buf/bufinfo |
debugfs |
详细列出每个活跃 DMA-BUF 的持有者与大小,用于泄漏排查 |
总结
读完本文,你应该能回答这些问题:
/sys/kernel/dmabuf/buffers 和 /sys/kernel/dma_heap/total_pools_kb 为什么是互斥的?把它们加在一起代表什么含义?- 当一块 DMA-BUF 被进程关闭(fd 引用计数归零)后,它的物理页会经历哪几个步骤才能重新被分配?
- DMA-Heap System Heap Pool 的三档 Order(1MB / 64KB / 4KB)各自针对什么场景?Pool hit 与 Pool miss 的区别是什么?
- Android GKI 的 Shrinker 机制是如何决定从 Pool 中回收多少物理页的?
shrink_count 和 shrink_scan 分别扮演什么角色? - 在实战排查中,如果
dmabuf/buffers 长期偏高但 App 已经退出,应该优先检查哪些方面?如果 total_pools_kb 长期居高不下,说明什么,是否需要干预?