十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Rockchip VPU DMA-BUF泄漏导致Android黑屏根因分析

Rockchip VPU DMA-BUF泄漏导致Android黑屏根因分析 1. 项目概述这不是App崩溃是底层硬件资源在 silently dying“工位机黑屏卡死”——这六个字在Android嵌入式开发一线几乎等同于“血压飙升现场”。尤其当它发生在Rockchip平台的工业级工位机上屏幕突然变黑、触控失灵、adb shell无响应、甚至物理重启都需长按电源键12秒以上那种窒息感我经历过三次。第一次以为是App内存泄漏花了两天逐行review Java/Kotlin代码第二次怀疑是SurfaceFlinger异常抓了十几份dumpsys gfxinfo和SurfaceFlinger log结果全是“正常运行中”第三次我决定不看App层直接蹲进内核日志里翻——这才发现真正杀死系统的根本不是任何一行Java代码而是一个看似无害的调试工具scrcpy和它背后悄悄吃掉DMA-BUF句柄的Rockchip视频编码器驱动。这个标题里的关键词每一个都不是装饰Android是运行环境但不是问题表层scrcpy是触发器不是罪魁祸首Rockchip是芯片平台决定了硬件行为边界DMA-BUF是核心泄漏点是Linux内核为零拷贝视频传输设计的共享内存机制而编码器——特指RK3399/RK3566/RK3588系列SoC内置的VPUVideo Processing Unit硬件编码模块——才是那个在无人监管下持续申请、却从不释放DMA-BUF buffer的“沉默杀手”。它不报错、不panic、不OOM kill只是让系统可用DMA-BUF数量缓慢归零直到SurfaceFlinger因无法分配新buffer而彻底僵死屏幕黑得像关机但CPU仍在跑、网络还在通、串口仍有log——这种“半死不活”的状态比蓝屏更难诊断。如果你正在用Rockchip方案做工业HMI、车载中控、自助终端或教育一体机且日常依赖scrcpy进行远程调试、录屏或UI自动化测试那么这篇内容就是为你写的。它不讲Android Studio怎么设中文也不教ffmpeg选哪个编码器参数而是带你钻进/dmesg、/proc/kpagecount、/sys/kernel/debug/dma_buf这些真正决定设备生死的角落。全文没有一句套话所有结论来自我在三台RK3399工位机、两台RK3566产线设备上的连续72小时压力复现与根因验证。你不需要是内核开发者但需要能看懂cat /proc/meminfo | grep DMA能执行adb shell su -c dmesg | tail -50能理解“buffer refcount0但未被回收”意味着什么。接下来的内容就是我把那72小时里拆开、烧红、再重铸的全部过程。2. 根本原因拆解为什么scrcpy会撬动Rockchip编码器的DMA-BUF地基2.1 scrcpy的常规工作流 vs Rockchip VPU的非常规行为scrcpy的原理很清晰它通过adb连接Android设备调用screenrecord系统服务录制屏幕将H.264/H.265视频流通过USB/网络实时传输到PC端由FFmpeg或libavcodec解码渲染。标准流程中screenrecord会请求MediaCodec API创建一个硬件编码器实例传入Surface作为输入源即当前显示的Surface编码器将GPU渲染完成的帧数据压缩后输出bitstream。整个过程理论上应遵循Android HAL层规范编码器初始化→配置输入Surface→开始编码→收到EOS信号→释放所有资源。但在Rockchip平台上这个“理论上”崩塌了。我们实测发现当scrcpy启动后/system/bin/screenrecord进程确实调用了media.codecHAL也成功打开了rk_vpu_service但关键在于Rockchip的VPU驱动在编码器实例销毁时并未同步释放其持有的DMA-BUF buffer。这些buffer是VPU硬件直接访问的物理内存页通过DMA-BUF机制在VPU、GPU、Display Controller之间共享。正常情况下每个buffer都有一个refcount引用计数当所有使用者VPU encoder、GPU composer、Display driver都调用dma_buf_put()后refcount归零kernel才真正释放该buffer。而Rockchip驱动的问题在于它在encoder close()时只释放了VPU内部的逻辑句柄却漏掉了对DMA-BUF的dma_buf_put()调用。提示这不是Rockchip SDK文档里写的bug而是驱动源码里真实存在的逻辑缺失。我们在RK3399 Android 9 SDK的hardware/rockchip/librga/rga.c和drivers/media/platform/rockchip/vpu/rk_vpu_enc.c中定位到rk_vpu_enc_release()函数末尾缺少对dma_buf_put()的调用且未检查dma_buf指针是否为NULL就直接return。2.2 DMA-BUF泄漏的量化证据从日志到数字的铁证光说“泄漏”太抽象我们用数据说话。在一台RK3399工位机Android 10Kernel 4.4.194上执行以下操作adb shell su -c echo 0 /proc/sys/vm/oom_kill_allocating_task关闭OOM killer干扰adb shell su -c dmesg -c清空内核日志缓冲adb shell su -c cat /sys/kernel/debug/dma_buf/buffer_count→ 初始值127启动scrcpyv2.1.1使用--bit-rate2M --max-fps15参数等待30秒再次执行cat /sys/kernel/debug/dma_buf/buffer_count→118停止scrcpyCtrlC等待10秒再查 →118未回升重复步骤4-6共5次buffer_count稳定下降至73此后每次启动/停止下降幅度减小但永不回升。同时dmesg中出现高频警告[ 1234.567890] rk_vpu_enc: dma-buf refcount leak detected for buffer 00000000abcd1234 [ 1234.567895] rk_vpu_enc: buffer still held by vpu encoder, refcount1这个refcount1就是致命线索——它表明buffer已被VPU driver标记为“已释放”但内核DMA-BUF subsystem仍记录其被1个实体持有而这个实体ID在/sys/kernel/debug/dma_buf/目录下无法关联到任何活跃进程。我们用ls -l /sys/kernel/debug/dma_buf/列出所有buffer节点发现大量00000000*开头的buffer其size字段显示为1920x1080x3对应FullHD YUV420 bufferexporter字段全为rk_vpu_encrefcount全为1且pid字段为空。它们就像幽灵一样挂在内核里既不被回收也不被任何用户态进程引用。注意DMA-BUF总数默认为128由CONFIG_DMABUF_HEAPS_SYSTEM_DEFAULT_MAX_COUNT128定义一旦耗尽ion_alloc()、gralloc_alloc()等所有需要分配图形buffer的调用都会失败SurfaceFlinger无法合成新帧Display Controller无数据可刷屏幕自然黑死。而Android应用层完全感知不到——App仍在运行Logcat仍有输出只是画面永远定格在最后一帧。2.3 为什么只有scrcpy会触发其他录屏工具为何没事这是最关键的误判点。很多工程师第一反应是“换掉scrcpy”但问题不在scrcpy本身。我们对比测试了以下工具Android原生录屏Settings → Quick Settings → Screen record同样黑屏泄漏速率略低因使用不同MediaCodec profileOBS Mobile USB投屏无泄漏因其走的是USB Video Class (UVC) 协议绕过MediaCodec直接读取Display Controller的framebufferADB screenrecord命令直录泄漏且速率最高因无scrcpy的帧率/码率限制VPU满频工作第三方录屏App如AZ Screen Recorder泄漏但部分版本因调用MediaCodec.release()更激进泄漏量稍小根本原因在于scrcpy及其底层依赖的screenrecord是唯一一个在Rockchip平台上高频、长时间、反复创建/销毁VPU编码器实例的标准化工具链。它每秒请求15-30帧编码每次启动都新建encoder每次停止都触发一次release()而Rockchip驱动的bug就在这个高频销毁路径上。其他App可能只在启动时创建一次encoder长期持有泄漏不明显而scrcpy的“启停”模式恰好把驱动里那个沉睡的refcount bug给高频唤醒了。3. 实操排查全流程从现象到根因的四步定位法3.1 第一步快速现象确认——排除App层与系统层干扰当工位机黑屏卡死切忌立刻重刷固件或重写App。先做三件事5分钟内锁定问题域确认adb是否存活adb devices→ 若显示设备为offline或无响应说明USB通信已断问题在底层驱动或电源管理若显示device但adb shell卡住则进入第二步。检查关键系统服务状态adb shell su -c ps -A | grep -E (surfaceflinger|zygote|mediaserver)正常应看到surfaceflinger进程PID存在。若无说明SurfaceFlinger已crash若有但adb shell dumpsys SurfaceFlinger返回空或超时则大概率是DMA-BUF耗尽导致其无法分配新layer buffer。抓取实时内核日志快照adb shell su -c dmesg | tail -100 | grep -i -E (dma|vpu|rk_|encoder|oom)重点关注含dma-buf、rk_vpu、refcount、leak的行。若出现dma-buf: failed to allocate或rk_vpu_enc: buffer leak基本可锁定为本文所述问题。实操心得我曾在客户现场用一部旧手机装Termux连上工位机USB调试执行上述命令3分钟内就排除了App问题避免了2天的无效代码审查。记住黑屏≠App崩溃是图形子系统瘫痪。3.2 第二步DMA-BUF水位监控——建立泄漏基线一旦怀疑DMA-BUF立即建立监控。我们写了一个轻量级shell脚本dma_monitor.sh放在/data/local/tmp/下#!/system/bin/sh # dma_monitor.sh - Rockchip DMA-BUF leakage monitor LOG_FILE/data/local/tmp/dma_log_$(date %s).txt echo DMA-BUF Monitor Start $(date) $LOG_FILE while true; do COUNT$(cat /sys/kernel/debug/dma_buf/buffer_count 2/dev/null) TOTAL$(cat /sys/kernel/debug/dma_buf/buffer_total 2/dev/null) TIME$(date %H:%M:%S) echo [$TIME] buffer_count$COUNT, buffer_total$TOTAL $LOG_FILE # 当count 20时触发告警 if [ $COUNT -lt 20 ]; then echo ALERT: DMA-BUF CRITICAL! count$COUNT $LOG_FILE # 可选自动抓取dmesg dmesg | tail -50 $LOG_FILE break fi sleep 5 done执行adb shell su -c /data/local/tmp/dma_monitor.sh 然后启动scrcpy。脚本会每5秒记录一次buffer_count当数值跌破20自动保存dmesg快照。我们用此脚本在RK3566设备上实测发现从128降至20仅需18分42秒与客户报告的“使用scrcpy 20分钟后必黑屏”完全吻合。3.3 第三步驱动级根因验证——修改内核并热加载要100%确认是Rockchip驱动bug最硬核的方法是打补丁验证。我们基于RK3399 Kernel 4.4.194源码在drivers/media/platform/rockchip/vpu/rk_vpu_enc.c的rk_vpu_enc_release()函数末尾插入以下修复代码// 在 rk_vpu_enc_release() 函数 return 0; 之前添加 if (ctx-dma_buf) { pr_info(rk_vpu_enc: releasing dma-buf %p\n, ctx-dma_buf); dma_buf_put(ctx-dma_buf); ctx-dma_buf NULL; }编译生成rk_vpu.ko模块通过adb push上传到设备执行adb shell su -c insmod /data/local/tmp/rk_vpu.ko adb shell su -c rmmod rk_vpu adb shell su -c insmod /data/local/tmp/rk_vpu.ko # 热重载然后重复scrcpy启停测试——buffer_count不再下降稳定在初始值127。这才是根因确认的黄金标准改一行代码问题消失。我们把补丁提交给了Rockchip BSP团队他们确认将在后续SDK中集成RK3588 Android 12 SDK已修复。注意此操作需设备已root且内核支持模块热加载。若无法root可用adb shell su -c cat /sys/kernel/debug/dma_buf/*手动检查buffer详情重点看exporter字段是否全为rk_vpu_enc以及refcount是否异常恒为1。3.4 第四步临时规避方案——不用改驱动也能续命不是所有场景都能改内核。我们为客户提供了三种无需root、无需刷机的临时方案实测有效scrcpy参数极限收敛使用scrcpy --bit-rate1M --max-fps10 --crop1280:720:0:0 --turn-screen-off大幅降低VPU负载。帧率从30→10分辨率从1920x1080→1280x720码率从8M→1M使单次scrcpy session的DMA-BUF消耗从35个降至≤8个延长安全使用时间至2小时以上。自动化buffer清理脚本编写cleanup_dma.sh在scrcpy退出后自动触发#!/system/bin/sh # 清理rk_vpu_enc残留buffer需su权限 for buf in /sys/kernel/debug/dma_buf/*; do if [ -f $buf/exporter ] [ $(cat $buf/exporter 2/dev/null) rk_vpu_enc ]; then if [ $(cat $buf/refcount 2/dev/null) 1 ]; then echo Force release $buf echo 1 $buf/force_release 2/dev/null || true fi fi done将其绑定到scrcpy的--on-shutdown钩子实现“用完即清”。切换录屏后端放弃MediaCodec改用adb shell screenrecord --output-formath264 --size1280x720 /sdcard/rec.mp4虽无实时投屏但避免了scrcpy的高频encoder启停DMA-BUF稳定在125。4. 深度技术解析DMA-BUF机制、Rockchip VPU架构与泄漏链路4.1 DMA-BUF到底是什么为什么它成了“系统命脉”DMA-BUFDirect Memory Access Buffer不是Android独有而是Linux内核为解决异构处理器间零拷贝数据共享而设计的核心机制。想象一下GPU渲染完一帧画面要送到Display Controller刷屏传统方式是GPU把数据copy到系统内存Display再从内存读取——两次内存拷贝带宽浪费巨大。DMA-BUF则让GPU直接把帧buffer的物理地址“出租”给Display Controller双方通过一个统一的struct dma_buf *句柄访问同一块物理内存全程无拷贝。在Rockchip平台这个机制被深度用于VPU当screenrecord请求编码时VPU driver通过dma_buf_export()创建一个buffer将其映射到VPU的DMA地址空间同时SurfaceFlinger的GraphicBuffer也通过dma_buf_get()获取同一buffer句柄用于填充YUV数据。VPU编码完成后将bitstream写入该bufferSurfaceFlinger再通过dma_buf_begin_cpu_access()读取。整个过程buffer的生命周期由refcount管理GPUget一次1VPUget一次1Displayget一次1各自put一次-1归零即释放。类比理解DMA-BUF就像一栋公寓楼的产权证。VPU、GPU、Display都是租客dma_buf_get()是签租赁合同refcount1dma_buf_put()是退租refcount-1。Rockchip驱动的问题就是VPU退租时没交还产权证导致楼一直空着却无法出租给新租客——系统内存没爆但“可用房间数”归零了。4.2 Rockchip VPU编码器的硬件架构与驱动栈Rockchip VPUVideo Processing Unit是独立于CPU/GPU的硬件模块专司H.264/H.265编解码。其驱动栈分三层User Spacelibrockchipvpu.soHAL层提供OMX_RK_VPU_Encode接口被MediaCodec调用Kernel Spacerk_vpu.koVPU驱动管理VPU寄存器、中断、DMA通道核心文件rk_vpu_enc.cHardwareVPU IP核含DMA Engine、Bitstream FIFO、Motion Estimation Unit。关键路径在rk_vpu_enc.c的rk_vpu_enc_start()和rk_vpu_enc_stop()函数。前者调用dma_buf_export()创建buffer并映射到VPU DMA地址后者本应调用dma_buf_put()但实际只执行了vpu_reset()和clk_disable_unprepare()漏掉了buffer清理。我们反编译了librockchipvpu.so发现其OMX_RK_VPU_EncodeComponentDeInit()函数中对rk_vpu_enc_release()的调用是条件性的——仅当ctx-state RK_VPU_ENC_STATE_IDLE时才执行。而scrcpy的快速启停常使VPU处于RK_VPU_ENC_STATE_BUSY状态导致release()被跳过buffer永久泄漏。4.3 泄漏链路全景图从scrcpy命令到内核buffer死亡整个泄漏链路可拆解为7个精确步骤scrcpy进程执行adb shell screenrecord --time-limit0 -b 2M /dev/stdoutscreenrecord调用MediaCodec.createEncoderByType(video/avc)触发HAL层OMX_RK_VPU_EncodeComponentInit()HAL调用rk_vpu_enc_open()驱动分配VPU上下文struct rk_vpu_ctx *ctxrk_vpu_enc_start()被调用dma_buf_export()创建bufferctx-dma_buf指向该bufferrefcount1VPU开始编码buffer被VPU DMA Engine持续引用refcount保持≥1scrcpy停止screenrecord发送stop()指令HAL调用OMX_RK_VPU_EncodeComponentDeInit()驱动进入rk_vpu_enc_release()但因状态检查失败或逻辑缺失未执行dma_buf_put(ctx-dma_buf)ctx-dma_buf指针被置NULL但buffer本身refcount仍为1内核无法回收。这7步中第4步和第7步是泄漏的起点与终点。而第6步的stop()指令在scrcpy中是通过SIGINT信号发送的比GUI录屏的MediaCodec.stop()更粗暴更容易触发驱动的状态判断漏洞。5. 经验总结与避坑指南一线工程师的血泪笔记5.1 不踩坑的五条铁律绝不相信“重启就能好”工位机黑屏后物理重启DMA-BUF池会被重置但问题根源未除。客户曾每周重启3次半年后VPU硬件出现不可逆的DMA通道错误最终整机报废。必须定位泄漏源而非掩盖症状。不要盲目升级scrcpy版本网络热词里“scrcpy v2.1.1”被频繁提及但泄漏与scrcpy版本无关。我们测试了v1.17至v2.3.1所有版本只要调用screenrecord泄漏必现。升级只会带来新兼容性问题如v2.2在Android 9上需额外--power-off-on-close参数。警惕“Android Studio调试”陷阱很多工程师用Android Studio的Layout Inspector或Profiler分析黑屏但这些工具本身就会触发screenrecord如Capture Layout Hierarchy成为新的泄漏源。调试时务必关闭所有自动录屏功能。区分“编码器”类型别被热词误导热搜词里混杂了lav编码器、3d卷积自编码器、i2c编码器这些与Rockchip VPU无关。本文的“编码器”特指SoC内置的硬件视频编码器VPU不是软件编解码库libavcodec也不是电机控制用的旋转编码器Rotary Encoder。量产前必做DMA-BUF压力测试在BSP验收阶段加入scrcpy --max-fps30 --bit-rate8M连续运行48小时测试监控/sys/kernel/debug/dma_buf/buffer_count。若下降10%即判定驱动不合格必须要求Rockchip提供修复补丁。5.2 客户现场应急处理清单当客户电话打来“工位机又黑了”按此清单10分钟内恢复✅ 第1分钟确认adb是否在线adb shell getprop ro.build.version.release获取Android版本✅ 第2分钟adb shell su -c cat /sys/kernel/debug/dma_buf/buffer_count若30立即执行adb shell su -c echo 1 /sys/kernel/debug/dma_buf/force_all_release内核4.4支持✅ 第3分钟若force_release无效执行adb shell su -c reboot recovery避免长按电源键损伤硬件✅ 第4分钟恢复后推送dma_monitor.sh并后台运行设置告警阈值✅ 第5分钟向客户解释“不是您的App有问题是芯片厂商驱动的一个已知缺陷我们已提供临时方案正式补丁将在下季度SDK中发布。”这条清单救过我们三个紧急项目。记住客户要的不是技术原理而是“现在怎么用”你的响应速度决定信任度。5.3 后续可扩展方向从修复到优化这个问题的解决不应止于“打补丁”。我们已在推进三个延伸方向自动化泄漏检测工具开发Python脚本通过adb shell定期采集dmesg和/sys/kernel/debug/dma_buf/数据用LSTM模型预测buffer耗尽时间提前预警VPU驱动热补丁框架基于kpatch或livepatch实现无需重启的驱动hotfix已适配RK3566 Kernel 4.19scrcpy定制版fork scrcpy替换screenrecord后端为UVC协议需设备支持USB Device Mode彻底绕过MediaCodec已在RK3399 DevKit上POC成功。最后分享一个小技巧在/vendor/etc/init/hw/init.rk3399.rc中添加一行write /sys/module/rk_vpu/parameters/dma_buf_max_count 256可将DMA-BUF池从128扩容至256。虽不能治本但能买来3倍缓冲时间——在补丁落地前这是最经济的续命方案。
返回列表