Rockchip VPU DMA-BUF泄漏导致scrcpy黑屏根因分析
1. 项目概述这不是App崩溃是硬件驱动层的“慢性失血”你有没有遇到过这种场景一台跑着Android系统的工位机日常用scrcpy做远程调试或投屏突然某天开始频繁黑屏、卡死重启后能撑一两个小时接着又挂——但App日志里干干净净ANR没触发OOM没报警SurfaceFlinger没报错连logcat里都找不到像样的异常堆栈我上周就在产线现场撞上了这个鬼问题。三台Rockchip RK3399平台的定制工控终端全部在接入scrcpy后2~4小时出现无响应触摸失灵、HDMI输出黑屏、adb shell失联但串口console依然能ping通系统进程还在跑只是图形子系统彻底僵死。最迷惑的是换掉所有上层App、清空/data/app、甚至刷回出厂固件问题照旧而只要不启动scrcpy设备就能连续稳定运行72小时以上。这显然不是应用层的问题——它藏得更深深到Linux内核的DMA-BUF内存管理子系统里。核心关键词“scrcpy”“Rockchip”“DMA-BUF”“编码器”在这里不是并列关系而是因果链scrcpy作为用户态投屏工具通过adb调用Android的screenrecord服务获取H.264视频流screenrecord依赖MediaCodec调用底层硬件编码器Rockchip平台的硬件编码器VPU在实现中大量使用DMA-BUF进行零拷贝内存共享而DMA-BUF的引用计数管理一旦出现泄漏就会导致物理内存页长期被锁住无法释放最终耗尽CMAContiguous Memory Allocator区域触发GPU驱动拒绝分配新buffer进而让SurfaceFlinger停摆、HWCHardware Composer失效、Display Engine无帧可刷——黑屏卡死只是表象本质是内存资源被无声无息地“抽干”。这不是理论推演是我们用kmemleakdump_stackrockchip-vpu-debugfs实锤的现场证据。这篇文章不讲怎么装scrcpy也不教Android Studio配中文我们要拆解的是当一个看似轻量的投屏工具撞上特定SoC的驱动实现缺陷时如何从黑屏现象一路逆向追踪到DMA-BUF refcount漏减的汇编指令级根因。适合嵌入式Android开发者、驱动工程师、以及那些被“莫名卡死”折磨过三次以上的产线运维同事——你不需要会写驱动但得知道该看哪几行dmesg、该抓哪几个debugfs节点、该用什么命令验证泄漏是否真实存在。2. 根因定位思路为什么先排除App再锁定DMA-BUF2.1 排除上层干扰的四步法面对黑屏卡死第一反应往往是杀App、清缓存、重装APK。但我们这次直接跳过这一步原因很实在产线设备只跑一个定制Launcher和数据采集Service连WebView都没集成且问题复现与App更新完全无关——上周五刚部署的版本周一就出问题而中间没有任何OTA推送。更关键的是我们做了个极简复现设备冷启动不启动任何业务Appadb shell screenrecord /dev/null --time-limit 1手动触发一次硬编立即执行scrcpy --bit-rate2M --max-fps15观察2小时黑屏必现。这个操作排除了所有App逻辑干扰把问题收敛到“scrcpy screenrecord Rockchip VPU”这个最小三角。接下来要回答是scrcpy本身有bug还是Android framework层MediaCodec封装有问题抑或是Rockchip驱动有缺陷我们的排查路径非常明确从用户态往内核态逐层下沉用可观测性指标代替主观猜测。提示不要相信logcat里的“一切正常”。在DMA-BUF泄漏场景下logcat往往安静得可怕因为错误发生在内存管理子系统而非进程调度或文件IO路径。真正有用的信号在/proc/kmsg、dmesg环形缓冲区、以及/sys/kernel/debug下的各驱动debugfs节点。2.2 为什么DMA-BUF是首要怀疑对象DMA-BUF是Linux内核为解决跨设备内存共享而设计的通用框架。在Android图形栈中它的典型流转路径是App申请Surface → Gralloc分配ION buffer → 通过DMA-BUF fd传递给HWC → HWC传给GPU driver → GPU渲染后通过DMA-BUF fd交给VPU编码器 → 编码器输出bitstream via DMA-BUF fd回传给MediaCodec。整个过程要求每个环节严格遵循“get/put”配对原则拿到buffer引用就inc refcount用完必须dec refcount。一旦某个环节忘记put比如VPU驱动在异常中断处理中跳过了cleanuprefcount就永远卡在1buffer无法被回收。Rockchip VPU驱动drivers/media/platform/rockchip/vpu正是在这个环节埋了雷——其vpu_enc_stop_streaming()函数在部分错误路径下未调用dma_buf_put()导致每次scrcpy断连重连都有一块CMA内存永久泄漏。我们验证这一点的方法很粗暴但有效在设备上执行echo 1 /sys/kernel/debug/kmemleak启用内存泄漏检测运行scrcpy 30分钟echo scan /sys/kernel/debug/kmemleakcat /sys/kernel/debug/kmemleak | grep -A 10 vpu—— 果然扫出数十个dma_buf_export对象backtrace直指rockchip_vpu_enc_stop_streaming。这比看源码更快定位问题模块因为kmemleak能直接告诉你“哪些内核对象没被释放”而不是“哪里可能没释放”。2.3 Rockchip编码器与scrcpy的耦合点在哪scrcpy本身不直接调用VPU它走的是标准Android MediaCodec路径scrcpy (user space) → adb forward → scrcpy-server.apk (on device) → MediaCodec.createEncoderByType(video/avc) → frameworks/av/media/libstagefright/codec2/hal/Codec2Client.cpp → vendor/rockchip/hardware/omx/1.0/.../RockchipOMXPlugin.cpp → kernel driver: drivers/media/platform/rockchip/vpu/vpu_enc.c关键耦合点在于scrcpy的默认参数它强制启用--tunnel-mode隧道模式这意味着编码器输出不走传统bitstream buffer queue而是通过DMA-BUF fd直接映射到scrcpy-server的用户态内存。这个模式极大降低延迟但对DMA-BUF生命周期管理要求更高——VPU驱动必须确保每次stop_streaming都完成buffer cleanup而Rockchip旧版驱动v1.3.0之前恰恰在此处留了缺口。我们对比过Amlogic和MTK平台它们的VPU驱动在同样隧道模式下无此问题说明这是Rockchip特定实现缺陷非Android通用问题。2.4 为什么不是Android Studio或ADB的问题热搜词里一堆“Android Studio怎么设置中文”“adb shell sh xxx”看似相关实则干扰项。Android Studio是开发工具其行为不影响已烧录固件的运行时稳定性ADB是调试桥它只负责转发命令真正的编码工作在设备端完成。我们做过对照实验关闭ADB daemon (adb kill-server)仅用串口console执行screenrecordscrcpy-server问题依旧换用adb connect无线调试问题复现频率不变甚至拔掉USB线用scrcpy --tcpip走WiFi黑屏时间从2小时缩短到1.5小时——说明网络带宽影响泄漏速度但不改变泄漏本质。这证实问题锚定在设备端内核驱动与开发环境完全无关。那些“android studio下载”“sdk官网”的搜索词只是反映了大量新手把开发环境问题和运行时问题混为一谈而我们要做的是帮老手快速剥离噪音。3. 核心细节解析DMA-BUF泄漏的技术原理与Rockchip驱动缺陷3.1 DMA-BUF refcount机制详解不是“用了就要还”而是“拿了就必须还”DMA-BUF的核心是struct dma_buf结构体其refcount字段是struct kref类型本质是一个原子整数。每次调用dma_buf_get(fd)获取bufferrefcount加1调用dma_buf_put(buf)释放refcount减1。只有当refcount减到0时内核才触发dma_buf_release()真正释放物理内存页。这个机制看似简单但在中断上下文、错误处理路径、多线程竞争场景下极易出错。Rockchip VPU驱动的问题代码位于drivers/media/platform/rockchip/vpu/vpu_enc.c的rockchip_vpu_enc_stop_streaming()函数v1.2.8版本static void rockchip_vpu_enc_stop_streaming(struct vb2_queue *q) { struct rockchip_vpu_dev *vpu q-drv_priv; struct rockchip_vpu_ctx *ctx vpu-ctx; // 正常路径清理所有buffer vpu_enc_cleanup(ctx); // BUG此处缺少对DMA-BUF的put操作 // 正确应有dma_buf_put(ctx-enc_buf); // 但实际代码直接return; }当scrcpy因网络抖动断连VPU驱动收到VB2_BUF_STATE_ERROR状态进入stop_streaming流程。正常情况下vpu_enc_cleanup()会释放所有待编码buffer但enc_buf编码器输入buffer的DMA-BUF引用是在start_streaming时通过dma_buf_get()获取的按对称原则必须在stop_streaming中put。而旧版驱动把这个put放在了vpu_enc_cleanup()里但cleanup函数在错误路径下被跳过——导致refcount永远不减。注意这个bug不会立即导致OOM因为CMA区域通常有64MB~128MB每次泄漏约128KBH.264 I帧buffer大小需要500~1000次断连才会耗尽。这就是为什么问题“偶发”且“渐进式恶化”。3.2 Rockchip VPU编码器的内存模型CMA vs ION为什么泄漏必死Rockchip RK3399平台采用双内存池设计CMAContiguous Memory Allocator专供VPU、GPU等硬件加速器要求物理地址连续大小固定dts中配置linux,cma-size 0x4000000即64MBION供CPU/GPU通用buffer支持碎片化分配容量更大但不保证连续。VPU编码器强制使用CMA因为H.264硬件编码需要DMA引擎直接访问连续内存。当DMA-BUF泄漏发生时泄漏的是CMA内存页而CMA区域无法像ION那样通过内存压缩或swap缓解——一旦耗尽dma_alloc_coherent()直接返回NULLVPU驱动probe失败后续所有编码请求均被拒绝。此时SurfaceFlinger尝试合成新帧HWC调用VPU准备output buffer却拿不到CMA页整个display pipeline卡死表现为黑屏触摸无响应。我们用cat /sys/kernel/debug/dma_buf/summary确认了这一点total 128 buffers, 64MB total size rockchip-vpu: 112 buffers, 56MB allocated ← 占用率87.5% ion: 16 buffers, 8MB allocated而正常设备该值应5%。这个数字每分钟增长0.3%2小时后突破95%触发内核OOM killer对surfaceflinger的误杀——这才是logcat里突然出现system_server killed的真相。3.3 scrcpy隧道模式如何放大泄漏效应scrcpy的--tunnel-mode参数让问题雪上加霜。普通模式下MediaCodec通过dequeueOutputBuffer()获取编码后的bitstreambuffer由framework管理生命周期清晰隧道模式则绕过frameworkVPU驱动直接将编码完成的DMA-BUF fd写入scrcpy-server的socket。这带来两个风险fd传递链更长VPU → MediaCodec HAL → scrcpy-server → libusb → PC端任一环节未close fdrefcount就不减重连频率更高隧道模式对网络延迟敏感WiFi环境下scrcpy平均每3分钟重连一次每次重连都触发一次start/stop_streaming也就触发一次泄漏。我们抓包发现scrcpy-server在断连时会发送STOP_STREAMING命令给VPU但驱动未正确响应。解决方案不是禁用隧道模式那会牺牲30%性能而是修复驱动中的stop_streaming逻辑。3.4 如何用debugfs精准定位泄漏源头除了kmemleakRockchip提供了更直接的诊断接口/sys/kernel/debug/rockchip-vpu/enc_stats显示当前编码器实例数、buffer分配统计/sys/kernel/debug/dma_buf/rockchip-vpu列出所有VPU相关的DMA-BUF含size、refcount、importer/sys/kernel/debug/cma/cma-0显示CMA剩余内存、最大连续块。关键命令组合# 实时监控CMA水位 watch -n 1 cat /sys/kernel/debug/cma/cma-0 | grep -E (used|total) # 查看VPU DMA-BUF详情refcount1即可疑 cat /sys/kernel/debug/dma_buf/rockchip-vpu | awk /refcount/ {if($31) print} # 统计泄漏速率 for i in {1..10}; do echo $(date %s),$(cat /sys/kernel/debug/cma/cma-0 | grep used | awk {print $2}); sleep 60; done cma_log.csv我们用这个脚本跑了2小时得到线性下降曲线斜率对应每次泄漏的128KB与理论值完全吻合——这比任何代码审查都更有说服力。4. 实操过程从现象到补丁的完整复现与修复4.1 复现环境搭建三台设备一个命令30分钟见真章要复现这个问题你不需要RK3399开发板一台量产工控机足矣。我们用的设备是SoCRockchip RK3399v1.2.8 kernelAndroid9.0Pievendor image基于Rockchip SDK v2.2.0scrcpyv1.25server apk built from master复现步骤严格按顺序清空设备状态adb shell su -c echo 3 /proc/sys/vm/drop_caches重置CMAadb shell su -c modprobe -r rockchip_vpu modprobe rockchip_vpu启动scrcpyscrcpy --tunnel-mode --bit-rate2M --max-fps15 --crop1200:800:0:0模拟网络抖动在PC端执行ping -i 5 192.168.1.100 | head -n 60 /dev/null 制造间歇性丢包观察adb logcat -b events | grep -i display\|hwc\|vpu等待HWC: failed to commit出现。关键观察点第一次HWC: failed to commit出现时cat /sys/kernel/debug/cma/cma-0显示used从10MB飙升至55MB此时cat /sys/kernel/debug/dma_buf/summary中rockchip-vpu条目数激增dmesg | tail -20出现rockchip-vpu enc: no memory for output buffer警告。这个复现过程控制在30分钟内比看源码快得多。记住能复现才能验证修复是否有效。4.2 补丁编写三行代码两个补丁彻底根治修复方案分两层补丁1驱动层修复根本解修改drivers/media/platform/rockchip/vpu/vpu_enc.c--- a/drivers/media/platform/rockchip/vpu/vpu_enc.c b/drivers/media/platform/rockchip/vpu/vpu_enc.c -1234,6 1234,9 static void rockchip_vpu_enc_stop_streaming(struct vb2_queue *q) struct rockchip_vpu_dev *vpu q-drv_priv; struct rockchip_vpu_ctx *ctx vpu-ctx; if (ctx-enc_buf) dma_buf_put(ctx-enc_buf); ctx-enc_buf NULL; vpu_enc_cleanup(ctx); }这个补丁确保无论cleanup是否执行enc_buf的refcount都被正确减少。我们测试了1000次断连CMA水位稳定在5%以下。补丁2scrcpy-server层加固防御性在scrcpy/app/src/main/jni/videocapture/encoder.c中增加DMA-BUF fd关闭检查// 在encoder_destroy()中添加 if (encoder-dma_buf_fd 0) { close(encoder-dma_buf_fd); encoder-dma_buf_fd -1; }虽然驱动修复后此步非必需但它构成双重保险防止未来类似bug。4.3 补丁编译与烧录不用重刷整个固件Rockchip驱动是ko模块无需编译整个kernel# 在kernel源码根目录执行 make Mdrivers/media/platform/rockchip/vpu modules # 生成rockchip-vpu.ko adb push drivers/media/platform/rockchip/vpu/rockchip-vpu.ko /data/local/tmp/ adb shell su -c insmod /data/local/tmp/rockchip-vpu.ko验证lsmod | grep vpu显示新模块已加载dmesg | tail无error。实操心得不要用modprobe rockchip-vpu因为vendor分区里的旧模块会优先加载。必须用insmod强制加载新ko并确保/system/lib/modules/rockchip-vpu.ko被覆盖需remount rw。4.4 验证修复效果量化指标比“不卡了”更可信修复后我们用三组数据验证指标修复前修复后改善CMA used (2h)62MB → OOM8.2MB ±0.3MB90%↓scrcpy重连成功率42%断连后需重启99.8%自动恢复2.3x↑黑屏平均间隔1.8h168h7天∞↑特别注意不要只测“一次不卡”要测“持续72小时”。我们把修复后设备放在温箱里45℃模拟产线高温环境72小时无异常——这才是工业级验证。5. 常见问题与排查技巧实录那些踩过的坑和省下的三天5.1 问题速查表看到这些现象直接按表索骥现象可能原因快速验证命令解决方案scrcpy连接后10分钟内黑屏logcat无错误DMA-BUF泄漏初期cat /sys/kernel/debug/cma/cma-0 | grep used检查VPU驱动版本应用补丁1adb shell screenrecord单独运行也卡死VPU驱动全局缺陷dmesg | grep -i vpu|cma升级kernel或打补丁黑屏后串口可ping通但adb devices消失USB gadget驱动异常ls /sys/class/udc/是否为空重启USB controllerecho 0 /sys/bus/platform/drivers/dwc3-rockchip/unbindscrcpy报错could not open audio但视频正常Audio HAL配置错误adb shell dumpsys media.audio_flinger与VPU无关检查audio_policy.confCMA used稳定在30MB但设备仍卡顿其他驱动泄漏如GPUcat /sys/kernel/debug/dma_buf/summary | grep -v rockchip用perf分析GPU driver refcount5.2 避坑指南三个血泪教训教训1别信“厂商说没问题”Rockchip FAE最初回复“这是scrcpy兼容性问题建议降级”。我们坚持用kmemleak抓到泄漏点后他们才承认驱动缺陷。所有硬件厂商的“没问题”都要用dmesg和debugfs验证。教训2CMA大小不能盲目调大有同事提议把CMA从64MB扩到256MB来“缓解”。这是饮鸩止渴——泄漏仍在发生只是爆发时间延后且挤占了RAM给Android Runtime的空间导致GC更频繁。治标不如治本补丁比调参可靠。教训3logcat过滤要带-b all默认logcat只显示main和system buffer而VPU错误在events和radiobuffer。正确命令adb logcat -b events -b radio -b main -b system \| grep -i vpu\|cma\|hwc。漏掉任何一个buffer都可能错过关键线索。5.3 工业现场应急方案没源码怎么办如果客户设备是封闭固件无法修改驱动我们提供临时缓解方案脚本化内存回收adb shell su -c echo 1 /proc/sys/vm/compact_memory每30分钟执行一次强制内存整理对CMA效果有限但聊胜于无限制scrcpy重连在PC端用scrcpy --restart-on-connect-failure0禁用自动重连改为手动触发降级scrcpyv1.17之前版本未启用隧道模式泄漏速率降低70%。这些是权宜之计但能争取到打补丁的时间窗口。5.4 扩展思考其他SoC平台是否安全我们横向测试了Amlogic S905X3驱动中vpu_stop_streaming有完整dma_buf_put安全MTK MT8173使用ION而非CMA泄漏影响较小Qualcomm SM8150Adreno GPU驱动DMA-BUF管理严谨无此类问题。结论Rockchip RK3399/RK3328/RK3288系列kernel 4.4~4.19均存在此缺陷RK3566/RK3588已修复。如果你用的是这些芯片务必检查驱动版本。6. 经验总结从黑屏到补丁我学到的三件事这个问题折腾了我们团队11天从怀疑App到锁定内核从抓dmesg到读汇编最后三行代码解决问题。过程中最深刻的体会有三点第一“黑屏”不是故障终点而是故障入口。它像一个症状背后可能是电源管理、时钟树、内存控制器、GPU驱动、VPU驱动中任意一环的失效。必须用可观测性工具debugfs/kmemleak/perf代替经验猜测把模糊的“卡死”转化为精确的“CMA used63MB”。第二scrcpy不是背锅侠而是压力探针。它的高频率重连、隧道模式、低延迟要求把Rockchip驱动里潜伏多年的refcount bug暴露得淋漓尽致。没有scrcpy这个bug可能再潜伏两年——因为它只在特定负载路径下触发。第三开源的价值不在代码本身而在可追溯性。如果这是个闭源驱动我们只能靠厂商patch而Rockchip公开的kernel源码让我们能精准定位到vpu_enc.c第1237行。这也是为什么我坚持所有嵌入式项目必须用主线kernel或可审计的vendor分支。最后分享一个小技巧下次遇到类似问题先执行adb shell su -c cat /sys/kernel/debug/dma_buf/summary | head -20如果看到某个driver占用buffer数远超其他模块比如rockchip-vpu占80%恭喜你已经找到了90%的答案。剩下的只是补丁和验证的事。