1. 项目背景与核心思路拆解
1.1 QMVS到底是什么,解决什么问题
QMVS全称Qualcomm Memory Verification System,是高通平台针对内存子系统做验证的一套测试环境。很多刚接手高通项目的兄弟一听"验证系统"就发怵,觉得是个多神秘的东西。其实拆开看就一件事:用一套标准化用例,在真机上把DDR读写带宽、Cache一致性、CMA分配、DMA-BUF映射这些内存相关模块全部跑一遍,确认硬件时钟频率、带宽表现和稳定性都在规格范围内。
我最早接触QMVS是在做某款中端平台的board bringup阶段。当时DDR频率调到2133MHz后,跑Android CTS里的某些大内存用例就随机crash,kernel log里全是"Unable to handle kernel paging request at virtual address"。硬件同事说是软件问题,软件怀疑DDR参数没配对,两边扯皮。后来就是用QMVS里的一组压力用例,把DDR频率、电压、时序组合全部扫了一遍,才定位出是某组tRCD时序参数在高温下不稳定。
所以QMVS真正解决的痛点是:内存这类底层问题往往不是线性复现的,必须靠稳定、可重复的测试压测来暴露。自带的用例覆盖面广,从单bit读写到整片DDR翻转,从单核访问到多核并发压力,都有现成套路。这篇文章就是把我从零搭这套环境的全过程写出来,包括环境准备、工具链、编译部署、上板测试,以及我踩过的那些坑。适合正在做高通平台bringup的驱动工程师、做memory相关验证的测试工程师,以及被"out of memory"和"memory space overlap"这类问题折磨的同学参考。
1.2 为什么选QMVS而不是直接跑memtester
很多人会问,Linux下现成的内存测试工具多了去了,memtester、stressapptest、memtest86+都能跑,为什么非要折腾QMVS?我的回答是:场景不一样。
memtester和stressapptest本质上是应用层工具,它们只能访问Linux内核分配给用户空间的普通内存。但QMVS的用例可以覆盖到reserved-memory区域、CMA大块连续内存、IOMMU映射的DMA-BUF,这些区域用memtester根本碰不到。而且QMVS内置了与硬件寄存器交互的接口,能直接读写DDR控制器的状态寄存器,记录频率切换过程中的时序数据。这些能力是纯用户态工具做不到的。
另一个关键因素是可重复性和对比性。内测工具每次跑的结果只能说明"这次有没有问题",但QMVS的用例有标准化的进入条件和退出条件,同一用例在不同芯片版本、不同DDR频率下跑的分数可以直接对比,方便定位是芯片版本引入的性能衰退还是配置导致的异常。做量产前的内存验证,这种对比能力比单次测试结果值钱得多。
2. 环境准备:主机、目标设备与工具链
2.1 主机端环境要求
QMVS的编译对主机要求不算苛刻,但有几个硬性指标需要注意:
- 操作系统:Ubuntu 18.04或20.04 x86_64,我用的20.04,整体兼容性最好。用Fedora这类系统也能跑,但编译时经常会遇到GCC版本和构建脚本不匹配的问题,不想给自己添堵就直接上Ubuntu。
- 内存:至少16GB。虽然编译本身吃不了这么多内存,但跑make -j8时,编译器会同时起多个进程,每个进程吃1-2GB是比较正常的。我一开始在8GB内存的旧笔记本上编译,经常"Killed"信号直接把编译进程干掉,就是这个原因。
- 磁盘:源码加编译产物至少预留40GB。QMVS依赖的kernel源码和工具链解压后体积不小,而且编译中间文件特别占空间。
- SWAP:如果主机内存确实不够,建议至少分配4GB swap。但注意,swap空间只解决"编译不死"的问题,跑并行编译时如果频繁用swap,编译时间会翻好几倍,能用物理内存就别指望swap救场。
主机准备好后,需要安装基础依赖库:
sudo apt update sudo apt install git make gcc g++ flex bison libncurses-dev \ libssl-dev libelf-dev python3 python3-pip \ android-tools-adb android-tools-fastboot其中libssl-dev和libelf-dev最容易漏装,kernel编译时缺这两个库会报openssl/elf头文件找不到,新手很容易卡在这一步。
2.2 目标设备端准备
目标设备的准备分为系统层面和调试层面两块。
系统层面,设备上跑的kernel必须开启对应的内存调试配置。QMVS的测试模块需要以下内核配置项支持:
| 配置项 | 作用 | 缺失表现 |
|---|---|---|
| CONFIG_CMA | 连续内存分配器 | 大块连续内存用例直接失败 |
| CONFIG_DMA_CMA | DMA连续内存分配 | DMA相关用例无法分配buffer |
| CONFIG_DEBUG_FS | debugfs调试文件系统 | 部分监控用例无法读取数据 |
| CONFIG_ION(旧内核) | ION内存管理 | ION heap测试用例失败 |
| CONFIG_DMA_SHARED_BUFFER | DMA-BUF共享缓冲 | buffer共享用例失败 |
| CONFIG_FTRACE | 内核函数追踪 | 时延分析用例不可用 |
调试层面,开启ADB调试,确保设备能通过USB被主机识别。最好将设备置于root状态,因为QMVS部分用例需要读取物理地址映射的寄存器值,非root权限下这些操作会被拒。设备上需要预留至少500MB的/data空间,测试镜像和日志文件都往这里放。
2.3 工具链选择:官方工具链还是开源GCC
工具链是QMVS环境里最容易被忽略却又最容易出问题的一环。简单说有两种选择:高通向合作伙伴提供的闭源工具链,或者Linaro/ARM官方开源工具链。
我的建议是:能用开源工具链就别折腾闭源那套。闭源工具链的优势是经过高通验证,在某些极端优化场景下编出来的代码跑得更稳,但获取和许可流程繁琐。开源工具链用aarch64-linux-gnu-前缀的GCC 9.3及以上版本,兼容性已经很成熟了。
sudo apt install gcc-aarch64-linux-gnu aarch64-linux-gnu-gcc --version这里有个关键点:工具链的glibc版本必须和target设备的rootfs匹配。如果设备Android版本较老(Android 10以下),rootfs里的libc版本低,用新GCC编译的测试程序会报GLIBC_2.33 not found之类的错误。解决办法是先查看设备的libc版本,再决定工具链版本。
# 目标设备上执行 adb shell getprop ro.build.version.release adb shell strings /system/lib64/libc.so | grep GLIBC_ | sort -u | tail -n 5拿到设备libc版本后,选择对应版本的交叉编译器。这一条经验在QMVS环境里比其他任何场景都重要——我见过太多人把工具链升级到最新版,结果测试程序推到设备上直接起不来。
3. 实操过程:从源码编译到上板跑测
3.1 获取QMVS源码与版本选择
QMVS源码来源主要分两种渠道:高通发布给ODM/OEM的打包版本,以及CodeAurora(CAF)上开源的部分组件。对于小团队或个人开发者,如果拿不到完整商业包,可以从CAF拉取kernel源码配合自写测试脚本的方式构建简化版Q MVS环境。
如果拿到的是完整源码包,目录结构通常是这样的:
qmvs/ ├── Makefile ├── build/ │ └── qmvs_defconfig ├── host/ # 主机端管理工具 ├── target/ # 目标设备端测试程序 │ ├── suite/ │ │ ├── ddr_bw_test.c # DDR带宽测试 │ │ ├── cma_stress_test.c # CMA压力测试 │ │ ├── dma_mapping_test.c # DMA映射测试 │ │ └── cache_test.c # Cache一致性测试 │ └── lib/ ├── scripts/ # 自动化脚本 └── docs/版本选择的原则是:优先选与目标kernel版本匹配的QMVS版本。用错版本最常见的表现是内核头文件API对不上,编译时报enum定义冲突或函数参数类型不匹配。CAF上的kernel分支名通常带平台代号,比如SM8250、SM7250等,QMVS源码包里会有一个platform_config目录,里面按平台区分了编译配置,先确认你的平台在里面再动手。
3.2 交叉编译配置与命令详解
编译的第一步是配置defconfig。这块踩坑点密集,需要仔细说。
cd qmvs export ARCH=arm64 export CROSS_COMPILE=/usr/bin/aarch64-linux-gnu- make qmvs_defconfig如果qmvs_defconfig不完全匹配你的平台,需要进menuconfig做微调:
make menuconfig通常在"Platform Support -> SoC Selection"里选择对应平台,在"Memory Subsystem"里确认DDR频率档位和地址映射方式。这一步里有个容易忽略的点:QMVS的地址映射表必须和kernel中的设备树(DTS)保持一致。如果kernel里通过reserved-memory为某个模块预留了物理地址区域,但QMVS的地址映射表不知道这块区域,测试时去访问它就会触发memory space overlap错误。
编译执行:
make -j8编译生成的产物一般在target/out/目录下,主要文件包括:
qmvs_test:主测试程序qmvs_module.ko:内核测试模块- 若干个
*.so动态库和测试数据文件
一个常见的坑是编译时没指定CFLAGS,导致用户态测试程序没有用上硬浮点单元。指令集不匹配在运行时表现为Illegal instruction (core dumped),特别难排查。所以编译时最好显式声明:
make CFLAGS="-O2 -mfpu=neon -mfloat-abi=softfp" -j8注意softfp和hard的选择要参考设备rootfs的编译方式,用readelf -A查看设备上任意一个可执行文件的Tag即可确认。
3.3 部署到目标设备的具体操作
编译产物到位后,部署步骤比较直接:
# 连接设备并确认识别 adb devices # 以root重启adb服务 adb root # 创建测试目录 adb shell mkdir -p /data/local/tmp/qmvs # 推送测试文件 adb push target/out/qmvs_test /data/local/tmp/qmvs/ adb push target/out/qmvs_module.ko /data/local/tmp/qmvs/ adb push target/out/data/ /data/local/tmp/qmvs/data/ # 赋执行权限 adb shell chmod -R 777 /data/local/tmp/qmvs # 加载内核测试模块 adb shell insmod /data/local/tmp/qmvs/qmvs_module.ko加载内核模块这一步经常出问题,最常见的报错是operation not permitted。原因一是kernel开启了模块签名校验,二是SELinux策略拦截。碰到模块签名问题,可以在kernel源码里关闭CONFIG_MODULE_SIG重新编译kernel刷入设备;SELinux问题则可以临时关闭验证:
adb shell setenforce 0如果insmod返回Unknown symbol,大概率是模块编译时用的kernel版本和设备上跑的版本不一致。确认设备kernel版本:
adb shell uname -a然后回到kernel源码目录检查编译时的./include/generated/utsrelease.h,两边必须完全一致。
3.4 测试用例选择与参数配置
QMVS的用例不是无脑全部跑。内存验证场景不同,用例组合完全不同。我的经验是分三个层次:
第一层:基础连通性验证。刚上电或者刚改完DTS内存映射,先跑ddr_bw_test和cache_test的basic模式,确认内存基本读写正常。这层有几个用例就够,跑完时间控制在10分钟内。
adb shell /data/local/tmp/qmvs/qmvs_test --suite=basic --duration=600第二层:压力稳定性验证。量产前的稳定性测试,重点跑cma_stress_test和dma_mapping_test的stress模式,同时并发多个实例模拟真实负载:
adb shell "/data/local/tmp/qmvs/qmvs_test --suite=cma_stress --iterations=10000" & adb shell "/data/local/tmp/qmvs/qmvs_test --suite=dma_mapping --iterations=5000" &第三层:边界和异常场景。包括DDR频率热切换时的稳定性、低内存场景下的分配失败处理、CMA大块分配失败后的回收机制等。这类用例耗时最长,通常放在夜间自动化执行。
跑测之前有个必须做的准备:手动记录DDR配置基线。通过设备节点读取当前DDR频率和时序:
adb shell cat /sys/kernel/debug/ddr_freq/current_freq adb shell cat /sys/kernel/debug/ddr_freq/current_timing这些值在测试结束后的结果分析阶段是重要的对照数据。如果不记录基线,测试报告的黄金参考值就无法建立,后面的对比分析无从谈起。
4. 常见问题与排查技巧实录
4.1 编译期问题:工具链冲突和内核头文件错位
编译期的坑主要集中在工具链和内核头文件这两个方向。
问题1:GLIBC版本不匹配
表现:编译能过,但推到设备上执行时报./qmvs_test: /lib/aarch64-linux-gnu/libm.so.6: version 'GLIBC_2.29' not found。
排查思路:先确认设备libc版本,再换对应版本的工具链,不要盲目追新。通过aarch64-linux-gnu-gcc -print-file-name=libc.so.6查看工具链自带的libc版本。
问题2:内核头文件版本与设备kernel不一致
表现:编译模块时大量error: implicit declaration of function或multiple definition错误。
排查思路:QMVS源码包里通常有个kernel_headers目录,检查里面内核版本是否与设备一致。不一致时直接把设备对应的kernel源码里的include/uapi目录拷贝覆盖,重新编译。千万别图省事用系统默认的/usr/include/linux头文件,那个是给x86用户态用的,交叉编译环境中用错内核头文件是最隐蔽的坑。
问题3:链接阶段undefined reference
表现:链接时报undefined reference to 'ion_alloc'之类。
排查思路:多数情况是在编译用户态测试程序时没有链接对应库,检查Makefile中LDLIBS是否包含-lion或-ldma_buf。如果库文件缺失,说明QMVS的lib目录编译不全,回到target/lib下单独编译后再链接。
4.2 运行期问题:CMA分配失败和memory space overlap
问题4:CMA分配失败,内核报错
表现:cma_alloc: failed to allocate 512 MiB,测试程序返回-ENOMEM。
排查思路:先看CMA总容量和当前碎片情况:
adb shell cat /proc/meminfo | grep -i cma adb shell cat /d/dma_buf/bufinfoCMA总容量由kernel cmdline中的cma=参数决定,比如cma=256M表示保留256MB给CMA。分配失败通常是两个原因:一是CMA总容量太小,二是大量page被其他驱动pin住无法迁移。前者改cmdline扩容,后者需要排查到底是哪个驱动长期占用了CMA内存。可以在kernel开启CONFIG_CMA_DEBUG,通过dmesg定位长时间占用CMA的调用栈。
问题5:memory space overlap
表现:测试程序访问某段物理地址时,kernel报BUG: Bad page state in process,或者memremap失败返回NULL。
排查思路:这个错误大概率是设备树reserved-memory区域与QMVS配置的地址映射冲突。检查kernel DTS中所有reserved-memory节点的address和size,对照QMVS的配置文件platform_config/<platform>/mem_map.cfg,确保两者的地址区间没有重叠。
我遇到过一次最典型的case:某个外设驱动在DTS里reserved了一块4KB地址做寄存器映射,但QMVS的地址映射表把它当成普通DDR去做读写测试,一写就触发系统panic。这种情况没有任何工具能自动发现,只能在搭建阶段人工核对所有reserved-memory区域。建议写个脚本解析DTS并和QMVS配置文件交叉比对:
#!/bin/bash # 简单解析DTS reserved-memory并输出地址区间 grep -A5 "reserved-memory" arch/arm64/boot/dts/vendor/platform.dtsi | \ grep -E "(reg|size)" | sed 's/[;,]/ /g' | awk '{print $2, $3}'问题6:kernel自动kill测试进程
表现:Out of memory: Killed process 1234 (qmvs_test) total-vm:... anon-rss:...,测试中途无故退出。
排查思路:设备总内存不足时,kernel的OOM killer会挑占用最大的进程下手,qmvs_test这类吃内存跑测的自然首当其冲。网上有些建议是关掉OOM killer,但我强烈不建议在生产环境中这么干。正确做法是:降低测试进程的内存占用档位,或者先停掉设备上的其他大内存应用,保证测试独占大部分内存。如果必须同时跑多个用例,用cgroup把qmvs_test单独放进一个memory控制组,限制其内存使用上限,比关OOM killer安全得多。
4.3 结果分析问题:数据异常时的判断方法
问题7:带宽测试数据忽高忽低
表现:同一用例在同一设备上跑,DDR带宽测试结果浮动超过15%。
排查思路:大概率是测试期间DDR进入了频率调节。移动设备默认开了cpufreq和DDR的DVFS,负载低的时候DDR会自动降到低频率档位。跑带宽测试前,先把DDR频率锁到目标档位:
adb shell echo 0 > /sys/kernel/debug/ddr_freq/enable_dvfs adb shell echo 2133000000 > /sys/kernel/debug/ddr_freq/current_freq如果是高通平台某个特定chrome分支,节点路径可能不同,可以用find /sys -name "*ddr*freq*"找一下。频率锁定后数据波动应控制在3%以内。
问题8:测试耗时异常
表现:同样的用例,有时候1小时跑完,有时候4小时跑不完。
排查思路:先排除设备温度因素。测试设备如果散热不好,温度过高触发thermal throttling,DDR会自动降频,不仅耗时会拉长,还会导致压测结果失真。跑长时间用例时建议外接散热风扇,或者定时监控温度:
adb shell while true; do cat /sys/class/thermal/thermal_zone*/temp; sleep 10; done温度超过85摄氏度时,谨慎分析该轮测试的有效性。我一般有个结论可以使用:超过90度还硬跑,测出来的稳定性数据不采信。
5. 实操心得:这些设计能避免你少走三个月弯路
5.1 环境隔离与版本管理
搭QMVS环境最容易被忽视的设计缺陷,是把不同平台的配置混在同一套代码里。QMVS源码在不同平台下编译,会生成不同的二进制。我见过团队里有人把SM8250编译的qmvs_test直接推到SM7250设备上跑,跑出来的DDR带宽数据比预期低20%,找了半天原因,最后发现是频率档位配置表不匹配。
从第一天开始就严格区分平台的编译输出目录:
make O=build/SM8250 qmvs_defconfig make O=build/SM8250 -j8输出到不同目录,后续就不会有二进制串平台的问题。同时建议写一个简单的setup_env.sh,把平台、工具链路径、kernel版本这些变量固化下来,留档备查。每次环境变更都记录变更日志,否则半年后你会完全想不起来当前这套环境是怎么搭出来的。
5.2 自动化跑测脚本的威力
手工敲命令跑QMVS用例不是不行,但用例一多就管理不过来。我后来写了个简单的shell脚本,把常用用例按顺序打包跑:
#!/bin/bash SUITE=$1 DURATION=$2 LOG_DIR=/data/local/tmp/qmvs/logs/$(date +%Y%m%d_%H%M%S) adb shell mkdir -p $LOG_DIR for case in basic cma_stress dma_mapping cache; do echo "[$(date)] run case: $case" adb shell "/data/local/tmp/qmvs/qmvs_test --suite=$case --duration=$DURATION" 2>&1 | \ tee $LOG_DIR/${case}.log # 检查返回码并记录结果 if [ ${PIPESTATUS[0]} -eq 0 ]; then echo "PASS" >> $LOG_DIR/summary.txt else echo "FAIL" >> $LOG_DIR/summary.txt fi done这个脚本的价值不只是省手工,更重要的是每次跑测的退出码都有标准记录。肉眼扫日志很累,而且容易漏掉中间某个小错误。把结果规范化为PASS/FAIL后,问题才能第一时间暴露出来。
5.3 跑测期间看住这几个关键日志
跑QMVS长测时,我习惯在另一个终端窗口实时关注几个关键输出:
- dmesg:内核日志,出现
panic、Oops、BUG等关键词立刻记录上下文 - /proc/buddyinfo:观察内存碎片化程度,碎片严重时CMA分配失败率会明显上升
- 温度节点:温度异常是所有内存稳定性问题的隐形杀手
顺手写个简单的实时监控组合命令,避免每次手动敲三遍:
adb shell "dmesg -w > /data/local/tmp/qmvs/dmesg.log &"跑测结束后优先检查dmesg里关键错误,再对照summary.txt看哪些用例失败。先看dmesg再看用例结果,这个顺序能省下很多排查时间。
5.4 数据记录与定位价值
最后分享一个基于前期工作的小建议:每次跑测,不管通过不通过,都保留完整的配置基线记录。
记录内容包括:kernel版本、DDR频率档位、VDDRQ电压、工具链版本、QMVS版本、测试时长、温度范围、测试结果摘要。这个基线数据库开始可能看不出价值,但当你需要排查"为什么这台设备量产两周后DDR开始不稳"时,回查基线数据能帮你快速确认"硬件是否发生了变化,还是我们改过配置没留下记录"。
我在这套环境搭建过程中踩过的最大跟头,就是早期没有做环境隔离和基线记录。后果是某次DDR参数微调后,所有测试数据都无法和历史数据对比,白跑了一周的系统性验证重测。从那时起,基线记录就成了强制习惯。这套QMVS环境跑通之后,后续每次平台迭代的memory验证都省了很多心,所有变更都能在第一时间用标准用例验证影响面。