2026/9/12 15:02:03

Rockchip VPU DMA-BUF内存泄漏导致黑屏故障排查与修复

Rockchip VPU DMA-BUF内存泄漏导致黑屏故障排查与修复 1. 项目概述这不是App崩溃是硬件驱动层的“慢性失血”你有没有遇到过这种场景一台跑着定制Android系统的工位机日常只做视频采集画面投屏用的是scrcpy远程控制突然某天开始频繁黑屏、卡死重启后能撑两小时又挂——但App日志干净得像没出过事adb logcat里找不到ANRSurfaceFlinger没报错ActivityManager也无异常堆栈。我上周就在产线现场撞上这事儿三台同型号Rockchip RK3399设备全部在连续运行12~18小时后出现不可恢复的黑屏触摸无响应ADB断连连强制长按电源键都无效只能硬断电重启。一开始所有人都盯着App代码是不是内存泄漏是不是Surface没释放是不是Handler消息堆积我们花了整整两天重走所有UI生命周期、检查TextureView销毁逻辑、抓Heap Dump比对GC频率——结果全是正常值。直到第三天凌晨我在设备彻底卡死前0.3秒抓到一段被忽略的dmesg输出“dma-buf: dma_buf_release: 00000000abcd1234 still has 157 references”而同一时间/sys/kernel/debug/dma_buf/目录下赫然躺着237个未释放的buffer对象每个size都在4MB~16MB区间浮动。那一刻我才意识到问题根本不在Java层甚至不在HAL层而在Linux内核的DMA-BUF子系统与Rockchip视频编码器驱动的交互缝隙里。这个标题说的不是“又一个App Bug”而是一次典型的跨栈故障Cross-Stack Failure用户态工具scrcpy触发了内核驱动Rockchip VPU中一个长期存在的DMA-BUF引用计数管理缺陷导致物理内存持续泄漏最终耗尽CMAContiguous Memory Allocator区域使GPU无法分配新帧缓冲Display Engine停摆整机进入不可中断睡眠D-state。它不报OOM不触发Watchdog不写panic log就像血管里悄悄凝结的微小血栓直到某次关键帧编码时突然堵死整条通路。如果你正在用RK3399/RK3566/RK3588做工业视觉终端、车载DVR或边缘AI盒子且依赖scrcpy做远程调试或画面回传这篇文章就是为你写的——它不教你如何写App而是告诉你当屏幕变黑时该去哪个文件系统路径敲命令该看哪几行dmesg该改哪一行驱动源码补丁。2. 故障根因深度拆解DMA-BUF不是“缓冲区”而是内存所有权的契约2.1 DMA-BUF的本质跨设备内存共享的“产权证”很多人把DMA-BUF简单理解为“一块可以被GPU和CPU同时访问的内存”这是危险的简化。DMA-BUF真正的核心是Linux内核为解决异构设备间零拷贝内存共享而设计的一套内存所有权契约机制。它不像malloc分配的普通内存而像一份带法律效力的房产证dma_buf结构体 房产证原件含唯一handle、refcount、ops函数表dma_buf_attachment 租赁合同绑定到具体设备如VPU或GPUsg_table 实际地块坐标物理页帧号PFN数组dma_buf_exporter/dma_buf_importer 房产中介负责在不同驱动间传递产权当scrcpy通过MediaCodec请求H.264编码时流程是这样的scrcpy调用MediaCodec.createEncoder()→ HAL层调用Rockchip VPU驱动的rk_vpu_enc_create()驱动向CMA申请连续物理内存如16MB用于YUV420帧缓冲→dma_alloc_coherent()驱动将这块内存封装成dma_buf对象 →dma_buf_export()生成handle这个handle被传递给scrcpy进程 → scrcpy通过dma_buf_fd_get()拿到fdscrcpy再把这个fd传给libavcodecFFmpeg做软解码或格式转换关键陷阱在这里Rockchip VPU驱动在rk_vpu_enc_stop()中只调用了dma_buf_put()减少引用计数却漏掉了对attachment的显式detach操作。而标准DMA-BUF规范要求只要attachment存在refcount就不能归零buffer就永远不会被dma_buf_release()真正释放。提示你可以用cat /sys/kernel/debug/dma_buf/summary实时查看当前所有dma_buf对象的refcount。正常情况应稳定在1~3之间exporter importer attachment各占1。一旦看到某个buffer refcount持续10且缓慢上涨基本就是泄漏源头。2.2 Rockchip编码器驱动的“引用计数断点”我们反编译了RK3399 SDK v2.1.0中的rockchip_vpu_enc.c定位到问题函数// drivers/media/platform/rockchip/vpu/rk_vpu_enc.c 行 1247 static int rk_vpu_enc_stop(struct rk_vpu_dev *vpu, struct rk_vpu_ctx *ctx) { // ... 省略编码器停止逻辑 ... if (ctx-dma_buf) { dma_buf_put(ctx-dma_buf); // ❌ 只减refcount未detach ctx-dma_buf NULL; } return 0; }对比上游Linux主线驱动如drivers/media/platform/amlogic/aml-vcodec/正确写法应为if (ctx-dma_buf ctx-attachment) { dma_buf_detach(ctx-dma_buf, ctx-attachment); // ✅ 先detach dma_buf_put(ctx-dma_buf); // ✅ 再put ctx-dma_buf NULL; ctx-attachment NULL; }为什么Rockchip官方驱动会漏掉这一步因为他们的测试场景几乎全是“单次编码-立即释放”模式如拍照APPattachment生命周期与encoder完全同步refcount自然归零。但在scrcpy这种长连接、多帧连续编码场景下scrcpy会复用同一个MediaCodec实例持续送帧每次dequeueInputBuffer()都可能触发新的dma_buf分配而旧buffer的attachment因未detach始终挂着refcount卡在2exporter attachment永远无法释放。注意这个bug在RK3399 SDK v2.1.0 ~ v2.3.2中普遍存在在RK3566/RK3588的早期BSP包中同样存在。它不是偶发bug而是设计缺陷——驱动开发者把“attachment生命周期由用户态保证”当成了铁律却忽略了scrcpy这类工具会跨多次encode调用复用同一context。2.3 scrcpy的“推波助澜”非标准的MediaCodec使用模式scrcpy本身没有错但它用MediaCodec的方式恰好踩中了Rockchip驱动的软肋。标准Android App调用MediaCodec的流程是create() → configure() → start() → [loop: queueInputBuffer → dequeueOutputBuffer] → stop() → release()而scrcpy为了降低延迟采用了预分配循环复用策略// scrcpy/src/main/java/com/genymobile/scrcpy/VideoEncoder.java private void prepareEncoder() { // 一次性创建并configure之后永不release mediaCodec MediaCodec.createEncoderByType(video/avc); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mediaCodec.start(); } private void encodeFrame(ByteBuffer input, long presentationTimeUs) { // 每帧都queueInputBuffer但绝不调用stop()/release() int inputBufferIndex mediaCodec.dequeueInputBuffer(10000); if (inputBufferIndex 0) { ByteBuffer buffer mediaCodec.getInputBuffer(inputBufferIndex); buffer.put(input); mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.limit(), presentationTimeUs, 0); } }这意味着MediaCodec实例存活整个scrcpy进程生命周期数小时每次queueInputBuffer()都可能触发VPU驱动分配新dma_buf而stop()只在scrcpy退出时才调用一次中间所有编码帧产生的dma_buf attachment全靠rk_vpu_enc_stop()清理——但这个函数有缺陷于是形成恶性循环第1小时分配100个buffer → 释放95个5个因refcount未归零残留第2小时再分配100个 → 释放95个 前5个仍残留 → 总残留10个第12小时残留buffer达120个 × 平均8MB 960MB物理内存被锁死→ CMA pool耗尽 → GPU无法分配新frame buffer → Display Engine hang → 黑屏3. 实操排查全流程从现象到定位三步锁定DMA-BUF泄漏3.1 第一步快速现象确认5分钟内完成不要一上来就抓logcat黑屏卡死时adb shell往往已失效。必须用硬件级诊断通道方法A串口Console直连推荐准备USB转TTL模块CH340/CP2102接RK板UART0TX/RX/GND电脑端用PuTTY/minicom波特率1500000RK3399默认设备卡死后观察串口是否有输出若仍有[ 1234.567890] rk_vpu_enc: encoder stopped类日志 → 驱动仍在工作问题在显示链路若串口完全静默无任何字符输出 → CPU已hang需查供电或DDR初始化若持续刷[ 1234.567890] dma-buf: dma_buf_release: 00000000abcd1234 still has X references→直接命中DMA-BUF泄漏方法Badb reboot recovery后紧急抓取适用于还能adb的初期阶段# 卡死前执行建议写成脚本每5分钟自动运行 echo DMA-BUF STATUS /data/local/tmp/dma_debug.log cat /sys/kernel/debug/dma_buf/summary /data/local/tmp/dma_debug.log echo MEMORY INFO /data/local/tmp/dma_debug.log cat /proc/meminfo | grep -E (MemTotal|MemFree|CmaTotal|CmaFree) /data/local/tmp/dma_debug.log echo DMESS LATEST /data/local/tmp/dma_debug.log dmesg -T | tail -n 50 /data/local/tmp/dma_debug.log重点看三组数据CmaFree是否持续下降正常应50MB泄漏时5MB/sys/kernel/debug/dma_buf/summary中total行数字是否200安全阈值dmesg末尾是否有still has X references警告X5即危险3.2 第二步精准泄漏源定位30分钟当确认是DMA-BUF泄漏后要找到是哪个驱动在泄漏。Rockchip平台有多个DMA-BUF使用者VPU、GPUMali、ISP、VOPDisplay。用以下命令逐个排查# 1. 查看所有dma_buf持有者按driver name分组 cat /sys/kernel/debug/dma_buf/summary | awk {print $3} | sort | uniq -c | sort -nr # 典型输出 # 127 rk_vpu_enc # 45 mali # 12 rkisp # 3 vop # 2. 深入分析rk_vpu_enc相关的buffer详情 for f in /sys/kernel/debug/dma_buf/*; do if grep -q rk_vpu_enc $f/name 2/dev/null; then echo $(basename $f) cat $f/name $f/size $f/attachments 2/dev/null echo fi done /data/local/tmp/vpu_dma_detail.log你会看到类似 00000000abcd1234 rk_vpu_enc 16777216 /dev/vpu:1其中/dev/vpu:1表示该buffer被VPU设备的第1个实例占用。attachments字段若显示/dev/vpu:1而非空则证明attachment未detach——这就是泄漏铁证。3.3 第三步驱动级验证与补丁测试2小时验证补丁有效性获取Rockchip Linux SDK源码如rockchip-linux-sdk-v2.1.0.tar.gz定位drivers/media/platform/rockchip/vpu/rk_vpu_enc.c在rk_vpu_enc_stop()函数末尾添加detach逻辑// 补丁位置rk_vpu_enc.c 行 1247 后 if (ctx-dma_buf ctx-attachment) { dma_buf_detach(ctx-dma_buf, ctx-attachment); dma_buf_put(ctx-dma_buf); ctx-dma_buf NULL; ctx-attachment NULL; }编译并烧录测试# 在SDK根目录执行 make ARCHarm64 rockchip_defconfig make ARCHarm64 -j$(nproc) Image dtbs modules # 替换boot.img中的Image和modules adb push drivers/media/platform/rockchip/vpu/rk_vpu.ko /data/local/tmp/ adb shell insmod /data/local/tmp/rk_vpu.ko验证方法运行scrcpy持续编码建议用scrcpy --bit-rate 8M --max-fps 30模拟高负载每30分钟执行一次cat /sys/kernel/debug/dma_buf/summary | head -n 5 # 观察total数值是否稳定在50~80不再上涨运行12小时后CmaFree应保持100MB设备无黑屏实操心得我们实测发现即使打了补丁首次启动scrcpy时仍有少量buffer残留约3~5个这是因为scrcpy初始化阶段的buffer分配逻辑。但后续运行中refcount严格稳定证明泄漏已被阻断。这属于可接受范围不必追求绝对零残留。4. 根治方案与工程化落地不止打补丁更要建防线4.1 驱动层修复Rockchip BSP补丁标准化单纯打补丁不够必须让修复融入BSP发布流程。我们为Rockchip客户制定了三步走方案Step 1短期热修复立即生效编译修复后的rk_vpu.ko制作成vpu-fix-202406.ko在设备init.rc中添加on property:sys.boot_completed1 insmod /vendor/lib/modules/vpu-fix-202406.ko配合system/bin/vpu_fix_init.sh检测并替换原驱动Step 2中期BSP升级3个月内向Rockchip提交PR我们已提交至https://github.com/Rockchip-linux/kernel/pull/1234要求在v2.4.0 SDK中合并补丁包含rk_vpu_enc_stop()修复新增rk_vpu_enc_cleanup()函数确保进程退出时强制释放所有buffer在rk_vpu_enc_open()中添加refcount监控告警当refcount50时打印WARNStep 3长期架构优化下一代芯片推动Rockchip在RK3588平台采用DMA-BUF fence机制每个buffer关联一个sync_fence由VPU硬件自动标记完成状态用户态通过sync_wait()阻塞等待避免手动管理attachment生命周期此方案已在高通SM8150平台验证泄漏率降为04.2 scrcpy侧适配主动规避泄漏风险既然驱动修复需要周期scrcpy作为使用者也应主动防御。我们在fork的scrcpy仓库中实现了两项关键改进① MediaCodec实例生命周期管理新增--vpu-restart-interval minutes参数默认180分钟到期后自动mediaCodec.stop()mediaCodec.release()new MediaCodec()避免单实例长期运行将泄漏窗口从“无限”压缩到“3小时”② DMA-BUF健康度监控在scrcpy Java层嵌入Native JNI定期读取/sys/kernel/debug/dma_buf/summary当total 150时自动触发MediaCodec重启并记录告警日志WARN VideoEncoder: DMA-BUF count167 threshold150, forcing codec restart编译此版本scrcpy后产线设备黑屏率从100%降至0.3%仅剩极少数硬件偶发故障。4.3 系统级防护构建内存泄漏熔断机制在Android系统层加装“内存保险丝”防患于未然方案ACMA Usage Monitor Service推荐编写System Service监听/proc/meminfo当CmaFree 10MB持续30秒自动执行adb shell am broadcast -a com.scrcpy.ACTION_RESTART_ENCODER # 触发scrcpy重启MediaCodec服务开机自启无需root权限方案BKernel Level OOM Killer增强修改drivers/base/node.c在node_set_state()中加入if (cma_free 5 * 1024 * 1024) { // 5MB panic(CMA exhausted by DMA-BUF leak, check rk_vpu driver); }强制panic并保存vmcore便于事后分析泄漏源头注意事项方案B会引发整机重启适合研发环境方案A更温和适合产线部署。我们建议两者并用——A作为第一道防线B作为最后兜底。5. 常见问题与避坑指南那些让你多折腾三天的细节5.1 “我打了补丁但dmesg还是报still has references”这通常是因为补丁未生效而非补丁错误。请按顺序排查确认ko文件已正确加载adb shell lsmod | grep vpu # 应显示vpu模块名及size adb shell dmesg | grep -i vpu.*init # 查看驱动加载日志若显示rk_vpu: loading out-of-tree module taints kernel说明是旧驱动在运行。检查模块签名RK3399 Android 10启用强制签名adb shell cat /proc/sys/kernel/modules_disabled # 应为0 adb shell modprobe -r rk_vpu insmod /data/local/tmp/rk_vpu_fix.ko # 若报Invalid module format需用相同kernel config重新编译验证补丁是否真被编译进koarm-linux-gnueabihf-objdump -d rk_vpu.ko | grep -A 10 rk_vpu_enc_stop # 查看反汇编中是否有dma_buf_detach调用5.2 “scrcpy重启MediaCodec后首帧延迟高达2秒”这是MediaCodec重建的固有代价。解决方案预热机制scrcpy启动时先编码10帧空数据ByteBuffer.allocateDirect(1)丢弃输出只建立pipeline双Codec轮询维护两个MediaCodec实例A编码时B预热A超时则无缝切B参数优化scrcpy --encoder-options profilehigh,level4.1,rc-modecbr,bitrate8000000 # 避免使用baseline profile它会禁用B帧增加buffer压力5.3 “/sys/kernel/debug/dma_buf/目录不存在”说明kernel未启用DMA-BUF debugfs。需在defconfig中开启CONFIG_DEBUG_FSy CONFIG_DMA_SHARED_BUFFERy CONFIG_DMABUF_HEAPS_SYSTEMy # 必须有这行否则debugfs无dma_buf子目录 CONFIG_DMABUF_HEAPSy重新编译kernel并烧录。5.4 “Rockchip官方说‘这不是bug是预期行为’怎么办”这是典型的技术沟通障碍。请用以下事实回应标准合规性Linux Kernel Documentation/driver-api/dma-buf.rst明确要求“driver must detach before put”竞品对比Amlogic AML-G12A、Allwinner H6的VPU驱动均已实现detach可提供git commit hash复现证据提供dmesg截图/sys/kernel/debug/dma_buf/summary原始数据内存泄漏曲线图商业影响列出因该问题导致的客户投诉案例如某汽车DVR厂商月损200台设备我们曾用此话术推动Rockchip在v2.3.3 SDK中承认问题并承诺修复。5.5 “我的芯片是RK3566补丁能直接用吗”不能直接复制。RK3566的VPU驱动位于drivers/media/platform/rockchip/rk3566/vpu/rk3566_vpu_enc.c函数名和结构略有不同rk_vpu_enc_stop()→rk3566_vpu_enc_stop()ctx-dma_buf→ctx-enc_bufctx-attachment→ctx-attach但修复逻辑完全一致找到stop函数添加detach put组合。我们已整理RK3399/RK3566/RK3588三套补丁可按需索取。6. 经验总结给硬件工程师和Android开发者的三条铁律这次排查让我深刻体会到在嵌入式Android领域“黑屏”从来不是显示问题而是内存管理的终局审判。它不给你ANR弹窗不写Crash日志只用最沉默的方式宣告系统死亡。作为十年扎根一线的嵌入式老兵我想把这次教训浓缩成三条必须刻进DNA的铁律第一永远相信dmesg而不是logcat。Java层日志是应用视角的“故事”dmesg才是内核视角的“真相”。当屏幕变黑第一时间抓串口或recovery下的dmesg里面藏着比任何Java堆栈都真实的线索。我们曾因过度信任logcat白白浪费36小时——而dmesg里那行still has 157 references早在第一次卡死时就已存在。第二DMA-BUF泄漏不是“内存泄漏”而是“所有权泄漏”。它不消耗RAM却锁死CMA物理内存它不触发OOM Killer却让GPU寸步难行。排查时别只看free -h要盯死CmaFree和/sys/kernel/debug/dma_buf/summary。记住refcount 1 且持续上涨就是泄漏的指纹。第三不要等Rockchip发新版BSP自己掌握驱动编译能力。产线设备等不了三个月的SDK迭代。花一天时间搭建Rockchip交叉编译环境学会make menuconfig、make Image、fastboot flash boot你就能把修复时间从“月级”压缩到“小时级”。我们团队现在要求所有Android工程师入职第一周必须成功编译并烧录一次自定义kernel——这不是炫技而是生存技能。最后分享一个真实案例某客户用RK3399做医疗影像终端因黑屏问题被医院退回200台设备。我们用本文方法3天内定位到DMA-BUF泄漏5天内交付补丁版固件7天完成全量升级。现在那批设备已稳定运行14个月零黑屏。技术的价值从来不在炫酷的新功能而在守住那块不该黑的屏幕。