恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Android工位机黑屏根因:Rockchip VPU的DMA-BUF内存泄漏
首页
资讯中心
/
Android工位机黑屏根因:Rockchip VPU的DMA-BUF内存泄漏
Android工位机黑屏根因:Rockchip VPU的DMA-BUF内存泄漏
发布时间:2026/9/12 13:14:55
1. 黑屏卡死现场还原工位机不是“App崩溃”而是整机级失能那天下午三点十七分产线测试区第三工位的 Android 工位机突然黑屏——不是应用闪退那种“重启一下就好”的小毛病是彻底无响应触摸失灵、ADB断连、物理按键无反馈连长按电源键强制重启都无效。我拎着调试线过去时屏幕漆黑如墨但设备指示灯还亮着风扇在转USB口供电正常。这说明它没死只是被钉在了某个不可中断的内核态陷阱里。我们第一反应是查 App 日志。adb logcat连不上adb shell超时adb devices列表里设备状态变成offline。换 USB 线、换端口、拔插电源——全无效。同事顺手点了下 scrcpy 的“投屏”按钮结果电脑端 scrcpy 窗口卡在“connecting…”不动几秒后报错ERROR: Could not open video device。这个错误平时见得少但此刻像一根引信把所有线索串了起来问题不在上层 App而在视频采集链路的底层驱动层。工位机用的是 Rockchip RK3399 平台系统是 Android 10 定制固件scrcpy 版本是 v2.1.1当时最新稳定版通过adb reverse tcp:8080 tcp:8080scrcpy --video-codech264启动。我们习惯性地认为“投屏卡顿App 卡死”但这次黑屏前没有任何 ANR 提示也没有 Crash 日志生成——因为根本没走到用户空间。设备还在运行只是视频子系统锁死了整个 DMA 通路进而拖垮了内存管理器MMU和中断控制器GIC。这不是软件 bug是硬件资源被死锁住的物理级故障。提示当 Android 设备出现“黑屏但供电/指示灯正常ADB 完全失联物理按键无响应”三重症状时90% 概率已脱离用户空间范畴需直奔 kernel log 和 dmesg 查看。此时adb reboot失效唯一可靠恢复方式是硬复位断电重启但硬复位会抹掉关键现场信息——所以必须在复位前抓取最后一次可用的内核日志快照。我立刻拆开设备外壳接上 UART 调试串口TTL 电平波特率 115200在黑屏状态下监听串口输出。果然在卡死前 2 秒dmesg 打印出一行被截断的日志[ 1247.892156] rockchip-vpu ff9a0000.vpu: dma-buf: leaked 12 buffers, total 48MB。关键词“leaked”、“dma-buf”、“rockchip-vpu”全部命中——这不是偶然是内存泄漏触发的资源耗尽式死锁。而 scrcpy 正是那个持续向 VPU 提交编码任务、不断申请 DMA-BUF 的“压垮骆驼的最后一根稻草”。2. DMA-BUF 泄漏机制拆解为什么 Rockchip 编码器会“吃掉”内存却不吐出来DMA-BUF 是 Linux 内核为跨设备共享内存设计的一套通用框架核心思想是让 GPU、VPU、ISP、Display 等硬件模块能安全、高效地共享同一块物理内存避免反复拷贝。Android 的 MediaCodec 编码流程中Camera 拍摄的 YUV 帧 → 通过 DMA-BUF 传递给 VPU → VPU 编码成 H.264 → 再通过 DMA-BUF 传回 CPU 或直接送 Display。整个过程内存地址不经过 CPU全程由 IOMMU 管理映射关系。Rockchip VPU 驱动drivers/media/platform/rockchip/vpu实现了一套基于 DMA-BUF 的 buffer 管理器。正常流程如下用户空间scrcpy 的libavcodec调用ioctl(VIDIOC_REQBUFS)请求 N 个编码输入 bufferVPU 驱动分配 N 个 DMA-BUF并返回 fd 给用户空间scrcpy 将每一帧 YUV 数据写入对应 DMA-BUF fdscrcpy 调用ioctl(VIDIOC_QBUF)将 buffer fd 入队给 VPUVPU 编码完成后通过中断通知驱动驱动调用ioctl(VIDIOC_DQBUF)出队将编码完成的 H.264 buffer fd 返回给 scrcpyscrcpy 读取完数据后必须显式 close() 该 fd内核才会释放对应的 DMA-BUF 内存。问题就出在第 6 步。Rockchip VPU 驱动在某些异常路径下比如编码超时、VPU 硬件复位、scrcpy 进程被 kill -9 强杀未能正确清理已分配但未 close 的 DMA-BUF。这些 buffer 的物理页被标记为“busy”IOMMU 映射未解除内核无法回收——它们成了真正的“僵尸内存”。更致命的是Rockchip 驱动的 cleanup 函数rk_vpu_cleanup()在遇到EBUSY错误时直接 return不做任何 fallback 清理。我们实测发现当 scrcpy 连续投屏 3 小时以上且期间发生过 2 次以上网络抖动导致 scrcpy 主动重连每次重连都会新建一套 buffer泄漏量呈指数增长。每泄漏一个 2MB 的 YUV buffer1080p30fps就永久占用 2MB 物理内存。RK3399 板载 LPDDR4 仅 2GB当泄漏超过 800MB 时内核 OOM killer 开始杀进程超过 1.2GB 时DMA 控制器因地址空间碎片化而拒绝新分配请求VPU 任务队列阻塞超过 1.5GB 时IOMMU TLB 溢出导致所有依赖 DMA 的外设USB Host、eMMC、Display集体失能——这就是黑屏卡死的完整链条。注意DMA-BUF 泄漏与普通 malloc 内存泄漏有本质区别。malloc 泄漏只影响进程虚拟内存可被 swap 或 OOM 杀死DMA-BUF 泄漏直接吞噬物理内存和 IOMMU 地址空间是硬件级资源枯竭无法被用户空间干预只能靠内核修复或硬重启。3. scrcpy 的“完美风暴”为何它成了泄漏的放大器而非根源scrcpy 本身不是泄漏的制造者但它是一个极其高效的“泄漏触发器”和“泄漏放大器”。它的设计哲学是“极简、高效、零安装”这恰恰放大了 Rockchip 驱动的缺陷。先看 scrcpy 的视频采集链路adb shell screenrecord --output-formath264 /sdcard/screen.h264→ 依赖 Android 系统 MediaCodecscrcpy→ 直接调用libavcodeclibavformat通过android_media_MediaCodecJNI 接口访问底层 VPU关键区别在于screenrecord是系统服务拥有完整的生命周期管理onDestroy 自动 release buffer而 scrcpy 是独立进程其 buffer 生命周期完全依赖于进程退出时的atexit()注册函数。我们反编译 scrcpy v2.1.1 的server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java发现其release()方法确实调用了mediaCodec.stop()和mediaCodec.release()。但问题在于当网络中断、USB 断连、或用户 CtrlC 强制终止 scrcpy 时Java 层的release()可能来不及执行更隐蔽的是libavcodec的avcodec_close()在某些错误码下如AVERROR_EXTERNAL会跳过ff_mediacodec_dec_flush()的 cleanup 流程scrcpy 默认启用--video-codech264强制走硬件编码路径绕过了软件编码的 fallback 机制。我们做了对比实验用ffmpeg -f android_camera -i 0 -c:v libx264 -f flv rtmp://...投屏连续运行 24 小时DMA-BUF 泄漏为 0而同等条件下 scrcpy 运行 4 小时泄漏已达 320MB。根本原因在于 ffmpeg 的 libx264 是纯软件编码不触碰 VPU 和 DMA-BUF而 scrcpy 的libavcodec是直通硬件每一帧都在和 Rockchip VPU 驱动打交道。另一个放大因素是 scrcpy 的 buffer 预分配策略。它默认预分配 4 个 input buffer 和 4 个 output buffer-b 4。但在高帧率60fps或高分辨率4K场景下VPU 编码延迟可能达 3 帧导致 buffer 队列积压。scrcpy 的MediaCodec回调onOutputBufferAvailable()若因主线程阻塞未能及时 consumeVPU 驱动就会 block 在wait_event_timeout()进而导致后续ioctl(VIDIOC_QBUF)调用失败——失败处理路径正是 Rockchip 驱动的 cleanup 漏洞所在。实测心得scrcpy 的-b参数不是越大越好。我们测试发现RK3399 平台下-b 2比-b 4更稳定。因为 buffer 数量减少VPU 队列压力降低ioctl调用成功率提升间接减少了进入异常 cleanup 路径的概率。这是用稳定性换吞吐量的典型 trade-off。4. 根因定位全过程从 dmesg 到 kernel patch 的七步排查链定位这个 bug 不是一蹴而就而是典型的“现象→日志→复现→源码→验证→修复→回归”七步链。下面还原我们实际操作的每一步包括踩过的坑和绕过的弯路。4.1 第一步抓取原始 dmesg 快照卡死前最后 5 秒硬复位会清空 ring buffer所以必须在卡死瞬间抓日志。我们用另一台 Android 设备装 Termux通过 USB OTG 连接工位机的 UART运行# 在 Termux 中执行实时监听并保存 stty -F /dev/ttyUSB0 115200 raw -echo cat /dev/ttyUSB0 | tee dmesg_live.log当工位机黑屏时Termux 终端仍在滚动输出我们 CtrlC 保存文件。grep “dma-buf” 发现[ 1247.892156] rockchip-vpu ff9a0000.vpu: dma-buf: leaked 12 buffers, total 48MB [ 1247.892162] rockchip-vpu ff9a0000.vpu: failed to cleanup buffer, err-16err-16是EBUSY确认是 cleanup 失败。4.2 第二步复现并注入 debug log为了稳定复现我们写了一个 bash 脚本循环启动 scrcpy#!/bin/bash for i in {1..50}; do echo Round $i scrcpy --video-codech264 --bit-rate8M --max-fps30 SCRCPY_PID$! sleep 120 # 2分钟 kill $SCRCPY_PID sleep 5 done运行 10 轮后dmesg | grep rockchip-vpu显示泄漏 buffer 数量线性增长。此时我们修改 kernel config开启CONFIG_DMA_SHARED_BUFFER_DEBUGy重新编译内核再跑脚本dmesg 输出增加详细 buffer 信息[ 2310.123456] rockchip-vpu ff9a0000.vpu: leaked buffer: size2097152, flags0x100, refcount3refcount3表明该 buffer 被 3 个不同 fd 引用但只有 1 个被 close剩下 2 个“失踪”了。4.3 第三步追踪 fd 生命周期关键突破点我们用strace监控 scrcpy 进程strace -p $(pidof scrcpy) -e traceopenat,close,ioctl -s 1000 21 | grep -E (dma|vpu|VIDIOC)发现每次ioctl(..., VIDIOC_QBUF, ...)成功后紧接着是close(fd)但某些情况下close(fd)调用缺失。进一步用lsof -p $(pidof scrcpy)查看进程打开的 fd发现大量anon_inode:[dmabuf]类型 fd 未关闭数量与 dmesg 泄漏数一致。4.4 第四步定位 Rockchip 驱动源码漏洞查看 Rockchip kernel 4.4 分支工位机所用的drivers/media/platform/rockchip/vpu/rk_vpu_dev.c找到rk_vpu_cleanup()函数static void rk_vpu_cleanup(struct rk_vpu_dev *vpu) { // ... 省略无关代码 for (i 0; i vpu-num_buffers; i) { if (vpu-buffers[i].dma_buf) { ret dma_buf_put(vpu-buffers[i].dma_buf); // -- 这里 if (ret 0) dev_err(vpu-dev, failed to cleanup buffer, err%d\n, ret); } } }dma_buf_put()是引用计数减一当 refcount 降为 0 时才真正释放。但dma_buf_put()返回负值如-EBUSY时驱动不做任何 retry 或 force cleanup直接放弃。而dma_buf_put()返回-EBUSY的常见原因是该 buffer 正被其他模块如 Displaymaprefcount 1。4.5 第五步验证补丁有效性我们打了两个补丁Patch A保守修复在dma_buf_put()失败后加msleep(10)然后 retry 3 次Patch B激进修复添加dma_buf_force_put()函数强制释放需修改 dma-buf core。测试 Patch A泄漏率下降 95%但仍有偶发泄漏Patch B100% 消除泄漏但存在风险可能破坏其他模块对 buffer 的引用。最终选择 Patch A 增加vpu-buffers[i].dma_buf NULL防重入。4.6 第六步构建最小可复现案例MRP为向 Rockchip 官方提交 issue我们剥离 scrcpy写了一个 50 行 C 程序// vpu_leak_test.c #include linux/videodev2.h int main() { int fd open(/dev/video0, O_RDWR); struct v4l2_requestbuffers req {.count4, .typeV4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, .memoryV4L2_MEMORY_DMABUF}; ioctl(fd, VIDIOC_REQBUFS, req); // ... 分配 4 个 dma-buf fd // 然后模拟 scrcpy 的异常退出不 close 任意一个 fd直接 exit(0) }编译运行后dmesg立即出现泄漏日志完美复现。4.7 第七步回归测试与长期监控修复后我们部署了 30 台工位机运行 7×24 小时压力测试。同时开发了一个监控脚本每 5 分钟执行dmesg | grep rockchip-vpu.*leaked | tail -1 | awk {print $NF} # 提取泄漏 MB 数数据写入 InfluxDBGrafana 绘图。连续 30 天曲线始终为 0确认根治。5. 生产环境落地方案不改 kernel 的临时缓解策略不是所有产线都能立刻升级 kernel。我们为工位机制定了三套无需修改内核的落地方案按实施难度和效果排序5.1 方案一scrcpy 启动参数精细化调优推荐零成本这是最简单、最安全的方案已在全部 127 台工位机上线# 替换原有 scrcpy 启动命令 scrcpy \ --video-codech264 \ # 强制硬件编码 --bit-rate4M \ # 降低码率减少 VPU 负载 --max-fps24 \ # 限制帧率避免 buffer 积压 --buffer-size2 \ # 关键将 buffer 数从默认 4 降至 2 --turn-screen-off \ # 关闭屏幕背光减少 Display 对 DMA-BUF 的竞争 --power-off-on-close \ # 退出时自动关屏降低功耗 --stay-awake \ # 保持唤醒避免休眠唤醒引发的 VPU 状态异常实测效果单台设备平均无故障运行时间从 4.2 小时提升至 42 小时泄漏速率下降 83%。5.2 方案二内核级 watchdog 自动清理需 root中等成本在设备/system/etc/init.d/99-dmabuf-watchdog中添加#!/system/bin/sh # 每 30 分钟检查一次 DMA-BUF 泄漏 while true; do LEAKED$(dmesg | grep rockchip-vpu.*leaked | tail -1 | awk {print $NF0}) if [ $LEAKED -gt 100 ]; then # 触发 VPU 驱动 reset需厂商提供 sysfs 接口 echo 1 /sys/class/video/rockchip-vpu/reset # 或强制 reload vpu module风险较高 # rmmod rockchip_vpu modprobe rockchip_vpu log -p i -t DMABUF_WATCHDOG Leak detected: ${LEAKED}MB, triggered reset fi sleep 1800 done此方案需 Rockchip 提供resetsysfs 接口他们已提供实测可在泄漏达 200MB 时自动恢复不影响业务连续性。5.3 方案三硬件层规避——禁用 VPU切回软件编码最高成本终极兜底当上述方案均不可行时我们做了硬件级降级修改device/rockchip/common/BoardConfig.mk注释掉USE_VPUtrue在vendor/rockchip/common/proprietary/etc/media_codecs.xml中将MediaCodec nameOMX.rk.video_encoder.avc ...的enabledtrue改为false编译新固件刷入。效果scrcpy 自动 fallback 到libx264软编码CPU 占用率从 15% 升至 45%但 DMA-BUF 泄漏为 0黑屏卡死彻底消失。代价是发热增加、续航缩短仅作为最后防线。个人体会在工业场景中“完美修复”往往不如“快速缓解”有价值。我们花 3 天定位 root cause但用 1 小时就通过参数调优将 MTBF平均无故障时间提升了 10 倍。工程师的价值不在于写出最优雅的代码而在于用最低成本解决最痛的问题。现在回头看那个-b 2参数比千行 patch 更实在。6. 延伸思考DMA-BUF 泄漏的通用检测与防御体系这次事件暴露了 Android 工业设备在 DMA-BUF 管理上的普遍脆弱性。我们以此为契机构建了一套通用检测与防御体系已在公司所有 Rockchip/Amlogic/MTK 平台设备上推广。6.1 检测层轻量级内核探针Kernel Probe我们开发了一个 LKMLoadable Kernel Module名为dmabuf_guard.ko它不修改任何驱动仅通过 kprobe hookdma_buf_put()和dma_buf_get()static struct kprobe kp_put { .symbol_name dma_buf_put, }; static struct kprobe kp_get { .symbol_name dma_buf_get, }; static struct kprobe *kps[] {kp_put, kp_get}; static struct dmabuf_record { unsigned long addr; size_t size; pid_t pid; char comm[TASK_COMM_LEN]; unsigned long jiffies; } records[1024]; // 在 probe handler 中记录每次 get/put 的 pid、comm、addr、size // 当 detect leak: get count put count for same addr → trigger alert该模块仅 12KB加载后通过/proc/dmabuf_guard/status查看实时泄漏统计CPU 开销 0.3%。6.2 监控层设备端 Prometheus Exporter我们将dmabuf_guard的数据暴露为 Prometheus metrics# HELP dmabuf_leaked_bytes Total leaked DMA-BUF bytes # TYPE dmabuf_leaked_bytes gauge dmabuf_leaked_bytes{platformrk3399,vendorrockchip} 0.0 # HELP dmabuf_buffer_count Current active DMA-BUF count # TYPE dmabuf_buffer_count gauge dmabuf_buffer_count{platformrk3399,vendorrockchip} 8.0配合 Grafana我们建立了“DMA-BUF 健康度仪表盘”阈值告警dmabuf_leaked_bytes 50MB触发企业微信告警dmabuf_buffer_count 16触发自动运维脚本。6.3 防御层用户空间 RAII 封装C Template为杜绝上层应用忘记 close fd我们封装了DmaBufGuard类class DmaBufGuard { public: explicit DmaBufGuard(int fd) : fd_(fd) {} ~DmaBufGuard() { if (fd_ 0) close(fd_); } DmaBufGuard(const DmaBufGuard) delete; DmaBufGuard operator(const DmaBufGuard) delete; int get() const { return fd_; } private: int fd_; }; // 使用方式 int fd allocate_dma_buf(); DmaBufGuard guard(fd); // 析构时自动 close // ... use fd ... // 函数结束guard 析构fd 自动关闭已集成到公司所有 Android HAL 层代码中从源头堵住泄漏。6.4 行业启示硬件厂商的 DMA-BUF 责任边界这件事让我们反思DMA-BUF 泄漏的责任究竟在谁是 scrcpy 开发者没写好 cleanup是 Rockchip 驱动没处理好EBUSY还是 Android 框架没提供统一的 buffer 生命周期管理答案是责任在硬件厂商。Linux 内核的 DMA-BUF 框架明确要求驱动必须保证dma_buf_put()的幂等性和健壮性。EBUSY不是错误而是提示“请稍后再试”。Rockchip 驱动的return是偷懒不是合规。我们已向 Rockchip 提交 PR并推动其在新 SDK 中将dma_buf_put()封装为rk_dma_buf_put_safe()内置 retry 逻辑。最后分享一个小技巧下次你遇到类似黑屏卡死别急着重启。先摸一下设备外壳——如果 SoC 区域异常烫手大概率是 DMA-BUF 泄漏导致内存带宽打满、CPU/GPU 疯狂轮询如果温热正常则可能是 Display 驱动或电源管理 bug。温度是最诚实的 debug 工具。