一块手掌大的开发板上同时跑着四五个完整的Linux服务器,每个都能独立重启、配置网络、跑数据库、对外提供SSH服务,互相之间互不干扰——这不是x86机房里的场景,而是我最近在RK3588 ARM平台上用KVM虚拟化捣鼓出来的效果。ARM架构 + KVM,这两个词放在一起,曾经让人觉得是“能跑但别指望好用”的组合,但实际折腾下来,RK3588这颗芯配上KVM,完全能当一台正经的虚拟化宿主机来用。
这篇文章把我从零开始踩坑、调优、稳定运行的全过程整理出来,包含环境搭建、虚拟机创建、CPU/内存/IO性能调优和常见问题排查。适合手里有RK3588开发板(RK3588芯片的各类开发板均可)、想在ARM平台上跑虚拟化玩意的朋友,也适合需要在嵌入式设备上做多系统隔离的开发者参考。我会把每一步为什么这么做、参数怎么选、踩了哪些坑都写清楚,尽量做到照着操作就能复现。
1. 为什么要在RK3588上折腾KVM
1.1 一块芯片里的多台“服务器”
RK3588是瑞芯微的旗舰级SoC,CPU部分由4个Cortex-A76大核和4个Cortex-A55小核组成,典型配置下能跑到2.4GHz左右,GPU、NPU、8K视频编解码单元一应俱全,内存支持到LPDDR4X / LPDDR5,最大容量可以到32GB。这种规格放在几年前就是一台中端台式机的水准,多核性能和内存容量完全撑得起多个虚拟机的开销。
但光是“性能够”还不够。KVM虚拟化要真正跑得好,必须依赖CPU硬件虚拟化扩展和中断控制器的虚拟化支持。传统的ARM 32位时代,大部分SoC不支持硬件虚拟化,只能靠QEMU做纯软件模拟(TCG模式),性能损耗大得离谱,跑个Linux启动过程都要好几分钟。RK3588采用了Armv8.2-A架构,自带了完整的虚拟化扩展(Virtualization Extensions),包括EL2异常级别、嵌套虚拟化支持、GIC中断虚拟化等。有了这些硬件基础,KVM才能在ARM平台上接近原生地运行虚拟机。
我实际用下来,在RK3588上创建的虚拟机跑系统负载,CPU开销比纯QEMU模拟少了一个数量级,虚拟机内跑编译任务和宿主机直接编译的速度差几乎可以忽略,这种体验在几年前是难以想象的。
1.2 KVM、容器和纯模拟方案怎么选
在ARM平台上做“多系统”方案,我见过不少人走了弯路。简单把三个主流方案梳理一下,你就明白为什么最终需要KVM。
容器(Docker/LXC)是最轻量的隔离方案,但它共享宿主机内核。如果你只是跑Web服务、数据库这种应用,容器足够;但如果客户机需要不同内核版本、需要修改内核模块、需要自己定制系统镜像,容器就会卡死。我做嵌入式系统验证,经常要测试不同发行版、不同内核配置的启动兼容性,容器完全满足不了,只能上真正的虚拟化。
QEMU纯软件模拟(TCG模式)不用任何硬件辅助,兼容性最好,什么架构都能模拟,但性能瓶颈极大。TCG模式下每条指令都要经过翻译执行,CPU开销是KVM的数倍到数十倍。在RK3588上跑一个arm64的虚拟机,TCG模式启动都要几十秒,运行系统负载更是卡到怀疑人生。这只能用来做临时测试,完全不适合当日常方案。
KVM走的是硬件辅助虚拟化路线,客户机的CPU指令大部分直接运行在物理CPU上,虚拟化开销主要在内存地址转换(Stage-2 MMU)和中断虚拟化上。在RK3588上的实测表现,CPU密集型任务的性能损耗通常在5%到10%这个范围内,完全在可用区间。所以这个项目的最终选择就是:宿主机跑标准Linux系统,通过KVM创建多个ARM64虚拟机,各自独立运行,互不干扰。
2. 硬件选型与系统基础准备
2.1 板卡选择与系统版本
在RK3588上做KVM,板卡选择比较关键。市面上基于RK3588芯片的开发板不少,Orange Pi 5系列、Radxa Rock 5系列、瑞芯微官方EVB,以及各种RK3588核心板,理论上KVM功能都支持,但不同板卡的U-Boot固件、内核版本、设备树配置差异很大,直接影响虚拟化能力是否正常启用。
我这里有两点经验:第一,选Soc厂商官方或生态成熟度高的板子,U-Boot和内核更新及时,踩坑后能搜到解决方案的概率大得多;第二,内存容量不要少于8GB,KVM虽然能跑,但要同时跑多个带2GB内存的虚拟机,加上宿主机自身的开销,8GB是起步,16GB才舒服。
系统镜像方面,我用的是Ubuntu 24.04的rootfs,内核版本6.1以上。这里有个细节,RK3588的官方BSP内核有时落后于主线内核,而KVM相关功能(比如GIC虚拟化支持、vCPU调度优化)在主线内核上更加完善。有条件的话优先使用接近主线的新内核,可以省去很多后续调优的麻烦。我自己在迁移到6.6内核后,虚拟机的网络中断处理性能明显提升。
2.2 确认硬件虚拟化能力是否就绪
系统跑起来后,第一件事不是急着装KVM,而是确认硬件虚拟化能力有没有被正确暴露。ARM平台和x86不一样,没有vmx或svm这种直接可见的CPU标志位,但我们可以通过几个途径来确认。
先看/proc/cpuinfo,确认CPU feature列表中有没有evtstrm、asimddp这类特征字段,同时用lscpu查看CPU支持的扩展,重点关注Armv8-A虚拟化相关的部分。更直接的方法是查看内核启动日志和KVM模块是否加载成功:
dmesg | grep -i kvm dmesg | grep -i virtualization ls -l /dev/kvm如果KVM已经正常工作,/dev/kvm文件会存在,dmesg里会出现类似“kvm [1]: Hyp mode initialized successfully”的日志。我遇到过一种情况:内核已经装好了,但设备树里没配置GIC的虚拟化控制相关节点,导致/dev/kvm一直没有出现。这时候需要检查设备树配置和U-Boot启动参数,有些固件需要显式传递kvm-arm相关的参数才能打开虚拟化扩展。
还有一种比较隐蔽的坑:部分RK3588板卡的U-Boot默认禁止了EL2异常级别的进入权限,需要更新固件或者改启动参数。出现这种情况时KVM模块加载会失败,日志里会提示“CPU does not support virtualization”之类的话,排查方向要往bootloader上找,而不是怀疑内核。
3. KVM环境搭建:从工具链到第一台虚拟机
3.1 安装QEMU、libvirt与guest工具
确认KVM就绪后,开始安装虚拟化管理工具链。在Ubuntu上,核心安装包包括QEMU系统模拟器、libvirt守护进程、虚拟机管理工具virtinst和可选的管理界面virt-manager。
sudo apt update sudo apt install -y qemu-system-arm qemu-utils \ libvirt-daemon-system libvirt-clients virtinst \ virt-manager cloud-image-utils这里要解释一下,ARM64平台上虽然包名叫qemu-system-arm,但安装后提供的是qemu-system-aarch64命令,用于模拟ARM 64位系统。另外libvirt-daemon-system会创建libvirt-qemu用户和libvirtd服务,这是管理虚拟机的核心服务。
安装完成后,把当前用户加入libvirt和kvm用户组,否则普通用户无法管理虚拟机:
sudo usermod -aG libvirt,kvm $USER sudo systemctl enable --now libvirtd记得重新登录或者执行newgrp libvirt让用户组生效。我建议顺手把/etc/libvirt/libvirtd.conf里的unix_sock_group和unix_sock_rw_perms看一眼,确认默认配置允许libvirt组访问管理套接字,这样后续用virsh list --all时才不会被权限问题卡住。
3.2 准备虚拟机镜像与发布源
创建虚拟机之前,需要准备用户客户机的根文件系统镜像。这一步有几种做法,我推荐用官方云镜像,省去手工安装系统和配置引导的麻烦。
Ubuntu官方提供ARM64的cloud镜像(ubuntu-24.04-server-cloudimg-arm64.img),这个镜像是qcow2格式,内置cloud-init,启动后可自动配置用户和网络。先用qemu-img把镜像复制一份并扩容:
sudo mkdir -p /var/lib/libvirt/images cd /var/lib/libvirt/images sudo wget https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img sudo cp ubuntu-24.04-server-cloudimg-arm64.img rk3588-vm1.qcow2 sudo qemu-img resize rk3588-vm1.qcow2 20G也可以直接用virt-install从ISO镜像安装系统,但云镜像方式更快更可复现,而且方便后续批量创建多个虚拟机。如果你手里有已经制作好的rootfs(比如自己裁剪的嵌入式文件系统),也可以直接打包成qcow2镜像用--import导入。
3.3 用virt-install创建第一台虚拟机
环境准备好后,用virt-install创建虚拟机。这是整个流程中最核心的一步,参数选择直接决定虚拟机的性能和稳定性。
sudo virt-install \ --name vm1 \ --memory 4096 \ --vcpus 4 \ --cpu host-passthrough \ --arch aarch64 \ --import \ --disk path=/var/lib/libvirt/images/rk3588-vm1.qcow2,format=qcow2,bus=virtio \ --network network=default,model=virtio \ --os-variant ubuntu24.04 \ --graphics none \ --console pty,target_type=serial \ --boot hd逐个解释关键参数的含义:
--cpu host-passthrough:让虚拟机直接透传宿主机的CPU特性,而不是使用QEMU默认的兼容CPU模型。ARM平台上这个参数对性能影响很大,我测试过不开启时虚拟机内的CPU特性受限,某些依赖新指令集的程序会跑不起来。--disk ... bus=virtio:磁盘使用virtio半虚拟化驱动。virtio是KVM性能的基础,比默认的ide或sd模拟方式高数倍IO性能。--network network=default,model=virtio:libvirt默认NAT网络,客户机通过virtio-net访问外网。--graphics none --console pty,target_type=serial:无图形界面,通过串口控制台访问客户机,这符合开发板场景的无头操作习惯。
创建后启动虚拟机,用串口控制台进入系统:
sudo virsh start vm1 sudo virsh console vm1cloud-init会在首次启动时自动完成系统初始化。默认登录用户一般是ubuntu,初始密码在镜像发布说明里能查到,也可以提前用cloud-localds命令做一个包含密码或SSH公钥的seed镜像挂载上去。
3.4 网络模式选型:NAT还是桥接
virt-install默认创建的虚拟机网络是NAT模式,宿主机转发客户机流量到外网。这种模式配置简单,但客户机之间的网络隔离较差,外部设备也无法主动访问虚拟机内的服务,对于需要对外提供服务的场景(比如在RK3588上同时跑Web服务器和数据库服务)就不太合适了。
如果需要让虚拟机像独立设备一样接入局域网,桥接网络是更好的选择。创建桥接设备并让物理网卡加入桥接:
sudo nmcli connection add type bridge ifname br0 con-name br0 sudo nmcli connection modify br0 ipv4.addresses 192.168.1.100/24 ipv4.gateway 192.168.1.1 ipv4.dns 192.168.1.1 ipv4.method manual sudo nmcli connection add type ethernet slave-type bridge con-name eth0-br0 ifname eth0 master br0 sudo nmcli connection up br0然后在创建虚拟机时把--network network=default改成--network bridge=br0,model=virtio即可。桥接模式下客户机的网络性能更接近物理机,延迟也更低,我实际跑下来比NAT模式高大约15%到20%的吞吐。
4. 性能调优实战:把KVM的潜力榨出来
4.1 CPU优化:vCPU绑定与调度策略
虚拟机创建简单,但要让性能接近物理机,CPU这块必须仔细调。ARM平台和x86类似,vCPU是宿主机上的线程,如果放任系统调度器自由分配这些线程,cache局部性差、跨核调度频繁,虚拟机的性能会明显波动。
我把vCPU固定绑定到大核上,首先查看CPU拓扑和线程映射:
lscpu -eRK3588的CPU编号在两个cluster之间交替排列,大核对应的CPU编号需要从lscpu输出确认。然后通过virsh vcpupin把每个vCPU绑定到指定物理CPU:
virsh vcpupin vm1 0 4 virsh vcpupin vm1 1 5 virsh vcpupin vm1 2 6 virsh vcpupin vm1 3 7如果没有特殊需求,我一般把大核留给虚拟机和关键中断处理,小核跑系统服务。这里的关键是不要让多个vCPU争抢同一个物理CPU,否则虚拟化开销会直线上升。绑定好后可以用virsh vcpuinfo vm1查看绑定结果。
接着设置CPU调频策略。RK3588默认的调频策略是schedutil或ondemand,负载上来时频率切换有延迟,虚拟机会感觉到卡顿。把CPU调频模式固定到performance能稳定提升响应:
sudo cpupower frequency-set -g performance如果有多个CPU核心都切换到performance模式,注意同时执行。这个设置在重启后失效,需要写成systemd服务或放入rc.local。
4.2 内存优化:大页内存与balloon取舍
内存虚拟化最大的开销来自TLB(转换后备缓冲)Miss后的stage-2地址翻译,启用HugePages能显著减少TLB Miss,提高大内存虚拟机的内存访问性能。
在宿主机上预留大页内存池。RK3588支持2MB和1GB两种大页大小,我建议用2MB,灵活性更好。预留方式:
echo 4096 > /proc/sys/vm/nr_hugepages mkdir -p /dev/hugepages mount -t hugetlbfs hugetlbfs /dev/hugepages为了让配置重启后依然生效,修改/etc/sysctl.d/99-hugepages.conf:
vm.nr_hugepages=4096然后在libvirt的虚拟机XML中添加内存配置。用virsh edit vm1修改<memoryBacking>和<memory>区域:
<memory unit='KiB'>4194304</memory> <currentMemory unit='KiB'>4194304</currentMemory> <memoryBacking> <hugepages/> </memoryBacking>修改后重启虚拟机生效。可以在客户机内检查大页是否生效:
grep -i hugepages /proc/meminfo另外要注意,libvirt默认会启用内存balloon设备,允许动态调整虚拟机内存,但这在ARM平台上有时会引发客户机内存识别异常。性能优先的虚拟机建议把balloon禁用,在XML中去掉<memballoon>相关配置即可。内存分配到位后,虚拟机内的大内存应用(比如编译大型项目)能感受到明显改善。
4.3 存储优化:virtio与缓存策略
存储方面,virtio-blk是必须的。创建虚拟机时如果忘了指定bus=virtio,后面可以用virsh edit把磁盘总线改成virtio并重启。但光有virtio还不够,缓存策略对性能影响也很大。
我通常把磁盘缓存策略设为none,配合io=threads,这样数据直接通过页面缓存交换,避免QEMU进程空间内的额外拷贝。修改XML中的<driver>行:
<driver name='qemu' type='qcow2' cache='none' io='threads'/>如果用裸设备(比如直接物理分区或整块NVMe盘)做虚拟机磁盘,cache=none是最佳选择;如果是qcow2镜像,还需要考虑discard和detect_zeroes参数,可以在<driver>里加上discard='unmap'和detect_zeroes='unmap',让虚拟机内的fstrim指令真正释放宿主机的镜像空间。
多队列支持是另一个容易被忽略的性能点。现代内核的virtio-blk驱动支持多队列,每个IO队列绑定一个vCPU,可以提升并发IO吞吐。在libvirt XML中,给磁盘加上<driver ... queues='4'>,客户机内确认/sys/block/vda/device/queue目录下出现多个队列即可。
4.4 网络优化:开启vhost-net与多队列
网络IO是虚拟机最容易出现瓶颈的地方。默认的virtio-net虽然比纯模拟好很多,但每个包都要经过QEMU进程,上下文切换开销不小。启用vhost-net后,报文处理的一部分会移动到内核态内核模块中完成,减少用户态和内核态的切换,网络吞吐和延迟都能改善。
在libvirt中启用vhost-net,编辑虚拟机XML的<interface>部分:
<interface type='network'> <source network='default'/> <model type='virtio'/> <driver name='vhost' queues='4'/> </interface>queues='4'对应客户机CPU数量,开启后客户机网卡会创建多个virtqueue,配合客户机的多核中断处理能力,多连接传输性能提升明显。用ethtool -L ens3 combined 4(客户机内网卡名可能不同)设置多队列中断,然后用ethtool -S ens3查看接收和发送队列统计,确认多个队列都在处理包。
需要留意的是,vhost-net依赖内核模块vhost_vsock(实际应为vhost_net,下称vhost子模块)的正常加载。如果宿主机内核没有编译该模块,<driver name='vhost'>配置会导致虚拟机网络无法启动,此时需要安装或编译内核模块。在Ubuntu上可以用modprobe vhost_net手动加载,并把模块加入/etc/modules-load.d/避免重启后丢失。
4.5 实测数据参考
调优完成后,我在宿主机和虚拟机内分别跑了几个通用测试,这里给一组供参考的数据(RK3588,8G内存,宿主Ubuntu 24.04 kernel 6.6,4 vCPU):
| 测试项 | 宿主机原生 | KVM虚拟化(未调优) | KVM虚拟化(调优后) |
|---|---|---|---|
| 多线程编译Linux内核模块耗时 | 100%基准 | 116% | 105% |
| 网络TCP吞吐(本机iperf3) | 940 Mb/s | 720 Mb/s | 930 Mb/s |
| 磁盘顺序读(fio,qd=32) | 使用NVMe实测值 | 约为原生75% | 约为原生92% |
| 内存读写带宽(mbw) | 100%基准 | 87% | 96% |
调优的核心就是把宿主机的硬件资源尽可能直接暴露给客户机,减少QEMU的翻译层和调度层参与。ARM平台上虽然虚拟化扩展不如x86那么成熟,但明确按路线图做下来,效果差距还是很大的。
5. 常见问题与排查技巧实录
5.1 /dev/kvm不存在或权限不足
这是最容易遇到的第一个拦路虎。如果你执行ls -l /dev/kvm报没有这个文件,先检查内核模块:
modprobe kvm modprobe kvm_arm # 有些内核模块命名是kvm-arm如果模块加载失败,dmesg会给出具体原因,多数情况是CPU虚拟化扩展没有被bootloader开启。我在一块RK3588工控板上遇到过U-Boot环境变量里禁用了EL2支持的情况,在U-Boot命令行里检查cpu virtualization相关设置(具体变量名因固件而异),或者在板卡供应商的文档里找启用虚拟化的开关。这部分不同板卡差异很大,网上搜一下自己型号就能找到方案。
如果你能看到/dev/kvm但普通用户无法访问,执行:
sudo chown root:kvm /dev/kvm sudo chmod 0660 /dev/kvm不过这种设置重启后会失效,更好的方式是确认/etc/udev/rules.d/里有没有kvm设备的udev规则。Ubuntu默认有60-kvm.rules,会创建kvm用户组并设置权限,没有的话手动补一条。
5.2 虚拟机启动报错“KVM not supported”或“failed to initialize KVM”
这个报错会出现在virsh start时,或者在QEMU命令行中看到kvm: failed to initialize KVM: Function not implemented。排查思路分两层。
第一层确认KVM模块是否加载成功。执行kvm-ok(安装cpu-checker包后可用),如果输出包含“KVM acceleration can be used”之类的内容说明硬件条件满足。如果提示“Your CPU does not support KVM”,基本可以断定是固件或内核配置问题,按照5.1的方式检查。
第二层是确认QEMU是否以KVM加速模式运行。在libvirt中需要保证虚拟机XML里<cpu mode='host-passthrough'>和<features>配置没问题,特别是有没有误设<cpu mode='custom'>且指定了不支持虚拟化的CPU model。我用virtinst --cpu host-passthrough创建时就没有这个问题,但手动创建或修改XML很容易踩这个坑。
5.3 大页内存分配失败
配置了hugepages后,虚拟机启动报“unable to map backing store for guest RAM”或“Cannot allocate memory”错误,一般是宿主机大页内存不足。
先看预留是否生效:
cat /proc/meminfo | grep Huge如果HugePages_Total为0,说明sysctl配置没成功,检查/etc/sysctl.d/99-hugepages.conf的语法和是否在initramfs阶段就挂载了hugepages。另一个原因是物理内存碎片严重,虽然总量充足但连续大页不足。可以试试重启宿主机再预留,或者在BIOS/U-Boot里预留内存给大页(有些ARM平台需要用mem=内核参数让出连续内存区域)。
我的一个经验是,不要把宿主机所有内存都分给虚拟机。大页内存一旦被虚拟机和QEMU占用,回收起来很麻烦,预留量建议控制在物理内存的一半左右,保守一点能避免很多麻烦。
5.4 客户机网络吞吐低,vhost没有生效
虚拟机里iperf3吞吐上不去,先检查宿主机的vhost内核模块:
lsmod | grep vhost_net没有的话加载vhost_net模块,并确认虚拟机XML中<driver name='vhost'/>正确。如果模块加载成功且配置正确,可以在宿主机上用ethtool -S virbr0之类的命令看桥接端口的统计,确认数据确实走了vhost路径。
还有一种比较隐蔽的问题:客户机内的网卡可能没有正确加载多队列驱动。执行ethtool -l ens3查看当前队列数,如果只有1个队列,用ethtool -L ens3 combined 4上调。部分ARM发行版的网络配置工具(如netplan)会覆盖ethtool设置,需要把ethtool命令写入rc.local或systemd服务,确保开机后自动生效。
5.5 客户机休眠/暂停功能异常,RTOS或系统挂起
ARM平台上的ACPI支持目前仍不完善,很多RK3588板卡在客户机内核里都没有启用ACPI或休眠功能。如果你在虚拟机里执行suspend或睡眠命令,可能会直接挂起,因为宿主机的GIC和U-Boot不擅长处理从EL2虚拟化状态下恢复的流程。
我踩过这个坑,解决方案很粗暴:在客户机BIOS(如果是UEFI guest)或内核启动参数里禁用ACPI的suspend相关功能。比如在客户机GRUB配置里加入:
acpi=off但这会一并失去正常关机管理等功能,不太推荐。更好的办法是在客户机里直接用systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target禁用休眠,需要重启虚拟机时用virsh reboot或virsh shutdown可靠完成。
5.6 嵌套虚拟化支持
如果你尝试在KVM客户机里再跑一个KVM(比如在虚拟机里创建虚拟化测试环境),会遇到和“VMware Workstation 不支持嵌套虚拟化”类似的问题——ARM平台上默认也是不允许的,因为客户机内的KVM需要宿主机的虚拟化扩展再次透传进去。
ARMv8.3开始的嵌套虚拟化支持还比较有限,但通过KVM的kvm-arm.nested=true参数可以尝试开启(不同内核版本支持度不同)。需要宿主机内核、QEMU和客户机内核都支持嵌套虚拟化特性,这个配置链比较长。我的建议是,尽量避免嵌套KVM,如果需要多级虚拟化测试,改用QEMU的TCG模式做第二层,代价是性能很低,但能完成功能性验证。
6. 一点额外的实操心得
最后分享两个我实际使用中的小技巧。
第一个是给虚拟机启停设置主机CPU的自动调频联动。裸机跑重负载时,RK3588的频率提升需要一定时间,虚拟机里的负载不一定能立刻触发调频。我写了一个简单的脚本,在virsh start时强制把CPU governor设为performance,关机后恢复为schedutil。这样既不浪费平常待机的功耗,又能在虚拟机负载上来时避免卡顿。你可以根据自己的使用习惯改这个逻辑。
第二个是针对长时间运行的虚拟机,建议定期在宿主机侧观察vmstat和pidstat里QEMU进程的CPU占用。如果某个vCPU线程的CPU占用长期超过90%,大概率是客户机内有CPU密集型任务在跑,检查客户机进程而不是直接怀疑宿主机。反过来如果所有vCPU线程的CPU都很低,但虚拟机内却卡成PPT,那问题多半出在IO路径上,重点看块设备队列长度和网络中断分布。
KVM在ARM平台上的成熟度确实不如x86,但随着RK3588这一代高性能ARM SoC的普及,很多以前在ARM上做不了的玩法现在都能落地了。我折腾这套东西的初衷只是想在开发板上多跑几个独立的系统环境做测试,结果越用越觉得它适合当作一台低功耗家庭服务器或开发实验室里的多租户平台。希望这篇文章能帮你少走几步弯路,少熬几个夜。