news 2026/9/12 12:26:53

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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内存,第二件事是抓dmesgdmesg在没有完全卡死之前一般还能抓到内容,这台设备当时刚好在故障前还能操作,我抓到了几条非常关键的日志。

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 平台的编码器与 MPP

Rockchip 的硬件编码器一般叫 VPU,软件层对应的库叫 MPP(Media Process Platform)。在 Android 系统里,上层 MediaCodec 通过 HAL 层走到 MPP,MPP 再调用内核的 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 的典型流程:

  1. 从 dma-buf heap(或者旧的 ION)里分配一块内存,拿到一个fd
  2. 把这个fd映射到自己设备的地址空间
  3. 使用完以后,关闭fd,对应的引用计数减一
  4. 所有设备都关闭了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 <pid>App 内存正常不代表系统没问题
编码器状态adb shell dumpsys media.codec编码会话/实例异常堆积
内核日志adb shell dmesg | grep -i vpufailed to allocatetimeout等错误
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 的核心逻辑是把显示器的VirtualDisplayMediaCodec绑定,然后从编码器输出端读取数据。它有一个Codec类和一个SurfaceEncoder类,负责管理 MediaCodec 的生命周期。

常规情况下,scrcpy 会在投屏结束时调用stop()release()。但在异常断开(比如 USB 松动、adb 掉线、PC 端强退)时,server 的释放流程有可能没有完整走到。更底层的问题是,Rockchip 的 MPP 库在释放编码会话时,如果还有 buffer 挂在驱动未回收,驱动层面的free函数没有被正确调用,DMA-BUF 引用计数就会永久大于 0。

这一点在 Rockchip 的驱动源码里也能看到,rockchip_vpu_encrelease函数需要逐个释放msg->buffers里的 DMA-BUF。如果上层传入的 buffer 列表不完整,或者某个 buffer 已经被上层 close 但驱动还在使用,就有可能出现泄漏。国产 SoC 的编码器驱动在边界场景下处理得比较粗糙,这种问题并不罕见。

4. 根因梳理与修复方案

4.1 泄漏点到底在哪

结合实验数据和驱动代码分析,这次泄漏的根因可以归纳为一条链路:

  1. 使用 scrcpy 连接工位机,开始视频投屏
  2. scrcpy-server 创建 MediaCodec 编码会话,Rockchip VPU 开始工作,通过 MPP 向内核申请了大量 DMA-BUF
  3. 在某个时刻,连接异常断开(adb 服务重启、网络波动、PC 端窗口关闭),scrcpy-server 的编码器释放流程没有完整走到release
  4. 部分 DMA-BUF 没有被释放,引用计数没有归零
  5. 下一次投屏重新建立编码会话,又申请了一批新 buffer,旧的还挂着
  6. 反复多次后,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 修复后的验证方式

我在验证修复效果时,用了最土但最有效的办法:压测。

修复后,我在设备上反复执行下面的操作循环跑了一整天:

  1. 启动 scrcpy 投屏,保持 15 分钟
  2. 通过adb kill-server模拟异常断开
  3. 重新启动 adb 和 scrcpy
  4. 观察/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 failedrockchip-vpu: failed to allocate bufferOut of memory之类的关键字
  • 设备有投屏、录屏、视频通话、相机预览之类的多媒体功能在跑
  • 故障前往往有一次异常断开、信号不稳、设备重启等非正常操作

还有一种很容易被忽视的场景:像工位机这种设备,维护人员图省事会挂一个scrcpy进程做远程看护,但看护本身变成了杀手。这也是我写这篇文章的另一个用意——调试工具在产线设备上的使用,真的需要评估它自身的稳定性。

5.3 几个工程上的实操建议

最后说几个这次排查过程中沉淀下来的实操建议,都是踩过的坑换来的。

第一,产线设备一定要开内核的dma_bufdebug 节点。很多 BSP 默认只开了CONFIG_DMA_BUF,没开对应的 debug,导致出了问题没法查。如果你发现自己看不到/sys/kernel/debug/dma_buf/bufinfo,可以试试先挂载 debugfs:

adb shell mount -t debugfs none /sys/kernel/debug

如果 BSP 连内核配置都不支持,那建议提前把内核的CONFIG_DEBUG_FSCONFIG_DMA_BUF打开,否则排查这类问题会非常被动。

第二,远程维护工位机时,给 scrcpy 这类工具加个简单的“可用性探针”。比如每隔 5 分钟检查一次adb devices状态和 VPU buffer 数量,超过阈值自动重启 scrcpy-server。产线场景宁可让投屏短暂中断,也不能让它带着泄漏跑到黑屏。

第三,如果是自己开发的投屏工具或者定制系统,注意 MediaCodec 的异常路径。测试用例里一定要加“边编码边杀进程”、“传输通道强制断掉”、“系统休眠唤醒”这类破坏性场景。芯片原厂的 BSP 代码在异常路径的处理上普遍比应用层糙,需要自己做好兜底。

这次排查给我留下的一个重要习惯:现在遇到任何 Android 设备黑屏卡死,我会先看dmesg,再看dma_buf/bufinfo,最后才打开 Android Studio 查代码。工具错位才是排查效率低的最大元凶。希望这篇记录能帮你少走这段弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 12:25:38

从VC++6到现代实践:拆解50万行MFC电子档案系统的架构与部署经验

简介&#xff1a;电子档案编制系统全套VC6源码是一份面向软件开发人员与建筑工程资料管理岗位的完整工程资源&#xff0c;覆盖档案编制、施工日志编制、文档管理三大核心功能&#xff0c;并内置重庆建筑工程全部资料模板库&#xff0c;支持所见即所得的编辑模式&#xff0c;用户…

作者头像 李华
网站建设 2026/9/12 12:24:50

企业微信API主动调用技术实战与优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 12:23:33

Codex Token统计Skill实战:一句话打开每日用量看板

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 12:21:25

如何将 Graphiti MCP 服务器以 stdio 方式接入 Claude Desktop?

如何将 Graphiti MCP 服务器以 stdio 方式接入 Claude Desktop&#xff1f; 【免费下载链接】graphiti Build Real-Time Knowledge Graphs for AI Agents 项目地址: https://gitcode.com/GitHub_Trending/grap/graphiti Claude Desktop 只支持 stdio 传输&#xff0c;而…

作者头像 李华