RK3568 Android工位机黑屏之谜:scrcpy与DMA-BUF泄漏排查指南

发布时间:2026/9/12 4:00:21
RK3568 Android工位机黑屏之谜:scrcpy与DMA-BUF泄漏排查指南 先说结论这台 RK3568 平台的 Android 工位机连续跑两三天必然黑屏卡死排查到最后锅不在我们自己写的 MES App而在 scrcpy 远程投屏与 Rockchip 硬件编码器驱动之间的 DMA-BUF 泄漏。这个问题的典型程度很高。工厂产线上大量 Android 工位机要做集中监控和远程协助scrcpy 几乎是标配工具。但恰恰是这种大家都觉得没问题的工具配合国产 SoC 的编码器驱动会踩出一个非常隐蔽的坑。症状表现为系统级黑屏、触摸无响应、logcat 几乎不出日志只要断电重启就恢复但过两天又复现极难定位。如果你正在做 Rockchip、全志这类芯片的 Android 定制系统或者你的产品里同时存在远程投屏 长时间待机这两个条件这篇文章建议认真看完。我会把从误判 App 到锁定 DMA-BUF 泄漏的完整排查过程、用到的命令、以及最后怎么止血和治本都写出来。1. 故障现象与初次误判1.1 工位机的真实部署形态这套系统的硬件是某国产 10.1 寸触摸一体机主控 RK3568四核 A554GB DDR4系统是基于 Android 11 的定制固件。设备部署在车间产线旁边7x24 小时不掉电跑一个基于 WebView 的 MES 工单操作程序同时坐席那边需要实时看到每个工位屏幕所以每个设备都常驻运行 scrcpy 的 server 端由中控电脑通过 adb 反向连接拉画面。问题上线一周后就暴露了设备运行约 48 到 72 小时后屏幕突然全黑触摸完全没反应整机像死了一样。更麻烦的是这个故障经常发生在夜班或者无人操作的时段说明它不是被谁点出来的而是系统自己在某个时间点彻底耗尽了资源。售后反馈说断电重启能恢复但治标不治本客户每天都有设备掉线生产节奏被严重干扰。我当时的第一反应和所有人一样先查 App。毕竟工位机不像是会自己出问题的设备MES 应用又是 WebView 套壳JavaScript 与原生层的内存管理本身就容易出幺蛾子。于是我在一台工位机上挂了各种抓取工具准备等下次复现后直接拉数据。1.2 第一轮排查App 背锅的理由我先在正常状态下采集了一轮基线数据。App 包名假设为 com.company.mes通过 adb 拿到它的内存占用adb shell dumpsys meminfo com.company.mes结果 Java Heap 大概 80MB 出头Native Heap 45MB总 PSS 不到 200MB对 4GB 内存的设备来说非常健康。再开 logcat 盯着崩溃和 ANR 事件adb logcat -b crash -b events -d | grep -iE anr|lowmemory|am_kill跑了一天除了几个无关紧要的 warning 之外没有看到系统 LMK 大面积杀进程的记录也没有 App 自身崩溃。为了确认 UI 线程是否长时间阻塞我还抓过一份/data/anr/目录里面干干净净。CPU 占用方面用adb shell top -H -b -n 1看 App 的所有线程主线程和 WebView 渲染线程的 CPU 占用都不超过 10%根本没有死循环或者 GC 风暴的特征。到这里App 的嫌疑其实已经可以洗掉了。不过当时还有一个干扰因素黑屏发生的时候工位机通过串口仍然能敲命令adb shell响应速度也还算正常但 SurfaceFlinger 已经完全拉不起界面了。这说明系统服务层本身没死更像是 Surface 缓冲或显示链路出了问题而这显然不是单个 App 能造成的影响范围。1.3 打断误判的一行内核日志真正让我放弃App 问题这个方向的是内核日志里的一行分配失败信息。黑屏发生后我通过串口登进设备执行dmesg | grep -iE alloc.*fail|dma_buf|ion|cma | tail -100日志里反复出现类似这样的报错[ 11352.673] rkvenc: alloc buffer failed, size 8294400 [ 11352.674] rkvenc: failed to get memory from gem [ 11352.676] cma: cma_alloc: alloc failed, req_size: 8294400rkvenc是 Rockchip 平台 H.264 硬件编码器的内核驱动cma_alloc失败说明内核连续内存区域已经被耗尽。我顺手看了一眼自己预埋的监控脚本发现内存从最初的 3.2GB available 一路掉到几十 MB而且cat /proc/meminfo里CmaTotal与CmaFree的差值触目惊心。这不是普通的内存泄漏这是内核态 DMA 缓冲区在持续泄漏。到了这一步目标已经很明确了真正的元凶必然和硬件视频编码器有关而现场环境里唯一会持续调用硬件编码器的就是 scrcpy。2. 顺着 scrcpy 链路追根溯源2.1 scrcpy 投屏在 Android 端到底干了什么scrcpy 的官方定义只是PC 上显示和控制 Android 设备的工具但它的服务端其实完整走了一遍 Android 的屏幕采集 硬件编码 流传输链路。服务端启动后会通过 MediaProjection 创建 VirtualDisplay把屏幕内容作为 Surface 输入交给 MediaCodec硬编码成 H.264 流再通过 adb 转发给 PC 端解码显示。这个过程在 PC 端的感知非常轻但在设备端它每时每刻都在做这些事情SurfaceFlinger 将屏幕帧送入 VirtualDisplay 的 BufferQueueMediaCodec 底层绑定 Rockchip 的rkvenc硬件编码器编码器拿到的是由内核 DMA-BUF 分配器提供的内存块每一块物理连续内存都被包装成一个dma_buf对象编码结束后dma_buf 的引用应该被正确释放内存归还给 CMA pool。只要中间任何一方少了一次引用释放这个 buffer 就不会真正归还。scrcpy 本身不会在正常运转时频繁创建和销毁编码器但一旦出现画面旋转、分辨切换、网络抖动导致会话重建、或者同时开多个投屏窗口等场景底层 MediaCodec 实例就会被反复拉起和释放。这时如果驱动在释放路径上漏了引用计数dma_buf 泄漏就会像滚雪球一样越来越大。我回头翻 scrcpy 版本用的是常见的 2.1.1。这个版本在多数手机上表现稳定但在 Rockchip 的 BSP 驱动面前稳定性完全取决于内核侧是否在关闭编码器时严格归还了所有 GEM/DMA-BUF 引用。2.2 控制变量实验关掉投屏世界安静了怀疑 scrcpy 以后我没有急着换固件而是先做了一组干净的控制变量实验。找了三台完全相同的工位机分别命名为 A、B、C固件版本、App 版本、网络环境都一样唯一区别是 scrcpy 的使用状态设备运行条件运行时长结果A只跑 MES App不启动 scrcpy72 小时一切正常无黑屏B启动 scrcpy但不人工操作保持静置约 50 小时界面开始卡顿触摸延迟明显C启动 scrcpy同时频繁切换窗口和旋转约 36 小时已经出现一次黑屏重启后继续复现A 台设备帮我彻底排除了 App 和固件自身的嫌疑。B 台设备证明了即使没有任何人工操作只要 scrcpy 在跑系统资源就会持续恶化。C 台设备则暴露出一个规律频繁触发编码器会话重建泄漏速度会成倍增加。在 C 台设备黑屏之前我每隔十分钟记录一次 DMA-BUF 数量命令非常简单adb shell su -c cat /sys/kernel/debug/dma_buf/bufinfo bufinfo_$(date %s).txt然后通过wc -l看条目数增长。正常状态下一台空闲设备的 dma_buf 条目数大约在 120 条左右其中和rkvenc相关的只有 2 到 4 条。但 scrcpy 连续跑 48 小时后条目数涨到 470 条rkvenc相关条目超过 120 条63 小时后条目数直接破千rkvenc相关条目 350 条以上紧接着系统就黑屏了。这组数据已经把整个案件的嫌疑人锁得死死的scrcpy 是触发的业务场景Rockchip 编码器驱动是实际的泄漏点。2.3 嫌疑锁定编码器会话反复重建光看 dma_buf 数量还不够我还想确认 MediaCodec 侧的会话是否真的在反复横跳。Android 系统提供了查看 Codec 状态的接口adb shell dumpsys media.codec | grep -E Codec|mRender|mState|mName | tail -120正常情况下scrcpy 只有一个连续运行的 H.264 编码会话dumpsys media.codec的输出应该稳定在一个 Codec 实例上。但我拿到的输出里出现了多次Released与Executing状态的交替甚至能看到同一时间存在两个 codec 实例的残留状态。配合 scrcpy 客户端那边的窗口切换动作基本可以还原出场景每次 PC 端调节窗口大小、切换省电模式、或 adb 网络短暂中断后scrcpy 都会重新连接并触发设备端 MediaCodec 重建。重建本身不是问题问题在于 Rockchip 的rkvenc驱动在释放旧会话时没有把每个 buffer 的 dma_buf 引用计数减到零。每次重建都留下几个无法回收的 GEM 对象日积月累就把 CMA 池吃干净了。3. DMA-BUF 泄漏的底层机制3.1 先搞懂 DMA-BUF 从哪里来、到哪里去DMA-BUF 是 Linux 内核里专门用于 DMA 缓冲区共享的一套机制Android 平台上大量多媒体通路都依赖它。硬件编解码器、GPU、显示控制器、ISP、Camera 等设备之间要共享物理内存又不能把用户态指针直接到处传于是内核把一块物理内存包装成一个dma_buf对象并给使用者发一个文件描述符大家通过 fd 来访问同一块内存。这块物理内存的生命周期由引用计数管理。每次使用者调用dma_buf_get()、dma_buf_map_attachment()或者通过 ion/dma-heap 分配 fd 时引用计数会加一每次关闭 fd、dma_buf_put()、dma_buf_unmap_attachment()时引用计数减一。当计数归零内核才会真正释放物理内存。用一个生活化的例子来理解这就像图书馆借书每本物理书对应一块内存。读者每次借书管理员在登记簿上记一笔借出1还书时记一笔归还-1。只有当登记簿上的数字变成零这本书才能重新回到书架供别人借。如果某个读者还书时偏偏没在登记簿上打勾那这本书就会一直处于借出状态别人永远借不到。驱动泄漏 dma_buf 引用本质就是在登记簿上少打了一个勾。在 Rockchip 平台上rkvenc编码器使用的内存通常来自 CMAContiguous Memory Allocator区域也就是内核启动时预留的一大块物理连续内存。媒体播放、大尺寸图像处理都靠它。当泄漏的 dma_buf 把 CMA 占满后任何新的内存分配申请都会失败不只是编码器连 SurfaceFlinger 的帧缓冲也拿不到内存最终表现就是黑屏卡死。3.2 Rockchip 编码器驱动为何会松手Rockchip 的硬件编码器驱动在 Android BSP 里通常由 MPPMedia Process Platform框架和rkvenc内核模块配合工作。正常的数据通路大致是MediaCodec 用户态调用底层 V4L2 M2M 接口驱动通过vb2框架申请 buffer每个 buffer 对应一块 GEM/CMA 内存打包成 dma_buf 返回给用户态编码结束后用户态关闭 fd驱动在stop_streaming或release时应该逐个释放 buffer。问题最常出现在释放路径。编码器驱动维护一个 buffer 队列如果stop_streaming时没把队列里所有 buffer 的dma_buf_fd关闭或者close时没有遍历完所有 attribution引用计数就不会归零。另一个常见原因是 MediaCodec 的输入 Surface buffer 与输出 buffer 生命周期不同步导致部分 buffer 的释放被延迟甚至漏掉。更隐蔽的是这类问题不会在每次会话关闭时 100% 触发往往取决于当时的 buffer 状态和硬件忙闲程度。这就解释了为什么不是每台设备都在同一时间黑屏而是有的 36 小时、有的 50 小时才出问题。资源泄漏类问题天然带有随机性这也是它难排查的另一个原因。3.3 证据链从 bufinfo 到 MemTotal 的对比数据为了让大家对泄漏严重程度有直观感受我把三台设备在实验结束时采集到的关键数字整理成一张表采集项正常基线App scrcpy 运行 48 小时即将黑屏前dma_buf 总条目数1214771023rkvenc 相关条目数2126351单个 rkvenc buffer 大小约 8MB约 8MB约 8MB系统可用内存3.1GB1.4GB约 90MBCMA Free 大小512MB约 180MB约 12MBrkvenc单个编码 buffer 的大小通常等于一帧 YUV 图像所需的内存比如 720p 分辨率大约需要 1.3MB1080p 大约需要 3MB如果再叠加编码器内部环形缓冲和多平面分配单次会话累计几十 MB 很正常。到了黑屏前的状态即使不做任何操作泄漏的 dma_buf 已经吞噬了绝大部分 CMA 内存系统连维持基本显示都做不到了。如果说前面还只是怀疑看完这组数据后我基本可以下断言这是一次典型的内核态 DMA 缓冲区泄漏泄漏源就是rkvenc编码器驱动与 scrcpy 会话重建逻辑共同作用的结果。4. 止血与治本三类解决方案4.1 短期止血调整 scrcpy 使用方式定位清楚之后第一步不是改内核而是先让产线恢复运转。针对 scrcpy 本身我做了几个调整第一限制帧率和码率。在 PC 端启动 scrcpy 时显式指定参数降低编码器的工作强度和 buffer 分配频率scrcpy --video-codech264 --max-fps15 --bit-rate4M --no-audio--no-audio可以省掉音频采集的额外 buffer--max-fps15把编码器负载降下来--bit-rate4M则避免码率过高导致内部缓冲膨胀。对于产线监控场景15 帧完全够用。第二修改投屏的使用习惯。不再让 scrcpy 长时间持续连接而是改成按需连接设备默认不投屏只有中控人员在 PC 端主动触发时才临时拉流观察完毕立刻断开。如果确实需要 7x24 小时监控就在中控加一个定时任务每隔 2 小时自动断开并重连 scrcpy。实测这样可以把单个设备黑屏的触发周期拉长到一周以上为后续根因修复争取时间。需要说明的是这些手段都只是减少泄漏速率并没有消除泄漏。降低帧率不会让驱动不再漏只会让漏得慢一点。真正干净的方案还在后面。4.2 中期方案补丁与自研轻量投屏针对rkvenc驱动的 dma_buf 泄漏正路是拿到 Rockchip BSP 的内核源码重点检查以下两个释放路径v4l2_close/vb2_streamoff时是否对每个 buffer 调用了dma_buf_put并关闭对应的 fdstop_streaming回调里是否遍历了所有挂起的 attribution并做了dma_buf_unmap_attachment。如果驱动是从旧版本 ION 框架迁移到新版本 DMA-BUF heap 的还要特别注意 ION fd 与 dma_buf fd 转换时是否引入了引用计数偏差。这类修复通常只需要在原厂最新 BSP 或内核主线里 diff 一下相关文件的变更历史就能找到补丁。对我们这种 ODM 项目来说最快的方式是直接找芯片原厂的 FAE 要到修复补丁或者把 BSP 内核升级到包含该修复的版本。如果短期内拿不到驱动补丁另外一个可行的替代方案是自研一个轻量投屏服务直接绕过 scrcpy 的特定会话管理逻辑使用 MediaProjection MediaCodec 实现最基本的屏幕推送。听起来工作量很大但其实核心代码量只有几百行申请 MediaProjection 权限、创建 VirtualDisplay、配置 MediaCodec 编码器、通过 socket 把 H.264 流推出去。这样做的好处是可以完全控制编码会话的创建和销毁时机避免 scrcpy 在某些边界情况下的异常重建行为。但这里必须泼一盆冷水如果根子上的驱动泄漏没修自研投屏也只是把泄漏触发频率降低了并不能根治。所以我把自研方案定位为应急替代而不是终点。4.3 长期保障dma_buf 数量监控与自动处置无论最终选择哪条路都建议在运维层面增加一道防线。dma_buf 泄漏不是只有 scrcpy 才会触发其他涉及硬件编解码、Camera、GPU 的业务也可能踩到类似的驱动问题。提前做好监控至少能在系统完全黑屏前发出告警。我写了一个简单的 shell 巡检脚本直接跑在工位机上后台每分钟记录一次关键指标#!/system/bin/sh # /data/local/tmp/monitor_dmabuf.sh while true; do total$(cat /sys/kernel/debug/dma_buf/bufinfo 2/dev/null | wc -l) venc$(grep -Ei rkvenc|vpu|hantro /sys/kernel/debug/dma_buf/bufinfo 2/dev/null | wc -l) mem$(cat /proc/meminfo | grep MemAvailable | awk {print $2}) echo $(date %F %T) dmabuf_total$total rkvenc$venc mem_avail_kb$mem sleep 60 done用nohup挂到后台输出重定向到/data/local/tmp/dmabuf_monitor.log。当rkvenc条目数超过预设阈值或者MemAvailable低于某个水位就触发中控告警甚至自动重启 scrcpy server 来释放部分资源。虽然自动重启只是治标但配合补齐驱动补丁之后的正常状态这套监控能保证任何新的内存泄漏苗头都被第一时间发现。5. 复盘与避坑实录5.1 排障顺序的一次深刻教训这次排障让我印象最深的一点是遇到 Android 设备黑屏卡死不要第一时间把矛头对准业务 App。App 导致的内存泄漏、ANR、OOM 很容易在应用层被定位到真正困难的往往在系统服务和内核侧。我当时浪费了整整两天时间在 App 的dumpsys meminfo、堆栈、WebView 缓存上面打转。如果第一天就去看内核 dmesg、拉一份dma_buf/bufinfo、再测一下设备的 CMA 水位整个排查时间可以压缩到半天以内。以后再做同类问题我的顺序一定是先看内核日志有没有分配失败和驱动报错再看系统级共享内存和 dma_buf 状态最后才轮到应用层。这不是反 App而是因为系统级资源耗尽往往会把真正的原因掩盖掉如果一开始只看应用层很容易被表象带偏。5.2 嵌入式产品线后续的改进清单经过这次事件我们对自己的产品流程做了几项调整虽然不是专门针对 scrcpy但相信对任何做 Android 工位机、Box、PDA 类产品的人都有参考价值固件引入任何远程投屏、录屏、视频编解码相关功能前必须先在目标芯片原厂的 BSP 上跑 72 小时压力测试重点观察 dma_buf 和 CMA 的变化趋势。所有设备出厂默认关闭 adb over network 和 scrcpy 常驻服务按需开启避免无谓的资源占用。产品固件里预置一个系统资源巡检服务上报内存、CPU、dma_buf、关键服务状态到中控台做到黑屏前发现而不是黑屏后抢救。在项目合同或需求阶段就把固件内核版本、BSP 分支、驱动 patch 归属写清楚避免出了问题各方互相推诿。5.3 最后一个实用技巧黑屏瞬间抓现场最后分享一个专门针对黑屏卡死类问题的技巧。这类问题最大的痛点是设备都已经黑屏卡死了很多日志还来不及落盘重启后现场就没了。所以一定要想办法在系统崩溃前保留现场。我通常会在排查阶段给设备提前接好串口并开启内核的早期日志输出。对于支持pstore/ramoops的 Rockchip 平台重启后可以通过以下目录找回上一次崩溃的残留日志adb shell ls /sys/fs/pstore/ adb shell cat /sys/fs/pstore/console-ramoops-0如果设备连 pstore 都没有就在系统还活着的时候每隔 10 分钟把/sys/kernel/debug/dma_buf/bufinfo和/proc/meminfo拷贝到/data/local/tmp/下的持久化目录。黑屏重启后对比最后两份快照基本就能判断出是不是内存耗尽。这个土办法在工业设备上非常有效因为工位机一般都有存储空间10 分钟一份全量快照也不会占多少容量。踩过这次坑之后我自己的习惯也改了凡是引入涉及硬件编解码的 Android 工具链第一件事就是确认底层驱动的 dma_buf 生命周期而不是想当然地信任上层应用的表现。经过这一轮整改我们这套工位机系统到现在已经稳定运行了三个月没有再生过一次黑屏。如果你也遇到类似症状建议先把你设备上的/sys/kernel/debug/dma_buf/bufinfo拉出来看一眼答案往往就在那几百行文本里。