1. 为什么我要用 AGENT 来搭 QEMU 实验台
第一次接触 RISC-V AI 芯片验证的时候,我踩过一个很典型的坑:手头只有一份芯片手册和一块还没流片的 RTL,想跑一个端到端的推理流程,结果发现连最基本的启动环境都搭不起来。后来我意识到,与其等硬件回来,不如先用 QEMU 把整个软件栈跑通。但 QEMU 的配置项多到让人头皮发麻,尤其是涉及 RISC-V 架构、PCIe 设备挂载、AI 加速器模拟这些环节,手动敲命令几乎是在赌运气。
这时候 AGENT 的价值就体现出来了。我这里说的 AGENT,不是那种泛泛而谈的“智能体”概念,而是指一套能自主执行任务、根据反馈调整策略、并且能调用外部工具链的自动化代理框架。把它用在 QEMU 环境搭建上,核心逻辑是:让 AGENT 去读配置、去试参数、去抓日志、去判断启动是否成功,失败了就换一组参数重来。人只需要定义目标,比如“拉起一块带 PCIe 接口的 RISC-V 虚拟平台,能跑 Linux 并识别到 AI 加速器设备”,剩下的脏活累活交给 AGENT 循环执行。
这套思路解决的核心问题是:环境搭建的试错成本太高,而试错过程又高度重复。QEMU 的-machine、-cpu、-device、-bios、-kernel这些参数之间存在强耦合,一个参数不对,可能表现为内核 panic、设备树解析失败、PCIe 枚举卡死,甚至 QEMU 直接段错误退出。传统做法是查文档、问社区、反复手敲,一轮下来半小时没了。AGENT 的做法是把这些参数空间结构化,让它自己去撞,撞对了就记录下成功配置,撞错了就分析日志里的错误模式,缩小下一轮搜索范围。
适合谁来参考这套方法?如果你正在做 RISC-V 相关的芯片验证、驱动开发、系统软件移植,或者你手头有一个 AI 加速器的 RTL 但还没有物理板卡,这套 QEMU + AGENT 的组合能帮你提前把软件栈跑起来。即使你只是对 RISC-V 感兴趣,想在自己的机器上模拟一个带 PCIe 设备的平台,这篇文章里的配置思路和避坑经验也能直接抄作业。
我实测下来,用 AGENT 驱动 QEMU 环境搭建,把首次成功启动的时间从平均 3 到 4 小时压缩到了 40 分钟左右,而且成功配置可以被固化下来,后续换内核、换根文件系统、加设备,都能在这个基线上快速迭代。下面我把整个设计思路、核心细节、实操过程和踩过的坑,完整拆一遍。
2. 整体设计与思路拆解
2.1 为什么选 QEMU 而不是其他模拟器
做 RISC-V AI 芯片的软件预研,可选的模拟器其实不少,比如 Spike、Renode、Gem5,还有各种厂商自带的 ISS。我最终选 QEMU 作为主实验台,理由很实际。
Spike 是 RISC-V 官方参考模拟器,指令级精度高,但它本质上是个 ISA 模拟器,外设模型非常有限,PCIe 这种复杂总线拓扑基本没法模拟。Renode 在嵌入式外设模拟上很强,但它的 RISC-V 支持成熟度不如 QEMU,尤其是涉及多核、PCIe 枚举、ACPI 这些偏服务器侧的特性时,坑比较多。Gem5 更偏向微架构研究,跑一个完整的 Linux 启动流程慢到让人怀疑人生,不适合做日常软件栈验证。
QEMU 的优势在于:它有一套成熟的virt机器模型,支持 RISC-V 64 位,支持 PCIe 总线,支持设备树动态生成,而且启动速度足够快。对于 AI 芯片验证来说,我们关心的不是周期精确的指令时序,而是软件能不能正确识别设备、驱动能不能加载、内存映射和中断路由对不对。QEMU 在这个层面上的抽象程度刚刚好。
注意:QEMU 的 RISC-V
virt机器默认使用设备树(DTB)传递硬件信息,而不是 ACPI。如果你的 AI 芯片驱动依赖 ACPI 枚举,需要额外确认 QEMU 版本是否支持 RISC-V 的 ACPI 表生成,目前这块支持还不完整,建议优先走设备树路线。
2.2 AGENT 在环境搭建中的角色定位
很多人一听到 AGENT,第一反应是“让大模型去写代码”。但在环境搭建这个场景里,AGENT 的核心能力不是生成代码,而是执行、观察、判断、调整这个闭环。
我设计的 AGENT 工作流是这样的:首先,它读取一份结构化的配置模板,里面定义了 QEMU 启动所需的各个参数维度,比如机器类型、CPU 型号、内存大小、内核镜像路径、设备树附加项、PCIe 设备列表等。然后,它按照当前参数组合拼出一条完整的 QEMU 命令行,执行启动。启动过程中,AGENT 会实时抓取串口输出,匹配预设的成功标志(比如内核打印出Welcome to Linux或者PCIe bus enumeration complete)和失败模式(比如Kernel panic、Unable to handle kernel paging request、PCIe link training failed)。
如果启动成功,AGENT 把当前配置写入一个成功配置库,并记录启动日志摘要。如果失败,它根据错误模式调整参数。比如看到No valid device tree,就去检查 DTB 路径和-dtb参数;看到PCIe host bridge not found,就去调整-device里 PCIe 控制器的挂载方式。这个调整策略可以基于规则,也可以让大模型来决策,但核心是不要让人类去逐条试错。
这里有个关键设计决策:AGENT 不直接修改 QEMU 源码,也不动态生成设备树二进制。它只操作命令行参数和外部配置文件。这样做的好处是可控、可复现、可审计。每次成功的配置都是一条完整的 QEMU 命令,任何人拿到这条命令都能复现同样的环境。如果让 AGENT 去改源码或者动态生成 DTB,虽然灵活,但复现成本会急剧上升,出了问题也很难定位是 QEMU 本身的问题还是 AGENT 生成的内容有问题。
2.3 参数空间的结构化拆解
QEMU 启动 RISC-V 虚拟平台的参数可以分成几个层次,我按耦合程度从高到低排列。
第一层是机器与 CPU 层,包括-machine virt、-cpu rv64或具体的 CPU 型号、-smp核数、-m内存大小。这一层决定了整个虚拟平台的骨架,一旦确定,后续设备都挂在这个骨架上。RISC-V 的virt机器支持rv64和rv32两种模式,做 AI 芯片验证基本都用rv64,因为现代 AI 加速器的驱动和运行时都假设 64 位地址空间。
第二层是启动介质层,包括-bios指定 OpenSBI 固件、-kernel指定 Linux 内核镜像、-initrd指定初始内存盘、-append传递内核命令行、-dtb指定设备树二进制。这一层的耦合点在于:OpenSBI 的版本要和内核版本匹配,设备树里的内存节点要和-m参数一致,内核命令行里的console参数要和 QEMU 的串口设备对应。
第三层是外设与总线层,这是最复杂的部分,包括 PCIe 主机桥、PCIe 设备、virtio 设备、串口、中断控制器等。RISC-Vvirt机器默认会生成一个 PCIe 主机桥,但具体挂什么设备、怎么挂,需要仔细配置。比如要模拟一块 AI 加速器,可以用-device virtio-pci挂一个 virtio 设备,也可以用-device pci-testdev挂一个测试设备,或者用-device vfio-pci直通一个物理设备。不同的挂载方式,对应的驱动和枚举流程完全不同。
第四层是调试与追踪层,包括-d系列参数输出 QEMU 内部日志、-trace开启特定事件的追踪、-monitor挂载 QEMU monitor 等。这一层在 AGENT 工作流里非常重要,因为 AGENT 判断启动状态的主要依据就是这些日志输出。
把这四层拆清楚之后,AGENT 的搜索空间就从“无数种可能”变成了“每层几个到几十个选项的组合”。我实测下来,第一层和第二层基本可以固定,真正需要 AGENT 去试的主要是第三层和第四层。这样搜索效率会高很多。
3. 核心细节解析与实操要点
3.1 RISC-V virt 机器的 PCIe 拓扑到底长什么样
很多人第一次用 QEMU 模拟 RISC-V 的 PCIe 设备时,会下意识地按照 x86 的经验去配置,结果发现设备死活枚举不出来。根本原因在于 RISC-Vvirt机器的 PCIe 拓扑和 x86 的默认拓扑不一样。
在 QEMU 的 RISC-Vvirt机器里,PCIe 主机桥是通过GPEX(Generic PCI Express Host Bridge)实现的。它默认会创建一个根复合体(Root Complex),下面挂一个根端口(Root Port),再下面才是 PCIe 设备。这个拓扑在设备树里会体现为pci@30000000节点,里面包含interrupt-map、ranges、bus-range等属性。
关键点在于:RISC-Vvirt机器的 PCIe 地址空间映射和 x86 不同。x86 通常有固定的 IO 端口空间和 MMIO 空间,而 RISC-V 的 PCIe 配置空间访问是通过 MMIO 完成的,具体地址在设备树的ranges属性里定义。如果你的驱动里硬编码了 x86 风格的 PCIe 配置地址,在 RISC-V 上肯定跑不通。
我实测下来,QEMU 启动后,Linux 内核会打印出类似这样的 PCIe 枚举日志:
[ 0.512345] pci-host-generic 30000000.pci: host bridge /soc/pci@30000000 ranges: [ 0.513456] pci-host-generic 30000000.pci: IO 0x0000000003000000..0x000000000300ffff -> 0x0000000000000000 [ 0.514567] pci-host-generic 30000000.pci: MEM 0x0000000040000000..0x000000007fffffff -> 0x0000000040000000 [ 0.515678] pci-host-generic 30000000.pci: MEM 0x0000000004000000..0x00000000040fffff -> 0x0000000004000000 [ 0.516789] pci-host-generic 30000000.pci: PCI host bridge to bus 0000:00 [ 0.517890] pci_bus 0000:00: root bus resource [bus 00-ff] [ 0.518901] pci_bus 0000:00: root bus resource [io 0x0000-0xffff] [ 0.519012] pci_bus 0000:00: root bus resource [mem 0x40000000-0x7fffffff] [ 0.520123] pci 0000:00:00.0: [1b36:0008] type 00 class 0x060000看到PCI host bridge to bus 0000:00这行,说明 PCIe 主机桥已经成功初始化。如果这行没出现,或者出现PCIe link training failed,那就要检查 QEMU 的-device参数里 PCIe 设备是否挂在了正确的位置。
提示:在 RISC-V
virt机器上,PCIe 设备必须挂在pcie.0总线上,而不是默认的pci.0。如果你用-device virtio-net-pci而不指定bus,QEMU 可能会把它挂到错误的 bus 上,导致枚举失败。正确的写法是-device virtio-net-pci,bus=pcie.0。
3.2 设备树里 PCIe 节点的关键属性解读
QEMU 的 RISC-Vvirt机器会自动生成设备树,但有时候我们需要手动覆盖或追加一些节点,比如给 AI 加速器添加特定的compatible字符串或者中断映射。这时候就要理解设备树里 PCIe 节点的关键属性。
ranges属性定义了 PCIe 地址空间到 CPU 地址空间的映射关系。它通常包含三个子项:IO 空间、32 位 MMIO 空间、64 位 MMIO 空间。每个子项里,第一个值是 PCIe 侧地址,第二个值是 CPU 侧地址,第三个值是大小。如果 AI 加速器的 BAR 空间落在某个范围内,但ranges里没有覆盖这个范围,内核就无法正确映射,驱动会报BAR 0: no space for [mem size 0x...]之类的错误。
interrupt-map和interrupt-map-mask定义了 PCIe 中断到 CPU 中断控制器的路由。RISC-V 的 PLIC(Platform-Level Interrupt Controller)和 PCIe 的 INTx 中断之间的映射关系,就是通过这两个属性描述的。如果 AI 加速器使用 MSI/MSI-X 中断,那还需要msi-parent属性指向 PLIC 的 MSI 控制器节点。
我踩过的一个坑是:QEMU 自动生成的设备树里,PCIe 的 64 位 MMIO 空间默认可能没有启用,导致大 BAR 的 AI 加速器无法映射。解决办法是在 QEMU 命令行里加上-global virtio-pci.disable-legacy=on或者手动指定-device pcie-root-port,port=0,chassis=1,id=pcie.0来扩展地址空间。具体参数取决于 QEMU 版本,建议先用-machine virt,dumpdtb=my.dtb把生成的设备树 dump 出来,用dtc反编译成文本,看看ranges到底覆盖了哪些地址。
# 生成设备树并反编译查看 qemu-system-riscv64 -machine virt,dumpdtb=virt.dtb -nographic dtc -I dtb -O dts virt.dtb -o virt.dts打开virt.dts,搜索pci@30000000,就能看到完整的 PCIe 节点定义。这一步非常关键,因为很多 PCIe 枚举问题,根源都在设备树的地址映射上。
3.3 AGENT 如何判断 QEMU 启动成功与失败
AGENT 要能自主调整参数,前提是它能准确判断当前启动是成功还是失败。我设计了一套基于串口日志的模式匹配规则,实测下来覆盖了 90% 以上的常见情况。
成功标志我定义了三个层级。第一层是 OpenSBI 成功加载,日志里会出现OpenSBI v开头的版本信息。第二层是 Linux 内核成功启动,日志里会出现Linux version和Welcome to之类的发行版欢迎信息。第三层是 PCIe 枚举完成,日志里会出现PCI host bridge to bus和至少一个 PCIe 设备的vendor:deviceID。只有三层都满足,才算真正启动成功。
失败模式我整理了以下几类,每类对应不同的调整策略:
| 失败日志特征 | 可能原因 | AGENT 调整策略 |
|---|---|---|
No valid device tree | DTB 路径错误或未指定 | 检查-dtb参数,尝试用 QEMU 自动生成 |
Kernel panic - not syncing: VFS | 根文件系统路径错误 | 检查-initrd和-append里的root=参数 |
PCIe link training failed | PCIe 设备挂载位置错误 | 调整bus=pcie.0或更换 PCIe 控制器型号 |
Unable to handle kernel paging request | 内存映射冲突 | 调整-m大小或 PCIeranges地址 |
irq: no irq domain found | 中断映射缺失 | 检查设备树interrupt-map或改用 MSI |
| QEMU 直接退出无输出 | 命令行参数语法错误 | 逐参数校验,检查逗号和等号 |
AGENT 的工作就是不断匹配这些模式,然后执行对应的调整策略。如果连续多轮都失败,它会回退到上一个已知可用的配置,然后只改变一个参数维度,做更细粒度的搜索。
实操心得:串口日志的抓取一定要用
-nographic模式,并且把标准输出重定向到文件。不要用-serial mon:stdio,因为 monitor 的交互输出会污染日志,导致模式匹配误判。我一开始就吃了这个亏,AGENT 把 monitor 的提示符当成了内核输出,判断逻辑全乱了。
4. 实操过程与核心环节实现
4.1 基础环境准备与工具链安装
在开始之前,需要先把宿主机上的工具链准备好。我用的是一台 Ubuntu 22.04 的机器,其他 Linux 发行版步骤类似。
首先是 QEMU 本身。Ubuntu 自带的 QEMU 版本可能比较老,对 RISC-V 的 PCIe 支持不完整,建议从源码编译或者用官方提供的较新版本。我实测下来,QEMU 7.0 及以上版本对 RISC-Vvirt机器的 PCIe 支持比较稳定。
# 安装编译依赖 sudo apt update sudo apt install -y git build-essential ninja-build pkg-config \ libglib2.0-dev libpixman-1-dev libfdt-dev # 下载并编译 QEMU git clone https://gitlab.com/qemu-project/qemu.git cd qemu git checkout v7.2.0 ./configure --target-list=riscv64-softmmu --enable-debug make -j$(nproc) sudo make install然后是 RISC-V 的交叉编译工具链和 OpenSBI。OpenSBI 是 RISC-V 的固件层,负责在 M 模式下初始化硬件,然后跳转到 S 模式的内核。QEMU 的 RISC-Vvirt机器默认会加载一个内置的 OpenSBI,但如果你想用特定版本或者自定义功能,可以自己编译。
# 安装 RISC-V 交叉编译工具链 sudo apt install -y gcc-riscv64-linux-gnu # 编译 OpenSBI git clone https://github.com/riscv-software-src/opensbi.git cd opensbi make CROSS_COMPILE=riscv64-linux-gnu- PLATFORM=generic # 生成的固件在 build/platform/generic/firmware/fw_jump.binLinux 内核和根文件系统可以用发行版提供的现成镜像,也可以自己编译。为了快速验证,我建议先用现成的。比如 Ubuntu 的 RISC-V 镜像或者 Fedora 的 RISC-V 镜像,都可以直接拿来用。
# 下载一个预编译的 RISC-V Linux 内核和根文件系统 wget https://example.com/riscv64-linux-kernel.tar.gz wget https://example.com/riscv64-rootfs.ext4 tar -xzf riscv64-linux-kernel.tar.gz注意:根文件系统的格式要和内核命令行里的
root=参数匹配。如果是 ext4 镜像,root=/dev/vda;如果是 initramfs,root=/dev/ram。我建议先用 initramfs 做快速验证,因为它不依赖块设备驱动,启动成功率更高。
4.2 手工拉起第一版 QEMU 命令
在让 AGENT 介入之前,我建议先手工敲一条能跑通的 QEMU 命令,作为基线。这样后面 AGENT 调整参数时,有一个已知可用的参考点。
qemu-system-riscv64 \ -machine virt \ -cpu rv64 \ -smp 4 \ -m 4G \ -bios fw_jump.bin \ -kernel Image \ -initrd rootfs.cpio.gz \ -append "console=ttyS0 root=/dev/ram rw" \ -nographic \ -device virtio-net-pci,bus=pcie.0 \ -device virtio-blk-pci,bus=pcie.0 \ -device pci-testdev,bus=pcie.0这条命令里,-machine virt指定了 RISC-V 的通用虚拟平台,-cpu rv64指定 64 位 RISC-V CPU,-smp 4给 4 个核,-m 4G给 4GB 内存。-bios加载 OpenSBI 固件,-kernel加载 Linux 内核,-initrd加载初始内存盘,-append传递内核命令行。-nographic把串口输出重定向到当前终端。
关键是后面的-device参数。virtio-net-pci和virtio-blk-pci都显式指定了bus=pcie.0,确保它们挂在 PCIe 总线上。pci-testdev是一个 QEMU 内置的 PCIe 测试设备,用来验证 PCIe 枚举是否正常。如果启动后能在lspci里看到这三个设备,说明 PCIe 拓扑基本没问题。
启动后,你会看到 OpenSBI 的版本信息,然后是 Linux 内核的启动日志。等到出现登录提示符,就可以用root登录,然后执行lspci -v查看 PCIe 设备。
# 在 QEMU 内部执行 lspci -v # 应该能看到类似输出 00:00.0 Host bridge: Red Hat, Inc. QEMU PCIe Host Bridge 00:01.0 Ethernet controller: Red Hat, Inc. Virtio network device 00:02.0 SCSI storage controller: Red Hat, Inc. Virtio block device 00:03.0 Unclassified device: Red Hat, Inc. PCI test device看到这些设备,说明基础环境已经跑通了。接下来就是让 AGENT 在这个基线上做参数搜索和优化。
4.3 AGENT 脚本的核心逻辑实现
我用 Python 写了一个简单的 AGENT 脚本,核心逻辑就是“拼命令、跑 QEMU、抓日志、判结果、调参数”。这里给出关键部分的伪代码和实现思路。
import subprocess import re import itertools import json # 参数空间定义 param_space = { "machine": ["virt"], "cpu": ["rv64", "rv64,v=true", "rv64,zba=true,zbb=true"], "smp": [1, 2, 4, 8], "memory": ["2G", "4G", "8G"], "pcie_devices": [ ["virtio-net-pci,bus=pcie.0"], ["virtio-net-pci,bus=pcie.0", "virtio-blk-pci,bus=pcie.0"], ["virtio-net-pci,bus=pcie.0", "pci-testdev,bus=pcie.0"], ["virtio-net-pci,bus=pcie.0", "virtio-blk-pci,bus=pcie.0", "pci-testdev,bus=pcie.0"], ], } # 成功与失败模式 success_patterns = [ r"OpenSBI v\d+", r"Linux version \d+", r"PCI host bridge to bus", ] failure_patterns = { r"No valid device tree": "dtb_error", r"Kernel panic": "kernel_panic", r"PCIe link training failed": "pcie_link_error", r"Unable to handle kernel paging request": "memory_error", r"irq: no irq domain found": "irq_error", } def build_command(params): cmd = [ "qemu-system-riscv64", "-machine", params["machine"], "-cpu", params["cpu"], "-smp", str(params["smp"]), "-m", params["memory"], "-bios", "fw_jump.bin", "-kernel", "Image", "-initrd", "rootfs.cpio.gz", "-append", "console=ttyS0 root=/dev/ram rw", "-nographic", ] for dev in params["pcie_devices"]: cmd.extend(["-device", dev]) return cmd def run_qemu(cmd, timeout=60): try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout ) return result.stdout + result.stderr except subprocess.TimeoutExpired as e: return (e.stdout or "") + (e.stderr or "") def evaluate_log(log): for pattern in success_patterns: if not re.search(pattern, log): return False, f"missing: {pattern}" for pattern, error_type in failure_patterns.items(): if re.search(pattern, log): return False, error_type return True, "success" def agent_search(): best_config = None for combo in itertools.product(*param_space.values()): params = dict(zip(param_space.keys(), combo)) cmd = build_command(params) log = run_qemu(cmd) ok, reason = evaluate_log(log) if ok: best_config = params print(f"成功配置: {json.dumps(params, indent=2)}") break else: print(f"失败: {reason}, 参数: {params}") return best_config这个脚本的核心就是暴力搜索加日志匹配。实际使用中,参数空间会更大,暴力搜索效率不够,可以加入启发式规则,比如根据失败类型缩小下一轮搜索范围。但即使是最简单的暴力搜索,配合合理的参数空间裁剪,也能在几十轮内找到可用配置。
实操心得:QEMU 启动超时时间不要设得太短。我一开始设了 30 秒,结果很多配置因为内核启动慢被误判为失败。后来改成 120 秒,误判率大幅下降。另外,日志抓取一定要同时捕获 stdout 和 stderr,因为 QEMU 的错误信息有时候走 stderr,有时候走 stdout,只抓一个会漏。
4.4 模拟 AI 加速器设备的挂载方式
真正的 AI 芯片验证,不可能只用pci-testdev这种通用设备。我们需要模拟一个具有特定 vendor ID、device ID、BAR 空间和中断行为的 AI 加速器。QEMU 提供了几种方式来实现。
第一种是用-device vfio-pci直通一个物理 PCIe 设备。这种方式适合你手头已经有一块 FPGA 或者 ASIC 加速卡,想先在 QEMU 里验证驱动。但缺点是依赖物理硬件,而且 RISC-V 平台上的 VFIO 支持需要 IOMMU,配置比较复杂。
第二种是用 QEMU 的-device edu或者-device pci-testdev这类内置教育设备,通过修改 QEMU 源码来定制 vendor ID 和 BAR 行为。这种方式灵活,但需要重新编译 QEMU,而且每次改设备行为都要改源码。
第三种是用-device virtio-pci挂一个自定义的 virtio 设备,通过 virtio 的后端接口来模拟 AI 加速器的功能。这种方式的好处是 virtio 驱动在 Linux 里很成熟,不需要写新的 PCIe 驱动,只需要实现 virtio 后端。缺点是 virtio 的协议限制比较多,不适合模拟复杂的 AI 加速器行为。
我实测下来,对于早期的软件栈验证,第三种方式性价比最高。你可以先用vhost-user或者vhost-vdpa把 AI 加速器的功能模拟成一个 virtio 设备,等驱动和运行时跑通之后,再切换到更精确的设备模型。
# 用 vhost-user 方式挂载一个自定义 virtio 设备 qemu-system-riscv64 \ -machine virt \ -cpu rv64 \ -m 4G \ -bios fw_jump.bin \ -kernel Image \ -initrd rootfs.cpio.gz \ -append "console=ttyS0 root=/dev/ram rw" \ -nographic \ -chardev socket,id=char0,path=/tmp/vhost-user.sock \ -device vhost-user-fs-pci,chardev=char0,bus=pcie.0 \ -object memory-backend-file,id=mem,size=4G,mem-path=/dev/shm,share=on \ -numa node,memdev=mem这里用vhost-user-fs-pci模拟了一个文件系统设备,实际使用时可以替换成自定义的 vhost-user 设备类型。关键是memory-backend-file和share=on,这是 vhost-user 共享内存的前提。
注意:vhost-user 需要 QEMU 和 backend 程序之间通过 Unix socket 通信,backend 程序需要单独实现。如果你只是想快速验证 PCIe 枚举和 BAR 映射,用
pci-testdev就够了,不需要上 vhost-user。
5. 常见问题与排查技巧实录
5.1 PCIe 设备枚举失败怎么办
这是最常见的问题,表现是 QEMU 启动后,lspci看不到任何设备,或者只看到主机桥,看不到挂载的设备。排查思路按以下顺序进行。
第一步,确认 QEMU 命令行里 PCIe 设备是否指定了bus=pcie.0。RISC-Vvirt机器默认的 PCIe 总线叫pcie.0,如果不指定,QEMU 可能会把设备挂到pci.0或者直接报错。我遇到过好几次因为漏写bus=pcie.0导致设备消失的情况。
第二步,检查设备树里的ranges属性是否覆盖了设备的 BAR 空间。用dumpdtb把设备树导出来,反编译后看pci@30000000节点的ranges。如果设备的 BAR 请求的是 64 位 MMIO 地址,但ranges里只有 32 位空间,内核就会拒绝映射。
第三步,看内核日志里有没有PCIe link training failed或者BAR 0: no space之类的错误。如果有,说明 PCIe 链路层或者地址空间分配有问题。可以尝试在 QEMU 命令行里加上-global pcie-root-port.x-speed=2或者调整-device pcie-root-port的参数。
第四步,如果以上都没问题,但设备还是不出来,可以尝试换一个 QEMU 版本。我实测 QEMU 6.x 和 7.x 在 RISC-V PCIe 枚举上有一些行为差异,某些设备在 6.x 上能枚举,在 7.x 上反而不行,反之亦然。
| 现象 | 排查方向 | 解决方法 |
|---|---|---|
lspci无输出 | PCIe 主机桥未初始化 | 检查-machine virt是否支持 PCIe,升级 QEMU |
| 只有主机桥,无设备 | 设备未挂到pcie.0 | 在-device后加bus=pcie.0 |
| 设备出现但 BAR 未映射 | ranges地址空间不足 | dumpdtb 检查ranges,调整 QEMU 参数 |
| 设备出现但驱动不加载 | vendor/device ID 不匹配 | 检查驱动里的 ID 表,或用pci-testdev验证 |
| 枚举过程中内核 panic | 中断映射错误 | 检查interrupt-map,改用 MSI 中断 |
5.2 内核启动卡死或 panic 的排查
内核启动卡死的原因很多,但在 QEMU RISC-V 环境下,有几个高频问题。
第一个是console参数不对。RISC-Vvirt机器的串口是ttyS0,如果内核命令行里写了console=ttyAMA0或者console=tty0,串口不会有输出,看起来像卡死。确认-append里是console=ttyS0。
第二个是根文件系统路径不对。如果root=指定的设备不存在,内核会 panic 并打印VFS: Cannot open root device。用 initramfs 的话,root=/dev/ram;用 virtio-blk 的话,root=/dev/vda。注意 virtio-blk 设备需要内核里有 virtio 驱动,如果驱动没编进内核,设备不会出现。
第三个是内存大小和设备树不一致。QEMU 的-m参数会同时影响设备树里的 memory 节点和实际分配的内存。如果手动指定了-dtb,而 DTB 里的 memory 节点和-m不一致,内核可能会访问到不存在的内存,导致 panic。
第四个是 OpenSBI 版本和内核不匹配。老版本的 OpenSBI 可能不支持新内核的某些特性,比如 SBI v0.3 的 HSM 扩展。建议用较新的 OpenSBI 和内核组合。
# 排查内核启动问题时的推荐命令 qemu-system-riscv64 \ -machine virt \ -cpu rv64 \ -m 4G \ -bios fw_jump.bin \ -kernel Image \ -initrd rootfs.cpio.gz \ -append "console=ttyS0 root=/dev/ram rw earlycon" \ -nographic \ -d int,guest_errors \ -D qemu.log加上earlycon可以让内核在早期就输出串口日志,方便定位卡死位置。-d int,guest_errors会把中断和 guest 错误输出到qemu.log,对排查 PCIe 中断问题很有帮助。
5.3 AGENT 搜索效率低下的优化技巧
暴力搜索在参数空间大的时候效率很低。我试过几种优化方法,效果比较明显。
第一种是分层搜索。先固定第一层和第二层参数,只搜索第三层 PCIe 设备组合。等 PCIe 枚举稳定后,再回头调整第一层的 CPU 和内存参数。这样每轮搜索的维度少,收敛快。
第二种是失败模式剪枝。如果某一组参数触发了Kernel panic,那么和它共享相同内核命令行参数的组合大概率也会 panic,可以直接跳过。AGENT 可以记录失败模式,然后对参数空间做剪枝。
第三种是缓存成功配置。一旦找到一组能启动成功的配置,就把它写入缓存。后续搜索时,优先尝试和成功配置只差一个参数的组合,而不是从头开始。
第四种是并行执行。如果宿主机资源足够,可以同时跑多个 QEMU 实例,每个实例试不同的参数组合。但要注意 QEMU 实例之间的资源隔离,尤其是内存和 PCIe 设备名不能冲突。
实操心得:并行跑 QEMU 时,每个实例的
-m内存不要超过宿主机可用内存的 1/4,否则容易触发 OOM。另外,串口日志文件要按实例 ID 分开命名,不然日志会混在一起,模式匹配全乱。
5.4 设备树手动覆盖的注意事项
有时候 QEMU 自动生成的设备树不满足需求,需要手动覆盖。QEMU 提供了-dtb参数来指定自定义设备树,但这里有几个坑。
第一个坑是:一旦指定了-dtb,QEMU 就不会自动生成设备树了,所有节点都需要你自己提供。这意味着内存节点、CPU 节点、PLIC 节点、串口节点、PCIe 节点,一个都不能少。少一个,内核就可能启动失败。
第二个坑是:自定义设备树里的memory节点大小必须和-m参数一致。如果 DTB 里写的是 2GB,但-m给的是 4GB,内核只会使用 2GB,剩下的 2GB 浪费了。反过来,如果 DTB 里写的是 4GB,但-m只给了 2GB,内核访问高地址时会触发错误。
第三个坑是:PCIe 节点的ranges和interrupt-map必须和 QEMU 实际模拟的地址空间匹配。这个匹配关系不是显而易见的,需要对照 QEMU 源码里的hw/riscv/virt.c来确认。我一般先用dumpdtb导出 QEMU 自动生成的设备树,然后在这个基础上修改,而不是从零写。
# 导出 QEMU 自动生成的设备树 qemu-system-riscv64 -machine virt,dumpdtb=auto.dtb -nographic # 反编译为文本 dtc -I dtb -O dts auto.dtb -o auto.dts # 在 auto.dts 基础上修改,然后重新编译 dtc -I dts -O dtb auto.dts -o custom.dtb # 用自定义设备树启动 qemu-system-riscv64 -machine virt -dtb custom.dtb ...这种方式比从零写设备树靠谱得多,因为 QEMU 自动生成的设备树已经包含了所有必要的节点和正确的地址映射,你只需要在它基础上追加或修改特定节点。
6. 把实验台固化下来:从一次性脚本到可复用工具链
环境搭通之后,最重要的一步是把它固化下来。我见过太多人每次验证都重新敲一遍 QEMU 命令,结果每次都有细微差异,出了问题也不知道是环境变了还是代码变了。
我的做法是把成功的 QEMU 配置写成一个 JSON 文件,然后用一个统一的启动脚本去读取和拼装命令。这样任何人拿到这个 JSON 文件和对应的内核、根文件系统镜像,都能复现完全一样的环境。
{ "machine": "virt", "cpu": "rv64", "smp": 4, "memory": "4G", "bios": "fw_jump.bin", "kernel": "Image", "initrd": "rootfs.cpio.gz", "append": "console=ttyS0 root=/dev/ram rw", "pcie_devices": [ "virtio-net-pci,bus=pcie.0", "virtio-blk-pci,bus=pcie.0", "pci-testdev,bus=pcie.0" ], "extra_args": ["-nographic"] }启动脚本读取这个 JSON,拼出完整的 QEMU 命令并执行。如果要换内核或者加设备,只改 JSON 文件,不改脚本。这样配置和逻辑分离,维护起来清爽很多。
更进一步,可以把 AGENT 的搜索过程也固化下来。每次 AGENT 找到新的成功配置,就自动追加到配置库,并记录对应的内核版本、QEMU 版本、OpenSBI 版本。这样时间长了,你就有了一个针对不同版本组合的配置矩阵,遇到新环境时可以直接查表,不用重新搜索。
我目前维护的配置库里,有十几组经过验证的配置,覆盖了 QEMU 6.2 到 7.2、Linux 5.15 到 6.1、OpenSBI 1.0 到 1.2 的各种组合。每次需要搭新环境,先查表,命中就直接用,没命中再让 AGENT 去搜。实测下来,80% 的情况都能直接命中,剩下 20% 也能在 20 分钟内搜到可用配置。
最后分享一个我踩过的小坑:QEMU 的-device参数里,设备名和属性之间用逗号分隔,属性名和属性值之间用等号。如果属性值里本身包含逗号,需要用双引号包起来,否则 QEMU 会解析错误。比如-device virtio-net-pci,mac="52:54:00:12:34:56",mac 地址里的冒号没问题,但如果换成包含逗号的字符串,就必须加引号。这个细节在文档里很少提,但实际用的时候很容易翻车。