尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android工位机周期性黑屏卡死:一次DMA-BUF泄漏定位与修复全记录

Android工位机周期性黑屏卡死:一次DMA-BUF泄漏定位与修复全记录 凌晨两点半产线值班群连着弹了三张现场照片3号工位机黑屏、触摸无响应、ADB 也掉线了。这已经是一周内的第三次每次都是重启恢复但每次重启后都撑不过 15 个小时。刚开始我几乎认定是自研的报工 App 内存泄漏连续换了三个测试版本问题纹丝不动。直到我丢掉应用层思维、把目光转到内核日志才发现藏在 App 背后的真正元凶——scrcpy 投屏进程 Rockchip 编码器驱动之间持续十几个小时的 DMA-BUF 泄漏。这篇文章就是完整的排查复盘从现象、证据链到根因和修复全部摊开来讲。如果你是做 Android 工位机、广告机、自助终端这类 7x24 小时设备的开发或运维这可能是你迟早会遇到的一类问题。它不只在 RK 平台出现全志、Amlogic 的编码器同样存在类似隐患。文章里的思路和命令可以直接抄到你的项目里。1. 现象与第一轮排查为什么我一开始咬着 App 不放1.1 工位机的运行环境与黑屏的真实面貌这台设备是典型的线边工位机Android 系统金属外壳挂在产线立柱上屏幕常亮显示工单和物料信息员工通过触摸屏完成报工、叫料、异常上报。它的核心特点是无人值守、常年不关机、ROI 敏感——一旦停机整条线的看板数据就断档损失按分钟计算。所谓黑屏其实有三种不同的状态排查方向完全不同屏幕完全是黑色的没有任何 LED 背光这往往是显示面板或 LVDS/eDP 信号问题背光亮但图像全是噪点或冻结常见于显示控制器异常背光亮、画面停留在某个界面但触摸完全没反应同时 ADB 也进入半死状态——这是系统级僵死。我们这次遇到的是第三种。刚开始症状不算严重只是触摸偶尔失灵 10 秒又恢复到后期直接变成整个设备无响应adb shell执行任何命令都卡在等待只能断电重启。这里有一个关键细节重启时间点非常规律稳定运行 11~15 小时后必现一次。规律性复发意味着存在确定性的泄漏路径而不是随机的硬件波动。1.2 第一轮排查logcat 干净得吓人App 换掉也无济于事既然怀疑 App我第一件事就是抓 logcat。结果出乎意料在死机前一个小时内logcat 里几乎没有来自我们 App 的异常堆栈只有一些低频的 GC 日志和系统警告。连 ANR 都没有。这台设备的内存配置本身就不高2GB一开始团队里有人信誓旦旦说是我们自研 App 的 Bitmap 泄漏我并没有立即否定而是做了两个实验。实验一把自研 App 完全卸载手机桌面空跑模拟日常点击和滑动。结果跑到第 13 个小时黑屏照旧。实验二换另一台 App 完全不相关的测试机内存从 2GB 加到 4GB用同样的操作脚本压测。寿命确实延长到了 20 小时左右但最终依然卡死。这个结果非常微妙内存加大只是拖延了死亡时间并没有改变必然会死的结局。说明泄漏是真实存在的但它不消耗普通 Java 堆内存而是消耗了另一种更底层的东西。1.3 一个差点被忽略的关键变量scrcpy 投屏连接后来复盘现场部署情况时我发现一个所有人都习以为常的部署动作为了远程看板和技术支持产线上长期挂着一路 scrcpy 投屏。主管的电脑上 scrcpy 窗口经常一开就是一整天设备端对应的 server 进程也就常驻一整天。我做了个对照实验把 scrcpy 完全停掉设备连续跑了三天一次黑屏都没出现。再一开启 scrcpy不到 12 小时又死。到这里矛头已经非常明确地指向 scrcpy但 scrcpy 本身只是一个投屏工具它凭什么能让整机卡死接下来的问题是——它是直接凶手还是它背后的某个系统组件是凶手2. 倒查内核dmesg、DMA-BUF 与物理内存的证据链2.1 抓 dmesg 的时机与正确姿势既然用户态日志一片祥和那问题必然出在内核态或系统服务层。第一次死机后我们犯了个错误重启完再抓日志结果什么都没有。这类周期性卡死问题必须预埋抓取手段在设备濒临崩溃的窗口期抢日志。我采用的方案很简单写一个后台循环脚本每 5 分钟执行一次adb shell echo --- /dev/kmsg; date; cat /proc/meminfo; cat /proc/buddyinfo把输出重定向到 PC 端文件。同时对关键设备节点做一张快照表每 10 分钟采样一次。如果你的设备有串口UART直接接串口抓内核日志是更好的选择。工位机主板一般都会预留调试串口这次我们运气不错主板上有 RX/TX/GND 三个点接了一个 USB 转 TTL 小板从启动到崩溃全程记录。但是在最接近死机的几分钟里串口日志同样会被刷屏大量分配失败的报错重复出现一眼就能看出是内存子系统在挣扎。2.2 dmesg 里那几条一眼定生死的日志在崩溃前约 30 分钟dmesg 里开始陆续出现下面几类日志[ 7153.123456] vpu_service: alloc dma buffer failed, size3145728 [ 7153.129871] cma: cma_alloc: alloc failed, req-size: 3145KB, retry later [ 7156.887654] ion: dma_buf buf_notify: add new buffer failed, out of memory [ 7241.000123] Out of memory: Kill process 523 (surfaceflinger) score 795 or sacrifice child这三条信息拼起来的信息量很大vpu_service、ion、dma_buf同时出现说明视频编码器在申请 DMA-BUF 时已经拿不到内存cma_alloc分配失败说明系统连续物理内存区域CMA已经耗尽surfaceflinger被 OOM 杀掉这是黑屏的直接导火索——Android 的 UI 合成核心进程没了屏幕自然全黑。我把dmesg里的日志按时间线整理后发现最早出现分配失败是在死机前约 2 小时而且越到后面失败频率越高。这像是某个东西在持续消耗内存并且从不归还直到把整个 CMA 区域吃干抹净。2.3 /sys/kernel/debug/dma_buf/bufinfo泄漏的可视化铁证内核提供了查看所有 DMA-BUF 缓冲区的接口也就是 debugfs 下的bufinfo文件。前提是内核开启了CONFIG_DEBUG_FS并且已挂载 debugfsadb shell mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/dma_buf/bufinfo /sdcard/bufinfo_before.txt正常情况下一个 Android 工位机运行 1 小时整个系统累积的 DMA-BUF 数量应该稳定在几百个。我在设备刚开机时采样一次数量约 320等到第 10 小时再采样直接变成了 6800第 13 小时采样时超过 15000而设备已经接近卡死。看具体字段size flags mode exp_name pid comm 0x003ee000 0x00000002 RW: rockchip-mpp 1234 scrcpy_server 0x0027c000 0x00000002 RW: rockchip-ion 523 surfaceflingerexp_name这一列非常重要它直接告诉你是谁导出的这块 DMA-BUF。在这个案例里rockchip-mpp和rockchip-ion的条目占了 90% 以上而对应进程名写着scrcpy_server。到这里scrcpy已经不仅仅是一个可疑变量它和Rockchip 编码器以及DMA-BUF这三者形成了完整的证据闭环。3. 根因锁定Rockchip 编码器会话的 DMA-BUF 引用泄漏3.1 DMA-BUF 到底是什么为什么编码器非用不可DMA-BUF 是 Linux 内核提供的跨设备共享内存机制专门用来让不同硬件模块之间传递缓冲区。Android 设备里GPU、显示控制器Display、视频编码器VPU、ISP 这些硬件都要访问物理内存但它们不能像 CPU 那样直接访问虚拟地址而是需要拿到一块物理连续或者至少总线地址固定的内存。我有一个百讲不厌的类比DMA-BUF 就像公司会议室的公共插座。GPU、VPU、Display 都是要插电的设备DMA-BUF 就是插座本身。设备每次使用前都要插上拿引用用完后必须拔掉放引用。如果有人插了不拔会议室最终会变成一个插满了充电器却没有任何可用插座的地方。编码器工作的时候要分配三种类型的 DMA-BUF输入帧缓冲存放屏幕采集到的 YUV 图像、输出码流缓冲存放 H.264 编码结果、运动估计参考缓冲存放参考帧。一次新建的 MediaCodec 编码会话大约需要 6~10 个 DMA-BUF总大小在 20MB 到 60MB 之间。这个数量高不高如果会话正常关闭这些 buffer 全部会被释放但如果会话关闭路径上有引用计数 bug那每新建一次会话系统里就多出几十 MB 永远无法回收的内存。3.2 逐步缩小范围从进程排查到内核模块拿到 bufinfo 之后我把问题拆成了两个子问题是哪个进程在分配以及是谁没释放。从 bufinfo 的comm字段看分配者主要是scrcpy_server这是 scrcpy 在设备端运行的 Java 进程。但分配者并不等于泄漏者。我做了另一个实验把 scrcpy server 正常退出不是强杀设备后等待 10 分钟再查看 bufinfoscrcpy_server对应的条目居然还在数量只减少了一小部分。这就很说明问题了。如果是普通的进程内存泄漏进程一退出内核会自动回收该进程所有用户态资源。但现在进程已经退出内存仍然没有被归还说明泄漏点在内核驱动而不是 Java 层。驱动在某个操作路径上把 DMA-BUF 的引用计数加了 1之后却从来没有减 1。这就是典型的 dma_buf reference leak。我还检查了/proc/pid/fd目录下文件描述符的数量发现 scrcpy server 的 fd 总数在 500 以内并没有异常膨胀。这进一步验证了不是文件描述符泄漏而是内核驱动层面的引用计数泄漏两者的定位方法完全不同。3.3 与 surfaceflinger、CMA 和黑屏的最终因果链我把整条因果链串起来解释给团队听scrcpy 创建编码会话驱动通过 DMA-BUF 分配内存在某些操作后面会细说下驱动没有释放 DMA-BUF 的引用随着投屏时间变长泄漏的 buffer 越积越多彻底吃掉 CMA 连续内存区域当 surfaceflinger 需要分配帧缓冲区时cma_alloc拿不到物理连续内存申请失败系统内存压力到达阈值内核对高内存进程开刀surfaceflinger 被 OOM 杀掉surfaceflinger 一死屏幕失去合成输出于是出现假死黑屏。这也能解释为什么加大内存只能延缓而不是避免问题4GB 设备比 2GB 设备能多撑几小时但泄漏速率不变早晚还是会吃满。如果你遇到 Android 设备周期性卡死重启后又能恢复且时间间隔高度规律我强烈建议你第一时间检查 DMA-BUF 和 CMA 的状态而不是只在 logcat 里找 App 的崩溃堆栈。4. scrcpy 为什么脱不了干系投屏链路与编码器生命周期4.1 scrcpy 的工作原理一个小巧但极深的调用链很多人在 PC 上双击 scrcpy 就完事了没有意识到它背后在设备端做了多少事情。scrcpy 的执行链路大致是adb shell在 Android 设备上启动一个 server 进程新版包名是com.genymotion.scrcpy.server这个 server 进程申请MediaProjection录屏权限创建一个VirtualDisplay虚拟显示的输出 Surface 直接接到MediaCodec编码器的输入 SurfaceMediaCodec 底层通过 ACodec / OMX / C2 组件调用 Rockchip 的 VPU/MPP 驱动编码出的 H.264 码流通过 adb tunnel默认转发到本机 27183 端口送回 PC 端显示。所以 scrcpy 不是一个简单的截图工具它本质上是在设备端持续运行一个硬件编码器会话。只要 scrcpy 窗口开着编码器就一刻不停地在工作。工位机的常开投屏等于让编码器永远处于高负载状态。4.2 MediaCodec → ACodec → MPP一次编码器会话的生命周期Rockchip 新平台RK3568/RK3576/RK3588的硬件编码器由 MPPMedia Process Platform驱动管理设备节点是/dev/mpp_service。一次完整的编码会话生命周期如下创建会话MediaCodec 调用configure()传入编码格式、分辨率、码率底层驱动根据参数计算 buffer 个数和大小并分配对应的 DMA-BUF。运行会话屏幕帧通过 Surface 不断送入编码器的输入 buffer编码器把 H.264 输出到输出 buffer用户态不断取走。销毁会话MediaCodec 调用release()驱动遍历该会话的所有 buffer逐一调用dma_buf_put()释放引用。在这个案例里问题出在销毁会话这一步。当 scrcpy 的编码会话在非理想条件下被销毁时驱动中释放 DMA-BUF 的逻辑存在 hole。具体来说是session teardown 路径上漏掉了对dma_buf_put的调用导致部分 buffer 的引用计数永远不为 0被内核当作仍然在使用而一直保留。旧平台 RK3288/RK3399 上用的是/dev/vpu-service也大概率存在同类问题而且那个时代的 ION/DMA-BUF 补丁更不完善。4.3 哪些操作最容易触发泄漏通过反复构造实验我总结出以下操作最容易让编码器会话进入异常泄漏路径触发场景实际发生频率泄漏严重程度scrcpy 窗口被强制关闭再重新连接高每次新增 5~10 个 buffer 不释放设备屏幕旋转 90/180/270 度中编码会话需要重建旧会话释放不彻底scrcpy 网络闪断后自动重连高与强制关闭类似反复重连 反复泄漏同时开多个 scrcpy 窗口多屏低呈线性叠加泄漏投屏过程中切换 USB 线或 USB 掉线中驱动感知不到客户端已断开会话悬挂产线上的实际情况是管理员的 scrcpy 窗口一开一整天中间会因为锁屏、插拔 USB、网络抖动等原因断线重连。每重连一次泄漏一批屏幕旋转一次再泄漏一批。十几个小时下来就足以把 4GB 内存拖垮。4.4 升级 scrcpy 版本有帮助但不是根本解改到 scrcpy 新版本后我发现设备端 server 对连接断开时的处理规范了一些确实能减少泄漏触发频率。但别高兴太早scrcpy 的改进只是让上层更加文明地关闭 MediaCodec并不能修复 Rockchip 驱动层的引用计数漏洞。有些泄漏是驱动 bug上层再规范也绕不过去。所以在 Andorid 设备的疑难杂症里上层文明永远覆盖不了底层 bug。如果底层驱动的 teardown 路径不修复scrcpy 无论怎么升级都只是降低泄漏速度而不能杜绝泄漏。5. 修复落地与验证驱动补丁优先应用层兜底兜得住5.1 首选的根治方案合入厂商 BSP 的内核/驱动补丁定位到 Rockchip MPP 编码器驱动的 DMA-BUF 引用泄漏后最靠谱的修复方式是升级 BSP 内核或者向原厂要针对性的补丁。这里我说找原厂而不是自己改是有原因的Rockchip 的 VPU/MPP 是闭源或半闭源模块代码里有大量硬件寄存器操作和历史遗留的兼容逻辑非原厂人员贸然改驱动很可能把编码稳定性 or 画质搞坏风险远大于收益。我们最后走的路径是把整体 BSP 从 kernel 4.4 升级到厂商发布的新 LTS内部包含 MPP 驱动的多处 DMA-BUF 修复同时在 release note 里看到了修复 VPU 会话销毁时 dma_buf 泄漏这样的描述。这条修复的核心逻辑我理解下来其实很简单在 session teardown 时对没走到正常 put 路径的所有 buffer 做一次兜底遍历释放。内核层面升级后我们没有马上切生产而是在实验室先跑了完整的回归测试。除了投屏还专门做了 200 次断开重连 旋转屏幕 切换 App的压力组合确认编码延迟、花屏、内存占用全部正常后才推送到产线工位机。5.2 不能立刻升级内核的临时方案应用层怎么止血现实里不是每个团队都能第一时间拿到厂商补丁特别是 ODM 定制设备或者已经停产的项目。这种情况下我建议至少做三层止血第一层限制 scrcpy 的资源消耗。启动 scrcpy 时加上参数限制编码规格scrcpy --max-size 1280 --video-bit-rate 4M --max-fps 30降低分辨率、码率和帧率后单个编码会话占用的 DMA-BUF 总量会明显下降泄漏同一数量 buffer 所消耗的内存也会成比例减少。能延长设备从开机到崩溃的窗口从 15 小时拉长到两三天的量级但不能根治。第二层给 scrcpy server 挂一个防呆看门狗。写一个简单脚本在设备端检测 scrcpy server 的运行时长超过阈值就强制结束进程由 PC 端的 adb 拉起重连#!/system/bin/sh while true; do PID$(pidof com.genymotion.scrcpy.server) if [ -n $PID ]; then START$(stat -c %Y /proc/$PID) NOW$(date %s) ELAPSED$(( (NOW - START) / 3600 )) if [ $ELAPSED -ge 4 ]; then kill -9 $PID fi fi sleep 600 done每 4 小时杀一次 server强制驱动清理会话K 掉泄漏的累积。虽然粗暴但在无法升级内核时是最有效的兜底方案。注意杀 server 会短暂中断投屏最好在管理员休息时间执行或者写成先检测当前是否有人操作再杀。第三层不投屏时彻底关掉 scrcpy。我见过很多项目scrcpy 窗口在 PC 端挂着人已经下班了设备端还在持续编码投屏白白消耗功耗和内存。管理员不在现场时直接断开会话连接是最省钱、最省心的策略。5.3 验证数据修复前后对比驱动补丁合入后我把同一台工位机按产线真实工况跑了 72 小时包含全天候 scrcpy 投屏和定时的屏幕旋转。采样结果如下采样时间点DMA-BUF 数量MemAvailable / MB设备状态开机 1 小时修复前3151520正常开机 13 小时修复前18000280接近崩溃开机 1 小时修复后3051580正常开机 24 小时修复后3181525正常开机 72 小时修复后3261510正常修复后的 DMA-BUF 数量和可用内存基本是一条直线不再有抬升趋势。这套数据也侧面验证了泄漏确实出在驱动层跟业务 App 的关系确实不大。6. 这类问题的通用排查工具与经验总结6.1 工位机黑屏卡死的第一步判断灯光、日志、内存三连以后你再遇到工位机或 Android 自助设备黑屏先不要急着怀疑 App按下面这个顺序快速分流看背光背光不亮优先查屏幕供电和 LVDS/eDP 排线背光亮但画面冻结进入第 2 步看内核dmesg | grep -i oom\|cma\|dma_buf\|ion有没有大量分配失败看内存cat /proc/meminfo看MemAvailable和MemFree再看cat /proc/buddyinfo看物理内存碎片和连续内存耗尽情况看 DMA-BUFcat /sys/kernel/debug/dma_buf/bufinfo | wc -l统计 buffer 总数如果这个数字随运行时间单调递增且不回归那就是典型的泄漏。这三步不会直接解决所有问题但能把显示链路问题、用户态内存泄漏、内核态资源泄漏这几类完全不同的方向快速区分开。避免团队在一开始就在错误方向上浪费两三天。6.2 DMA-BUF 泄漏排查速查表我把这次用到的排查命令整理成了一张速查表建议直接收藏目的命令查看全局 DMA-BUF 列表cat /sys/kernel/debug/dma_buf/bufinfo统计 DMA-BUF 总数cat /sys/kernel/debug/dma_buf/bufinfo | wc -l只看某个驱动导出者cat /sys/kernel/debug/dma_buf/bufinfo | grep rockchip-mpp查看进程占用的 buffer 数ls -l /proc/pid/fd | grep dmabuf | wc -l查看 CMA 使用量cat /proc/cma_info部分内核或cat /proc/meminfo | grep -i cma实时追踪内存分配失败adb shell echo 1 /proc/sys/vm/oom_dump_tasks dmesg -w启用 debugfsmount -t debugfs none /sys/kernel/debug有几个坑要提醒第一bufinfo不是所有内核都默认开启如果文件不存在确认CONFIG_DEBUG_FS已打开第二bufinfo里的pid列在某些内核版本中可能显示为 0不要慌comm列和exp_name列往往更有参考价值第三采样要有时间连续性不要只抓一次。真正的泄漏需要看趋势单帧快照说明不了问题。6.3 给同行的心理建设最不可能的地方往往是最需要盯紧的地方这次排查给我最大的感触是在一个看似稳定的系统里最容易被忽视的管理工具反而是最危险的常驻进程。scrcpy 在大家眼里是一个调试辅助工具谁会想到它能把整台工位机搞死但正是因为它常驻、它高频使用编码器、它又不属于业务 App 的常规监控范围才让问题隐藏了好几个星期。现在我做嵌入式 Android 设备项目启动脚本里一定会加两个东西一个是定时采集dma_buf/bufinfo数量的监控另一个是记录MemAvailable和CMA抖动的日志。成本很低但在下一次非线性增长出现时它能帮你快速判断到底是谁在吃掉系统资源。另外我也想说一句自查的话不要一提卡死黑屏就先甩锅给硬件或者第三方工具。很多系统底层问题会伪装成各种奇怪的应用症状但内核日志和内存分配行为不会说谎。把证据链拉完整再谈方案永远是解决这类问题的最短路径。
返回列表