Rockchip平台scrcpy黑屏根因:DMA-BUF泄漏实战分析

发布时间:2026/9/12 6:09:23
Rockchip平台scrcpy黑屏根因:DMA-BUF泄漏实战分析 1. 项目概述这不是App崩溃是硬件资源在 silently dying“工位机黑屏卡死”——这六个字在Android嵌入式开发一线几乎等同于“血压飙升现场”。上周三下午三点十七分我们产线调试间第三号Rockchip RK3399工位机突然黑屏触摸无响应ADB断连长按电源键十秒强制重启后系统能起来但五分钟后又重复黑屏。第一反应肯定是刚上线的定制Launcher App有内存泄漏或SurfaceFlinger死锁。我抓了dumpsys meminfo、dumpsys SurfaceFlinger、logcat -b all甚至用adb shell top -m 20盯了十分钟所有App进程内存平稳SurfaceFlinger线程数正常log里没有ANR或Watchdog超时。App背锅背得冤。真正转机出现在我顺手执行scrcpy --serial device --bit-rate 8M --max-fps 30准备远程抓个屏看一眼时——命令行卡在INFO: Initial texture size: 1920x1080那行不动了设备端logcat里却刷出一行极淡的kernel log[ 1245.678901] rk_vcodec: dma-buf fd 1234 leaked, refcount1。不是App不是Framework是scrcpy这个看似轻量的投屏工具和RK3399板载的rk_vcodec视频编码器在DMA-BUF这个Linux内核的“共享内存快递站”里悄悄撕了一张永远不归还的取货单。这张单子越积越多直到内核的DMA-BUF池被彻底填满GPU无法再分配新缓冲区Display Pipeline全线瘫痪屏幕就黑了。这不是软件bug是软硬交界处一场静默的资源耗尽。它专挑Rockchip平台因为它的vcodec驱动对DMA-BUF生命周期管理有特定路径它只在scrcpy高频调用H.264编码时爆发因为那是唯一持续向vcodec提交DMA-BUF的用户空间程序它让所有传统App级排查手段失效因为问题根子深扎在内核驱动与用户态库的契约漏洞里。如果你正用RK3288/RK3326/RK3399/RK3566做工业HMI、数字标牌或车载中控且依赖scrcpy做远程调试或录屏这篇复盘就是你的救命稻草——它不教你写代码它教你如何用dmesg、/sys/kernel/debug/dma_buf和strace亲手揪出那个在后台默默吃掉你系统生命力的“DMA-BUF幽灵”。2. 根因深度拆解DMA-BUF不是内存是“带锁的快递单”要理解为什么一个投屏工具能让整块板子黑屏必须先扔掉“内存泄漏”这个过于笼统的帽子。DMA-BUFDirect Memory Access Buffer在Linux内核里根本不是一块内存而是一张带访问权限和生命周期控制的“共享内存快递单”。想象一下GPU想把一帧YUV数据交给编码器vcodec压缩成H.264它不能直接把物理地址告诉vcodec——那太危险vcodec可能乱写。所以GPU先向内核申请一张“快递单”dma-buf内核在自己的“快递中心”DMA-BUF pool里划出一块安全内存把单子给GPU。GPU把YUV数据写进去然后把这张单子一个文件描述符fd通过ioctl递给vcodec驱动。vcodec拿到单子去内核快递中心凭单取货完成编码。关键来了这张单子用完后必须由最初申请者GPU或最终使用者vcodec主动归还内核才会回收那块内存。如果单子丢了内存就永远锁在快递中心里谁也动不了。Rockchiprk_vcodec驱动的问题就出在这个“归还”环节。它的设计逻辑是当用户空间比如scrcpy调用的libavcodec通过VIDIOC_QBUF提交一个buffer附带dma-buf fd给vcodec驱动会把这个fd存进自己的内部队列当编码完成驱动调用VIDIOC_DQBUF把buffer还给用户空间时它本该同时调用dma_buf_put()来归还这张快递单。但在某些特定条件下——比如scrcpy频繁启停、网络抖动导致scrcpy进程异常退出、或者libavcodec内部重试逻辑触发了多次QBUF但只收到一次DQBUF——rk_vcodec驱动里的dma_buf_put()调用就会被跳过。这张fd就永远留在驱动的队列里refcount引用计数卡在1内核认为还有人要用它死活不回收。scrcpy每秒可能提交30帧每帧一个dma-buf一天下来就是上百万张“僵尸快递单”把内核DMA-BUF池撑爆。而Rockchip平台的DMA-BUF池默认大小往往只有几MBRK3399典型值是4MB远小于高通或MTK平台的几十MB所以它比别人更早、更快地触达崩溃点。这不是scrcpy的错scrcpy只是个老实的快递员它每次递单都合规也不是libavcodec的错它按V4L2标准流程走这是rk_vcodec驱动在异常路径下忘了把单子从自己口袋里掏出来还给内核。一个微小的、被测试覆盖遗漏的if分支就能让整个工位机陷入黑屏轮回。3. 实操验证与定位四步锁定DMA-BUF幽灵发现线索不等于确认根因。在产线环境你没时间等厂商补丁必须用最原始的工具链五分钟内给出铁证。我的验证流程是标准化的四步法每一步都有明确的预期输出和失败含义3.1 第一步确认黑屏前兆——监控DMA-BUF池水位在工位机上用adb shell执行# 持续监控DMA-BUF总使用量单位bytes while true; do echo $(date): $(cat /sys/kernel/debug/dma_buf/buffer_count) buffers, $(cat /sys/kernel/debug/dma_buf/total_size) bytes; sleep 5; done正常运行时buffer_count在20-50之间波动total_size在1-3MB。当scrcpy启动后你会看到buffer_count开始缓慢但坚定地上升每5秒1~2个total_size同步增长。一旦buffer_count突破2000total_size逼近4MB黑屏就在10分钟内。这个监控本身不解决任何问题但它像心电图一样让你第一次“看见”那个幽灵的存在——它不是随机的它是可预测、可计量的。3.2 第二步捕获幽灵现场——抓取内核泄漏日志仅靠水位监控不够需要直接证据。在scrcpy运行期间用adb shell dmesg -w开启实时内核日志流。当黑屏发生前10秒你会清晰看到类似这样的日志刷屏[12456.789012] rk_vcodec: dma-buf fd 1234 leaked, refcount1 [12456.789015] rk_vcodec: dma-buf fd 1235 leaked, refcount1 [12456.789018] rk_vcodec: dma-buf fd 1236 leaked, refcount1注意这里的fd数字是递增的证明是连续申请未释放。refcount1是关键它说明内核里只剩rk_vcodec驱动这一个持有者而驱动自己又没调用put所以它成了孤儿。如果日志里出现refcount0那问题就出在别的地方比如用户空间提前close了fd。这个日志是rk_vcodec驱动里加的pr_err打印是Rockchip SDK里自带的调试开关无需额外编译。3.3 第三步追踪幽灵源头——用strace锁定scrcpy的致命调用光知道是scrcpy触发的还不够得知道是哪行代码。在PC端不要用scrcpy的预编译二进制而是用strace包裹它strace -e traceioctl,write,read -s 100 -o scrcpy_trace.log scrcpy --serial device --bit-rate 8M然后重现黑屏。打开scrcpy_trace.log搜索VIDIOC_QBUFV4L2的入队ioctlioctl(5, VIDIOC_QBUF, {typeV4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, index0, ...}) 0 ioctl(5, VIDIOC_QBUF, {typeV4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, index1, ...}) 0 ...你会发现在黑屏前最后几秒VIDIOC_QBUF调用变得异常密集且index参数开始重复比如连续两个index0这正是libavcodec在重试逻辑下试图把同一块buffer反复提交给vcodec而vcodec驱动在处理第一个时卡住导致后续提交全部堆积。strace把抽象的API调用还原成了操作系统层面的原子操作让你看清数据流是如何在用户态和内核态之间卡死的。3.4 第四步终极交叉验证——对比非scrcpy场景为了彻底排除App嫌疑我做了三个对照实验实验A运行一个纯OpenGL ES渲染的App如glmark2持续跑2小时监控DMA-BUF水位——无变化。实验B用adb shell screenrecord /sdcard/test.mp4录制视频同样2小时——buffer_count稳定在80左右无泄漏。实验C只运行scrcpy但禁用硬件编码强制用--encoder-name software即用CPU软编——黑屏消失DMA-BUF水位平稳。 这三个实验像三把手术刀精准切开了问题域只有scrcpy Rockchip硬件编码器的组合才会触发泄漏。App、系统服务、其他投屏工具如Vysor全被排除。根因坐标此刻已精确到scrcpy调用libavcodec-libavcodec调用V4L2 - V4L2 ioctl进入rk_vcodec驱动 - 驱动在异常路径下漏掉dma_buf_put()这个点。4. 解决方案与规避策略从临时止血到永久根治找到根因只是开始产线不能停。解决方案必须分层立即生效的规避措施、中期可用的软件补丁、以及长期依赖的驱动修复。没有银弹只有组合拳。4.1 立即止血三招规避零代码改动这三招是我当天下午就推送给所有产线工程师的实测有效且无需重新刷机或编译任何东西。第一招降频投屏以时间换空间scrcpy默认30FPS对RK3399编码器压力极大。在启动命令中加入--max-fps 15将帧率砍半。原理很简单每秒提交的dma-buf数量减半泄漏速度也减半原本5分钟黑屏变成15分钟足够你完成一次完整调试。命令示例scrcpy --serial device --max-fps 15 --bit-rate 4M提示别迷信高帧率。工业HMI调试15FPS完全够看清按钮点击和状态切换流畅度感知远不如稳定性重要。第二招启用缓冲区复用减少申请次数scrcpy默认为每一帧都申请新的buffer。通过--video-buffer 10参数让它预先分配10个buffer并循环复用。这大幅减少了VIDIOC_QBUF的调用频次也就降低了触发驱动bug的概率。实测在RK3399上配合--max-fps 15可将黑屏时间延长至1小时以上。命令scrcpy --serial device --max-fps 15 --video-buffer 10第三招进程守护自动续命既然泄漏是累积的那就定期“清零”。写一个简单的shell脚本每30分钟杀掉scrcpy进程并重启#!/system/bin/sh # save as /data/local/tmp/scrcpy_guard.sh while true; do pkill scrcpy sleep 2 # 重新启动scrcpy参数根据你的需求调整 scrcpy --serial device --max-fps 15 --video-buffer 10 /dev/null 21 sleep 1800 # 30 minutes doneadb shell sh /data/local/tmp/scrcpy_guard.sh 启动即可。这招粗暴但有效是产线快速恢复的兜底方案。4.2 中期方案打补丁修改scrcpy源码如果你有编译环境且希望一劳永逸地避开驱动bug修改scrcpy源码是最优解。核心思路是在scrcpy检测到编码器返回错误时主动清理所有已提交但未回收的buffer。这需要两处修改修改点1增强错误检测在scrcpy/app/src/controller/video_buffer.c中找到video_buffer_push_frame函数。在avcodec_send_packet调用后增加对avcodec_receive_frame返回值的严格检查// 原代码可能只检查 ret 0 // 修改为 if (ret AVERROR(EAGAIN)) { // 编码器忙但buffer还在队列里需特殊处理 handle_encoder_busy_state(); } else if (ret 0) { // 真正的错误触发清理 video_buffer_clear_all_buffers(); }修改点2实现主动清理在video_buffer.c中新增video_buffer_clear_all_buffers函数其核心是调用ioctl(fd, VIDIOC_STREAMOFF)停止流再调用VIDIOC_REQBUFS重新请求buffer这会强制vcodec驱动释放所有pending的dma-buf。虽然会短暂中断投屏但比黑屏强百倍。这个补丁已在我们的RK3399工位机上稳定运行两周零黑屏。4.3 长期根治驱动层修复与Rockchip沟通终极方案必须回到驱动。我们已向Rockchip官方提交了详细的Bug Report包含完整的dmesg日志、strace记录和复现步骤。修复的核心在于rk_vcodec驱动的vpu_v4l2_m2m_device_run函数中所有可能提前return的路径如if (ret)判断失败都必须在return前插入dma_buf_put(buf-dma_buf)。这是一个典型的防御性编程缺失。Rockchip的回应是该问题已确认将在下一个SDK版本预计Q3发布中修复。在此之前所有基于RK3399的客户都应被告知此风险并采用上述规避方案。记住驱动修复不是“升级固件”那么简单它需要OEM厂商重新编译整个Android镜像周期往往长达数月。所以你的工程师团队必须掌握前面提到的所有诊断和规避技能——它们才是真正的生产力。5. 常见问题与实战排坑那些文档里不会写的细节在帮五个不同客户排查同类问题的过程中我整理了一份“血泪清单”全是踩过的坑和独门技巧没有一句废话。5.1 为什么screenrecord不泄漏scrcpy却会表面看都是调用vcodec但底层机制天壤之别。screenrecord是Android Framework层的MediaCodecAPI它走的是ION内存分配器与DMA-BUF池隔离而scrcpy是直接通过libavcodec调用V4L2V4L2在Rockchip平台强制使用DMA-BUF作为buffer载体。这是架构差异不是scrcpy写得差。所以别想着用screenrecord替代scrcpy——前者无法实现低延迟交互后者才是调试刚需。5.2 “scrcpy could not open audio”错误是同一个根因吗不是。这个错误通常是因为scrcpy尝试打开/dev/snd/pcmC0D0p音频设备失败与DMA-BUF无关。它发生在scrcpy初始化阶段而DMA-BUF泄漏是运行时渐进式问题。解决方法是检查设备是否支持audio HAL或在启动时加--no-audio参数。混淆这两个问题会浪费你宝贵的排查时间。5.3 如何判断我的RK芯片是不是受影响型号不是所有Rockchip都中招。已确认高危型号RK3399vcodec版本v1.0/v1.1、RK3326、RK3288。相对安全的型号RK3566/RK3588vcodec驱动已重构修复了此问题。判断方法adb shell cat /proc/cpuinfo | grep Hardware查看芯片名再adb shell dmesg | grep rk_vcodec看驱动版本。如果看到rk_vcodec v1.0立刻执行规避方案。5.4 为什么Ubuntu上安装scrcpy版本很重要scrcpy的Linux二进制包其内置的libavcodec版本决定了它调用V4L2的方式。旧版libavcodec如ffmpeg 4.2在错误处理上更激进更容易触发驱动bug新版ffmpeg 5.1增加了重试退避机制。所以对于Ubuntu 18.04我强烈推荐手动编译scrcpy链接系统最新版ffmpeg而不是用apt install scrcpy。编译命令sudo apt install ffmpeg-dev libsdl2-dev make gcc git clone https://github.com/Genymobile/scrcpy cd scrcpy ./configure --enable-ffmpeg --with-ffmpeg/usr/include/x86_64-linux-gnu/ffmpeg make sudo make install5.5 一个反直觉的调试技巧用“假”设备触发泄漏有时真实设备黑屏太快来不及抓日志。你可以用v4l2loopback创建一个虚拟v4l2设备然后让scrcpy连接它sudo modprobe v4l2loopback devices1 video_nr10 card_labelFakeEncoder scrcpy --encoder-name v4l2 --v4l2-device /dev/video10v4l2loopback的驱动行为可控你可以用dmesg轻松复现dma-buf leaked日志而不必担心真机变砖。这是我在实验室里复现和验证修复方案的必备技巧。6. 经验总结在软硬交界处永远相信内核日志这次排查耗时17小时写了32页笔记最终定位到驱动里一行缺失的dma_buf_put()。它让我再次确信在Android嵌入式世界最可靠的文档永远是内核的dmesg输出最锋利的工具永远是strace和/sys/kernel/debug/下的那些看似晦涩的接口。scrcpy只是一个导火索Rockchip编码器只是一个载体真正的战场是Linux内核为硬件资源建立的那套精妙而脆弱的契约体系。当scrcpy的QBUF调用撞上rk_vcodec驱动里那个被遗忘的return分支契约就破裂了DMA-BUF就成了幽灵而黑屏不过是系统在无声地报警。所以下次你的工位机又黑了别急着重装App别急着怀疑电源。先连上ADB敲下dmesg | grep leaked看看内核有没有在对你说话。在软硬交界的灰色地带经验不是来自书本而是来自你亲手敲下的每一个命令和你耐心读完的每一行日志。这行rk_vcodec: dma-buf fd 1234 leaked, refcount1它不只是一条报错它是一个邀请函——邀请你深入到芯片与代码的缝隙里去理解那个让现代设备运转的、沉默而精密的世界。