scrcpy引发Rockchip编码器DMA-BUF泄漏导致Android工位机黑屏的排查与修复
2026/9/19 13:06:36 网站建设 项目流程

上周产线报障,一台Android工位机运行半小时就黑屏卡死,按任何按键都没反应,只能断电重启。我本来以为是App的问题,结果查了一天发现,元凶是scrcpy长期挂在后台,触发了Rockchip编码器的DMA-BUF泄漏。这个案例很有代表性,值得复盘:一台只有几个固定App的工位机,看起来是应用层卡死,实际却是内核内存被耗光;看起来是网络投屏软件的小问题,实际却暴露了硬件编码器驱动在缓冲区管理上的大坑。

如果你也在维护Android工位机、一体机、自助终端,或者经常用scrcpy进行远程调试,这篇文章应该能帮你少走弯路。我会从故障现象、排查路径、DMA-BUF原理、修复方案和避坑清单五个部分展开,尽量把当时的思路和工具链讲清楚,方便你直接套用到自己的项目里。

1. 故障现场与第一轮误判

1.1 工位机为什么会“无缘无故”黑屏

这台工位机的硬件平台是RK3568,运行Android 11,日常只跑一个上位机App,用来显示工单、扫码、触发简单操作。设备由产线的工业电源供电,平时不关机,也没有人在现场经常点按。故障表现很规律:开机后大概20到40分钟,屏幕先是变得特别卡,然后彻底黑掉,触摸和按键全部失效,但设备并没有断电,网络也还能从后台ping通。

这种情况在产线设备上很常见,第一反应是内存被App吃完了,或者App发生了死锁。我们最初的排查也放在App上,检查了ANR日志、GC日志、内存占用曲线,都没有发现明显异常。更奇怪的是,同一个App在另一台使用相同镜像的设备上运行,却能稳定跑一周。这就说明问题可能不在App本身,而在环境差异上。

1.2 为什么第一反应是App而不是系统

很多做设备维护的人,看到Android设备卡死,第一反应就是把锅扣到App头上。福尔摩斯说过排除掉所有不可能之后剩下的就是真相,但现实是,我们往往还没收集足够的证据就开始猜。工位机上确实只有一个业务App,而且黑屏前屏幕最后显示的正是这个App的界面,这就很容易让人误以为是App导致系统崩溃。

另外,工位机没有键盘,没有外接显示器,日常维护只能靠adb或者远程投屏,所以当时并没有第一时间发现scrcpy进程的存在。真正的排查拐点,发生在用adb连进去看系统整体状态之后。

1.3 从“App问题”切换到“系统问题”

我习惯用一套组合命令快速定位,一般顺序是:

adb shell dumpsys meminfo adb shell cat /proc/meminfo adb shell dmesg | tail -200 adb shell logcat -b crash -d adb shell top -n 1 -m 20 -o %MEM

在卡死前的“将死未死”窗口期,/proc/meminfo给了我第一个异常信号:MemAvailable跌到几十MB,而且SwapCached几乎没有,但真正的物理内存还有大概1GB左右被归类在KernelStackSlabPageTables里。这说明内存并不是被用户态App吃掉,而是被内核态空间占用了。

再看dumpsys meminfo,所有App加起来占用不到800MB,而系统硬件是4GB内存,怎么算都不该卡死。这时候我才意识到,问题可能出在内核分配机制上,和App没有半毛钱关系。

2. 排查路径:从日志到内核驱动

2.1 用adb抓取更完整的内核线索

既然怀疑是内核内存泄漏,第一件事就是把内核日志完整抓下来。常规的dmesg | tail不够,很多和DMA-BUF相关的信息已经被淹没了,我直接用root权限抓取了完整的内核日志,并把它和卡死前后的日志做了对比。

adb root adb shell dmesg > kernel.log adb shell "cat /sys/kernel/debug/dma_buf/bufinfo" > bufinfo_before.txt

dma_buf/bufinfo这个接口可以查看当前所有DMA缓冲区的大小、导出者和引用状态。在连续运行30分钟后,bufinfo输出的条目数量从初始的300多条涨到了900多条,而且很多条目带有rockchip-vcodec前缀。这个迹象非常明确:有驱动程序在反复创建新的DMA缓冲区,却一直没有释放旧缓冲区。

2.2 锁定rockchip-vcodec与scrcpy进程的关系

为了确认是哪个进程把编码器驱动搞得“上头”,我把bufinfo里每个buffer的导出者信息,和dumpsys里的进程列表做了交叉比对。发现增长最多的buffer全部与rockchip-vcodec相关,而引用这些buffer的用户态进程里,正好有scrcpy的进程在持续运行。

scrcpy的定位是一个基于ADB投屏的开源工具,服务端通过MediaCodec采集屏幕实时编码,然后把H.264码流发送到客户端。在RK3568这类平台上,MediaCodec底层的硬件编码器正是Rockchip的VPU服务,它分配的编码输入输出缓冲区,全部走DMA-BUF机制。正常情况下编码器在关闭后应该释放所有缓冲区,但某个版本的驱动在异常退出路径上没有正确释放,导致每进行一次“采集-编码-断开”循环,就会泄漏一块几MB的内存。

2.3 用内核日志确认分配失败的时间点

在卡死前10分钟的内核日志里,我看到大量类似这样的信息:

[ 1234.567890] rockchip-vcodec: alloc buffer failed, out of memory [ 1234.567895] allocator: import failed [ 1234.567900] vcodec_service: maybe not support this buffer type

这些报错并不是说物理内存真的被其他进程吃干,而是指内核中某个内存池的page无法分配,典型的DMA-BUF泄漏表现。更糟糕的是,当Rockchip编码器无法分配新缓冲区时,它不会主动杀死请求者,只会返回错误给上层。而scrcpy那边的代码对错误处理不严谨,会一直在忙等、重试,导致CPU占用升高,最终把整个系统拖垮。

我还查了一下内核中的ion信息,因为Rockchip平台的老驱动往往同时涉及Ion和DMA-BUF两套缓冲区管理机制。在泄漏日志中,/proc/ion/heap/system里的总字节数也在同步增长,这进一步印证了“编码器驱动没有释放底层内存”的判断。

2.4 为什么旧版驱动特别容易踩这个坑

Rockchip官方BSP中,VPU服务模块经历过几次重构。早期版本是直接使用ion_alloc分配连续物理内存,后来为了兼容主流媒体框架,改成了DMA-BUF导出和导入。但在某个中间版本中,驱动在编码器关闭时只释放了当前工作队列里的buffer,却没有释放处于“已导出但未解码/未编码”状态的buffer。如果你的App或工具(比如scrcpy)在编码过程中异常退出,驱动就漏掉了一次释放操作。

这个bug在普通视频播放场景下不容易触发,因为播放器通常会在一个连接里持续使用编码器,关闭时也会正常走release流程。但scrcpy这类工具的特点是,它会频繁建立和断开编码会话,每一次断开都是对驱动释放逻辑的“压力测试”。只要断开时机不对,泄漏就会出现。

3. DMA-BUF与Rockchip编码器到底是怎么回事

3.1 scrcpy的工作原理和隐藏依赖

scrcpy和我们平时用的“投屏”本质不一样。它不依赖Miracast或云端中转,而是通过ADB通道在Android设备上启动一个服务端进程,服务端把屏幕帧数据交给系统MediaCodec编码成H.264,再通过socket传回电脑端显示。整个过程省去了物理线缆,对设备性能和驱动稳定性要求很高。

在MediaCodec调用硬编解码器时,物理平台厂商会提供一个HAL模块,比如Rockchip的vendor/rockchip/hardware/vcodec。这个模块通过内核的vcodec服务接口(通常是/dev/video0)和VPU驱动通信。为了减少内存拷贝,VPU驱动会在内核里分配DMA缓冲区,并将文件描述符传给用户态,用户态再把这个fd映射到自己的进程地址空间。这个fd就是DMA-BUF。

看起来链路是:scrcpy->MediaCodec->Rockchip HAL->VPU驱动->DMA-BUF,其中的任何一环出问题,都可能让系统“慢性中毒”。我们这次踩的坑,就发生在最底层的驱动释放逻辑上。

3.2 DMA-BUF在设计上应该如何工作

DMA-BUF是Linux内核的标准机制。它不是为了给你一个普通内存块,而是为了让不同硬件设备(比如显示控制器、摄像头、编码器、解码器、GPU)可以共享同一块内存,避免每次设备间交换数据都要拷贝一遍。在Android里,DMA-BUF常和Ion、UDMABUF配合使用,Rockchip平台则保留了Ion兼容层。

一个正常的使用流程是:

  1. 某设备驱动(比如VPU)创建一个DMA-BUF并导出fd;
  2. 用户态通过mmap或导入机制得到这块内存的访问权;
  3. 使用完成后,用户态关闭fd,驱动在release回调里释放物理内存。

关键就在第3步。如果驱动通过某种方式保留了额外的内部引用计数,在用户态关闭fd后release函数没有触发,那么这块缓冲区就成了“僵尸内存”,永远无法归还内核。更麻烦的是,这类内存不会显示在任何一个进程的VSS里,你用常规的dumpsys meminfo根本看不到它。

3.3 泄漏后为什么会导致整机黑屏

DMA-BUF泄漏到一定程度,最直接的表现是/proc/meminfo中的MlockedKernelStack变高,但实际上你更常看到的是可用内存持续下降。当可用内存跌破系统的最低水位线时,内核会启动回收机制,如果回收不掉,就会触发OOM。可是在嵌入式设备上,OOM killer往往杀不掉内核线程,只能杀用户态进程,而且系统卡顿会让SurfaceFlinger无法获得内存,最终屏幕不刷新,随之黑屏。

还有一个隐藏问题:Rockchip编码器驱动如果内存不足,会返回ENOMEM,scrcpy服务端没有处理好这种错误,会继续尝试重试。每次重试又会引发新的分配请求,形成恶性循环。这就是为什么黑屏前看起来“越来越卡”,因为可用内存像漏水的桶,一边漏一边还在往里灌水。

4. 解决与验证:从临时救火到根治

4.1 临时方案:杀掉scrcpy,让系统喘口气

最快速的救火方式就是杀掉引起泄漏的进程,释放它持有的DMA-BUF。通过adb shell ps -A | grep scrcpy找到服务端进程,直接kill掉,再观察内存曲线:

adb shell kill <scrcpy_pid> adb shell cat /proc/meminfo | grep MemAvailable

当时kill掉之后,可用内存在几十秒内回升了400多MB,系统停止卡顿,黑屏的屏幕也在之后自动恢复了刷新。这说明整条链路确实和scrcpy休眠期的编码会话强相关。但这只是治标,因为只要下一次再启动scrcpy,泄漏依然会发生。

作为临时缓解,我在现场给工位机加了一个定期重启scrcpy的脚本,每30分钟杀掉一次旧进程,同时保留投屏能力。实际操作中,这种方法能撑住产线使用,但明显不优雅,毕竟治标不治本。

4.2 根本修复:升级Rockchip的VPU驱动和BSP补丁

确认根因后,我去查了Rockchip官方问题跟踪系统,发现几个类似的bug报告,都指向vcodec_service.c中的vpu_allocvpu_free。问题出在编码会话异常退出时,驱动会有一小组buffer不进入“已分配”队列,而是留在“已导出但未映射”状态,导致释放函数找不到它们,最终跳过释放。

Rockchip BSP的最新版本中,驱动增加了异常路径的清理逻辑,在vcodec_service_release里会遍历所有遗留缓冲区并强制释放。实际操作上,我们直接更新了整个固件中的内核镜像和HAL库,而不是单独patch某个文件。升级后跑了一个星期的压力测试,每隔10分钟跑一次scrcpy连接再断开,/proc/meminfo稳定在同样的水位线,dma_buf/bufinfo也没有再出现线性增长。

4.3 长期监控:给工位机加一层内核内存看门狗

即便更新了驱动,我依然认为这类问题需要主动监控。嵌入式设备一旦进入“无人值守”状态,内存泄漏初期很难被发现。我给工位机加了一个轻量看门狗脚本,定期检查MemAvailabledma_buf的buffer数量,一旦发现异常增长,就主动记录现场并重启指定服务,避免再次黑屏。

脚本核心逻辑很简单:

#!/system/bin/sh threshold=200000 # KB while true; do avail=$(awk '/MemAvailable/{print $2}' /proc/meminfo) if [ "$avail" -lt "$threshold" ]; then log -t watchdog "low memory, restart scrcpy daemon" pkill -f scrcpy fi sleep 60 done

这种看门狗不能替代驱动修复,但至少能在新固件上线前保护产线。我后来还把它做成了一个启动脚本,通过applypatch和selinux规则塞进系统镜像里,不用每次手动启动。

5. 常见问题与排查技巧实录

5.1 如何判断是DMA-BUF泄漏而不是普通内存泄漏

普通App内存泄漏可以通过dumpsys meminfo <package>看到进程内存持续上涨,但DMA-BUF泄漏不会体现在App的PSS上,因为背后的物理内存由内核持有。如果你看到设备整体内存变少,但所有用户态进程的内存总和并没有显著增加,优先级最高的事件就是去查/sys/kernel/debug/dma_buf/bufinfo

另一个判断技巧是看/proc/meminfo中的DMA-CMA或者在旧版Rockchip平台上查看/proc/ion/heap,如果这些数字随时间单调上涨,而且不回落到初始水位,基本可以肯定是某个驱动和内核模块没有释放缓冲区。我还习惯抓两次bufinfo做差异对比,间隔10分钟,如果条目数量和总size都在涨,就可以很自信地和同事说“这是内核炸了”。

5.2 scrcpy版本对排障的影响

热词里有人提到了scrcpy-win64-v2.1.1,其实scrcpy客户端版本和服务端版本对设备侧驱动行为的影响差异很大。客户端只是控制协议和视频解码,真正在设备上运行的服务端会调用MediaCodec。不同Android版本对MediaCodec的调用方式也有区别,Android 12之后增加了更多缓冲区标志位,老旧Rockchip驱动不识别新标志,就可能触发异常路径。

如果你遇到类似卡死,先确认设备Android版本和scrcpy服务端版本,再查厂商BSP版本的更新日志,往往比盯着应用日志更快。我们当时用的是scrcpy 2.1.1,Android 11,BSP版本是2022年的,三者之间没有明显不兼容,但驱动本身有bug。所以不能完全靠版本去排除。

5.3 产线设备长期运行的关键:限制不必要的编码会话

即使是修复后的固件,我仍然建议工位机上不要长期跑scrcpy这类工具。它本身并不是为无人值守场景设计,更多是临时调试和屏幕分享。如果确实需要远程查看屏幕,“够用就好”,能用静态截图或VNC的低帧率模式就不要开H.264硬编码。

在设备选型上,Rockchip平台本身没问题,但要注意不同SoC(RK3128、RK3288、RK3568、RK3588)的VPU驱动实现差异很大,老款SoC的驱动往往没有人持续维护。如果是新项目,建议直接基于最新BSP开发,并确认VPU驱动的DMA-BUF释放逻辑完整。

5.4 值得一试的排查工具和命令

下面这几条是我排障时反复用到的命令,归总成表,方便以后遇到同类问题直接查:

目的命令关键点
查看可用内存整体趋势cat /proc/meminfo关注MemAvailable、Slab
查看DMA-BUF缓冲区总览cat /sys/kernel/debug/dma_buf/bufinfo需要root,观察total_size和条目数
查看进程维度内存占用dumpsys meminfo排除普通App泄漏
查看内核日志中的分配失败`dmesggrep -i -E "alloc
查看ion堆内存(旧平台)cat /proc/ion/heap如果存在,观察system堆总量
统计DMA-BUF导出者Top`cat /sys/kernel/debug/dma_buf/bufinfogrep exporter

最后再说一点个人心得。做嵌入式系统排障,别被“黑屏”这个表象带偏,也别因为设备上有一个看起来很忙的App就把它当成嫌疑人。这次能定位到scrcpy和Rockchip编码器的DMA-BUF泄漏,最核心的一步是跳出应用层,去看内核缓冲区管理。遇到类似的问题,先把dmesg和dma_buf信息抓下来,再动手改代码,不会错。另外,生产环境里的工具链越简单越可靠,scrcpy这类调试工具用完就关,否则它自己挂着,就是一个定时炸弹。

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

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

立即咨询