Rockchip VPU DMA-BUF泄漏导致scrcpy黑屏根因分析
2026/9/12 19:52:21 网站建设 项目流程

1. 项目概述:这不是App崩溃,是硬件驱动层的“慢性失血”

你有没有遇到过这种场景:一台跑着Android系统的工位机,日常用scrcpy做远程调试或投屏,突然某天开始频繁黑屏、卡死,重启后能撑一两个小时,接着又挂——但App日志里干干净净,ANR没触发,OOM没报警,SurfaceFlinger没报错,连logcat里都找不到像样的异常堆栈?我上周就在产线现场撞上了这个鬼问题。三台Rockchip RK3399平台的定制工控终端,全部在接入scrcpy后2~4小时出现无响应,触摸失灵、HDMI输出黑屏、adb shell失联,但串口console依然能ping通,系统进程还在跑,只是图形子系统彻底僵死。最迷惑的是:换掉所有上层App、清空/data/app、甚至刷回出厂固件,问题照旧;而只要不启动scrcpy,设备就能连续稳定运行72小时以上。这显然不是应用层的问题——它藏得更深,深到Linux内核的DMA-BUF内存管理子系统里。

核心关键词“scrcpy”“Rockchip”“DMA-BUF”“编码器”在这里不是并列关系,而是因果链:scrcpy作为用户态投屏工具,通过adb调用Android的screenrecord服务获取H.264视频流;screenrecord依赖MediaCodec调用底层硬件编码器;Rockchip平台的硬件编码器(VPU)在实现中大量使用DMA-BUF进行零拷贝内存共享;而DMA-BUF的引用计数管理一旦出现泄漏,就会导致物理内存页长期被锁住无法释放,最终耗尽CMA(Contiguous Memory Allocator)区域,触发GPU驱动拒绝分配新buffer,进而让SurfaceFlinger停摆、HWC(Hardware Composer)失效、Display Engine无帧可刷——黑屏卡死只是表象,本质是内存资源被无声无息地“抽干”。这不是理论推演,是我们用kmemleak+dump_stack+rockchip-vpu-debugfs实锤的现场证据。这篇文章不讲怎么装scrcpy,也不教Android Studio配中文,我们要拆解的是:当一个看似轻量的投屏工具,撞上特定SoC的驱动实现缺陷时,如何从黑屏现象一路逆向追踪到DMA-BUF refcount漏减的汇编指令级根因。适合嵌入式Android开发者、驱动工程师、以及那些被“莫名卡死”折磨过三次以上的产线运维同事——你不需要会写驱动,但得知道该看哪几行dmesg、该抓哪几个debugfs节点、该用什么命令验证泄漏是否真实存在。

2. 根因定位思路:为什么先排除App,再锁定DMA-BUF?

2.1 排除上层干扰的四步法

面对黑屏卡死,第一反应往往是杀App、清缓存、重装APK。但我们这次直接跳过这一步,原因很实在:产线设备只跑一个定制Launcher和数据采集Service,连WebView都没集成,且问题复现与App更新完全无关——上周五刚部署的版本,周一就出问题,而中间没有任何OTA推送。更关键的是,我们做了个极简复现:

  1. 设备冷启动,不启动任何业务App;
  2. adb shell screenrecord /dev/null --time-limit 1手动触发一次硬编;
  3. 立即执行scrcpy --bit-rate=2M --max-fps=15
  4. 观察2小时,黑屏必现。

这个操作排除了所有App逻辑干扰,把问题收敛到“scrcpy + screenrecord + Rockchip VPU”这个最小三角。接下来要回答:是scrcpy本身有bug?还是Android framework层MediaCodec封装有问题?抑或是Rockchip驱动有缺陷?我们的排查路径非常明确:从用户态往内核态逐层下沉,用可观测性指标代替主观猜测

提示:不要相信logcat里的“一切正常”。在DMA-BUF泄漏场景下,logcat往往安静得可怕,因为错误发生在内存管理子系统,而非进程调度或文件IO路径。真正有用的信号在/proc/kmsg、dmesg环形缓冲区、以及/sys/kernel/debug下的各驱动debugfs节点。

2.2 为什么DMA-BUF是首要怀疑对象?

DMA-BUF是Linux内核为解决跨设备内存共享而设计的通用框架。在Android图形栈中,它的典型流转路径是:

  • App申请Surface → Gralloc分配ION buffer → 通过DMA-BUF fd传递给HWC → HWC传给GPU driver → GPU渲染后通过DMA-BUF fd交给VPU编码器 → 编码器输出bitstream via DMA-BUF fd回传给MediaCodec。

整个过程要求每个环节严格遵循“get/put”配对原则:拿到buffer引用就inc refcount,用完必须dec refcount。一旦某个环节忘记put(比如VPU驱动在异常中断处理中跳过了cleanup),refcount就永远卡在>1,buffer无法被回收。Rockchip VPU驱动(drivers/media/platform/rockchip/vpu)正是在这个环节埋了雷——其vpu_enc_stop_streaming()函数在部分错误路径下未调用dma_buf_put(),导致每次scrcpy断连重连,都有一块CMA内存永久泄漏。

我们验证这一点的方法很粗暴但有效:

  • 在设备上执行echo 1 > /sys/kernel/debug/kmemleak启用内存泄漏检测;
  • 运行scrcpy 30分钟;
  • echo scan > /sys/kernel/debug/kmemleak
  • cat /sys/kernel/debug/kmemleak | grep -A 10 "vpu"—— 果然扫出数十个dma_buf_export对象,backtrace直指rockchip_vpu_enc_stop_streaming

这比看源码更快定位问题模块,因为kmemleak能直接告诉你“哪些内核对象没被释放”,而不是“哪里可能没释放”。

2.3 Rockchip编码器与scrcpy的耦合点在哪?

scrcpy本身不直接调用VPU,它走的是标准Android MediaCodec路径:

scrcpy (user space) → adb forward → scrcpy-server.apk (on device) → MediaCodec.createEncoderByType("video/avc") → frameworks/av/media/libstagefright/codec2/hal/Codec2Client.cpp → vendor/rockchip/hardware/omx/1.0/.../RockchipOMXPlugin.cpp → kernel driver: drivers/media/platform/rockchip/vpu/vpu_enc.c

关键耦合点在于scrcpy的默认参数:它强制启用--tunnel-mode(隧道模式),这意味着编码器输出不走传统bitstream buffer queue,而是通过DMA-BUF fd直接映射到scrcpy-server的用户态内存。这个模式极大降低延迟,但对DMA-BUF生命周期管理要求更高——VPU驱动必须确保每次stop_streaming都完成buffer cleanup,而Rockchip旧版驱动(v1.3.0之前)恰恰在此处留了缺口。我们对比过Amlogic和MTK平台,它们的VPU驱动在同样隧道模式下无此问题,说明这是Rockchip特定实现缺陷,非Android通用问题。

2.4 为什么不是Android Studio或ADB的问题?

热搜词里一堆“Android Studio怎么设置中文”“adb shell sh xxx”看似相关,实则干扰项。Android Studio是开发工具,其行为不影响已烧录固件的运行时稳定性;ADB是调试桥,它只负责转发命令,真正的编码工作在设备端完成。我们做过对照实验:

  • 关闭ADB daemon (adb kill-server),仅用串口console执行screenrecord+scrcpy-server,问题依旧;
  • 换用adb connect无线调试,问题复现频率不变;
  • 甚至拔掉USB线,用scrcpy --tcpip走WiFi,黑屏时间从2小时缩短到1.5小时——说明网络带宽影响泄漏速度,但不改变泄漏本质。

这证实问题锚定在设备端内核驱动,与开发环境完全无关。那些“android studio下载”“sdk官网”的搜索词,只是反映了大量新手把开发环境问题和运行时问题混为一谈,而我们要做的,是帮老手快速剥离噪音。

3. 核心细节解析:DMA-BUF泄漏的技术原理与Rockchip驱动缺陷

3.1 DMA-BUF refcount机制详解:不是“用了就要还”,而是“拿了就必须还”

DMA-BUF的核心是struct dma_buf结构体,其refcount字段是struct kref类型,本质是一个原子整数。每次调用dma_buf_get(fd)获取buffer,refcount加1;调用dma_buf_put(buf)释放,refcount减1。只有当refcount减到0时,内核才触发dma_buf_release(),真正释放物理内存页。这个机制看似简单,但在中断上下文、错误处理路径、多线程竞争场景下极易出错。

Rockchip VPU驱动的问题代码位于drivers/media/platform/rockchip/vpu/vpu_enc.crockchip_vpu_enc_stop_streaming()函数(v1.2.8版本):

static void rockchip_vpu_enc_stop_streaming(struct vb2_queue *q) { struct rockchip_vpu_dev *vpu = q->drv_priv; struct rockchip_vpu_ctx *ctx = vpu->ctx; // 正常路径:清理所有buffer vpu_enc_cleanup(ctx); // BUG:此处缺少对DMA-BUF的put操作! // 正确应有:dma_buf_put(ctx->enc_buf); // 但实际代码直接return; }

当scrcpy因网络抖动断连,VPU驱动收到VB2_BUF_STATE_ERROR状态,进入stop_streaming流程。正常情况下,vpu_enc_cleanup()会释放所有待编码buffer,但enc_buf(编码器输入buffer)的DMA-BUF引用是在start_streaming时通过dma_buf_get()获取的,按对称原则必须在stop_streamingput。而旧版驱动把这个put放在了vpu_enc_cleanup()里,但cleanup函数在错误路径下被跳过——导致refcount永远不减。

注意:这个bug不会立即导致OOM,因为CMA区域通常有64MB~128MB,每次泄漏约128KB(H.264 I帧buffer大小),需要500~1000次断连才会耗尽。这就是为什么问题“偶发”且“渐进式恶化”。

3.2 Rockchip VPU编码器的内存模型:CMA vs ION,为什么泄漏必死?

Rockchip RK3399平台采用双内存池设计:

  • CMA(Contiguous Memory Allocator):专供VPU、GPU等硬件加速器,要求物理地址连续,大小固定(dts中配置linux,cma-size = <0x4000000>即64MB);
  • ION:供CPU/GPU通用buffer,支持碎片化分配,容量更大但不保证连续。

VPU编码器强制使用CMA,因为H.264硬件编码需要DMA引擎直接访问连续内存。当DMA-BUF泄漏发生时,泄漏的是CMA内存页,而CMA区域无法像ION那样通过内存压缩或swap缓解——一旦耗尽,dma_alloc_coherent()直接返回NULL,VPU驱动probe失败,后续所有编码请求均被拒绝。此时SurfaceFlinger尝试合成新帧,HWC调用VPU准备output buffer,却拿不到CMA页,整个display pipeline卡死,表现为黑屏+触摸无响应。

我们用cat /sys/kernel/debug/dma_buf/summary确认了这一点:

total 128 buffers, 64MB total size rockchip-vpu: 112 buffers, 56MB allocated ← 占用率87.5% ion: 16 buffers, 8MB allocated

而正常设备该值应<5%。这个数字每分钟增长0.3%,2小时后突破95%,触发内核OOM killer对surfaceflinger的误杀——这才是logcat里突然出现system_server killed的真相。

3.3 scrcpy隧道模式如何放大泄漏效应?

scrcpy的--tunnel-mode参数让问题雪上加霜。普通模式下,MediaCodec通过dequeueOutputBuffer()获取编码后的bitstream,buffer由framework管理,生命周期清晰;隧道模式则绕过framework,VPU驱动直接将编码完成的DMA-BUF fd写入scrcpy-server的socket。这带来两个风险:

  1. fd传递链更长:VPU → MediaCodec HAL → scrcpy-server → libusb → PC端,任一环节未close fd,refcount就不减;
  2. 重连频率更高:隧道模式对网络延迟敏感,WiFi环境下scrcpy平均每3分钟重连一次,每次重连都触发一次start/stop_streaming,也就触发一次泄漏。

我们抓包发现,scrcpy-server在断连时会发送STOP_STREAMING命令给VPU,但驱动未正确响应。解决方案不是禁用隧道模式(那会牺牲30%性能),而是修复驱动中的stop_streaming逻辑。

3.4 如何用debugfs精准定位泄漏源头?

除了kmemleak,Rockchip提供了更直接的诊断接口:

  • /sys/kernel/debug/rockchip-vpu/enc_stats:显示当前编码器实例数、buffer分配统计;
  • /sys/kernel/debug/dma_buf/rockchip-vpu:列出所有VPU相关的DMA-BUF,含size、refcount、importer;
  • /sys/kernel/debug/cma/cma-0:显示CMA剩余内存、最大连续块。

关键命令组合:

# 实时监控CMA水位 watch -n 1 'cat /sys/kernel/debug/cma/cma-0 | grep -E "(used|total)"' # 查看VPU DMA-BUF详情(refcount>1即可疑) cat /sys/kernel/debug/dma_buf/rockchip-vpu | awk '/refcount/ {if($3>1) print}' # 统计泄漏速率 for i in {1..10}; do echo "$(date +%s),$(cat /sys/kernel/debug/cma/cma-0 | grep used | awk '{print $2}')"; sleep 60; done > cma_log.csv

我们用这个脚本跑了2小时,得到线性下降曲线,斜率对应每次泄漏的128KB,与理论值完全吻合——这比任何代码审查都更有说服力。

4. 实操过程:从现象到补丁的完整复现与修复

4.1 复现环境搭建:三台设备,一个命令,30分钟见真章

要复现这个问题,你不需要RK3399开发板,一台量产工控机足矣。我们用的设备是:

  • SoC:Rockchip RK3399(v1.2.8 kernel)
  • Android:9.0(Pie),vendor image基于Rockchip SDK v2.2.0
  • scrcpy:v1.25(server apk built from master)

复现步骤(严格按顺序):

  1. 清空设备状态:adb shell su -c "echo 3 > /proc/sys/vm/drop_caches"
  2. 重置CMA:adb shell su -c "modprobe -r rockchip_vpu && modprobe rockchip_vpu"
  3. 启动scrcpy:scrcpy --tunnel-mode --bit-rate=2M --max-fps=15 --crop=1200:800:0:0
  4. 模拟网络抖动:在PC端执行ping -i 5 192.168.1.100 | head -n 60 > /dev/null &(制造间歇性丢包);
  5. 观察adb logcat -b events | grep -i "display\|hwc\|vpu",等待HWC: failed to commit出现。

关键观察点:

  • 第一次HWC: failed to commit出现时,cat /sys/kernel/debug/cma/cma-0显示used从10MB飙升至55MB;
  • 此时cat /sys/kernel/debug/dma_buf/summary中rockchip-vpu条目数激增;
  • dmesg | tail -20出现rockchip-vpu enc: no memory for output buffer警告。

这个复现过程控制在30分钟内,比看源码快得多。记住:能复现,才能验证修复是否有效

4.2 补丁编写:三行代码,两个补丁,彻底根治

修复方案分两层:
补丁1:驱动层修复(根本解)
修改drivers/media/platform/rockchip/vpu/vpu_enc.c

--- a/drivers/media/platform/rockchip/vpu/vpu_enc.c +++ b/drivers/media/platform/rockchip/vpu/vpu_enc.c @@ -1234,6 +1234,9 @@ static void rockchip_vpu_enc_stop_streaming(struct vb2_queue *q) struct rockchip_vpu_dev *vpu = q->drv_priv; struct rockchip_vpu_ctx *ctx = vpu->ctx; + if (ctx->enc_buf) + dma_buf_put(ctx->enc_buf); + ctx->enc_buf = NULL; vpu_enc_cleanup(ctx); }

这个补丁确保无论cleanup是否执行,enc_buf的refcount都被正确减少。我们测试了1000次断连,CMA水位稳定在5%以下。

补丁2:scrcpy-server层加固(防御性)
scrcpy/app/src/main/jni/videocapture/encoder.c中,增加DMA-BUF fd关闭检查:

// 在encoder_destroy()中添加 if (encoder->dma_buf_fd >= 0) { close(encoder->dma_buf_fd); encoder->dma_buf_fd = -1; }

虽然驱动修复后此步非必需,但它构成双重保险,防止未来类似bug。

4.3 补丁编译与烧录:不用重刷整个固件

Rockchip驱动是ko模块,无需编译整个kernel:

# 在kernel源码根目录执行 make M=drivers/media/platform/rockchip/vpu modules # 生成rockchip-vpu.ko adb push drivers/media/platform/rockchip/vpu/rockchip-vpu.ko /data/local/tmp/ adb shell su -c "insmod /data/local/tmp/rockchip-vpu.ko"

验证:lsmod | grep vpu显示新模块已加载,dmesg | tail无error。

实操心得:不要用modprobe rockchip-vpu,因为vendor分区里的旧模块会优先加载。必须用insmod强制加载新ko,并确保/system/lib/modules/rockchip-vpu.ko被覆盖(需remount rw)。

4.4 验证修复效果:量化指标比“不卡了”更可信

修复后,我们用三组数据验证:

指标修复前修复后改善
CMA used (2h)62MB → OOM8.2MB ±0.3MB90%↓
scrcpy重连成功率42%(断连后需重启)99.8%(自动恢复)2.3x↑
黑屏平均间隔1.8h>168h(7天)∞↑

特别注意:不要只测“一次不卡”,要测“持续72小时”。我们把修复后设备放在温箱里(45℃),模拟产线高温环境,72小时无异常——这才是工业级验证。

5. 常见问题与排查技巧实录:那些踩过的坑和省下的三天

5.1 问题速查表:看到这些现象,直接按表索骥

现象可能原因快速验证命令解决方案
scrcpy连接后10分钟内黑屏,logcat无错误DMA-BUF泄漏初期cat /sys/kernel/debug/cma/cma-0 | grep used检查VPU驱动版本,应用补丁1
adb shell screenrecord单独运行也卡死VPU驱动全局缺陷dmesg | grep -i "vpu|cma"升级kernel或打补丁
黑屏后串口可ping通,但adb devices消失USB gadget驱动异常ls /sys/class/udc/是否为空重启USB controller:echo 0 > /sys/bus/platform/drivers/dwc3-rockchip/unbind
scrcpy报错could not open audio但视频正常Audio HAL配置错误adb shell dumpsys media.audio_flinger与VPU无关,检查audio_policy.conf
CMA used稳定在30MB但设备仍卡顿其他驱动泄漏(如GPU)cat /sys/kernel/debug/dma_buf/summary | grep -v "rockchip"perf分析GPU driver refcount

5.2 避坑指南:三个血泪教训

教训1:别信“厂商说没问题”
Rockchip FAE最初回复:“这是scrcpy兼容性问题,建议降级”。我们坚持用kmemleak抓到泄漏点后,他们才承认驱动缺陷。所有硬件厂商的“没问题”,都要用dmesg和debugfs验证

教训2:CMA大小不能盲目调大
有同事提议把CMA从64MB扩到256MB来“缓解”。这是饮鸩止渴——泄漏仍在发生,只是爆发时间延后,且挤占了RAM给Android Runtime的空间,导致GC更频繁。治标不如治本,补丁比调参可靠

教训3:logcat过滤要带-b all
默认logcat只显示main和system buffer,而VPU错误在eventsradiobuffer。正确命令:adb logcat -b events -b radio -b main -b system \| grep -i "vpu\|cma\|hwc"。漏掉任何一个buffer,都可能错过关键线索。

5.3 工业现场应急方案:没源码怎么办?

如果客户设备是封闭固件,无法修改驱动,我们提供临时缓解方案:

  • 脚本化内存回收adb shell su -c "echo 1 > /proc/sys/vm/compact_memory"每30分钟执行一次,强制内存整理(对CMA效果有限但聊胜于无);
  • 限制scrcpy重连:在PC端用scrcpy --restart-on-connect-failure=0禁用自动重连,改为手动触发;
  • 降级scrcpy:v1.17之前版本未启用隧道模式,泄漏速率降低70%。

这些是权宜之计,但能争取到打补丁的时间窗口。

5.4 扩展思考:其他SoC平台是否安全?

我们横向测试了:

  • Amlogic S905X3:驱动中vpu_stop_streaming有完整dma_buf_put,安全;
  • MTK MT8173:使用ION而非CMA,泄漏影响较小;
  • Qualcomm SM8150:Adreno GPU驱动DMA-BUF管理严谨,无此类问题。

结论:Rockchip RK3399/RK3328/RK3288系列(kernel 4.4~4.19)均存在此缺陷,RK3566/RK3588已修复。如果你用的是这些芯片,务必检查驱动版本。

6. 经验总结:从黑屏到补丁,我学到的三件事

这个问题折腾了我们团队11天,从怀疑App到锁定内核,从抓dmesg到读汇编,最后三行代码解决问题。过程中最深刻的体会有三点:
第一,“黑屏”不是故障终点,而是故障入口。它像一个症状,背后可能是电源管理、时钟树、内存控制器、GPU驱动、VPU驱动中任意一环的失效。必须用可观测性工具(debugfs/kmemleak/perf)代替经验猜测,把模糊的“卡死”转化为精确的“CMA used=63MB”。
第二,scrcpy不是背锅侠,而是压力探针。它的高频率重连、隧道模式、低延迟要求,把Rockchip驱动里潜伏多年的refcount bug暴露得淋漓尽致。没有scrcpy,这个bug可能再潜伏两年——因为它只在特定负载路径下触发。
第三,开源的价值不在代码本身,而在可追溯性。如果这是个闭源驱动,我们只能靠厂商patch,而Rockchip公开的kernel源码让我们能精准定位到vpu_enc.c第1237行。这也是为什么我坚持所有嵌入式项目必须用主线kernel或可审计的vendor分支。

最后分享一个小技巧:下次遇到类似问题,先执行adb shell su -c "cat /sys/kernel/debug/dma_buf/summary | head -20",如果看到某个driver占用buffer数远超其他模块(比如rockchip-vpu占80%),恭喜你,已经找到了90%的答案。剩下的,只是补丁和验证的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询