CentOS 8上用QEMU搭建ARM64虚拟机完整指南

发布时间:2026/9/16 9:54:03
CentOS 8上用QEMU搭建ARM64虚拟机完整指南 前阵子有朋友问我能不能在办公用的x86服务器上搞一个ARM64的虚拟机。他手头有个项目必须用ARM64版的应用做验证但又不想立刻去租云上的ARM实例。这个需求我太熟了去年我在CentOS 8上把这套流程完整走了一遍中间踩的坑比想象中多UEFI固件找不到、VNC黑屏半小时、Kernel panic、网络不通、纯软件模拟慢到让人怀疑人生……标题里那几个热搜词什么“qemu模拟arm64”“arm64和amd64有何不同”“uefi引导修复”正好把大家最关心的点全点出来了。这篇文章我就把这套流程完整拆开从基础概念讲到装包、建机、UEFI配置再到排错思路和日常管理。目标是让一个没碰过ARM虚拟化的人照着文章也能在CentOS 8上把ARM64虚拟机跑起来并且知道出问题时该从哪里下手查。1. 先分清“模拟”与“虚拟化”这决定了你的宿主机该怎么选1.1 arm64和amd64的差异为什么会让KVM失效很多人第一次接触这事都会困惑都是64位怎么就不能用KVM了ARM64官方叫法AArch64和amd64大家常说的x86_64是两套完全不同的指令集底层的寄存器数量、指令编码、访存模型全都不一样。KVM的工作方式是把CPU的硬件虚拟化扩展直接暴露给虚拟机让guest的指令在物理CPU上原样执行。这个前提是guest架构和宿主架构一致x86的KVM只能跑x86 guestARM的KVM只能跑ARM guest。所以在x86宿主机上想跑ARM64 guestKVM这条路天然是不通的。那x86上怎么跑ARM64靠的是QEMU的TCG模式Tiny Code Generator纯软件做二进制翻译把ARM64指令逐条翻译成x86指令再执行。代价就是性能差距非常明显CPU密集任务通常只有原生性能的10%到20%有些场景连5%都不到。反过来如果你的宿主机本身就是ARM64服务器那就可以用KVM硬件加速性能和裸机几乎没区别整个流程也会顺很多。所以动手之前先问自己一句我的宿主是x86还是ARM这直接决定了后面所有参数尤其是--virt-type怎么填。1.2 一条命令确认当前宿主能走哪条路别猜直接查。先看KVM设备节点ls -l /dev/kvm有这个文件说明宿主CPU支持虚拟化且KVM模块已加载。再看libvirt对aarch64架构的能力上报virsh domcapabilities --machine virt --arch aarch64 | head -40这条命令比较关键。在ARM64宿主机上返回结果里会有domain typekvm这样的列表在x86宿主机上通常只有domain typeqemu意思是只能用TCG模拟。如果你装了QEMU还可以直接看加速器列表qemu-system-aarch64 -accel help会列出tcg、kvm等可用的加速后端。x86宿主上硬选kvm跑aarch64 guest启动时会报类似kvm_init_vcpu failed的错误原因就是前面说的架构不匹配。判断好之后在virt-install里对应填ARM64宿主--virt-type kvmx86宿主--virt-type qemu强制走TCG1.3 两种方案的性能预期与适用场景宿主架构加速方式性能表现适合干什么ARM64KVM接近原生CI构建、服务部署测试、交叉编译验证、跑容器x86_64TCGCPU密集任务下降明显临时验证、跑通流程、环境测试、学ARM64 LinuxTCG模式下我建议guest的vCPU控制在2到4个别再多了。QEMU的TCG在旧版本里是单线程翻译vCPU越多锁竞争越明显4核可能比2核还要慢。内存4G起步8G比较舒服。如果你要跑的是编译任务或者高并发服务趁早别指望TCG能扛生产负载老老实实上ARM真机或云实例。2. CentOS 8宿主环境准备源失效处理、依赖安装与设备验证2.1 处理CentOS 8源失效的坑这是CentOS 8用户绕不开的第一个坑CentOS 8在2021年底已经EOL默认的mirrorlist早就失效了直接跑dnf install几乎必然报错。官方把历史仓库挪到了vault.centos.org手动切过去就行cd /etc/yum.repos.d/ sed -i s/mirrorlist/#mirrorlist/g CentOS-* sed -i s|#baseurlhttp://mirror.centos.org|baseurlhttp://vault.centos.org|g CentOS-* dnf clean all dnf makecache如果你不想在这上面花时间也可以直接装Rocky Linux 8或AlmaLinux 8它们是CentOS 8的完全兼容替代下面所有命令在Rocky/Alma上一样跑得通。我个人现在做实验都直接用Rocky少踩很多源的问题。2.2 安装libvirt、QEMU与AAVMF固件需要装的包有四个部分QEMU模拟器、UEFI固件、libvirt管理栈、镜像辅助工具。dnf install -y qemu-system-arm edk2-aarch64 libvirt virt-install cloud-utils逐个说明一下qemu-system-arm提供qemu-system-aarch64这个模拟器二进制。如果提示找不到包先跑dnf provides */qemu-system-aarch64搜一下再不行就启用EPEL兜底。edk2-aarch64提供AAVMF固件也就是ARM64虚拟机用的UEFI固件。装完后检查/usr/share/AAVMF/目录里面会有AAVMF_CODE.fd和AAVMF_VARS.fd两个文件后面会细讲。libvirt和virt-install是管理栈virt-install是咱们建机的主力工具。cloud-utils提供cloud-localds命令用来生成cloud-init的seed镜像。如果你在x86宿主上还需要确认系统里有没有qemu-kvm这个基础包。CentOS 8的qemu-kvm虽然默认是给x86 KVM用的但有些版本会带上多架构模拟支持装了没坏处dnf install -y qemu-kvm2.3 启动服务并验证aarch64能力装完先启动libvirtdsystemctl enable --now libvirtd systemctl status libvirtd --no-pager然后做一组快速验证确保后面建机时不会因为环境问题卡住virsh version qemu-system-aarch64 --version ls -l /usr/share/AAVMF/ virsh domcapabilities --machine virt --arch aarch64 | grep -i firmware -A5最后一条很关键。如果AAVMF固件装好了这里会列出firmware相关的信息包括loader路径。如果这一行是空的说明libvirt没找到ARM64的UEFI固件后面virt-install时--boot uefi一定会报错。这时候回头检查edk2-aarch64是否真的装上了。3. 镜像准备为什么我推荐云镜像而不是ISO3.1 Ubuntu ARM64云镜像的下载与磁盘处理第一次搭ARM64虚拟机的人最容易犯的错是直接下个ISO去装。在TCG模拟下Ubuntu Server ARM64的安装器跑起来那个慢啊一个基础系统装两小时都算快的。所以我强烈建议用云镜像cloud image方案它是厂商预先做好的最小系统配合cloud-init在首次启动时完成配置几分钟就能进系统。以Ubuntu 22.04为例cd /var/lib/libvirt/images wget https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img下载下来的是个大约600MB的qcow2镜像自带GPT分区表。你可以用fdisk -l看一下通常是两个分区第一个是EFI System Partitionvfat格式里面放着grubaa64.efi和BOOTAA64.EFI第二个是ext4根分区。这正是它能直接UEFI启动的原因也是咱们后面验证固件有没有配对的依据。接下来把镜像拷一份作为虚拟机磁盘千万不要直接动原始文件cp ubuntu-22.04-server-cloudimg-arm64.img arm64-vm.qcow2 qemu-img resize arm64-vm.qcow2 20G原始云镜像根分区只有2G扩容之后cloud-init里的growpart会在首次启动时自动把根分区扩展到整个磁盘你不需要手工处理分区表。3.2 用cloud-init生成带密码和SSH的seed镜像云镜像是没有默认密码的第一次启动必须通过cloud-init注入配置。咱们用一个单独的seed镜像来传递这些信息cat user-data EOF #cloud-config password: YourPassword123 chpasswd: expire: False ssh_pwauth: True ssh_authorized_keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... yourkey EOF cat meta-data EOF instance-id: arm64-vm-001 local-hostname: arm64-vm EOF cloud-localds seed.img user-data meta-datacloud-localds会生成一个带cidata卷标的ISO镜像。如果没有cloud-utils也可以用mkisofs手动做mkisofs -output seed.img -volid cidata -joliet -rock user-data meta-data这个seed.img在virt-install里作为第二块磁盘挂给虚拟机cloud-init在首次启动时会自动识别并应用里面的配置。root密码、SSH公钥、主机名全都由它搞定。3.3 什么场景才需要走ISO安装现在讲讲ISO方案的适用边界。在ARM64宿主上用KVM跑ISO安装完全没问题速度跟装真机一样。但在x86宿主上用TCG模拟ISO安装就是噩梦Live installer的每个界面切换都要等几十秒。除非你需要的发行版没有官方云镜像否则不要受这个罪。非要ISO的话可选的有Debian ARM64 netinst、Fedora ARM64 Everything、CentOS Stream 9 ARM64这些。国内常用的openEuler、麒麟也有ARM64版本流程和Ubuntu完全一致只是云镜像的cloud-init配置细节略有差异。4. virt-install建机实战一条命令拆开讲透4.1 完整命令与参数含义下面这条命令是x86宿主机的版本ARM64宿主只需要改两处我后面会说。先看完整命令virt-install \ --name arm64-vm \ --memory 4096 \ --vcpus 4 \ --arch aarch64 \ --machine virt \ --cpu cortex-a72 \ --os-variant ubuntu22.04 \ --disk path/var/lib/libvirt/images/arm64-vm.qcow2,devicedisk,busvirtio,formatqcow2 \ --disk path/var/lib/libvirt/images/seed.img,devicedisk,busvirtio,formatraw \ --network networkdefault,modelvirtio \ --graphics vnc,listen127.0.0.1 \ --boot uefi \ --virt-type qemu \ --noautoconsole逐个参数解释--arch aarch64告诉virt-install要建ARM64虚拟机它会自动选择qemu-system-aarch64。--machine virtARM64虚拟机在QEMU里有好几种机型virt是官方推荐的通用机型支持UEFI和virtio设备。不要用老旧的integratorcp、versatilepb这些。--cpu cortex-a72TCG模式下指定一个具体的ARM CPU型号。这里千万不能用host因为宿主是x86libvirt没法把x86的CPU特性映射给ARM64 guest。--os-variant ubuntu22.04让virt-install按照Ubuntu 22.04的默认特性做优化。如果提示找不到这个系统用osinfo-query os | grep -i ubuntu查一下可用的variant名字实在不行就填generic。--disk第一块是系统盘第二块是cloud-init的seed盘。busvirtio是性能最好的选择后面会细说。--network networkdefault,modelvirtio使用libvirt默认的NAT网络网卡模型用virtio。--graphics vnc,listen127.0.0.1只监听本机VNC避免暴露到公网。需要远程访问时用SSH隧道。--boot uefi让libvirt自动为ARM64虚拟机寻找AAVMF固件。--virt-type qemu强制使用TCG模拟。这是x86宿主机上最关键的一个参数不填的话virt-install可能报“找不到适合aarch64的hypervisor”。--noautoconsole不自动附加控制台防止命令卡在console界面。ARM64宿主上只需要改两处--cpu host-passthrough \ --virt-type kvm \--cpu host-passthrough直接把物理CPU特性透传给guest性能最好。如果不想指定CPU让libvirt用默认值也行。4.2 UEFI固件的两种指定方式与NVRAM机制--boot uefi是懒人写法virt-install会调用libvirt的能力查询自动找到/usr/share/AAVMF/AAVMF_CODE.fd并为这台虚拟机复制一份独立的AAVMF_VARS.fd到/var/lib/libvirt/qemu/nvram/arm64-vm_VARS.fd。如果不放心自动查找的结果或者你的AAVMF路径比较特殊可以手动指定--boot loader/usr/share/AAVMF/AAVMF_CODE.fd,loader_royes,nvram_template/usr/share/AAVMF/AAVMF_VARS.fd这里有两个细节建议理解一下因为后面排错经常用到第一个是loader_royes。QEMU把固件当pflash设备映射给虚拟机固件代码区必须只读防止虚拟机运行时把固件本体写坏。可写的NVRAM里只放UEFI变量比如BootOrder、Boot####这些启动项记录。第二个是NVRAM为什么要为每台虚拟机单独复制。如果所有虚拟机共用一份VARSA虚拟机写的启动项会被B虚拟机读到两台机器的BootOrder互相干扰轻则启动变慢重则直接找不到系统。libvirt在创建虚拟机时自动从模板复制一份独立VARS文件就是为了隔离这种状态。4.3 创建后的验证与首次登录命令执行完没有报错先确认虚拟机状态virsh list --all virsh start arm64-vm virsh domifaddr arm64-vm --source leasedomifaddr会从libvirt默认网络的DHCP租约里读出guest的IP地址。拿到IP之后SSH登录ssh ubuntuIPUbuntu云镜像默认用户是ubuntu密码就是你在user-data里设置的那个或者用你注入的SSH密钥直接登录。登录进去第一件事df -h uname -muname -m输出aarch64就说明你已经成功进了一个真正的ARM64系统。df -h看看根分区是否已经自动扩容到20G。5. AAVMF固件工作机理从pflash到grubaa64.efi5.1 AAVMF_CODE.fd与AAVMF_VARS.fd的分工做过x86平台的同学应该对OVMF不陌生AAVMF和OVMF同源都是EDK2工程编出来的UEFI实现区别只在于目标架构。OVMF对应x86_64AAVMF对应aarch64。AAVMF包含两个文件AAVMF_CODE.fd固件本体只读。里面是完整的UEFI实现包括CPU初始化、串口和显卡驱动、设备枚举逻辑、Boot Manager。AAVMF_VARS.fdNVRAM变量存储可写。保存了BootOrder、Boot####启动项条目以及Secure Boot的密钥库。打一个不太严谨但很好记的比方CODE像主板的BIOS芯片VARS像主板上的CMOS加电池。BIOS芯片里的程序不能被普通指令改写但CMOS里的配置会被每次开机时的设置项改写。这两个文件在virt-install里的协作方式前面已经说过了libvirt把CODE映射为只读pflash把VARS的副本映射为可写pflash。如果你手动改过domain XML会看到类似这样的结构os type archaarch64 machinevirthvm/type loader readonlyyes typepflash/usr/share/AAVMF/AAVMF_CODE.fd/loader nvram/var/lib/libvirt/qemu/nvram/arm64-vm_VARS.fd/nvram /os5.2 ARM64虚拟机完整的UEFI启动链路理解启动链路对排查问题非常重要。一台ARM64虚拟机从按下启动到进入系统大致经过这几步QEMU启动CPU从复位向量开始执行AAVMF_CODE被映射到virt机型的pflash地址。AAVMF初始化CPU、内存、中断控制器GIC、串口、显示设备。UEFI驱动枚举virtio总线上的设备识别出磁盘和网卡。Boot Manager读取NVRAM里的BootOrder启动顺序。对每个启动项UEFI去对应磁盘上扫描EFI System Partition尝试加载\EFI\BOOT\BOOTAA64.EFI可移动介质路径或具体的grub路径。grub加载grub.cfg读取内核vmlinuz和initrd.img跳转到内核入口。Linux内核启动挂载根分区进入系统。Ubuntu ARM64云镜像的ESP分区里同时有EFI/BOOT/BOOTAA64.EFI和EFI/ubuntu/grubaa64.efi所以UEFI固件怎么找都能引导成功。这也是为什么我用Ubuntu云镜像作为教程示例——它是检验UEFI配置是否正确的最好载体。5.3 进了UEFI Shell怎么排查启动项当NVRAM里的BootOrder被搞乱或者ESP分区里的引导文件缺失时AAVMF会掉进UEFI Shell。不要慌这在某种程度上是好事——说明固件、内存、磁盘控制器都是好的问题出在启动项配置上。在Shell里可以手工加载引导器Shell fs0: FS0:\ ls FS0:\ cd \EFI\BOOT FS0:\EFI\BOOT BOOTAA64.EFI如果能进展到grub菜单说明引导文件本身没问题纯粹是BootOrder丢了。用bcfg命令修复启动项FS0:\ bcfg boot add 0 FS0:\EFI\BOOT\BOOTAA64.EFI Ubuntu FS0:\ bcfg boot dump如果折腾了一圈还是起不来还有一个更粗暴但有效的办法重置NVRAM。把虚拟机关机后用virsh undefine --nvram arm64-vm把domain定义连同NVRAM一起删掉再用原来的virt-install命令重新建一次。磁盘镜像还在数据不丢只是把UEFI变量库恢复到出厂状态。6. 常见坑点实录黑屏、panic、慢到怀疑人生的完整排查6.1 坑启动黑屏无任何输出这是问得最多的一个问题。virt-install命令执行成功virsh list也能看到虚拟机在跑但VNC窗口一片黑。我的排查链路是这样的第一步先确认系统是不是真的没启动。执行virsh console arm64-vm如果能看到grub菜单或者内核日志说明系统在正常跑问题出在显卡输出链路上。Ubuntu的ARM64云镜像默认配置了consolettyAMA0,115200的内核参数所以串口控制台是能用的。第二步看domain XML里的video设备配置virsh edit arm64-vm如果libvirt默认生成了virtio-gpu在TCG模式下渲染会比较慢容易出现长时间黑屏。可以改成bochs或cirrus这种简单的显卡模型video model typebochs/ /video第三步如果串口也没输出那就是固件阶段就出问题了大概率是loader路径不对或者固件根本没加载。检查virsh edit里的loader配置是否指向真实存在的AAVMF文件。6.2 坑Kernel panic / VFS Unable to mount root能进grub但内核启动时报VFS: Unable to mount root fs on unknown-block(254,0)这个错误几乎都是root设备名对不上导致的。ARM64云镜像的grub配置里写死了root设备是virtio-blk盘的第二个分区也就是/dev/vda2。如果你把磁盘bus换成了sata或ide设备名会变成/dev/sda2内核按root/dev/vda2去找就必然扑空。解决办法很简单磁盘统一用busvirtio不要手工覆盖内核的root参数。如果你用的是自己裁剪的内核确认编译时打开了CONFIG_VIRTIO_BLKy和CONFIG_VIRTIO_PCIy否则内核根本没有virtio磁盘驱动连vda设备都枚举不出来。6.3 坑网络不通虚拟机起来了IP就是拿不到。排查链路如下先看宿主侧的libvirt网络virsh net-list --all virsh net-dumpxml defaultdefault网络应该存在且是active状态。再看domain XML里的interface配置interface typenetwork source networkdefault/ model typevirtio/ /interface确认网卡模型是virtio。进guest看内核有没有识别到网卡dmesg | grep virtio-net如果没有输出要么是内核缺virtio_net模块要么是网卡模型和驱动不匹配。可以试试把网卡模型改成e1000ARM64 virt机型支持这个模拟设备驱动基本都编译在内核里兼容性比virtio更稳虽然性能差一些。还有一种常见情况是防火墙干扰。CentOS 8的firewalld在某些版本和libvirt配合不好会挡住vnet和virbr0桥之间的流量。排查时可以临时停掉防火墙看是否恢复systemctl stop firewalld能恢复的话就把libvirt相关接口和网段加入到firewalld的信任区或者干脆配置成开机不启动防火墙内网测试环境这么做问题不大。6.4 坑TCG模拟慢到无法安装系统如果你是倔强地选择ISO安装又是在x86宿主上那这个坑基本躲不掉。TCG模拟本身就慢Ubuntu的Live Server安装器又是个重量级应用每个安装步骤的界面渲染都是几何级的时间消耗。我的建议很直接换云镜像。这是最彻底的解法从下载到进系统只要几分钟。vCPU控制在2到4个别贪多。CPU模型用cortex-a72别用max。max会开启更多指令集特性某些特性在TCG下会走慢路径。磁盘加cachewriteback参数减少模拟层的同步开销。宿主机上执行dnf module list virt看看有没有更新版本的QEMU模块流可以启用新版本QEMU的多线程TCG改进明显。如果你正在跑的是apt update这类的网络操作TCG下慢是正常的毕竟翻译层在CPU密集和IO路径上都有开销。忍着点多等一会儿就好。6.5 坑CPU模型、磁盘总线与串口参数最后集中列几个容易忽略的细节CPU模型TCG下不要用--cpu hostlibvirt会尝试把x86宿主CPU特性映射给ARM64 guest这在架构层面就是无效的。用cortex-a72或cortex-a76。固件别混用AAVMF和OVMF是两套不同架构的固件ARM64 guest里指定OVMF路径会直接启动失败反之亦然。别图省事从一个虚拟机复制XML到另一个。osinfo版本过旧CentOS 8自带的libosinfo数据库比较老可能不认识ubuntu22.04这个variant。用osinfo-query os | grep -i ubuntu查一下大不了用generic功能上没损失。串口控制台需要靠串口登录的时候guest内核cmdline必须带consolettyAMA0,115200。Ubuntu云镜像默认配好了但你自己折腾其他系统镜像时要留意。TCG下用virsh console连串口比等VNC出来要可靠得多。时间漂移TCG模拟下guest时钟容易漂移装上NTP服务让它自动同步比手动date强一万倍。7. 日常管理快照、克隆、调优与文件交换7.1 virsh常用命令速查虚拟机建好之后日常维护基本都靠virshvirsh list --all # 查看所有虚拟机 virsh start arm64-vm # 启动 virsh shutdown arm64-vm # 优雅关机 virsh reboot arm64-vm # 重启 virsh domifaddr arm64-vm --source lease # 查IP virsh console arm64-vm # 连串口控制台 virsh dominfo arm64-vm # 查看配置摘要 virsh edit arm64-vm # 编辑XML配置 virsh vcpuinfo arm64-vm # 查看vCPU状态7.2 快照与克隆的注意事项含NVRAM与machine-id快照是测试场景的好帮手virsh snapshot-create-as arm64-vm clean_base 刚装好的干净系统 virsh snapshot-list arm64-vm virsh snapshot-revert arm64-vm clean_base这里有个容易忽略的细节libvirt的虚拟机快照默认不包含NVRAM文件。也就是说你回滚到某个快照时磁盘回到了过去但UEFI变量库还是当前状态。大多数情况下这没什么影响但如果回滚前后系统的引导方式变了可能出现启动顺序错乱。保险起见重要节点手动备份一下VARS文件cp /var/lib/libvirt/qemu/nvram/arm64-vm_VARS.fd ~/arm64-vm_VARS.fd.bak克隆也经常用virt-clone --original arm64-vm --name arm64-vm-2 --file /var/lib/libvirt/images/arm64-vm-2.qcow2virt-clone会生成新的MAC地址但不会改guest内部的machine-id。两台克隆出来虚拟机如果machine-id一样DHCP、systemd日志、某些集群软件都会出诡异问题。启动克隆机后进guest执行sudo rm -f /etc/machine-id /var/lib/dbus/machine-id sudo systemd-machine-id-setup sudo cloud-init clean --machine-id然后再重启一次让cloud-init重新生成实例身份。7.3 性能调优方向与宿主机文件传输TCG模式下能做的调优有限核心就几点vCPU别多、CPU模型别选max、磁盘加writeback、guest里跑NTP。ARM64宿主的KVM模式下空间就大了比如给virtio-blk配置iothreadiothreads1/iothreads ... disk typefile devicedisk driver nameqemu typeqcow2 cachewriteback iothread1/ ... /disk这种配置会让磁盘IO有独立的处理线程多队列场景下吞吐提升明显适合跑数据库之类IO密集的应用。文件传输方面最省事的就是SSH。先从virsh domifaddr --source lease拿到IP然后scp、rsync随便用。如果想要更顺滑的共享目录体验也可以guest里搭NFS或SMB服务但这属于额外的运维工量了平时传个包用scp完全够。最后说点题外话。折腾ARM64虚拟机这件事第一次走通确实有点门槛但抓住两条主线就全通了宿主架构决定加速方式UEFI固件决定引导链路。我现在的习惯是把这套流程固化成脚本镜像路径、seed内容、virt-install参数全部参数化换台机器直接跑几行命令就能开出一台ARM64环境。如果你也经常需要ARM64的测试机器强烈建议做同样的沉淀省下来的时间够多喝好几杯咖啡了。