坑边闲话:KVM (Kernel-based Virtual Machine) 是一个基于 Linux 内核的开源虚拟化技术,它在 2006年 由 Avi Kivity 和他的团队开发,并于 2007年 合并到 Linux 内核中,成为主流虚拟化解决方案之一。KVM 使用较为方便,生态非常良好,本文介绍如何在 Linux 上配置并使用 KVM 虚拟机,同时会介绍 Intel VT-D、AMD-V、IOMMU 等硬件功能。

图 1. KVM 架构图。

1. 初始化 Linux 系统·

在开始之前,读者需要有一个支持裸机安装的 Linux 系统,本文以 Debian 12 为例。

强烈建议使用最小化安装程序,因为有些新手非常依赖图形化界面配置虚拟机,导致迟迟无法深入理解技术细节,学到最后发现只是学了一个类似 VMware Workstation Pro 的软件。GUI 是用户友好型发明,但是对于学习技术、了解细节、拓展思维并不友好

1.1 调整 Console 界面字体大小·

最小化安装的 Linux 没有桌面环境,只有一个 console tty,因此建议执行以下命令,将界面字体设置为 Termius,并按照自己的喜好和视力设置一个合适的字体大小。

1
sudo dpkg-reconfigure console-setup

2560x1600 分辨率的 16 英寸显示器,建议选择 Termius 最大号字体。

图 2. Debian 12 最小化安装的效果。

1.2 确定 CPU 是否支持硬件虚拟化·

1
lscpu | grep Virtualization

如果输出不为 0,则可认为该平台支持硬件虚拟化。

1.3 安装 KVM 组件·

如果根据笔者的这篇文章进行了安装,则无需安装下列任何软件,因为安装 cocipit-machines 时,已经把与 KVM 虚拟机相关的组件都安装好了。否则,请根据下列内容执行。

1
2
sudo apt install bridge-utils
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients

如果要使用 Linux 图形化桌面,可以再安装一组图形化工具以方便日常查看,实现类似 VMware Workstation 的效果。命令如下:

1
sudo apt install virt-manager virt-viewer

组件释义

虚拟化是一个很大的话题,其中包含的架构模块较多,新手在短时间内可能无法掌握所有工具。如果读者想要迅速了解虚拟化架构以及各个模块的作用,可简单阅读本节。

虚拟机软件架构含有多个正交的模块,比如

  • 后端虚拟化技术:实现虚拟机内存模型的创建、资源分配与隔离、外设实现等;
  • 通用虚拟机的虚拟控制台,如 VNC 或 SPICE;
  • 用来控制虚拟机运行状态的库,如启动、挂起、停止、强制停止某个虚拟机,列出当前所有的虚拟机等。

由此可见,现代虚拟机软件架构并不依赖于单一的“大而全”的解决方案,而是强调模块间的层层解耦和职责划分。各模块保持低耦合、高内聚,通过标准化接口相互协作,确保系统具备良好的可扩展性和维护性。这种架构思想不仅有利于不同团队专注于各自领域的创新,也方便集成第三方组件,形成丰富多样的虚拟化生态。

libvirt 作为虚拟机管理领域的重要开源项目,正是基于这种理念设计和实现的。它通过提供统一的管理 API,兼容并支持多个虚拟化后端(如 KVM、QEMU、Xen 等),同时解耦虚拟机生命周期管理与虚拟机底层实现。libvirt 使得用户和开发者无需关注底层细节,即可方便地控制和管理各种类型的虚拟机。

网络类

  • bridge-utils:用于创建桥接网络

KVM 核心组件

  • qemu-kvm:KVM-QEMU 虚拟化软件包。
    • KVM 允许 Linux 作为一个虚拟机管理程序(hypervisor)运行,使主机能够运行多个被称为 guest. KVM 实质上为 Linux 提供了虚拟化功能,也就是说,内存管理器、调度器、网络栈等虚拟化组件都是由 Linux 内核本身提供的。每个虚拟机在本质上都是一个普通的 Linux 进程,由标准的 Linux 调度器调度,并配有专用的虚拟硬件,如虚拟网卡等。
    • QEMU 是一个托管型的虚拟机监控器(virtual machine monitor,VMM),通过模拟(emulation)为客户机提供一套多样的硬件和设备模型。QEMU 可以与 KVM 搭配使用,借助硬件虚拟化扩展实现接近原生性能的虚拟机运行。qemu 命令行程序允许用户指定 QEMU 所需的全部配置参数。
  • libvirt-daemon-system:Libvirt 守护进程,用于管理虚拟机。libvirt 本质上是一个接口层,用于将 XML 格式的虚拟机配置翻译成 QEMU 的命令行调用。它还提供一个管理守护进程,用于配置 QEMU 子进程,使得 QEMU 本身无需以 root 身份运行。比如,在 OpenStack Nova 想要启动一个虚拟机时,它会通过 libvirt 调用 QEMU,为每一个虚拟机启动一个 QEMU 进程,从而实现虚拟机的创建与运行。
  • libvirt-clients:与 libvirt 虚拟化管理框架相关的一组客户端工具和命令行工具,它们用于管理虚拟机和虚拟化资源。其中就包含重要的 virtsh 命令。

可有可无的 Linux 桌面图形化工具

  • virt-manager:图形化虚拟机管理工具。
  • virt-viewer:本地 VNC 工具。如果需要图形化界面访问虚拟机的桌面(例如通过 VNC, SPICE),你可以在其他计算机上安装 virt-viewer,通过 VNC 协议访问虚拟机的图形界面。

总结

  • libvirt 本身是一个提供虚拟化管理的 API,它支持多种虚拟化技术,如 KVM, Xen, QEMU 和 LXC 等。
  • libvirt-clients 包含了用于与这些虚拟化技术交互的命令行工具。

图 3. libvirt 组件扮演的角色定位。

2. 网络虚拟化·

一个完整的虚拟机创建流程应该是:

  1. 准备系统
  2. 配置网络
  3. 准备镜像和磁盘
  4. 定义并创建虚拟机
  5. 启动并连接虚拟机

网络是虚拟机与外部环境通信的最重要的途径,因此本文需要详细介绍虚拟机架构的网络模型。

值得指出的是,尽管 CPU, GPU, SSD 等存储、计算模块均有虚拟化相关知识,但是它们的虚拟化一般只是为了优化效能、增加便利性,普通用户在一般场景下无需深入了解。然而,网络的虚拟化是必须要搞清楚弄明白的,因为网络虚拟化的拓扑和技术模型直接决定了虚拟机的可用性及效率。

2.1 虚拟交换机模型·

虚拟交换机是对现实中的物理交换机的模拟,简单说来就是使用软件实现一个交换模型,每台虚拟机分配虚拟网卡,虚拟网卡与虚拟交换机建立通信。

虚拟交换机的核心职责是:

  1. 连接虚拟机:将同一台物理主机上的多个虚拟机的虚拟网卡(vNIC)连接在一起。
  2. 转发数据:像物理交换机一样,它工作在数据链路层(L2),根据 MAC 地址表来决定数据帧(Frame)应该被转发到哪个虚拟机的端口。
  3. 连接外部网络:它还需要一个“上行链路”(Uplink)来连接到物理网络,从而让虚拟机能够与外部世界通信。

通过下列命令可以创建虚拟网桥设备:

1
2
3
sudo nmcli connection modify br0 ipv4.method manual ipv4.addresses 10.4.1.21/16 ipv4.gateway 10.4.1.10
sudo nmcli connection modify br0 ipv4.dns "10.4.1.10 1.1.1.1"
sudo nmcli connection up br0

虚拟交换机

在 Linux KVM 的世界里,实现虚拟交换机概念最常见的技术就是 Linux Bridge. 一般我们认为上行链路貌似是一张插在虚拟交换机上的网卡,这个比喻很生动形象,然而在交换机上加网卡本身就是个很离谱的概念。

构造一批虚拟网卡比如 TAP 设备并将其分配给某些虚拟机是可行的,这种简单纯粹的虚拟化模型可以解决虚拟机之间通信的问题。然而如何才能让虚拟机与外部通信呢?

首先,网卡既然是一个二层设备,那么它肯定有操纵控制二层流量的能力。当我们在 Linux 中创建一个网桥 br0 并将物理网卡 eth0 接入时,eth0 的角色就从一个独立的网络接口,降级为 br0 虚拟交换机的一个端口。此时,虚拟机的 TAP 设备(如 vnet0)也作为端口接入 br0. br0 就像一个真正的交换机,它维护一个 MAC 地址表,学习哪个 MAC 地址在哪个端口(eth0vnet0)。

  • 外部流量进入:发往某台虚拟机的网络帧到达 eth0,br0 根据目标 MAC 地址,将其转发给对应的 vnet0 端口。
  • 内部流量发出:虚拟机通过 vnet0 发出的帧到达 br0,如果目标 MAC 在外部,br0 就将其从 eth0 端口转发出去。

为了让主机自身也能通信,我们需要在给主机也分配一张虚拟网卡并接入 br0,由此虚拟机和主机在网络模型实现了对等、平权。

上述结构是设计的内部思想表示,为了方便使用,我们可以把 br0、接入主机的虚拟网卡合并为一个网络设备,该设备既可以实现二层分流,也可以拥有自己的三层地址。这种“合并”思想在Linux网络虚拟化中非常普遍,例如 Open vSwitch (OVS) 中的 ovs-system bridge 接口也是类似的角色。

2.2 PCI 网卡直通·

上一节描述的纯软件交换方式非常优雅,但是性能一直很成问题,而且依赖软件交换就不可避免地造成 CPU 资源浪费,一旦流量大起来,CPU 的利用率也会飙升。

一个可行的方案是 PCIe 设备直通。如果主机的物理网卡数量足够,我们完全可以把某个网卡通过 PCIe 直通的方式,让某个虚拟机能完全控制该网卡。在主机看来,这个网卡就消失了,而在虚拟机看来,自己多了一个真实的网卡。该技术有以下优势:

  1. 性能最好,网络通信的资源占用率较低;
  2. 兼容性最好,由于使用的是真实的网络设备,因此一般没有任何兼容性问题。

同时,该技术也有以下缺陷:

  1. 基于 IOMMU/VT-d 技术,某些老 CPU 可能不支持该技术;
  2. 虚拟机持有某个真实网卡之后,某些虚拟化平台会锁定虚拟机的内存,导致主机的内存资源迅速耗光。
  3. 使用了真实设备之后,虚拟机将与该节点绑定,无法实现自动迁移。

2.3 SR-IOV 技术·

PCIe 直通的方式在网卡有限的情况下会很难扩展,因为一个主机开七八台虚拟机是很常见的,但是主机却一般没有这么多网卡。

网络无非就是交换数据,因此多个虚拟机共享一张硬件网卡是很正常的,而且网卡天然就是基于分组传输的设备,来自多个虚拟机的数据包可以同时在一个网卡的队列里排队,彼此几乎互不干扰。然而,一个网卡的 PCI 地址只有一个,无法被多个虚拟机同时使用,否则会造成内存寻址的错乱。为此工程师发明了 SR-IOV(Single Root I/O Virtualization)技术。

SR-IOV 是一个概念,厂商有自己的具体实现。通俗来说,SR-IOV 将一个物理设备(Physical Function, PF)虚拟成多个虚拟功能(Virtual Function, VF). 每个 VF 都可以被直通给一个虚拟机。宿主机管理 PF,而虚拟机直通 VF。这样既保留了直通的高性能,又解决了物理网卡数量有限的问题,是目前高性能虚拟化网络的主流方案。

虽然 SR-IOV 看上去是个很不错的技术,但是在使用过程中笔者也发现一些问题。理论上所有隶属于同一张网卡 Physical Function 的 VF 应该能在网卡芯片内部进行数据交换,然而某些网卡在不接入物理交换机的情况下,片上的交换功能也被关闭了,造成虚拟机之间无法通信。此外,还有一些老交换机没有片上交换功能。

最后,启用 SR-IOV 通常需要在 BIOS 中开启相关选项,并在宿主机上加载驱动并配置需要创建的 VF 数量。具体说要要完成以下操作:

  • 在 UEFI 的网卡配置界面(或系统内的配置界面)开启网卡的 SR-IOV 功能,并配置合适数量的 VF.
  • 开启主板的 PCI 直通功能;
  • 开启 Intel VT-d 或者 AMD 的 IOMMU 功能;
  • 在操作系统内部开启 SR-IOV 支持;
  • 在虚拟机内部安装 SR-IOV 专用的网卡驱动。

2.4 VirtIO 网卡·

纯软件实现的虚拟化在架构上比较和谐,但是效率很低。基于 PCI 直通或 SR-IOV 的硬件虚拟化效率极高,但是要求硬件平台具有相关特性,而且还部分地限制了虚拟化的灵活性。因此我们不禁在想,有没有一种更灵活更高效的方式实现虚拟机的外设通信呢?

通过观察软件虚拟化的性能制约因素我们发现,在传统的 I/O 中数据通常需要在内核态和用户态之间产生多次拷贝。比如,guest 用户态到 guest 内核、host 内核到 host 用户态至少有两次拷贝。2007 年,Rusty Russell 在 IBM 从事虚拟化开发时 VirtIO 首次提出了 VirtIO 的概念。这个概念较好地解决了频繁的内存拷贝问题。

既然虚拟机作为用户态进程存在,所以理论上通过主机用户态和内核态共享内存的方式实现内存零拷贝。VirtIO 就是在这种思想启发下的产物。它规定了 guest 和 host 通信时使用的数据结构和通信协议,一般只需要传递控制信号,尽量少甚至无需数据拷贝,由此可以大大提高网卡、SSD 等设备的 I/O 效率。

VirtIO 的核心数据结构是 vring, 它由以下三个具体的数据结构组成。

结构名 作用方向 由谁写入 由谁读取 主要用途
Descriptor Table buffer 元信息表 guest driver host device 存放数据 buffer 的地址、长度、标志位等信息
Available Ring 告知设备可用项 guest driver host device 表示 guest 已准备好、等待处理的描述符
Used Ring 通知驱动处理完 host device guest driver 表示设备处理完毕的数据 buffer

不同于全软件虚拟化(简称全虚拟化),VirtIO 需要对虚拟机作一定的修改,否则无法实现高效的零拷贝通信。因此,这种需要对虚拟机内核作修改的外设通信方式也被称为半虚拟化。一般我们认为半虚拟化的性能远高于全软件虚拟化,比较接近 PCI 设备直通等硬件辅助虚拟化

3. 存储设备虚拟化·

3.1 基于文件的虚拟磁盘·

在描述基于 SR-IOV 技术的硬件辅助虚拟化时我们曾提到,网卡天生就很适合多虚拟机共享。这算是某种基于网络数据包 packet 的「量子化」。而块设备是另一种特殊的设备,它让虚拟化存储变得更加灵活。

一般我们认为块设备是一个连续的块数组。以硬盘为例,它被抽象为一个巨大的扇区数组,单个扇区为 512/4K 字节。如果在这个硬盘上创建现代文件系统,就可以进一步得到文件概念。文件是由元数据索引的一个字节数组,与硬盘的扇区相比,文件的元素粒度更细。抽象地看,文件和硬盘是很类似的,它们都支持按照某种最小单元进行寻址。所以我们可以把文件模拟为一个虚拟机的磁盘。常见的 qcow2、vmdk、vhdx 等格式就是虚拟磁盘文件格式,它们本质上是文件,但是可以被 hypervisor 解释为一个虚拟机硬盘。

如果我们要创建一个 512GB 的虚拟磁盘文件,最简单的方法是直接在文件系统上申请一个容量为 512GB 的空间,然后像对待普通硬盘一样对这个文件进行读改写。但是这么做很不灵活,而且在写入过程中发生突然断电会有可能导致数据损毁。qcow2 是 QEMU 使用的一种特殊文件格式,它支持 Copy-on-Write 特性,即修改后的数据块只会被写入到文件末尾的新位置,然后更新元数据(块映射表)指向这个新位置。此外,qcow2 的 CoW 机制使得虚拟磁盘文件可以按需增长,一个 512GB 的虚拟磁盘,初始时在宿主机上可能只占用几 MB 的空间,该特性一般被称为精简置备。最后,VHDX 和 VMDK 等主流商业格式也通过类似的技术实现了精简置备和快照等高级功能。

3.2 基于卷管理器的块设备·

使用 LVM、ZFS 等工具可以在物理磁盘组的基础之上创建出 LV 和 ZVol 等逻辑块设备。这种块设备也有自己的路径(一般是 /dev 目录下的某个块设备路径),而且可以像文件一样被虚拟机作为硬盘使用。这种存储的优势是性能比较高,LVM 和 ZFS 已经被广泛验证过,其存储的可靠性和性能均非常优秀。缺点是存储迁移会比较麻烦,不如文件类型来得快捷方便。

3.3 控制器直通·

跟 PCI 网卡直通类似,我们也可以把 NVMe 控制器或 SAS/SATA 控制器直通给虚拟机,让虚拟机直接调用物理硬盘。

3.4 raw 设备映射·

在某些特殊情况下,我们希望虚拟机可以直接利用到某个具体的物理硬盘,但是因为条件限制我们无法使用 PCI 直通。

前面我们提到,硬盘是一个大的块设备。如果我们创建一个与物理硬盘容量、规格一致的指针文件并分配给虚拟机,然后 hypervisor 主动拦截虚拟机对该块设备的所有 I/O 并在真实的硬盘上重放一遍,就能实现类似硬盘直通的效果。这种硬盘分配方式一般被称为裸设备映射或 raw 设备映射,在 VMware 语境里被称为 RDM 直通。

  • 通过这种方式,我们可以保证硬盘在接入真实机器时仍然能被正确读出来,无需面对物理机不能识别 qcow2 虚拟磁盘文件的问题。
  • 此外,指针文件的体积很小,里面只记录了对应的物理磁盘地址。
  • 而且经过实测,该方案性能也比较能令人接受。

raw 设备映射的 I/O 模型也可以利用 VirtIO 思想进行优化。传统的 raw 设备映射需要在 host 内核和 guest 内核之间拷贝一次,通过 VirtIO 可以实现零拷贝,从而大大提升存储 I/O 效率。

不过,raw 设备映射也有以下缺点:

  • 丧失了虚拟化的部分灵活性:使用 RDM 的虚拟机与特定的物理硬件绑定,这使得虚拟机的迁移(如 VMware 的 vMotion)变得复杂或不可能。
  • 快照功能受限:对 RDM 磁盘进行 hypervisor 级别的快照通常是不支持的,因为它直接操作物理设备,绕过了虚拟磁盘文件的抽象层。

4. 使用 virsh 创建并管理虚拟机·

首先启动 libvirtd 守护进程:

1
sudo systemctl enable --now libvirtd

可以通过以下命令检查 KVM 是否已经安装并正常工作:

1
sudo virsh list --all

4.1 使用 virsh 创建并管理虚拟机·

virsh 是一个强大的命令行工具,用于管理虚拟机。你可以通过它来创建、启动、停止、查看虚拟机等。

本节后续将按照该逻辑链条展开介绍。

4.1.1 定义虚拟机·

define 是最重要的 libvirt 命令,它读取 XML 格式的虚拟机描述文件,生成 libvirt 虚拟机对象。不执行该定义过程,后续所有操作均无法进行!

1
sudo virsh define --file vm.xml

后期如果修改了 vm.xml, 则需要重新定义。

1
2
sudo virsh undefine $VM_NAME
sudo virsh define --file vm.xml

注意,这里的 VM_NAME 是虚拟机 XML 描述文件里的一个标识符,与 XML 文件名没有关系。

4.1.2 启动虚拟机·

1
sudo virsh start $VM_NAME

4.1.3 停止虚拟机·

1
sudo virsh shutdown $VM_NAME

如果用户急切地要杀死当前虚拟机,可以使用 destroy 命令:

1
sudo virsh destroy $VM_NAME

4.1.4 查看虚拟机的状态·

1
sudo virsh dominfo $VM_NAME

5. 深入理解 XML 定义文件·

libvirt 项目始于 2005 年左右,当时 XML 已经是业界非常成熟、广泛应用的结构化数据描述格式。而更加人性化的 JSON 和 YAML 要么还没出现,要么还不怎么流行,因此 libvirt 就这样选择了用对用户不太友好的 XML 格式描述虚拟机配置。不过好在现在有了 ChatGPT,我们可以让大模型帮我们生成、修改配置。从这种角度看,大模型的出现是对原教旨主义者重大利好。

XML 格式非常复杂,但是其中的各个模块却很简洁,几乎可以认为是自解释的。

1
2
3
4
5
<disk type='block' device='disk'>
<driver name='qemu' type='raw'/>
<source dev='/dev/md/storage'/>
<target dev='vdb' bus='virtio'/>
</disk>

笔者对 libvirt 虚拟机描述文件的结构进行如下总结:

  • domain: 虚拟机定义的根节点,type 指定后端虚拟化技术
    • name 虚拟机名称
    • uuid 虚拟机唯一标识(可选)
    • memory 分配内存大小
    • vcpu CPU 核数
    • os
      • type 类型
        • arch 架构(如 x86_64, aarch64 等)
        • machine 机器类型(如 pc-i440fx-2.9, q35 等)
      • boot dev=‘cdrom’ 第一启动设备
      • boot dev=‘hd’ 第二启动设备(可选)
    • features CPU 特性支持
      • acpi
      • apic
      • pae
    • cpu CPU 配置
    • clock 时钟设置
    • on_poweroff 电源事件行为
    • on_reboot 重启事件行为
    • on_crash 崩溃事件行为
    • devices 设备定义模块
      • emulator QEMU 路径
      • disk 磁盘设备
        • type 磁盘类型(file, block, network 等)
        • device 设备类型(disk, cdrom 等)
        • driver 磁盘驱动
        • source 磁盘源文件或设备
        • target 目标设备名称和总线类型
      • interface 网络接口
        • mac 地址
        • source 网络源
        • model 网络模型类型(如 virtio, e1000 等)
      • input 输入设备
      • graphics 图形显示配置
        • listen
        • image

一般情况下用户无需亲自设定每个字段,直接从网上找官方 demo 然后修改一下设备类型和文件地址即可。

中间插曲

由于 Ryzen 7840HS 有 Linux 硬件兼容性问题,因此本教程几乎一度中止。后来在 EPYC 和 Xeon 平台上最终完成了本文。不过这距离本文开始写作,已经过去半年的时间了。

由此可以看出,要想虚拟化用得顺心,CPU 很关键。并不是所有消费级芯片都有能力实现完美的虚拟化。

总结·

本文详细描述了 Linux 平台的虚拟化技术,从后端虚拟化引擎到前端控制工具都有所介绍。虚拟化是运维练手的绝佳项目,它可以锻炼运维人员对 Linux 网络、存储的掌控能力。笔者在这个过程中深深体会到了 ZFS 块设备的灵活与强大,学到了许多知识。