1. 先把预期摆正:x86 主机上跑 arm64 虚拟机到底能得到什么
上个月为了验证一个交叉编译出来的 arm64 二进制包,我在 x86 工作站上开了一台 arm64 虚拟机。同一份最小化安装镜像,同样给 4 核 4GB 内存,x86 那台 6 分钟进系统,arm64 这台跑了 47 分钟才看到登录提示符。这中间没有配置写错,也没有镜像损坏,就是这类环境最真实的一面:在 x86 主机上用 QEMU 创建 arm64 架构下的 Linux 虚拟机,CPU 指令是一条一条被翻译执行的,速度天然要打一个大折扣。
所以动手之前,先把两件事想清楚。第一,你要的是"跑起来能用",还是"跑起来够快"。第二,你手上的主机是什么架构。如果主机的 CPU 本身就是 arm64(比如苹果 M 系列芯片的笔记本、arm64 服务器),那 QEMU 能借助硬件虚拟化能力,arm64 虚拟机跑起来几乎和原生一样快;如果主机是 x86_64,那就只能用纯软件仿真,性能掉一个数量级是常态,但换来的是不需要任何 arm64 物理设备就能完成软件包验证、内核模块编译冒烟、发行版安装流程演练、驱动加载测试这类工作。
我见过很多人卡在这里:装完之后发现"怎么这么卡",然后开始怀疑参数、怀疑镜像、怀疑磁盘格式,折腾一整天。其实根源就是没接受仿真运行这个前提。后面我会给出两种具体的创建方式——一种是直接手搓qemu-system-aarch64命令行,另一种是交给 libvirt 用virt-install做托管式部署。两种方法我都在实际项目里用过,各有取舍,下面把能踩的坑、能省的步骤一次讲透。
1.1 arm64 与 amd64 在虚拟化层面的三处硬差异
很多人第一次创建 arm64 虚拟机会失败,不是因为命令写错,而是因为思维还停留在 x86 上。arm64 平台的虚拟机有三处地方和 amd64 完全不同,必须提前建立认知。
第一,没有传统 BIOS,只有 UEFI 固件。x86 上你可以不加任何固件参数,QEMU 会给你一份默认的 SeaBIOS,开机就能看到引导画面。arm64 的virt机器没有这种东西,它需要一份 arm64 版的 UEFI 固件(文件名通常叫QEMU_EFI.fd或者AAVMF_CODE.fd),你不提供,虚拟机上电之后就是一片空白或者直接退出。这不是"可选优化",是必需项。
第二,没有默认显示设备。x86 机器默认挂一块 VGA 兼容显卡,装系统时图形界面自动就出来了。arm64 的virt机器默认什么都没接,你要看图形界面,得自己加virtio-gpu-pci并且配上 USB 键盘鼠标;不看图形界面,就得把串口(console)用起来。这也是为什么 arm64 服务器发行版的安装镜像,默认输出都往串口走。
第三,CPU 型号必须显式挑。x86 上写-cpu host表示直通宿主机 CPU 特性;arm64 在 x86 主机上跑,-cpu host直接报错,因为宿主和客户机根本不是同一套指令集。你得从cortex-a53、cortex-a57、cortex-a72、max、neoverse-n1里选一个,这个选择会直接影响客户机能否用上某些指令集扩展。
把这三条记住,后面所有参数就都能理解了,而不是照着抄。
1.2 仿真与硬件加速:什么情况下才谈得上"能用"
QEMU 的加速后端有三种常见形态,搞清楚它们的适用边界比记住参数重要得多:
| 加速方式 | 可用条件 | 性能量级 | 典型场景 |
|---|---|---|---|
| KVM | 宿主与客户机同架构,且宿主为 arm64 | 接近原生 | arm64 服务器上跑多台 arm64 虚拟机 |
| HVF | macOS 宿主且为 Apple Silicon | 接近原生 | Mac 本地做 arm64 开发环境 |
| TCG | 任意架构组合 | 原生性能的 1/8 至 1/20 | x86 主机上验证 arm64 软件包、跑安装流程 |
我实测过的几个数字供参考:在 8 核 x86 工作站上,cortex-a72四核 TCG 客户机编译一个中等规模的 C 项目,耗时大约是同一台机器上原生编译的 12 倍;跑apt装几百个包,大约 15 到 20 分钟;纯 IO 类操作(拷贝文件、打包)反而没慢那么多,因为这类任务 CPU 占比低。
判断标准很简单:你的任务如果对 CPU 算力敏感,就别在 x86 上用 TCG 跑 arm64 虚拟机;如果任务只是"验证 arm64 上能不能装、能不能起、配置对不对",那 TCG 完全够用,还省下一台物理机的钱。另外有一点要提前说清楚:跨架构的硬件虚拟化直通是不存在的,x86 主机上无论你怎么写-accel kvm,都不可能让 arm64 客户机用上 KVM,这是硬件层面的限制,不是软件配置问题。
2. 落地前的清单:主机依赖、固件与镜像怎么选
我习惯在动手前把三样东西备齐:宿主机的 QEMU 及配套工具、arm64 版的 UEFI 固件、以及一份靠谱的系统镜像。这三样缺任何一样,后面都会以各种奇怪的报错形式回报你。
2.1 主机侧软件包与版本核对
不同发行版包名不一样,我列一下实际用的:
# Debian / Ubuntu sudo apt update sudo apt install -y qemu-system-arm qemu-utils qemu-efi-aarch64 \ libvirt-daemon-system libvirt-clients virtinst # Fedora / RHEL 系 sudo dnf install -y qemu-kvm qemu-img edk2-aarch64 \ libvirt libvirt-daemon-kvm virt-install注意 Debian 系里 arm64 的 QEMU 二进制打在qemu-system-arm这个包里,名字看着像只支持 32 位,其实qemu-system-aarch64也在里面,别被包名误导。装完先做版本核对:
qemu-system-aarch64 --version qemu-system-aarch64 -machine virt -cpu help | head -20 virt-host-validate 2>/dev/null | head -20第二条命令能列出所有可选的 arm64 CPU 型号,如果你看到的列表里没有cortex-a72或max,说明包版本太老,建议升级到发行版自带的较新版本。另外virt-host-validate在跨架构场景下会报几条 KVM 相关的警告,直接忽略即可,因为我们本来就要走 TCG。
提示:QEMU 6.0 以下版本对 arm64 的
virt机器支持稍弱,尤其是 GICv3 和virtio-blk-pci的 bootindex 处理上有已知问题。如果条件允许,尽量用 7.x 及以上版本,省掉很多无谓的排查。
2.2 UEFI 固件是 arm64 虚拟机的"BIOS",来源与配法
固件文件的位置随发行版不同:
| 发行版 | 固件路径 | 说明 |
|---|---|---|
| Debian / Ubuntu | /usr/share/AAVMF/AAVMF_CODE.fd | 只读代码区 |
| Debian / Ubuntu | /usr/share/AAVMF/AAVMF_VARS.fd | 变量存储模板,需要拷贝一份用 |
| Fedora / RHEL | /usr/share/edk2/aarch64/QEMU_EFI-pflash.raw | 代码区 |
| Fedora / RHEL | /usr/share/edk2/aarch64/QEMU_VARS-pflash.raw | 变量存储模板 |
这里有个关键细节:固件分成"代码区"和"变量区"两部分。代码区只读,可以多个虚拟机共用同一份;变量区保存的是 UEFI 启动项、启动顺序这些会变的内容,每台虚拟机必须有自己独立的一份,否则会互相干扰。所以正确做法是把VARS文件拷贝一份出来,用虚拟磁盘(pflash)的方式挂上去,而不是图省事用-bios一次性加载。
用-bios能不能跑?能,安装过程一般没问题,但装完之后 UEFI 记不住启动项,每次开机都要手动进引导菜单选硬盘,反而更麻烦。我一开始就是图快用了-bios,结果每次重启虚拟机都得敲一次命令,两天之后老老实实换成了双 pflash 方案。
2.3 选 ISO 安装盘还是 cloud image
这一步很多人随便选,其实两种镜像的用法差别不小:
| 对比项 | ISO 安装镜像 | cloud image |
|---|---|---|
| 首次启动耗时(TCG) | 30 到 90 分钟 | 3 到 8 分钟 |
| 分区自由度 | 完全可控 | 固定布局,改起来麻烦 |
| 初始账号 | 安装时自己设 | 需要通过 cloud-init 注入 |
| 适合场景 | 演练真实安装流程、验证分区方案 | 快速起一台可用的 arm64 环境 |
| 推荐度(跨架构仿真) | 按需 | 优先 |
我现在的习惯是:如果只是要一台能登录、能编译、能测试的 arm64 环境,直接用 cloud image 加 cloud-init 注入 SSH 公钥,几分钟就能 ssh 进去;只有需要验证分区、引导方式、加密盘这类安装流程本身,才去走 ISO。这一点在 TCG 环境下尤其明显——省下的不只是第一次安装的几十分钟,还有后续每次重建环境的时间。
3. 方法一:qemu-system-aarch64 手工命令行直启
命令行方式最大的好处是所见即所得:虚拟机由哪几块设备组成、每个设备什么参数,全部写在一行命令里,没有隐藏的中间层。排查问题时这一点非常值钱。
3.1 十条参数搭出最小可引导环境
先建目录、准备固件变量区和系统盘:
mkdir -p ~/vms/arm64 && cd ~/vms/arm64 # 拷贝一份可写的 UEFI 变量存储 cp /usr/share/AAVMF/AAVMF_VARS.fd ./AAVMF_VARS.fd # 系统盘:qcow2 格式,上限 40G,元数据预分配减少碎片 qemu-img create -f qcow2 -o preallocation=metadata arm64-root.qcow2 40G # 校验镜像完整性,避免用坏文件折腾半天 sha256sum ubuntu-24.04.2-live-server-arm64.iso然后启动安装:
qemu-system-aarch64 \ -name arm64-install \ -machine virt,gic-version=3 \ -accel tcg,thread=multi \ -cpu cortex-a72 \ -smp 4,sockets=1,cores=4,threads=1 \ -m 4096 \ -drive if=pflash,format=raw,readonly=on,file=/usr/share/AAVMF/AAVMF_CODE.fd \ -drive if=pflash,format=raw,file=./AAVMF_VARS.fd \ -drive if=none,id=hd0,format=qcow2,file=./arm64-root.qcow2 \ -device virtio-blk-pci,drive=hd0,bootindex=1 \ -drive if=none,id=cd0,media=cdrom,format=raw,file=./ubuntu-24.04.2-live-server-arm64.iso \ -device virtio-scsi-pci,id=scsi0 \ -device scsi-cd,drive=cd0,bootindex=2 \ -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 \ -device virtio-net-pci,netdev=net0 \ -device virtio-rng-pci \ -nographic敲下去之后,正常情况下十几秒内串口就会滚出 UEFI 固件的启动日志,然后是 GRUB 菜单,接着是安装器的输出。虚拟机上电到看到画面如果超过一分钟还是空白,先别急着改参数,看看终端有没有报错——多半是固件路径不对。
3.2 每个参数为什么这么写:连点成线
把参数当成"咒语"背下来毫无意义,知道每一段在干什么,出问题时才知道该删哪一段:
| 参数 | 作用 | 不写会怎样 |
|---|---|---|
-machine virt,gic-version=3 | 选通用虚拟平台并指定中断控制器版本 | 老内核可能卡在 earlycon 无输出 |
-accel tcg,thread=multi | 开启多线程 TCG,每个 vCPU 一个宿主线程 | 单线程执行,多核客户机反而更慢 |
-cpu cortex-a72 | 指定客户机 CPU 型号 | 必须显式指定,host在此场景不可用 |
-smp 4,sockets=1,cores=4,threads=1 | 拓扑写清楚 | 有些内核按 topology 做调度,模糊描述会有意外 |
-m 4096 | 内存 4G | 低于 1G 时 UEFI 可能直接引导失败 |
两条if=pflash | 固件代码区 + 可写变量区 | 只用-bios会丢失 UEFI 启动项 |
virtio-blk-pci,bootindex=1 | 虚拟硬盘,引导优先级 1 | 没有 bootindex 时引导顺序不可控 |
virtio-scsi-pci+scsi-cd | 光驱挂载 | arm64 的 virt 机器没有 IDE 总线,ide-cd加不上 |
-netdev user,hostfwd=... | 用户态网络加端口转发 | 客户机能出网,但外部访问不进来 |
-device virtio-rng-pci | 给客户机提供熵源 | 启动时 systemd 等随机数会卡几十秒 |
-nographic | 串口输出到当前终端,不开图形窗口 | 需要自己接显示设备 |
那张表里我个人认为最容易被忽略的是virtio-rng-pci。arm64 客户机在 TCG 下熵池积累特别慢,没有这个设备,systemd 起服务时会一直等随机数,表现出来就是"卡在某一行不动"。加一行参数,能省下几十秒的等待,值得。
3.3 图形窗口与文本串口:两种看安装界面的方式
-nographic适合服务器镜像,因为它把串口直接接到当前终端。退出方式是Ctrl-A然后按X,注意不是Ctrl-C。如果想同时保留监视器等能力,可以改用-serial mon:stdio。
需要图形界面的时候,把-nographic换掉:
-device virtio-gpu-pci \ -device qemu-xhci -device usb-kbd -device usb-tablet \ -display gtk \ -serial mon:stdiousb-tablet比usb-mouse好用,鼠标指针能和宿主对齐,不用来回点一下才动。但实话讲,在 TCG 环境下装带桌面环境的发行版是一件非常考验耐心的事,我试过一次,从开始装到进桌面花了将近四个小时,全程鼠标都拖影。所以除非你在验证桌面相关的功能,否则强烈建议走最小化安装。
服务端场景我还常用一种"后台跑"的写法,适合长时间安装任务,关掉终端也不影响:
-display none \ -serial file:serial.log \ -daemonize -pidfile qemu.pid \ -monitor unix:mon.sock,server,nowait装的时候tail -f serial.log看进度,中途想查状态就连监视器:socat - UNIX-CONNECT:mon.sock,进去敲info status、info block都能看。这套组合我在远程服务器上操作时用得最多。
3.4 装完之后:改引导顺序、端口转发、日常启停
系统装好后,把光驱那两行删掉,硬盘保留bootindex=1,网络换成开机即用:
qemu-system-aarch64 \ -name arm64-run \ -machine virt,gic-version=3 \ -accel tcg,thread=multi \ -cpu cortex-a72 -smp 4 -m 4096 \ -drive if=pflash,format=raw,readonly=on,file=/usr/share/AAVMF/AAVMF_CODE.fd \ -drive if=pflash,format=raw,file=./AAVMF_VARS.fd \ -drive if=none,id=hd0,format=qcow2,file=./arm64-root.qcow2 \ -device virtio-blk-pci,drive=hd0,bootindex=1 \ -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22,hostfwd=tcp:127.0.0.1:8080-:80 \ -device virtio-net-pci,netdev=net0 \ -device virtio-rng-pci \ -nographichostfwd可以叠加多条,主机上访问127.0.0.1:8080就等于访问客户机的 80 端口,调试 Web 服务很方便。SSH 是ssh -p 2222 用户名@127.0.0.1。
日常启停我直接写成两个 shell 脚本,一个start.sh一个stop.sh,stop.sh里用监视器发system_powerdown走优雅关机,实在不行再kill。因为 arm64 客户机在 TCG 下关机也要等十几秒,直接Ctrl-C容易把文件系统搞成需要 fsck 的状态,我吃过这个亏。
4. 方法二:交给 libvirt,virt-install 一条命令托管
命令行方式什么都好,就是每次要改点东西都得手工编辑一大串参数,时间长了容易出错。做了几个 arm64 环境之后,我逐渐把长期使用的虚拟机迁到了 libvirt 下面。
4.1 从"手搓命令"切换到"声明式定义"的理由
libvirt 带来的东西不是"更简单",而是可管理。它把虚拟机的定义存成一份 XML,你可以随时virsh dumpxml查看完整配置,可以virsh edit改一个字段,可以用virsh snapshot-create-as打快照,可以用virsh autostart设开机自启。这些能力在命令行模式下要么得自己写脚本实现,要么干脆没有。
另一个实际收益是日志和状态统一。命令行模式下虚拟机崩了,你得自己去翻终端输出;用 libvirt,journalctl -u libvirtd加上/var/log/libvirt/qemu/虚拟机名.log,出错信息一目了然。曾经有一次我的 arm64 虚拟机跑到一半挂掉,命令行方式下只能看到终端最后几行,换成 libvirt 之后立刻在日志里看到是内存不足被 OOM 杀掉的,问题定位时间从半小时缩短到两分钟。
代价是多了一层抽象,某些 QEMU 新特性和极冷门的参数不一定能从 libvirt 的 XML 里直接表达。我的做法是:长期存在的环境用 libvirt,临时验证、需要精细控制内核参数的环境还是手搓命令行。
4.2 virt-install 的关键参数与 virsh 常用动作
先确认 libvirt 已经识别到 aarch64 的模拟器:
virsh capabilities | grep -A3 aarch64 systemctl enable --now libvirtd能看到/usr/bin/qemu-system-aarch64就说明没问题。然后一条命令创建:
virt-install \ --connect qemu:///system \ --name arm64-vm \ --arch aarch64 \ --machine virt \ --cpu cortex-a72 \ --vcpus 4 \ --memory 4096 \ --disk path=/var/lib/libvirt/images/arm64-vm.qcow2,size=40,format=qcow2,bus=virtio,cache=none,io=native,discard=unmap \ --cdrom /var/lib/libvirt/images/ubuntu-24.04.2-live-server-arm64.iso \ --network network=default,model=virtio \ --graphics none \ --console pty,target_type=serial \ --boot uefi \ --osinfo name=ubuntu24.04几个关键点值得展开说:
--arch aarch64配合--machine virt,告诉 libvirt 走的是跨架构仿真路径,它会自动选择qemu-system-aarch64并在需要时加上-accel tcg。
--boot uefi依赖 libvirt 的固件自动选择能力。如果你的发行版版本偏老,这条可能报"firmware 找不到",那就换成长写法手动指定:
--boot loader=/usr/share/AAVMF/AAVMF_CODE.fd,loader_ro=yes,loader_type=pflash,nvram_template=/usr/share/AAVMF/AAVMF_VARS.fd--graphics none加上--console pty,target_type=serial是我最常用的组合,因为服务端镜像走串口最省事。想用图形安装就换成--graphics vnc,listen=0.0.0.0,然后virsh vncdisplay arm64-vm拿到地址,用任意 VNC 客户端连。
--osinfo是较新版本 virt-install 的写法,老版本叫--os-variant。这个参数看着无关紧要,其实很有用:它决定了 libvirt 给虚拟机生成的默认设备模型,选错了可能导致网卡型号不被客户机识别。
装好之后常用的几条:
virsh list --all # 看状态 virsh console arm64-vm # 串口登录,退出按 Ctrl-] virsh domifaddr arm64-vm # 查客户机 IP virsh net-dhcp-leases default # 从 DHCP 租约反查 virsh snapshot-create-as arm64-vm snap1 "装完基础环境" virsh edit arm64-vm # 改配置,改完重启生效 virsh autostart arm64-vm # 开机自启virsh domifaddr这条命令我几乎每次都用。libvirt 默认网络是 NAT 模式,客户机拿到的是 192.168.122.x 网段的地址,用这条命令比进客户机敲ip a快得多。
注意:
virsh console只有在客户机内部为串口设备启用了 getty 才能看到登录提示符,否则你只能看到内核日志,敲键盘没反应。这个问题后面第 6 章会专门讲怎么配。
4.3 网络与远程访问:default NAT、桥接、VNC 与串口登录
libvirt 默认网络(default)本质是一台 NAT 路由器:宿主机上多出virbr0这个虚拟网桥,客户机通过它出网,宿主机可以直接访问客户机的所有端口,但局域网里其它机器访问不到。对绝大多数开发验证场景,这已经够了。
需要局域网其它机器访问时,有两条路。一条是改 libvirt 网络定义,加静态端口转发:
virsh net-edit default在<forward mode='nat'>里加一段<port start='8080' end='8080'/>之类的映射,改完virsh net-destroy default && virsh net-start default生效。另一条是直接用桥接网络,把客户机接到物理网卡所在的二层网络里,相当于客户机在局域网里"隐身"成一台独立设备。桥接配置需要宿主机网络支持,有线网卡比较稳,无线网卡通常桥不了或者桥完不通,这一点先试再说,别配完发现宿主机掉线。
串口登录的配置在客户机内部,装好系统后执行:
sudo systemctl enable --now serial-getty@ttyAMA0.servicettyAMA0是 arm64virt机器上 PL011 串口的标准设备名,这一点和 x86 上的ttyS0不同,写错了就不生效。配好之后virsh console就能看到登录提示符了,虚拟机出网络问题时也能靠这条通道进去救场——这是我强烈建议每个 arm64 虚拟机都配上的东西。我就遇到过一次客户机网络配置写错导致 SSH 全断,全靠串口进去改回来的。
5. 两套方案放在一起比:成本、可控性与适用场景
做完几轮之后,我在团队里做了一张选型对照表,新同事按这个表选基本不会走弯路:
| 维度 | 手工 qemu-system-aarch64 | libvirt + virt-install |
|---|---|---|
| 上手成本 | 中等,需要理解每个参数 | 较低,抽象掉了大部分细节 |
| 参数控制粒度 | 极高,任何 QEMU 参数都能加 | 高,冷门参数需要手改 XML |
| 环境复现 | 靠脚本和文档 | XML 文件即定义,可直接版本管理 |
| 快照能力 | 需要qemu-img snapshot手工操作 | virsh snapshot-create-as一条命令 |
| 日志排查 | 终端输出或-serial file: | 统一的 libvirt 日志目录 |
| 内核调试(直接内核启动) | 天然支持,改-append即可 | 需要在 XML 里写<kernel>和<cmdline> |
| 适合场景 | 一次性验证、内核参数调试、精细控制 | 长期使用的开发环境、多环境并存 |
再补一句经验:这两套方案不冲突。我现在的做法是用 libvirt 管着几台常驻的 arm64 环境,同时保留一份命令行脚本,用于快速起一个用完就删的临时环境。临时环境用命令行的原因是开得快、删得干净,不会在 libvirt 里留一堆废弃定义。
6. 装完只是开始:TCG 环境下的性能与稳定性调优
系统装好能登录,这只是起点。arm64 虚拟机在 TCG 环境下的默认配置其实并不高效,下面这几处调整我每次都会做。
6.1 磁盘层:cache、io、discard 的组合拳
磁盘参数对体验的影响比想象中大。我在同一台机器上做过一轮对比,客户机跑make -j4:
| 磁盘参数组合 | 相对耗时 | 说明 |
|---|---|---|
cache=writeback,io=threads(默认) | 100% | 通用,安全性和性能折中 |
cache=none,io=native | 约 82% | 绕过宿主页缓存,性能更好 |
cache=unsafe,io=native | 约 70% | 最快,但宿主崩溃可能丢数据 |
追加discard=unmap且客户机定期 fstrim | 约 78% | 长期使用后避免镜像虚拟盘无限膨胀 |
cache=none配合io=native是我在宿主机磁盘是 SSD 时的首选,收益明显而且数据安全。cache=unsafe只在一次性、可重建的环境里用,比如做完就删的验证机。discard=unmap这条容易被忽略:qcow2 镜像有个特点,客户机删除文件后宿主镜像不会自动变小,长期跑下来能涨到几十 G。加上这个参数并在客户机里配好定期fstrim,镜像增长就能控制住。
另外镜像格式上,如果客户机需要频繁大量写,把系统盘从 qcow2 换成 raw 会快一些,代价是失去快照能力和精简置备。我的习惯是开发环境用 qcow2 图方便,跑 IO 密集型测试时临时转成 raw。
6.2 内存与 CPU:-smp、tb-size、内存超分
CPU 这块有三条经验。
第一,vCPU 数量不要超过宿主机物理核数。TCG 多线程模式下每个 vCPU 对应一个宿主线程,你给客户机 8 核而宿主只有 4 个物理核,结果就是线程互相抢时间片,整体反而更慢。我一般给物理核数的一半到三分之二,给宿主机留出余量。
第二,CPU 型号选max还是cortex-a72有讲究。cortex-a72是较早的型号,兼容性最好,几乎所有 arm64 发行版都能跑;max会暴露宿主 QEMU 支持的全部特性,包括 LSE 原子指令这类扩展,客户机内核如果支持,内部锁操作的性能提升相当明显。我的判断方式是:先用cortex-a72确保能装上,装好之后再改成max试一次,如果客户机启动正常并且dmesg里没有异常,就保留max。
第三,调大 TCG 翻译缓存。默认 32MB 的翻译块缓存对于大程序来说偏小,加上-accel tcg,thread=multi,tb-size=1024后,重复执行的代码命中缓存的概率更高。这条在编译类负载下能有 5% 到 10% 的改善,属于低成本收益。
内存方面,-m给足是前提,4GB 是舒适线,2GB 是底线。宿主内存紧张时可以用-overcommit mem-lock=off允许超分,但客户机实际使用量超过物理内存时表现会急剧恶化,不建议在跑编译任务时开。
6.3 时间漂移、串口日志与快照
TCG 环境下客户机时钟漂移是必然的,因为它依赖宿主提供的虚拟计时器,而仿真执行的时间和真实时间对不上。表现是客户机里date命令显示的时间越跑越偏,严重时会影响依赖时间戳的构建工具。解决方案很简单:客户机里装chrony或systemd-timesyncd并启用,网络通了之后自动校正。这个步骤我建议写进装机后的标准流程里。
串口日志值得单独留一份配置。把上面命令里的-nographic换成-display none -serial file:serial.log,客户机的所有 console 输出都会落到文件里,包括崩溃时的内核栈回溯。相比在终端里往上翻,文件更好搜、更好贴给同事看。这个习惯帮我定位过好几次内核 panic。
快照方面,qcow2 格式支持内部快照,命令行方式是:
qemu-img snapshot -c before-upgrade arm64-root.qcow2 qemu-img snapshot -l arm64-root.qcow2 qemu-img snapshot -a before-upgrade arm64-root.qcow2libvirt 那边则用virsh snapshot-create-as和virsh snapshot-revert。升级内核、改 fstab、动网络配置之前先打一个快照,翻车了能一秒回退,这个习惯值得养成。我有一次在客户机里改/etc/fstab写错了分区 UUID,重启直接进不去系统,靠快照回退省了重装的两小时。
6.4 补充一条捷径:直接内核启动做内核与驱动调试
如果你做的是内核模块开发或者驱动验证,前面那套完整 UEFI 引导流程其实可以跳过。QEMU 支持直接指定内核和 initramfs:
qemu-system-aarch64 \ -machine virt,gic-version=3 \ -accel tcg,thread=multi \ -cpu max -smp 2 -m 2048 \ -kernel /boot/vmlinuz-6.8.0-generic \ -initrd /boot/initrd.img-6.8.0-generic \ -append "console=ttyAMA0 root=/dev/vda1 rw nokaslr" \ -drive if=none,id=hd0,format=raw,file=./rootfs.img \ -device virtio-blk-pci,drive=hd0 \ -nographic内核和 initramfs 从哪来?把它们从已装好的客户机镜像里拷出来就行(挂载镜像或者从客户机里scp出来)。这条路省掉了 UEFI 初始化和引导加载器的全部时间,我实测下来启动快一半以上,而且-append里想加什么内核参数随手就加,做earlycon、nokaslr、init=/bin/bash这类调试时特别顺手。
有一点要注意:root=后面得写客户机里真实的根分区标识,用PARTUUID最稳,避免设备名变化导致找不到根分区。可以直接从客户机里blkid拿到。
7. 报错排查实录:几个最典型的翻车现场
我把这几年遇到的报错按出现频率整理了一下,前面那个是"症状",后面是"实际原因"。排查的关键是不要一上来就改参数,先把症状和可能原因对照一遍。
7.1 UEFI 起来了,却直接掉进 EFI Shell
这是我最常遇到的一个。现象是:终端里能看到固件日志,但接着没有出现 GRUB 菜单,而是停在一个Shell>提示符下。
原因基本是三类。第一,光驱设备加的方式不对。arm64 的virt机器没有 IDE 总线,如果你用了-device ide-cd,设备压根没挂上,UEFI 自然找不到可引导介质。正确做法是virtio-scsi-pci加scsi-cd的组合。第二,bootindex没写或者两个设备的 bootindex 相同,UEFI 的启动顺序无法确定。第三,ISO 文件里的引导文件路径是EFI/BOOT/BOOTAA64.EFI,如果这个文件缺失(比如镜像下载不完整),也进不去。
在 EFI Shell 里可以手工验证一下:敲map看有哪些设备,fs0:切过去,ls EFI\BOOT\看文件在不在,然后在那个目录下执行BOOTAA64.EFI试试能不能拉起来。能拉起来说明镜像没问题,就是启动顺序的配置问题;拉不起来说明镜像本身有问题,去查第 7.3 条。
7.2 GIC 版本不对导致内核卡在 earlycon 无输出
症状是:GRUB 之后屏幕一片黑,或者只有一行很早期的内核输出,然后就没有然后了。加了earlycon参数也看不到东西。
这是中断控制器版本不匹配。virt机器默认可能给 GICv2,但较新的 arm64 内核默认按 GICv3 初始化,两者对不上,内核在早期初始化阶段就挂住了。解决办法是显式指定:
-machine virt,gic-version=3反过来也有情况:某些使用旧内核的 arm64 发行版只支持 GICv2,那你得写gic-version=2。判断方法很简单,两个都试一遍,哪个能启动就用哪个。
如果两个都试过还是卡住,可以在 guest 内核参数里加earlycon=pl011,0x9000000,强制把 PL011 串口的早期输出打出来。0x9000000是 arm64virt机器上串口的固定地址,这个值记一下,调试时会反复用到。
7.3 ISO 校验失败与源损坏伪装成安装报错
这个坑最隐蔽,因为它表现出来是各种各样的安装报错,很容易让人误以为是参数问题。我遇到过一次:GRUB 能起来,安装器也能启动,但走到解压 ramdisk 那一步报了个很含糊的镜像格式错误,看着很像"文件解压乱码"那种问题。
排查方式是回到根上做校验:
sha256sum ubuntu-24.04.2-live-server-arm64.iso # 与官网公布的校验值逐位比对对不上,就是文件坏了。这种情况多半是下载中断后续传、或者从非官方渠道拿到的文件不完整导致的。重新下载并校验,问题消失。所以我现在养成的习惯是:镜像拿到手第一件事就是校验,不校验不往下走。几分钟的校验能省掉几个小时的无效排查。
另外还有一类相关的现象:镜像文件挂载路径里有空格或者中文字符,QEMU 解析参数时被截断,表现是"文件找不到"。路径尽量用英文和短横线,是个成本极低的防御性习惯。
7.4 几个一句话能说完但很容易忘的坑
最后补几条零碎的,都是实际踩过的:
-cpu host在跨架构场景下会直接报错,提示需要 KVM 支持。别怀疑 QEMU 装错了,换成cortex-a72或max就行。
显式写-accel kvm会报"该目标架构不支持 KVM"。这不是配置问题,是硬件层面的限制,跨架构没有硬件虚拟化可用,老老实实用 TCG。
内存给到 512MB 时 UEFI 可能根本引导不起来。arm64 的 UEFI 固件本身要占一部分,加上内核解压需要空间,1GB 是安全底线,2GB 是舒适线。
-nographic模式下以为卡死了,其实是程序在等输入。Ctrl-A再按X是退出,Ctrl-A再按C是切到监视器,这两个快捷键记下来能省不少慌乱。我有一次以为虚拟机崩了,等了十分钟才发现是它早就退出了,终端只是停留在了监视器里。
虚拟机里的时间一天能漂出去十几分钟。装上时间同步服务之后这个问题就没了,且一定要在客户机里配,在宿主机上怎么调参数都没用。
我个人在实际操作中的体会是,arm64 虚拟机的搭建难度其实不在命令本身,而在于把"跨架构仿真"这个前提时刻记在脑子里。一旦接受了性能的量级、接受了 UEFI 固件必须自己准备、接受了显示和串口要显式配置这三件事,剩下的就是按流程走。现在我建一台新的 arm64 验证环境,从敲第一条命令到能 ssh 进去,cloud image 路线大约十五分钟,ISO 路线半小时到一小时,其中大部分时间是等它自己跑。真正需要动脑子的是出问题的时候——而几乎所有问题的答案,都藏在"这套流程里哪一步和 x86 不一样"这个问题里。