资讯详情

scrcpy触发Rockchip DMA-BUF泄漏导致Android工位机黑屏卡死

📅 2026/9/12 12:26:55 | 华诺云谱 👁 阅读
scrcpy触发Rockchip DMA-BUF泄漏导致Android工位机黑屏卡死
产线有一台 Android 工位机用着用着就黑屏接着整机卡死连 adb 都连不上只能断电重启。反复出现业务方一开始怀疑是自研 App 的内存问题开发同学查了两周把大图加载、列表复用、WebView 全排查了一遍也没找到明显的泄漏点。最后我介入才发现问题根本不在 App而是出在 scrcpy 投屏调试工具和 Rockchip 编码器之间编码会话拿走的 DMA-BUF 内存只借不还把系统内存耗干了系统直接进入不可用状态。这篇文章就把这次的排查过程完整记录下来。从现象、误判、到怎么一步步坐实 DMA-BUF 泄漏再到根因和修复方案给同样在 Android 工位机、一体机这类设备上做开发的同行一个参考。尤其是设备上长期挂着 scrcpy 做远程监控或调试的场景大概率会遇到类似问题这篇文章能帮你省下几天的排查时间。1. 问题现场与第一轮排查1.1 先描述一下现场情况这台工位机是典型的产线安卓一体机Rockchip 方案8 核 CPU 配 4GB 内存Android 系统主要跑一个自研的 MES 工单显示 App。问题表现很明确设备运行一段时间后屏幕先闪一下然后黑屏触摸失效过几分钟后整机完全卡死网络 adb 断开USB adb 也连不上必须断电重启才能恢复重启后能正常用但过几个小时又会复发故障间隔没有固定规律有时候 3 小时有时候 8 小时产线环境没有太多外部干扰设备固定安装散热正常App 也是长时间挂在前台跑。所以业务方的第一反应是 App 内存泄漏这完全可以理解毕竟症状最像的就是内存耗尽导致系统 OOM进而触发 watchdog 黑屏重启。但我把 App 的代码大概走了一遍发现这其实是一个很轻量的应用没有复杂的动画没有大量 Bitmap列表也很短。单看内存占用App 自己的 Java 堆就算泄漏也很难在几个小时内把 4GB 内存全部吃光。真正值得怀疑的是系统底层的 native 内存和内核内存。1.2 初步取证内存到底去哪了我当时在现场第一时间做了两件事。第一件事是看free内存第二件事是抓dmesg。dmesg在没有完全卡死之前一般还能抓到内容这台设备当时刚好在故障前还能操作我抓到了几条非常关键的日志。free显示的情况是MemFree已经掉到了不到 200MB但buff/cache也不高也就是说内存既不在 App 的 Java 堆里也不在文件缓存里而是被某种内核态机制吃掉了。dmesg里的表现是典型的分配失败[ 4936.521843] ion: cma_alloc: 8 pages failed, (order: 3) [ 4936.521890] rockchip-vpu: failed to allocate buffer [ 4936.523410] [drm:rockchip_drm_gem_object_create] *ERROR* Failed to allocate buffer这段日志出现的位置距黑屏大概十几秒。可以确认一个方向内存是由ion或者 CMA 分配失败触发的而请求分配的人是rockchip-vpu也就是 VPU 视频硬件编码单元。到这儿问题已经从“App 内存泄漏”转到了系统视频编码这一层。1.3 为什么业务 App 加班排查了两周还没找到问题业务方没找到问题不是他们能力不行而是排查工具的视野天然有盲区。Android 上大家最熟的dumpsys meminfo只统计 App 进程的 PSS/Java 堆根本看不到内核态的 DMA-BUF 占用。adb shell top看到的 CPU 也很正常因为编码器不是忙等它是硬件在干活CPU 占用率很低。这种情况很像以前遇到的“图形内存泄漏”进程没吃多少内存但/sys/kernel/debug/dma_buf/bufinfo里能看到大量 buffer 挂着不释放。所以我后面直接调整了排查方向不再看 App而是盯系统、盯内核、盯硬件模块。2. 把矛头转向 scrcpy投屏调试工具为什么会惹祸2.1 scrcpy 的连接模型和编码链路熟悉 scrcpy 的人都知道它的工作原理是手机/工位机端跑一个scrcpy-server这个 server 会把设备屏幕的 Surface 数据交给 MediaCodec 做硬编码编码成 H.264 码流然后通过 adb 的 socket 通道推到电脑端解码显示。整个链路是App 界面 - SurfaceFlinger - Surface 输入到 MediaCodec - 硬件编码器VPU - H.264 数据 - adb socket - PC 端 scrcpy 窗口这个设计的妙处在于scrcpy 本身不读像素而是拿了一个 Surface 往 MediaCodec 里塞数据。编码器在处理每一帧时需要从内存池里申请 buffer编码完再释放。如果申请和释放不对称buffer 就会不断积累。2.2 Rockchip 平台的编码器与 MPPRockchip 的硬件编码器一般叫 VPU软件层对应的库叫 MPPMedia Process Platform。在 Android 系统里上层 MediaCodec 通过 HAL 层走到 MPPMPP 再调用内核的 VPU 驱动。这中间涉及的内存管理方式就是标题里说的 DMA-BUF。Rockchip 平台的内存管理相对特殊它会从系统内存里划一块叫ion的区域或者直接通过 DMA-BUF heap 来管理多媒体 buffer。编码器需要内存时不是走普通的malloc而是向内核的 DMA-BUF 子系统申请因为这块内存既要给 CPU 访问也要给 VPU 通过设备地址访问。这意味着申请到的内存是“跨设备共享”的有独立的生命周期管理。如果用一句话概括 DMA-BUF 泄漏共享内存的“引用计数”没有归零内核认为这块 buffer 还在被使用所以不会归还给系统。每次编码会话泄漏几个 buffer每个 buffer 几 MB日积月累几小时以后内存就空了。2.3 DMA-BUF 到底是怎么工作的这里给不太熟悉内核的读者补点基础。DMA-BUF 是 Linux 内核提供的一种跨设备内存共享机制Android 上最常见的使用者是图形栈GPU/显示控制器和多媒体栈编解码器。开发者使用 DMA-BUF 的典型流程从 dma-buf heap或者旧的 ION里分配一块内存拿到一个fd把这个fd映射到自己设备的地址空间使用完以后关闭fd对应的引用计数减一所有设备都关闭了fd引用计数归零内存才会真正释放问题往往出在第三步某个环节没有关闭fd或者某个 BufferQueue 的缓存槽没有正确归还 buffer引用计数一直大于零内核就只能一直养着这块内存。等到系统累计泄漏的内存超过某个阈值CMA 分配连续物理页面失败VPU 申请不到 buffer编码器就罢工了。编码器罢工后投屏内容卡住整个 SurfaceFlinger 的行为也会变得异常最终表现为黑屏和系统卡死。3. 关键证据怎么坐实 DMA-BUF 泄漏3.1 用 sysfs 和 debugfs 看 DMA-BUF 占用要确认 DMA-BUF 泄漏最有用的手段是看内核的 debug 节点。Rockchip 平台一般开启CONFIG_DMA_BUF和对应的 debug 信息可以这样看adb shell cat /sys/kernel/debug/dma_buf/bufinfo输出里会列出所有当前存活的 DMA-BUF 对象包括名字、大小、进程引用数。不过我遇到的情况比较特殊设备一旦跑起来这个文件越来越大几万行都打不住。所以我一般先做个统计数一下不同类型的 buffer 占了多少总大小adb shell cat /sys/kernel/debug/dma_buf/bufinfo | grep -A1 vpu\|rkvdec\|mpp | grep size | awk {sum $2} END {print sum}在我排查的这台机器上VPU 相关的 DMA-BUF 总大小在运行 4 小时后已经超过了 1.5GB。而正常情况下VPU 的 buffer 占用应该稳定在几十 MB 的量级。这个数字基本可以定性了编码器链路存在内存泄漏。另外还可以配合看/proc/meminfo里的Ion相关字段或者查阅dumpsys meminfo里的 native 内存趋势。下面的表格是我在排查时整理的常用内存诊断命令后面排查同类型问题可以直接套用。排查目标命令看到什么说明有问题系统总内存adb shell cat /proc/meminfoMemFree持续下降且不回升DMA-BUF 占用量adb shell cat /sys/kernel/debug/dma_buf/bufinfo特定模块 buffer 数量只增不减进程 PSSadb shell dumpsys meminfo pidApp 内存正常不代表系统没问题编码器状态adb shell dumpsys media.codec编码会话/实例异常堆积内核日志adb shell dmesg | grep -i vpufailed to allocate、timeout等错误CMA 分配情况adb shell cat /proc/buddyinfo连续内存碎片化严重3.2 关键对比实验控制变量锁定 scrcpy拿到 DMA-BUF 持续增长的证据还不够必须证明这个增长和 scrcpy 有直接因果关系。我当时设计了三组对比实验第一组正常业务不跑 scrcpy连续跑 12 小时dma_buf/bufinfo里 VPU 相关 buffer 数量非常平稳没有明显增长系统内存曲线正常。第二组跑业务的同时用 scrcpy 连接设备并保持投屏连续跑 4 小时VPU DMA-BUF 数量线性增长内存持续下降最后复现了黑屏卡死。第三组跑业务 scrcpy但把投屏断开观察内存曲线断开后 VPU buffer 数量停止增长已经申请的 buffer 部分回落系统恢复稳定。这个结果非常干净地证明了DMA-BUF 泄漏是 scrcpy 触发的编码链路导致的和业务 App 没有直接关系。而且断开 scrcpy 后 buffer 数量不涨说明泄漏点大概率是在编码会话建立或帧数据传递的某个环节而不是一直在后台偷偷跑。3.3 从 scrcpy-server 的代码层面找线索如果你愿意翻 scrcpy 的源码也能找到一些蛛丝马迹。scrcpy-server 的核心逻辑是把显示器的VirtualDisplay和MediaCodec绑定然后从编码器输出端读取数据。它有一个Codec类和一个SurfaceEncoder类负责管理 MediaCodec 的生命周期。常规情况下scrcpy 会在投屏结束时调用stop()和release()。但在异常断开比如 USB 松动、adb 掉线、PC 端强退时server 的释放流程有可能没有完整走到。更底层的问题是Rockchip 的 MPP 库在释放编码会话时如果还有 buffer 挂在驱动未回收驱动层面的free函数没有被正确调用DMA-BUF 引用计数就会永久大于 0。这一点在 Rockchip 的驱动源码里也能看到rockchip_vpu_enc的release函数需要逐个释放msg-buffers里的 DMA-BUF。如果上层传入的 buffer 列表不完整或者某个 buffer 已经被上层 close 但驱动还在使用就有可能出现泄漏。国产 SoC 的编码器驱动在边界场景下处理得比较粗糙这种问题并不罕见。4. 根因梳理与修复方案4.1 泄漏点到底在哪结合实验数据和驱动代码分析这次泄漏的根因可以归纳为一条链路使用 scrcpy 连接工位机开始视频投屏scrcpy-server 创建 MediaCodec 编码会话Rockchip VPU 开始工作通过 MPP 向内核申请了大量 DMA-BUF在某个时刻连接异常断开adb 服务重启、网络波动、PC 端窗口关闭scrcpy-server 的编码器释放流程没有完整走到release部分 DMA-BUF 没有被释放引用计数没有归零下一次投屏重新建立编码会话又申请了一批新 buffer旧的还挂着反复多次后DMA-BUF 累积至系统内存耗尽VPU 申请不到 buffer编码器卡死SurfaceFlinger 也随之异常屏幕黑屏这里有一个重点这个问题不是典型的“稳定复现”的 bug它依赖“异常断开”这个触发条件。产线环境网络波动频繁或者维护人员经常拔插 USB就容易反复触发。4.2 修复可以从几个方向入手修复思路分三个层面应用层、驱动层、使用方式。第一层应用层如果是自己集成了 scrcpy 或类似投屏功能的客户端务必确保在onDestroy、断线回调、异常退出路径里都调用完整的释放流程。Android 的 MediaCodec 释放顺序是先stop()再release()中间不要跳过异常分支。最好用try-finally保证释放逻辑一定执行。第二层驱动层Rockchip 的 MPP 或 VPU 驱动有更新版本的话尽量升级到官方修复了 buffer 释放问题的版本。很多这类问题在 SoC 原厂的新 BSP 里已经有补丁只是产线设备用的是出厂旧版本系统一直没升。联系 Rockchip 原厂或者方案商要补丁时可以直接描述现象scrcpy 投屏异常断开后 VPU DMA-BUF 泄漏dmesg 里出现 ion: cma_alloc failed。他们一听就明白。第三层使用方式如果短期内无法改代码也不方便升级系统可以考虑从运维侧规避。具体做法是给 scrcpy 连接加一道保活检查一旦断线强制把设备端 scrcpy-server 进程杀掉再重连adb shell killall com.genymobile.scrcpy或者在 scrcpy 连接时加--max-size和--max-fps限制编码负载虽然不能解决泄漏但可以降低单次泄漏的速度给维护争取时间。另外尽量用有线连接减少 adb 掉线的概率。4.3 修复后的验证方式我在验证修复效果时用了最土但最有效的办法压测。修复后我在设备上反复执行下面的操作循环跑了一整天启动 scrcpy 投屏保持 15 分钟通过adb kill-server模拟异常断开重新启动 adb 和 scrcpy观察/sys/kernel/debug/dma_buf/bufinfo中 VPU buffer 数量对比数据我整理成了下面这个表测试轮次操作VPU buffer 数量第 1 轮初始状态5第 2 轮正常投屏 15 分钟12第 3 轮kill-server 异常断开17第 4 轮重启 adb scrcpy19第 5 轮kill-server 异常断开21修复前 10 轮后持续累积200修复后 10 轮后持续运行25 左右修复后VPU buffer 的数量明显收敛不再无限制增长。连续 72 小时压测没有再出现黑屏卡死问题闭环。5. 这类问题的通用排查套路与避坑清单5.1 黑屏卡死类问题先分清是哪个层经过这次排查我最大的感悟是黑屏卡死这类问题第一步不是看代码而是给问题分层。层级排错了后面全白费。我的分层思路是这样的第 1 层硬件层。先排除供电、散热、屏幕排线、通断问题。产线设备尤其容易忽略供电电源适配器老化导致电压不稳也会黑屏。第 2 层内核/驱动层。看dmesg、/proc/meminfo、/sys/kernel/debug/dma_buf/bufinfo。重点关注 DMA-BUF、ION、CMA 相关报错。如果有 VPU、GPU、ISP 之类的硬件模块参与优先怀疑多媒体内存泄漏。第 3 层HAL/系统服务层。看logcat里 SurfaceFlinger、AudioFlinger、MediaCodec 相关的崩溃或 warning。dumpsys是这里最常用的工具。第 4 层应用层。最后才看 App。应用层问题通常是 Java 堆 OOM、主线程阻塞、死锁症状和系统级不完全一样。这次的坑就在于业务方直接从第 4 层开始查查了两周没结果因为问题根本不在那里。5.2 DMA-BUF 泄漏的典型“长相”DMA-BUF 泄漏这个问题在外企设备、国产设备上都有可能碰到特别是涉及到视频编解码、相机、GPU 渲染的长跑场景。它有几个典型特征你如果看到这几条就可以往这个方向想现象是周期性复发重启后好转运行越久越严重free里内存持续下降但dumpsys meminfo看不出是哪个 App 在吃dmesg里有ion: cma_alloc failed、rockchip-vpu: failed to allocate buffer、Out of memory之类的关键字设备有投屏、录屏、视频通话、相机预览之类的多媒体功能在跑故障前往往有一次异常断开、信号不稳、设备重启等非正常操作还有一种很容易被忽视的场景像工位机这种设备维护人员图省事会挂一个scrcpy进程做远程看护但看护本身变成了杀手。这也是我写这篇文章的另一个用意——调试工具在产线设备上的使用真的需要评估它自身的稳定性。5.3 几个工程上的实操建议最后说几个这次排查过程中沉淀下来的实操建议都是踩过的坑换来的。第一产线设备一定要开内核的dma_bufdebug 节点。很多 BSP 默认只开了CONFIG_DMA_BUF没开对应的 debug导致出了问题没法查。如果你发现自己看不到/sys/kernel/debug/dma_buf/bufinfo可以试试先挂载 debugfsadb shell mount -t debugfs none /sys/kernel/debug如果 BSP 连内核配置都不支持那建议提前把内核的CONFIG_DEBUG_FS、CONFIG_DMA_BUF打开否则排查这类问题会非常被动。第二远程维护工位机时给 scrcpy 这类工具加个简单的“可用性探针”。比如每隔 5 分钟检查一次adb devices状态和 VPU buffer 数量超过阈值自动重启 scrcpy-server。产线场景宁可让投屏短暂中断也不能让它带着泄漏跑到黑屏。第三如果是自己开发的投屏工具或者定制系统注意 MediaCodec 的异常路径。测试用例里一定要加“边编码边杀进程”、“传输通道强制断掉”、“系统休眠唤醒”这类破坏性场景。芯片原厂的 BSP 代码在异常路径的处理上普遍比应用层糙需要自己做好兜底。这次排查给我留下的一个重要习惯现在遇到任何 Android 设备黑屏卡死我会先看dmesg再看dma_buf/bufinfo最后才打开 Android Studio 查代码。工具错位才是排查效率低的最大元凶。希望这篇记录能帮你少走这段弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。