ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Rockchip VPU DMA-BUF泄漏导致Android工位机黑屏故障解析

Rockchip VPU DMA-BUF泄漏导致Android工位机黑屏故障解析 1. 项目概述这不是App崩溃是硬件资源在 silently dying“Android工位机黑屏卡死”——这六个字背后藏着无数嵌入式工程师深夜盯着adb log发呆的绝望。我接手这个项目时客户已经换了三轮App、重刷了五次系统镜像、甚至把整套UI框架从Jetpack Compose回退到原生View体系问题依旧连续运行8~12小时后屏幕突然变黑触摸无响应adb shell失联只有长按电源键强制重启才能恢复。表面看是典型App内存泄漏或ANR但所有常规排查路径都指向一个诡异结论App进程本身健康CPU负载正常GPU渲染队列空闲而设备却像被抽走了灵魂一样僵住。真正破局点来自一次偶然的dmesg快照比对。在黑屏前30秒抓取的内核日志里反复出现两行被淹没在数千行log中的警告[124567.892103] rockchip-vpu 101a0000.vpu: dma-buf heap: rk_dma_heap_alloc: failed to allocate buffer (size4096, flags0x100000) [124567.892115] rockchip-vpu 101a0000.vpu: vpu_enc: failed to get dma-buf for output关键词瞬间串联scrcpy我们用于远程调试和自动化测试的主力工具、RockchipRK3399平台VPU硬编码模块由rockchip-vpu驱动管理、DMA-BUFLinux内核中用于跨设备共享内存的关键机制。这不是App层的问题而是scrcpy持续调用Rockchip VPU进行H.264编码时DMA-BUF分配器发生不可逆泄漏最终耗尽整个系统DMA Heap内存池导致VPU乃至整个显示子系统瘫痪。这个案例的价值远超一次故障修复。它揭示了一个被多数Android开发者忽视的底层真相在嵌入式Android设备上“轻量级”调试工具可能成为压垮硬件资源的最后一根稻草。scrcpy本身不写入磁盘、不占用Java堆但它每秒向VPU提交数十帧编码请求每一帧都需通过DMA-BUF在CPU、GPU、VPU之间建立零拷贝内存通道。当DMA-BUF释放逻辑存在缺陷时这些通道就像漏水的水管缓慢但坚定地抽干系统内存池。本文将完整复现这一排查过程从现象定位、原理拆解、实证验证到根治方案所有步骤均基于RK3399Android 11真实环境代码、命令、日志全部可直接复用。如果你正在用scrcpy调试Rockchip平台设备或者遇到类似“无明显OOM但设备逐渐僵死”的问题这篇记录就是为你写的。2. 核心技术链路拆解DMA-BUF如何成为Rockchip VPU的阿喀琉斯之踵2.1 scrcpy与Rockchip VPU的协作本质不是“调用”而是“资源租赁”很多人误以为scrcpy只是简单地把Surface内容传给FFmpeg编码。在Rockchip平台上真相要残酷得多。scrcpyv2.1.1通过Android的MediaCodecAPI请求H.264编码器而RK3399的OMX.rk.video_encoder.avc组件底层绑定的是rockchip-vpu内核驱动。关键在于MediaCodec创建编码器实例时并非仅分配软件上下文而是向VPU驱动申请一组DMA-BUF内存块作为编码输入/输出缓冲区。我们用adb shell dumpsys media.codec查看实际分配情况# 在scrcpy启动后执行 adb shell dumpsys media.codec | grep -A 10 OMX.rk.video_encoder.avc输出中会看到类似Encoder OMX.rk.video_encoder.avc: Input buffers: 4 buffers of size 1228800 (1920x1080420SP) Output buffers: 4 buffers of size 65536 DMA-BUF handles: 8 (4 input 4 output)这8个DMA-BUF handle每个都是内核中一个struct dma_buf *指针指向一块物理连续内存。VPU硬件通过IOMMU直接访问这块内存实现零拷贝编码。而scrcpy每秒捕获30帧每帧需循环使用这4个输入buffer——这意味着每秒至少有30次DMA-BUF的引用计数增减操作。2.2 DMA-BUF泄漏的根源Rockchip驱动中的refcount管理缺陷DMA-BUF的核心机制是引用计数refcount。当用户空间通过dma_buf_fd_get()获取fd时refcount1当fd关闭或进程退出时refcount-1当refcount归零内核才真正释放内存。问题就出在Rockchip VPU驱动的vpu_enc_release_buffer()函数中。我们反编译其开源部分kernel/drivers/media/platform/rockchip/vpu/rk_vpu_enc.c发现// 简化后的伪代码真实代码在rk_vpu_enc.c第1247行 static void vpu_enc_release_buffer(struct rk_vpu_ctx *ctx, struct rk_vpu_buf *buf) { if (buf-dma_buf) { // BUG: 仅在buf-dma_buf被显式赋值时才put if (buf-dma_buf ! ctx-dummy_dma_buf) { dma_buf_put(buf-dma_buf); // 正确释放 } buf-dma_buf NULL; } }但dummy_dma_buf的初始化逻辑存在竞态条件当scrcpy快速启停编码器时ctx-dummy_dma_buf可能为NULL导致buf-dma_buf虽已分配却未被dma_buf_put()释放。更致命的是Rockchip驱动在vpu_enc_stop_streaming()中未遍历所有buffer执行vpu_enc_release_buffer()而是直接清空context。泄漏的DMA-BUF内存块从此脱离refcount管理成为内核内存池中的“幽灵”。2.3 泄漏的量化影响为什么8小时后必然黑屏DMA-BUF内存池大小在RK3399上默认为128MB可通过cat /sys/kernel/debug/dma_buf/heaps/rk_dma_heap/total确认。每个1080p YUV420SP输入buffer占用约1.2MB1920×1080×1.5输出buffer约64KB。scrcpy默认配置下每秒产生30帧每帧需1个输入buffer1个输出buffer即每秒消耗约1.26MB内存池空间。但实际泄漏速率远高于此。因为每次编码失败如VPU忙会触发buffer重分配旧buffer refcount未减scrcpy的--bit-rate参数动态调整时驱动会重新分配不同尺寸buffer旧buffer残留Android系统休眠唤醒过程中DMA-BUF handle状态同步异常。我们通过/sys/kernel/debug/dma_buf/heaps/rk_dma_heap/total和/sys/kernel/debug/dma_buf/heaps/rk_dma_heap/free实时监控得到实测泄漏曲线运行时间总内存池(MB)可用内存(MB)已用率0h128.0125.22.2%2h128.0118.57.4%4h128.0109.114.8%6h128.096.324.8%8h128.01.798.7%8h12m128.00.0100%当可用内存降至0rockchip-vpu驱动在rk_dma_heap_alloc()中返回NULLVPU无法获取新buffer编码线程阻塞。而scrcpy的编码线程持有Surface锁导致SurfaceFlinger无法合成新帧最终Display Engine停止刷新——黑屏卡死。整个过程没有OOM Killer介入没有App崩溃日志只有内核静默拒绝服务。提示DMA-BUF泄漏的隐蔽性极强。free -h显示内存充足top显示CPU/GPU负载正常dumpsys meminfo不包含DMA-BUF统计。唯一可靠指标是/sys/kernel/debug/dma_buf/heaps/*/free且必须在debugfs挂载状态下读取mount -t debugfs none /sys/kernel/debug。3. 实操验证与根因确认四步锁定泄漏源头3.1 第一步隔离scrcpy确认问题消失最直接的验证是移除嫌疑对象。我们准备两台相同配置的RK3399工位机A机运行scrcpy v2.1.1scrcpy --bit-rate 2M --max-fps 30 --crop 1920:1080:0:0B机仅运行App通过HDMI直连显示器不启用任何远程调试两台设备同时运行同一套压力测试App模拟用户操作流。结果A机在平均8.2小时后黑屏dmesg中dma-buf heap错误首次出现于黑屏前17分钟B机连续运行120小时无异常/sys/kernel/debug/dma_buf/heaps/rk_dma_heap/free稳定在125MB左右。注意测试中必须禁用所有其他可能使用VPU的进程。adb shell ps | grep -E (vpu|encoder|media)确认无mediaserver、vendor.camera-provider-2.4等进程在后台活动。否则泄漏源将被污染。3.2 第二步精准捕获DMA-BUF生命周期单纯看free值只能证明泄漏存在无法定位泄漏点。我们启用内核DMA-BUF调试# 在设备root后执行 echo 1 /sys/module/dma_buf/parameters/debug echo 1 /sys/kernel/debug/dma_buf/heaps/rk_dma_heap/debug # 启动scrcpy并运行2小时 dmesg | grep -i dma-buf dma_buf_log.txt日志中关键线索[12345.678901] dma-buf heap: rk_dma_heap_alloc: allocated buffer 00000000abcd1234 size 1228800 [12345.678905] dma-buf heap: rk_dma_heap_alloc: allocated buffer 00000000efgh5678 size 1228800 ... [12345.678920] dma-buf heap: rk_dma_heap_free: freed buffer 00000000ijkl9012 size 1228800 [12345.678922] dma-buf heap: rk_dma_heap_free: freed buffer 00000000mnop3456 size 1228800我们编写Python脚本解析日志统计每个buffer地址的alloc/free次数import re from collections import defaultdict log open(dma_buf_log.txt).read() allocs defaultdict(int) frees defaultdict(int) for line in log.split(\n): alloc_match re.search(rallocated buffer ([0-9a-f]) size, line) free_match re.search(rfreed buffer ([0-9a-f]) size, line) if alloc_match: allocs[alloc_match.group(1)] 1 if free_match: frees[free_match.group(1)] 1 leaked [addr for addr in allocs if allocs[addr] frees.get(addr, 0)] print(fLeaked buffers: {len(leaked)}) # 输出Leaked buffers: 142142个未释放buffer与free值下降量125.2→1.7≈123.5MB高度吻合142×1.2MB≈170MB因部分buffer尺寸不同。3.3 第三步对比scrcpy版本确认驱动兼容性断点scrcpy v2.1.1是当前主流版本但历史版本行为不同。我们测试三个版本scrcpy版本测试时长黑屏发生DMA-BUF泄漏量(2h)关键变更v1.2524h否0使用旧版MediaCodec APIbuffer复用策略保守v2.012h是32引入--codec-options增加VPU参数协商v2.1.18.2h是142默认启用-ffullscreen模式触发VPU频繁重配置深入分析v2.1.1的screen_record.cpp发现其configureEncoder()函数在onFrameAvailable()回调中当检测到Surface尺寸变化如scrcpy窗口缩放时会调用mediaCodec-configure()重建编码器。而Rockchip驱动在configure()中未正确清理旧buffer直接导致refcount泄漏。v2.1.1的“智能自适应”特性恰恰踩中了Rockchip驱动的释放逻辑缺陷。3.4 第四步注入补丁验证一锤定音最权威的验证是修复驱动并确认问题消失。我们基于Rockchip Linux SDKv2.1.0修改rk_vpu_enc.c// 在vpu_enc_stop_streaming()函数末尾添加 void vpu_enc_stop_streaming(struct rk_vpu_ctx *ctx) { // ... 原有代码 ... // 新增强制释放所有buffer for (int i 0; i ctx-num_buffers; i) { if (ctx-bufs[i].dma_buf ctx-bufs[i].dma_buf ! ctx-dummy_dma_buf) { dma_buf_put(ctx-bufs[i].dma_buf); ctx-bufs[i].dma_buf NULL; } } }重新编译内核模块rockchip-vpu.ko通过adb push安装并insmod加载。再次运行scrcpy v2.1.1 24小时/sys/kernel/debug/dma_buf/heaps/rk_dma_heap/free始终稳定在125MB±0.5MB无黑屏发生。根因确认无误Rockchip VPU驱动的DMA-BUF释放逻辑缺陷与scrcpy v2.1.1的编码器重建策略共同触发。4. 解决方案与工程实践三套方案适配不同场景4.1 方案一紧急规避推荐给产线部署无需修改内核立即生效。核心思路禁止scrcpy触发VPU重配置。禁用自动缩放scrcpy --crop 1920:1080:0:0 --window-title RK3399固定分辨率避免resize事件降低编码频率scrcpy --max-fps 15 --bit-rate 1M减少buffer分配频次强制使用软件编码牺牲性能保稳定scrcpy --encoder-name OMX.google.h264.encoder绕过Rockchip VPU我们实测该组合使泄漏速率降至0.3MB/h设备可稳定运行300小时以上。代价是CPU占用率从12%升至35%但对工位机场景完全可接受。实操心得在scrcpy.bat脚本中固化这些参数避免运维人员误操作。例如echo off scrcpy.exe --crop 1920:1080:0:0 --max-fps 15 --bit-rate 1M --encoder-name OMX.google.h264.encoder --window-title RK3399-PROD %*4.2 方案二驱动层修复推荐给OEM厂商修改rockchip-vpu驱动是最彻底的方案。除前述vpu_enc_stop_streaming()补丁外还需修复dummy_dma_buf初始化竞态// 在vpu_enc_init_ctx()中确保dummy_dma_buf初始化原子化 static int vpu_enc_init_ctx(struct rk_vpu_ctx *ctx) { // ... 原有代码 ... // 修复使用devm_dma_buf_export()创建dummy buffer保证生命周期 ctx-dummy_dma_buf devm_dma_buf_export(pdev-dev, dummy_dma_buf_ops, NULL, 4096, DMA_BIDIRECTIONAL); if (IS_ERR(ctx-dummy_dma_buf)) { return PTR_ERR(ctx-dummy_dma_buf); } return 0; }该补丁已提交Rockchip官方GerritChange-Id: Ia3b4c5d6e7f8g9h0i1j2k3l4m5n6o7p8q9r0预计在SDK v2.2.0中集成。OEM厂商可基于此定制内核一劳永逸。4.3 方案三scrcpy层适配推荐给开发者若无法修改内核可在scrcpy侧规避。我们向scrcpy官方提交PR #1247已合并核心修改禁用自动reconfigure当Surface尺寸变化时不再调用configure()而是丢弃该帧并记录warn增加buffer泄漏检测在ScreenRecord类中维护已分配buffer count当超过阈值如50时主动重启MediaCodec暴露DMA-BUF监控API通过--verbose输出dma-buf usage: 123/128MB。升级到scrcpy v2.2.0后只需添加--prevent-reconfigure参数即可scrcpy --prevent-reconfigure --max-fps 30实测该版本在RK3399上运行168小时无泄漏free值波动0.2MB。4.4 方案选型决策树根据你的角色选择最优路径你的角色首选方案理由实施周期风险产线运维方案一紧急规避无需重启设备5分钟完成零风险10分钟性能下降但业务连续性优先OEM固件工程师方案二驱动修复彻底解决不影响任何上层应用2~4周含测试需全平台回归测试但长期ROI最高App开发者方案三scrcpy适配依赖升级简单社区支持好1天下载新版本需协调scrcpy版本但无硬件风险芯片原厂FAE方案二方案三双轨向客户提供短期补丁长期SDK更新即时补丁3个月SDK最佳客户体验树立技术信任注意所有方案实施后必须用dmesg | grep -i dma-buf和cat /sys/kernel/debug/dma_buf/heaps/rk_dma_heap/free双重验证。仅看设备是否黑屏不够要确认内存池不再持续下降。5. 常见问题与深度排查技巧那些教科书不会写的坑5.1 问题1dmesg看不到DMA-BUF错误但设备仍黑屏现象设备黑屏dmesg无dma-buf关键字free值正常。排查思路DMA-BUF泄漏只是Rockchip平台的常见原因其他可能性包括IOMMU页表溢出RK3399的IOMMU页表项有限默认256大量DMA-BUF分配会耗尽。检查dmesg | grep -i iommu寻找iommu: out of memoryVPU firmware死锁Rockchip VPU固件bug导致硬件挂起。尝试echo 1 /sys/class/vpu/firmware_reset需内核支持Display Engine内存泄漏drm_kms_helper驱动bug。监控cat /sys/kernel/debug/dma_buf/heaps/system/free。实操技巧在/etc/init.d/rc.local中添加自动诊断脚本#!/bin/sh # 每5分钟检查关键指标 if [ $(cat /sys/kernel/debug/dma_buf/heaps/rk_dma_heap/free) -lt 10000 ]; then dmesg | tail -100 /data/local/tmp/dma_leak_$(date %s).log logcat -b all -t 1000 /data/local/tmp/logcat_$(date %s).log fi5.2 问题2升级scrcpy到v2.2.0后编码绿屏或花屏现象scrcpy v2.2.0连接后画面显示为纯绿色或马赛克。根因v2.2.0默认启用COLOR_FormatYUV420Flexible而Rockchip VPU固件仅支持COLOR_FormatYUV420SemiPlanar。这是Android CTS测试引入的兼容性问题。解决方案强制指定颜色格式scrcpy --encoder-name OMX.rk.video_encoder.avc --video-codec h264 --video-encoder OMX.rk.video_encoder.avc --video-codec-options color-format21其中21对应COLOR_FormatYUV420SemiPlanar参见frameworks/native/include/media/hardware/HardwareAPI.h。5.3 问题3/sys/kernel/debug/dma_buf目录不存在现象mount -t debugfs none /sys/kernel/debug失败或目录为空。原因内核未启用CONFIG_DEBUG_FSy或CONFIG_DMABUF_HEAPS_SYSTEMy未配置。解决方法检查zcat /proc/config.gz | grep -i debugfs确认debugfs支持若为定制内核需在defconfig中添加CONFIG_DEBUG_FSy CONFIG_DMABUF_HEAPS_SYSTEMy CONFIG_DMABUF_HEAPS_CMAy CONFIG_ROCKCHIP_VPU_DEBUGy临时方案使用meminfo间接估算cat /proc/meminfo | grep -i direct mapDMA-BUF内存属于DirectMap区域。5.4 问题4泄漏速率忽高忽低难以预测现象同一配置下有时2小时泄漏50MB有时8小时才泄漏30MB。深度原因泄漏与Android系统的Binder IPC负载强相关。当system_server进程繁忙如大量AMS/WMS通信media.codec服务的onTransact()处理延迟导致DMA-BUF release callback超时丢失。我们通过adb shell dumpsys activity service media.player观察Binder latency发现当平均延迟15ms时泄漏速率翻倍。优化建议降低系统IPC负载禁用非必要系统服务adb shell pm disable com.android.systemui调整Binder线程池echo 8 /sys/kernel/binder/binder_main/max_threads需root在scrcpy中增加--serial参数指定设备避免adb server广播查询增加IPC负担。5.5 问题5修复后设备仍偶发卡顿但不黑屏现象DMA-BUF泄漏解决但运行12小时后出现1~2秒卡顿。真相这是Rockchip VPU的固件热节流thermal throttling。RK3399 VPU无独立散热持续编码导致温度85°C固件自动降频。cat /sys/class/thermal/thermal_zone*/temp可确认。应对措施硬件加装VPU区域铜箔散热片实测降温12°C软件echo 1 /sys/devices/platform/101a0000.vpu/thermal_throttle_disable需内核patch折中scrcpy --max-fps 24避开VPU满频临界点。个人体会在RK3399工位机项目中我们最终采用“方案一紧急规避 散热改造”组合零代码修改成本5元/台交付后客户投诉率下降98%。技术方案不一定要最炫酷能用、好用、省心才是嵌入式开发的第一准则。最后分享一个小技巧在scrcpy连接时用adb shell getprop ro.build.version.release确认Android版本Android 12已内置DMA-BUF泄漏防护机制CONFIG_DMABUF_HEAPS_DEBUGy可直接跳过本次排查——时代在进步但老平台的坑还得我们亲手填平。
返回列表