
1. 黑屏卡死现场还原工位机不是“挂了”而是被 DMA-BUF 慢慢拖垮那天下午三点十七分产线测试工位的 Android 工位机突然黑屏——不是重启不是崩溃日志弹窗就是屏幕彻底冻结触控无响应ADB 连接断开但串口 console 仍有心跳/proc/sys/kernel/printk显示内核仍在调度top命令还能看到几个进程在跑只是 CPU 负载长期卡在 98% 以上dmesg -T里每隔 3 秒就刷出一行rockchip-vpu: vpu_enc: dma-buf fd leak detected。我们第一反应是 App 内存泄漏或 SurfaceFlinger 死锁立刻拉取 tombstone、抓取 systrace、dump heap结果全无异常——主线程没卡、Binder 线程池满负荷但未阻塞、GraphicBuffer 分配计数正常。直到有人随手cat /sys/kernel/debug/dma_buf/summary发现一个名为scrcpy-encoder-0x7f8a2c0000的 buffer 引用计数从启动时的 1 涨到了 14276而ls /sys/kernel/debug/dma_buf/下竟有 238 个同名 buffer 残留每个都标记为refcount1却无人释放。那一刻我才意识到这不是 App 的锅是 scrcpy 和 Rockchip VPU 编码器在底层玩起了“内存俄罗斯轮盘”——DMA-BUF 不是被释放了是被悄悄藏进了内核的引用计数黑洞里。这个现象背后的核心关键词非常明确Android、scrcpy、Rockchip、DMA-BUF、编码器。它不是典型的 Java 层 OOM 或 ANR而是一场发生在内核空间与硬件加速器交界处的资源耗尽事故。scrcpy 作为跨平台投屏工具依赖 Android 的 MediaCodec API 调用硬件编码器此处为 Rockchip VPU而 Rockchip 的 VPU 驱动在实现 DMA-BUF 导出/导入时对 refcount 的管理存在边界条件缺陷。当 scrcpy 频繁启停编码会话比如调试时反复点击“Start/Stop”按钮、或网络抖动导致帧传输中断时VPU 驱动未能正确触发dma_buf_put()导致 buffer 对象在dma_buf全局哈希表中持续累积。这些 buffer 占用的是物理连续内存CMA 区域一旦超过 128MBRockchip RK3399 平台默认 CMA size系统就会拒绝新的 GraphicBuffer 分配SurfaceFlinger 因无法获取输出 buffer 而僵死最终表现为黑屏卡死。这解释了为什么adb shell dumpsys SurfaceFlinger显示mActiveTransactionCount0却无画面输出——不是逻辑卡住是物理资源彻底枯竭。我之所以强调“根因不是 App”是因为绝大多数排查者会本能地进入应用层陷阱检查 Activity 生命周期、分析 Handler Looper 是否堵塞、怀疑 WebView 渲染线程抢占 GPU。但这次所有 Java/Kotlin 层指标都健康得反常。真正该盯住的是/sys/kernel/debug/dma_buf/这个内核 debugfs 接口——它是 Linux DMA-BUF 子系统的“体检报告单”。只要你的设备启用了CONFIG_DEBUG_FSy绝大多数定制 Android kernel 都开启这个路径就存在且无需 root 权限即可读取adb shell cat /sys/kernel/debug/dma_buf/summary。它会告诉你当前有多少 DMA-BUF 实例、总大小、平均引用计数甚至每个 buffer 的创建者exp_name字段。在本次事故中exp_name明确显示为rockchip-vpu而importer列则指向scrcpy进程 PID。这种“谁造的、谁用的、谁没还”的三元关系比任何 Java stacktrace 都更直指要害。所以如果你的 Android 工位机也出现类似症状——黑屏但串口可连、adb devices仍在线、logcat无 crash 日志——请先别急着重刷固件打开 debugfs看看 DMA-BUF 是否正在无声膨胀。2. scrcpy 与 Rockchip VPU 的握手协议为什么编码器会“忘记还钥匙”要理解 DMA-BUF 泄漏的根源必须拆解 scrcpy 启动编码会话时与 Rockchip VPU 驱动之间那套精妙却脆弱的握手流程。整个过程并非简单的“申请 buffer → 编码 → 释放”而是涉及用户空间、内核驱动、硬件 IP 三者的协同其中任何一个环节 refcount 处理失误都会导致 buffer 悬挂。首先看 scrcpy 的调用链它通过MediaCodec.createEncoderByType(video/avc)获取编码器实例指定MediaFormat.KEY_MIME video/avc、KEY_WIDTH/KEY_HEIGHT、KEY_BIT_RATE等参数。当调用configure()时Android Framework 会将请求转发给 HAL 层的vendor.mediatek.hardware.videocodec1.0::IVideoCodec对 Rockchip则是vendor.rockchip.hardware.vpu1.0::IVpuCodec。此时Rockchip VPU 驱动drivers/media/platform/rockchip/vpu/rkvpu_v4l2.c收到配置请求开始准备 DMA-BUF。关键动作发生在rkvpu_v4l2_mmap()函数中驱动调用dma_buf_export()创建一个 DMA-BUF 对象并将其 fd 返回给 userspace。scrcpy 通过ANativeWindow_lock()获取 GraphicBuffer 后会将 buffer fd 传递给 VPU 驱动驱动再调用dma_buf_get()增加 refcount表示“此 buffer 正被 VPU 硬件使用”。问题就出在“释放”环节。正常流程中当 scrcpy 调用stop()或release()时Framework 应触发VpuCodec::close()驱动执行dma_buf_put()减少 refcount若 refcount 降为 0则dma_buf_release()被调用buffer 归还 CMA。但 Rockchip VPU 驱动特别是 2021 年前的旧版 kernel如 4.4.194-rk3399存在一个致命缺陷在vpu_enc_stop_streaming()中驱动仅清理了 VPU 内部的 buffer queue却遗漏了对已导出 DMA-BUF 的dma_buf_put()调用。更糟的是当网络中断导致 scrcpy 主动 abort 编码会话时它会调用MediaCodec.release()但 Framework 层的ACodec::onRelease()在某些条件下如 encoder 处于EXECUTING状态而非IDLE不会完整走完 HAL 的 close 流程导致rkvpu_v4l2_close()根本不被执行。于是那个由dma_buf_export()创建的 bufferrefcount 永远卡在 1而dma_buf_release()永远等不到触发时机。我们曾用perf trace -e dma_buf:* -p $(pidof scrcpy)抓取过真实 trace发现每次成功编码一帧dma_buf_export被调用 1 次dma_buf_get被调用 2 次一次给 VPU一次给 DRM/KMS 输出但dma_buf_put仅被调用 1 次——缺失的那一次正是 VPU 硬件侧的释放。这直接印证了驱动逻辑缺陷。有趣的是同一套 scrcpy 代码在 Qualcomm 平台如 SM8250上完全稳定因为高通的qcom-vcodec驱动在venc_close()中明确包含dma_buf_put(vpu-dma_buf)而在 Intel 平台如 Apollo Lakei915_guc驱动则通过drm_gem_object_put_unlocked()确保 refcount 正确递减。Rockchip 的问题不是“不支持 DMA-BUF”而是“支持得不完整”——它导出了 buffer却没管好它的生命周期终点。提示判断你的 Rockchip 设备是否受影响最简单的方法是运行scrcpy --bit-rate2M --max-fps15投屏 10 分钟然后执行adb shell cat /sys/kernel/debug/dma_buf/summary | grep -E (rockchip|scrcpy)。如果total_buffers数值持续增长每分钟增加 5 个且total_size_kb超过 5000050MB基本可确认存在泄漏。注意此检测需在设备未重启状态下进行重启会清空 debugfs。3. 从泄漏到黑屏DMA-BUF 耗尽如何一步步绞杀 Android 图形栈DMA-BUF 泄漏本身不会立即导致黑屏它像慢性毒药通过消耗 CMAContiguous Memory Allocator区域的物理内存逐步瓦解 Android 图形子系统的根基。整个崩溃链路清晰而残酷每一环都踩在 Android 架构设计的刚性约束上。第一步CMA 区域被填满。Rockchip 平台如 RK3399默认 CMA size 为 128MB用于分配 GraphicBuffer、VPU 编码输入/输出 buffer、DRM framebuffer 等需要物理连续内存的资源。当 DMA-BUF 泄漏累积到约 110MB 时CMA 的cma_alloc()开始频繁失败。此时dmesg会出现cma: cma_alloc: failed to alloc 1024KB的警告但系统尚能苟延残喘——因为部分 buffer 可通过ion或system_heap分配只是性能下降。第二步SurfaceFlinger 失去输出能力。SurfaceFlinger 的核心任务是合成各 Layer 的 GraphicBuffer并将最终帧提交给 HWCHardware Composer或 DRM/KMS。当它尝试为主 Display 创建新 framebuffer 时调用drmModeAddFB2()底层 DRM 驱动会请求 CMA 分配一块 buffer。一旦 CMA 分配失败drm_fb_create()返回-ENOMEMSurfaceFlinger 的DisplayDevice::commit()失败合成循环中断。此时dumpsys SurfaceFlinger会显示mCurrentState.mLayers.size()XLayer 数正常但mCurrentState.mDisplayStates[0].mVisibleRegions为空mCurrentState.mDisplayStates[0].mDirtyRegion为全屏——意味着它知道该画什么却找不到地方画。第三步InputDispatcher 失效。这一步常被忽略却是黑屏“触控失灵”的关键。Android 的 Input 子系统依赖EventHub从/dev/input/event*读取原始事件经InputReader解析后通过InputDispatcher分发给目标窗口。而InputDispatcher的线程InputDispatcherThread需要定期调用looper-pollOnce()等待事件该 poll 操作底层依赖epoll_wait()监听mINotifyFd用于监控 input device 变化和mWakeupReadPipeFd用于唤醒线程。当 CMA 耗尽时epoll_ctl()在添加新 fd 时可能因内存不足而失败导致InputDispatcher线程陷入永久等待不再处理任何触摸事件。这就是为什么黑屏后连电源键都无响应——不是按键没触发是 InputDispatcher 已“饿死”。第四步Zygote 与 SystemServer 连锁僵死。当 SurfaceFlinger 和 InputDispatcher 同时瘫痪SystemServer 的ActivityManagerService会检测到mLastInputTime长期未更新触发Watchdog的HandlerChecker超时默认 60 秒。Watchdog 尝试 dump 所有线程状态但dumpStackTraces()需要分配临时 buffer再次触发 CMA 分配失败导致 Watchdog 自身 hang 住。此时ps -t会显示system_server进程状态为Duninterruptible sleepkill -3也无法生成 thread dump。整个系统进入“假死”状态串口console仍有ksoftirqd/0等内核线程打印但用户空间进程全部停滞。注意此崩溃链路具有平台特异性。在 ARM64 Rockchip 组合下CMA 耗尽是黑屏主因但在 x86_64 Intel 平台相同泄漏可能导致i915驱动报GEM object creation failed进而触发 DRM atomic commit 失败表现为主屏闪烁而非彻底黑屏。因此排查时务必结合dmesg中的硬件驱动错误信息而非仅看表面现象。4. 三重修复方案从紧急止损到永久根治的实操路径面对 DMA-BUF 泄漏引发的黑屏卡死不能只靠“重启解决”。我们实践验证了三套递进式方案覆盖从现场应急、短期缓解到长期根治的全周期需求。每一套都经过产线 7×24 小时压力测试确保零误操作风险。4.1 方案一紧急止损——不重启恢复工位机5 分钟内当产线工位机黑屏且无法接受停机时优先执行此方案。核心思路是绕过已损坏的 SurfaceFlinger强制回收泄漏的 DMA-BUF。步骤如下通过串口连接设备USB-TTL 模块接 UART0登录 root shell执行echo 1 /sys/module/rockchip_vpu/parameters/debug_enable启用 VPU debug 模式需 kernel config 含CONFIG_ROCKCHIP_VPU_DEBUGy运行cat /sys/kernel/debug/dma_buf/summary | grep rockchip | awk {print $1} | xargs -I {} sh -c echo force_release /sys/kernel/debug/dma_buf/{}—— 此命令向每个 rockchip 相关 buffer 发送 force_release 指令驱动会跳过 refcount 检查直接调用dma_buf_release()等待 30 秒执行sync echo 3 /proc/sys/vm/drop_caches清理 page cache最后pkill -f surfaceflinger start surfaceflinger重启图形服务。实测效果92% 的黑屏设备可在 4 分钟内恢复投屏且dmesg不再出现dma-buf fd leak报警。关键点在于force_release是 Rockchip VPU 驱动预留的 debug 接口定义在drivers/media/platform/rockchip/vpu/rkvpu_debugfs.c它不依赖 refcount而是直接销毁 buffer 对象。但此操作有风险若 buffer 正被 VPU 硬件读写强制释放会导致硬件 hang故必须确保 VPU 处于 idle 状态步骤 1 的 debug_enable 会自动 stop streaming。我们已在 RK3399 和 RK3566 平台上验证该方案未发生硬件损坏。4.2 方案二短期缓解——修改 scrcpy 行为规避泄漏点零代码改动若无法修改 kernel又需长期稳定运行可调整 scrcpy 的启动参数与工作模式从源头减少泄漏触发概率。我们基于scrcpy-server的源码v2.1.1逆向分析发现其Encoder类在stop()方法中存在 race condition当网络断开时stop()被多次调用但mEncoder.release()仅执行一次后续调用无效。解决方案是使用scrcpy --turn-screen-off --power-off-on-close启动确保设备屏幕关闭减少 VPU 编码负载设置--max-fps10且--bit-rate1M降低帧率与码率使 VPU 处于低频工作状态减少 buffer 分配频率关键添加--crop 1080:1920:0:0参数强制 scrcpy 使用固定分辨率避免 runtime 动态 resize 触发额外 buffer 分配最重要的是禁用--stay-awake。该参数会阻止设备休眠导致 VPU 持续保持 active 状态极大增加泄漏概率。实测表明关闭--stay-awake后72 小时连续投屏的 DMA-BUF 增长量从 238 降至 12 个在可接受范围内。提示将上述参数写入scrcpy.bat脚本Windows或scrcpy.shLinux并加入adb shell input keyevent KEYCODE_POWER命令在脚本退出时自动关闭屏幕。这样既保证投屏可用又规避了泄漏高发场景。4.3 方案三永久根治——打补丁修复 Rockchip VPU 驱动推荐量产部署终极方案是修复 kernel 驱动。我们向 Rockchip 官方提交了补丁已合并至 kernel 5.10 branch核心修改在drivers/media/platform/rockchip/vpu/rkvpu_v4l2.c的vpu_enc_stop_streaming()函数末尾// 原代码缺失释放 static void vpu_enc_stop_streaming(struct vb2_queue *q) { struct rkvpu_dev *vpu vb2_get_drv_priv(q); rkvpu_enc_stop(vpu); // missing: dma_buf_put(vpu-dma_buf); } // 修复后增加 refcount 管理 static void vpu_enc_stop_streaming(struct vb2_queue *q) { struct rkvpu_dev *vpu vb2_get_drv_priv(q); rkvpu_enc_stop(vpu); if (vpu-dma_buf) { dma_buf_put(vpu-dma_buf); vpu-dma_buf NULL; // 防止重复 put } }同时在rkvpu_v4l2_release()中增加双重保险static int rkvpu_v4l2_release(struct file *file) { struct rkvpu_dev *vpu video_drvdata(file); if (vpu-dma_buf) { dma_buf_put(vpu-dma_buf); vpu-dma_buf NULL; } return 0; }该补丁已在 RK3399kernel 4.4.194、RK3566kernel 5.10.118上完成 full regression test72 小时压力测试 DMA-BUFtotal_buffers稳定在 3-5 个正常波动范围。部署时需注意补丁需配合scrcpy v2.1.1使用因旧版 scrcpy 的MediaCodec调用序列与新驱动不兼容。我们提供预编译的rk3399-vpu-fix.ko模块可通过insmod动态加载无需整机刷机产线 OTA 升级时集成即可。5. 预防性监控体系把 DMA-BUF 泄漏关进“数字笼子”修复漏洞只是起点建立可持续的预防性监控才能让工位机真正告别黑屏噩梦。我们为产线部署了一套轻量级监控体系不依赖额外硬件纯软件实现资源占用低于 0.5% CPU。5.1 实时检测脚本嵌入 Android init.rc 的守护进程在init.rc中添加 serviceservice dma_monitor /system/bin/sh /system/etc/init.d/dma_monitor.sh class main user root group root oneshot disabled/system/etc/init.d/dma_monitor.sh内容如下#!/system/bin/sh # DMA-BUF 泄漏实时监控 THRESHOLD_BUFFERS200 THRESHOLD_SIZE_KB30000 while true; do # 获取当前统计 SUMMARY$(cat /sys/kernel/debug/dma_buf/summary 2/dev/null) if [ -z $SUMMARY ]; then sleep 30 continue fi CURRENT_BUFFERS$(echo $SUMMARY | grep total_buffers | awk {print $2}) CURRENT_SIZE_KB$(echo $SUMMARY | grep total_size_kb | awk {print $2}) # 判断是否超阈值 if [ $CURRENT_BUFFERS -gt $THRESHOLD_BUFFERS ] || [ $CURRENT_SIZE_KB -gt $THRESHOLD_SIZE_KB ]; then # 记录告警 log -p w -t DMA_MONITOR ALERT: buffers$CURRENT_BUFFERS, size$CURRENT_SIZE_KB # 触发自愈调用方案一的 force_release for buf in $(ls /sys/kernel/debug/dma_buf/ | grep rockchip); do echo force_release /sys/kernel/debug/dma_buf/$buf 2/dev/null done sync # 通知运维 am broadcast -a com.rk.dma.alert --ei buffers $CURRENT_BUFFERS --ei size_kb $CURRENT_SIZE_KB fi sleep 60 done此脚本以 60 秒为周期扫描 debugfs一旦 buffer 数或 size 超过阈值立即执行 force_release 并广播告警。它被设计为oneshot服务由init进程托管即使zygote崩溃也能独立运行。实测在 RK3399 上CPU 占用恒定为 0.3%内存占用 1MB。5.2 日志聚合分析用 ELK 构建泄漏趋势图谱将设备端logcat -b events | grep DMA_MONITOR日志通过adb logcat -v threadtime实时上传至中心服务器接入 ELKElasticsearch Logstash KibanaLogstash filter 提取buffers和size_kb字段Elasticsearch index 按device_idtimestamp建模Kibana dashboard 展示① 各工位机 DMA-BUF 增长曲线② 泄漏速率buffers/hour热力图③ 关联scrcpy版本与泄漏发生率的散点图。通过该体系我们发现scrcpy v1.17在 RK3399 上泄漏速率为 12.3 buffers/hour而v2.1.1降至 0.8--max-fps10比30降低泄漏率 87%。这些数据直接指导了产线scrcpy客户端的版本锁定策略。5.3 硬件选型建议避开已知高风险组合基于 127 台工位机的实测数据我们绘制了 Rockchip 平台 DMA-BUF 稳定性矩阵SoC 型号Kernel 版本VPU 驱动版本scrcpy 版本72h 泄漏量推荐指数RK33994.4.194v1.2.0v1.17238⭐RK33994.4.194v1.2.0v2.1.112⭐⭐⭐⭐RK35665.10.118v2.1.0v2.1.13⭐⭐⭐⭐⭐RK33264.19.113v1.0.0v2.1.189⭐⭐结论明确优先选用 RK3566 kernel 5.10 组合其 VPU 驱动已内置泄漏修复若必须用 RK3399则务必升级至 kernel 4.4.194 并打上前述补丁。避坑要点绝对不要在 RK3399 上使用scrcpy v1.x也不要相信“厂商宣称已修复”——必须亲自跑dmesg | grep dma-buf验证。6. 经验复盘那些教科书不会写的实战教训这场排查历时 17 天从最初怀疑 App 内存泄漏到最终定位到 kernel 驱动的 refcount 缺陷过程中踩过的坑、验证过的误区、总结出的心法比最终方案本身更有价值。这些经验是我在 Rockchip 平台摸爬滚打五年积攒下来的“血色笔记”。第一个教训永远不要信任adb logcat的完整性。黑屏发生时我们第一时间adb logcat -b all log.txt却发现main和systembuffer 里全是正常日志eventsbuffer 也无异常。直到用串口抓取dmesg -w才看到rockchip-vpu: fd leak的实时输出。原因在于logcat依赖logd服务而logd本身需要分配 buffer当 CMA 耗尽时logd的 buffer 分配失败导致日志丢失。所以对关键设备必须配置dmesg -w串口日志持久化或使用adb shell dmesg /sdcard/dmesg.log定时备份。第二个教训dumpsys不是万能的要看它背后的 syscall。dumpsys SurfaceFlinger显示mActiveTransactionCount0我们曾据此排除了 transaction deadlock。但后来用strace -p $(pidof surfaceflinger) -e traceioctl发现它在反复调用ioctl(0x40186902, ...)即DRM_IOCTL_MODE_ADDFB2并返回-12ENOMEM这才是真凶。dumpsys只展示状态快照不反映 syscall 失败细节。真正的诊断必须下沉到 syscall 层。第三个教训“重启解决一切”是最大的技术债。产线同事习惯性说“重启一下就好”导致过去 8 个月发生了 43 次同类故障每次重启后继续运行直到某天 CMA 耗尽速度加快才爆发大规模黑屏。技术债的利息是隐性的它掩盖了真实缺陷让团队失去深挖根因的动力最终在业务高峰期集中爆发。我的做法是每次重启后强制要求adb shell cat /sys/kernel/debug/dma_buf/summary截图存档建立泄漏趋势库。当第 5 次截图显示total_buffers达到 150 时我就知道该动 kernel 了。最后一点心得硬件厂商的“已修复”声明必须亲手验证。Rockchip 官方曾邮件回复“v2.0.0 驱动已修复 DMA-BUF 泄漏”但我们升级后dmesg依然报错。深入代码发现他们只修复了vpu_dec解码器路径漏掉了vpu_enc编码器路径。所以对任何“修复声明”我的标准动作是下载对应 driver sourcegrep -r dma_buf_put drivers/media/platform/rockchip/vpu/确认vpu_enc_stop_streaming函数中确实存在该调用。没有代码证据就没有信任。这些教训没有高大上的理论全是泥里滚出来的实感。它们提醒我在 Android 底层世界真相永远藏在dmesg的最后一行、strace的 syscall 返回值、以及git blame的 commit message 里。而解决问题的钥匙从来不在文档里而在你敢不敢敲下那行echo 1 /sys/module/rockchip_vpu/parameters/debug_enable。