Linux 基础体系 · 第 82/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux KVM 虚拟化:QEMU、vCPU、virtio、存储网络和隔离边界

KVM 虚拟化常被简化成一句话:“QEMU 提供设备,KVM 提供加速”。这句话方向正确,但不够解释实际系统中的关键问题:一个虚拟机进程到底如何运行,vCPU 如何与宿主机线程和物理 CPU 对应,virtio 为什么比设备全模拟更快,虚拟磁盘和网络包经过哪些层,虚拟机的隔离边界又在哪里。

本文以现代主流 Linux 发行版上的 x86-64 KVM 为主线,也会指出架构、发行版、权限和生产环境差异。

1. 先建立整体模型

KVM 是 Linux 内核中的虚拟化基础设施,QEMU 是用户空间的虚拟机监控器和设备模型。现代系统通常还会使用 libvirt、virt-manager 或云平台作为管理层,但它们不是虚拟化执行的核心。

可以先把一次虚拟机运行拆成四层:

flowchart TB
    A[管理层<br/>libvirt / virt-manager / 云平台]
    B[QEMU 用户空间进程<br/>设备模型、内存、I/O、管理协议]
    C[KVM 内核模块<br/>KVM_RUN、虚拟 CPU、虚拟中断、二阶段地址转换]
    D[Linux 调度器与设备驱动]
    E[物理 CPU、内存、磁盘、网卡]

    A --> B
    B --> C
    C --> D
    D --> E

各层职责并不相同:

  • QEMU 创建虚拟机地址空间,模拟或暴露虚拟设备,并负责大量用户空间 I/O。
  • KVM 将一个虚拟 CPU 置于硬件虚拟化模式中运行,并处理虚拟机退出、虚拟中断和内存地址转换等内核侧工作。
  • Linux 调度器 调度 QEMU 的 vCPU 线程、I/O 线程和其他宿主机进程。
  • 物理设备驱动 负责真实磁盘、网卡和其他硬件。
  • libvirt 通常生成 QEMU 命令行、配置 cgroup、设置网络和生命周期,但它本身不执行 guest 指令。

因此,“虚拟机”不是一个内核中的单一对象,也不是一个普通进程这么简单。通常它表现为:

  1. 一个 QEMU 进程;
  2. 一个或多个 vCPU 线程;
  3. 若干 QEMU I/O 线程;
  4. KVM 内核对象;
  5. 一组宿主机文件描述符、内存映射、网络接口和设备资源。

2. KVM 如何运行 guest 指令

2.1 硬件虚拟化的前提

x86 主机通常需要:

  • Intel VT-x,或 AMD-V;
  • 内核启用 kvm 以及对应的 kvm_intelkvm_amd 模块;
  • BIOS/UEFI 中没有关闭硬件虚拟化;
  • 当前用户有权限访问 /dev/kvm

检查方式:

$ grep -E 'vmx|svm' /proc/cpuinfo | head

看到 vmx 通常表示 Intel VT-x,看到 svm 通常表示 AMD-V。这个检查只能说明 CPU 暴露了相关能力,不保证 BIOS 已启用,也不保证当前环境允许嵌套虚拟化。

继续检查 KVM 设备:

$ ls -l /dev/kvm
crw-rw----+ 1 root kvm ... /dev/kvm

$ lsmod | grep -E '^kvm(_intel|_amd)?'
kvm_intel ...
kvm ...

不同发行版的权限组名称、udev 规则和安全策略可能不同。用户即使加入了 kvm 组,也可能还受到 SELinux、AppArmor、容器设备过滤或云平台策略限制。加入组后通常需要重新登录,且生产系统不应仅通过放宽设备权限解决启动问题。

2.2 KVM 的基本执行循环

QEMU 使用 KVM API 创建虚拟机和 vCPU。简化后的流程如下:

QEMU:
  打开 /dev/kvm
  KVM_CREATE_VM
  创建 guest 内存映射
  KVM_SET_USER_MEMORY_REGION
  创建虚拟中断控制器和 vCPU
  KVM_CREATE_VCPU
  进入 vCPU 执行循环

vCPU 线程:
  KVM_RUN
      ├─ guest 指令在物理 CPU 上执行
      └─ 发生 VM exit,返回内核/用户空间
          ├─ I/O 端口访问
          ├─ MMIO 访问
          ├─ 外部中断或虚拟中断
          ├─ halt、关机、调试
          └─ 未处理或异常状态
  QEMU 根据退出原因模拟设备或更新状态
  再次 KVM_RUN

这里的关键是 VM exit。guest 大部分普通计算指令不需要 QEMU 逐条解释,而是直接在 CPU 的非 root 虚拟化模式中执行。只有需要由虚拟机监控器处理的事件才退出到 KVM,再可能回到 QEMU。

这也是硬件辅助虚拟化与纯软件解释器的根本差别:

  • 解释器逐条读取、解码和模拟 guest 指令;
  • KVM 让硬件直接执行绝大多数 guest 指令;
  • 设备访问、特权行为、地址转换异常等事件仍需要虚拟化层介入。

“直接执行”不等于“guest 获得了真实 CPU 的全部权限”。硬件通过 VMCS/VMCB 等虚拟化控制结构限制 guest 的执行模式,KVM 负责设置这些控制条件。

2.3 vCPU 不是 CPU,也不一定绑定一个物理核

vCPU 是虚拟机看到的逻辑处理器。对 Linux 宿主机而言,vCPU 通常对应 QEMU 中的一个线程;对 guest 而言,它表现为一个可被 guest 内核调度的 CPU。

因此至少存在两层调度:

guest 进程
    ↓ guest scheduler
guest vCPU
    ↓ 宿主机 Linux scheduler
QEMU vCPU 线程
    ↓
物理 CPU 逻辑处理器

假设虚拟机配置了 4 个 vCPU,但宿主机只给它 2 个可运行的物理 CPU:

  • guest 仍然认为有 4 个 CPU;
  • 4 个 vCPU 线程竞争 2 个宿主机 CPU;
  • guest 内部可能同时运行 4 个任务;
  • 宿主机调度器再决定哪些 vCPU 线程获得时间片。

这不会自动违反正确性,但会增加调度开销和延迟。若 guest 中有大量并行任务,虚拟 CPU 数量远大于可用物理 CPU 可能导致:

  • guest 调度器认为存在更多并行度;
  • vCPU 线程频繁被宿主机抢占;
  • guest 内出现 %steal 时间;
  • 锁竞争和尾延迟上升。

可以用 topmpstat 或 guest 内的 vmstat 观察,但这些指标只反映某一层的现象。宿主机还应观察 QEMU 线程调度延迟、CPU cgroup 限制和 NUMA 位置。

2.4 vCPU 的常见状态

一个 vCPU 线程不总是在执行 guest 指令。常见状态包括:

  • 运行 guest:线程进入 KVM_RUN,硬件执行 guest。
  • VM exit:发生 I/O、异常、中断或其他退出事件。
  • 等待中断:guest 执行 HLT 或等待事件,vCPU 线程可能睡眠。
  • 被宿主机调度出去:宿主机其他任务运行,或 vCPU 被 cgroup 限制。
  • 等待 I/O:设备模拟、磁盘、网络或用户空间后端未完成。
  • 暂停:迁移、快照、管理操作或错误处理导致虚拟机暂停。

所以“CPU 使用率低”不能直接推出“虚拟机很空闲”。如果 vCPU 大量时间在 I/O 等待、宿主机被限流或 guest 在等待锁,CPU 利用率都可能不高。

3. 内存虚拟化:从 guest 地址到主机内存

3.1 三种地址不要混淆

虚拟机中常见三类地址:

  1. GVA(Guest Virtual Address):guest 进程使用的虚拟地址。
  2. GPA(Guest Physical Address):guest 内核认为的物理地址。
  3. HPA(Host Physical Address):宿主机真实物理内存地址。

典型转换链:

guest 进程的 GVA
    ↓ guest 页表
GPA:guest 物理地址
    ↓ EPT(Intel)或 NPT/RVI(AMD)
HPA:宿主机物理地址

guest 内核只管理自己的 GVA 到 GPA 映射。宿主机 KVM 负责 GPA 到 HPA 的二阶段地址转换。

假设 guest 进程访问:

GVA 0x7f00_1234_5000

guest 页表将它映射到:

GPA 0x0000_4000_5000

QEMU 为虚拟机分配的内存可能由宿主机虚拟地址:

HVA 0x7f80_0000_0000

映射到宿主机物理页:

HPA 0x0000_9000_5000

KVM 通过 EPT/NPT 等硬件机制完成 GPA 到 HPA 的转换。实际地址和页表层级会因架构、页大小、内存气球、内存热插拔和内核实现而变化,但逻辑关系保持不变。

3.2 为什么内存访问仍可能退出

普通 guest 内存访问通常由二阶段地址转换处理,不需要每次访问都退出到 QEMU。但以下情况可能导致异常或退出:

  • guest 页表本身未建立映射;
  • GPA 没有有效的 KVM 内存槽;
  • 访问被配置为 MMIO;
  • 内存页尚未分配或需要用户空间处理;
  • 设备内存、共享内存或特殊内存区域被访问;
  • 调试、脏页跟踪或迁移机制改变了权限。

虚拟机的内存不是“自动从宿主机拿一块不可见的 RAM”。QEMU 通常先建立一段用户空间内存映射,再通过 KVM 注册为 guest 可见的内存区域。宿主机还要承担实际页分配、回收、换页和 cgroup 记账。

生产环境通常应避免让虚拟机内存被宿主机 swap。宿主机发生 swap 时,guest 可能已经在自己的内核中管理内存,双重分页会产生不可预测的延迟。使用 libvirt 时应结合内存 cgroup、锁定内存、NUMA 策略和宿主机 swap 策略进行验证,而不是简单地对所有 QEMU 进程设置无限制的 mlockall

4. QEMU:设备模型和用户空间后端

QEMU 不是只负责“启动 CPU”。它通常负责:

  • 虚拟 PCI 总线;
  • 虚拟磁盘控制器;
  • 虚拟网卡;
  • 虚拟串口和控制台;
  • 虚拟 RTC、定时器和中断控制器;
  • BIOS/UEFI 固件加载;
  • 磁盘镜像、快照、迁移和设备后端;
  • QMP 管理接口;
  • 用户空间网络和存储数据路径。

4.1 全模拟设备和半虚拟化设备

全模拟设备让 guest 看到一个真实硬件的行为模型,例如传统 IDE、某些 e1000 网卡或 LSI SCSI 控制器。guest 使用已有的硬件驱动,兼容性较好,但每次设备访问可能涉及更多寄存器模拟和 VM exit。

virtio 是半虚拟化设备。guest 不再把设备当成某个真实厂商硬件,而是使用 virtio 驱动,通过共享内存队列与宿主机后端交换请求。

两者不是简单的“旧”和“新”:

  • 全模拟设备适合兼容性、救援和某些安装场景;
  • virtio 通常适合性能和现代 guest;
  • virtio 仍然需要 guest 驱动、正确的机器类型和后端配置;
  • 设备性能受后端、存储协议、线程调度和队列配置共同影响。

4.2 QEMU 线程模型

一个 QEMU 进程可能包含:

  • 每个 vCPU 一个线程;
  • 主循环线程;
  • 磁盘 I/O 线程;
  • 网络后端线程;
  • vhost 相关线程;
  • migration、block job、监控和定时器线程;
  • 用户配置的 iothread。

因此,给 QEMU 进程绑定 CPU 并不等于精确绑定了所有工作。若需要调优,应区分:

  • vCPU 线程运行在哪些 CPU;
  • I/O 线程运行在哪些 CPU;
  • vhost 内核线程运行在哪些 CPU;
  • 宿主机中断和软中断运行在哪些 CPU。

taskset 可以临时观察或设置进程/线程亲和性:

$ ps -L -p "$QEMU_PID" -o pid,tid,psr,comm

psr 显示线程最近运行的逻辑 CPU,但它不是永久亲和性证明。查看实际掩码:

$ taskset -pc "$QEMU_PID"
pid 1234's current affinity list: 0-7

生产环境更适合通过 libvirt XML、systemd cgroup 或专门的 CPU/NUMA 策略配置,而不是手工对运行中的线程执行一次性命令。

5. virtio 的工作机制

5.1 virtio 的基本数据结构

virtio 设备和 guest 驱动之间通常通过 virtqueue 通信。一个 virtqueue 可抽象为三部分:

  • descriptor table:描述数据缓冲区的地址、长度和读写方向;
  • available ring:guest 提交了哪些 descriptor;
  • used ring:设备处理完哪些 descriptor。

以 virtio-blk 写请求为例:

guest 文件系统
    ↓
guest 块层
    ↓
virtio-blk 驱动
    ↓ 填写 descriptor
available ring
    ↓
QEMU 或 vhost 后端
    ↓
宿主机文件 / 块设备
    ↓
used ring
    ↓
virtio-blk 驱动完成请求

请求通常包含:

  1. 请求头:操作类型、扇区号等;
  2. 数据缓冲区:写请求中的待写数据,或读请求中的目标缓冲区;
  3. 状态字节:后端完成后写入成功或错误状态。

virtio 驱动和后端通过共享内存访问这些结构,避免每个数据字节都通过模拟硬件寄存器传递。提交和完成通常还需要通知机制,现代 virtio 会使用 event suppression、批处理和多队列减少中断与 VM exit。

5.2 virtio-blk、virtio-scsi 和 virtio-net

virtio-blk 是直接的虚拟块设备,结构简单、路径短,适合常见虚拟磁盘。

virtio-scsi 将虚拟磁盘挂在 virtio 提供的 SCSI 控制器上,适合需要多个磁盘、SCSI 命令或更复杂热插拔模型的场景。它并不因为名字中有 SCSI 就天然更快,实际性能取决于 guest 驱动、QEMU 后端、队列和存储介质。

virtio-net 提供虚拟网卡。guest 发送数据包时,virtio-net 驱动把数据放入发送队列,宿主机后端取出后写入 TAP、vhost 或其他网络后端。

5.3 vhost:把数据路径部分移入内核

传统 virtio-net 数据路径可能是:

guest virtio-net
    → KVM/QEMU
    → TAP
    → Linux bridge
    → 物理网卡

启用 vhost-net 后,数据平面的一部分由宿主机内核线程处理:

guest virtio-net
    → vhost-net 内核后端
    → TAP/bridge
    → 物理网卡

这减少了 QEMU 用户空间参与数据包搬运的开销,但不等于所有网络处理都绕过 QEMU,也不等于一定获得更低延迟。控制面、设备配置和某些异常路径仍由 QEMU 管理;vhost 线程还会消耗宿主机 CPU,并受到调度和中断亲和性影响。

检查 QEMU 是否使用 vhost,不能只看一项指标。可以结合 QEMU 命令行、线程列表、/dev/vhost-net 和实际网络统计判断。libvirt 的具体 XML 和默认值会随版本变化,应以当前生成的 QEMU 命令行与日志为准。

6. 虚拟存储:从 guest 写入到宿主机持久化

6.1 虚拟磁盘的层次

一个 guest 写请求可能经过以下层次:

guest 文件
  ↓
guest 文件系统
  ↓
guest 块层
  ↓
virtio-blk / virtio-scsi
  ↓
QEMU block layer
  ↓
qcow2 或 raw
  ↓
宿主机文件系统 / LVM / SAN / NVMe
  ↓
物理介质和控制器缓存

每一层都可能缓存、重排、合并或延迟请求。因此 guest 看到的“写成功”必须结合缓存模式和持久化保证理解。

6.2 raw 与 qcow2

raw 是连续或逻辑上直接表示块内容的格式,结构简单、额外元数据少,通常适合性能敏感或由 LVM、Ceph、NVMe 等后端直接提供的虚拟磁盘。

qcow2 是 QEMU 的写时复制镜像格式,支持:

  • 稀疏分配;
  • 快照;
  • backing file;
  • 压缩等功能;
  • 镜像元数据和引用计数。

但 qcow2 会增加元数据访问和写放大路径。快照链过长、宿主机文件系统空间不足、backing file 被误删或镜像被多个 QEMU 进程并发写入,都可能造成严重问题。

查看镜像信息:

$ qemu-img info disk.qcow2

可能看到:

file format: qcow2
virtual size: 40 GiB (42949672960 bytes)
disk size: 6.2 GiB
cluster_size: 65536

这里:

  • virtual size 是 guest 看到的容量;
  • disk size 是当前宿主机实际占用的大致空间;
  • 两者不同是稀疏分配的结果。

不要使用文本工具直接修改 qcow2。检查镜像可以使用:

$ qemu-img check --force disk.qcow2

--force 对某些检查场景有风险,尤其是镜像仍被虚拟机使用时。任何修复、转换或压缩操作前都应关闭 guest 或建立一致性快照,并确认没有其他进程持有写访问。

6.3 缓存、flush 和“写成功”

存储路径中的“完成”至少有三种含义:

  1. 数据已复制到某个内存缓冲区;
  2. 数据已被宿主机文件系统接受;
  3. 数据已到达具备持久性保证的设备介质。

guest 文件系统会通过 flush、FUA 或相关协议请求持久化顺序。QEMU 的 cache、AIO、直写模式和后端设备能力会影响这些请求如何落地。宿主机磁盘控制器或 SSD 如果错误报告 flush 完成,虚拟化层也无法凭空修复硬件谎报。

因此,数据库虚拟机的可靠性不能仅由“使用 virtio”推出,必须验证:

  • guest 是否看到了正确的 discard、flush 和写屏障能力;
  • QEMU 后端是否将请求正确传递;
  • 宿主机文件系统和存储阵列是否提供持久化语义;
  • 断电测试或设备文档是否支持目标保证。

6.4 一个可运行的最小启动示例

以下示例用于理解路径,不是完整生产配置。需要已安装 QEMU、KVM 和一个可启动的 guest 镜像:

qemu-system-x86_64 \
  -enable-kvm \
  -machine type=q35 \
  -cpu host \
  -m 4096 \
  -smp 2 \
  -drive file=disk.qcow2,if=virtio,format=qcow2 \
  -nic user,model=virtio \
  -nographic

各选项的因果关系:

  • -enable-kvm:请求使用 /dev/kvm,否则可能退回纯软件 TCG,性能和能力不同;
  • -machine type=q35:选择一种虚拟机器类型,设备拓扑由它决定;
  • -cpu host:尽量向 guest 暴露宿主机 CPU 特征,但会降低跨主机迁移兼容性;
  • -m 4096:给 guest 配置 4096 MiB 内存;
  • -smp 2:创建两个 vCPU;
  • if=virtio:使用 virtio 磁盘接口;
  • format=qcow2:明确镜像格式,避免格式探测或格式误判;
  • -nic user:使用 QEMU 用户网络,适合快速测试;
  • -nographic:将控制台接到当前终端,前提是 guest 配置了串口控制台。

如果 guest 没有串口输出,-nographic 可能看起来像“启动卡住”,实际只是输出设备不匹配。测试结束可以在 guest 内关机,或从另一个管理终端通过 QMP/监控接口执行有序关机。直接杀掉 QEMU 可能导致 guest 文件系统恢复、数据库回滚或镜像损坏。

-nic user 默认通常提供出站网络和用户态 NAT,但不等同于生产网络。入站连接、DHCP、端口转发、性能和安全策略都与 bridge、macvtap、OVS 或云网络不同。

7. 虚拟网络:TAP、bridge、NAT 和数据流

7.1 TAP 是什么

TAP 是宿主机上的二层虚拟网络设备。QEMU 可以打开 TAP 文件描述符,把它当作以太网帧的读写端:

guest virtio-net
    ↕
QEMU / vhost
    ↕
tap0
    ↕
Linux bridge br0
    ↕
物理网卡 eth0
    ↕
外部交换网络

guest 发出的以太网帧进入 virtio-net 队列,宿主机后端从队列取出并写入 TAP;Linux 网络栈或 bridge 再决定转发到哪里。反方向则相反。

bridge 工作在二层,通常把多个端口放入同一个广播域。典型端口包括:

  • 物理网卡;
  • 一个或多个 TAP;
  • VLAN 子接口;
  • 其他虚拟网络设备。

配置 bridge 时若把宿主机 IP 错放在物理从接口而不是 bridge 上,可能导致宿主机网络中断。生产变更应使用 out-of-band 控制台或具备自动回滚的网络管理工具。

7.2 三种常见网络模式

QEMU user-mode NAT

优点是启动简单,不要求 root 创建 bridge 或 TAP。缺点是:

  • 入站连接需要端口转发;
  • 性能和协议能力有限;
  • guest 不会直接成为宿主机物理网络中的独立二层节点;
  • 某些复杂协议和广播发现不适用。

适合实验和安装环境,不应仅因“能联网”就认为满足生产需求。

Linux bridge

guest 可以通过 TAP 接入宿主机 bridge,并像独立主机一样使用上游 DHCP、静态地址或 VLAN。它的行为容易理解,但需要正确处理:

  • 物理网卡与 bridge 的关系;
  • VLAN 过滤;
  • 防火墙;
  • 生成树和链路状态;
  • 网络管理服务的持久化配置。

macvtap、Open vSwitch 和云网络

macvtap 可以减少某些 bridge 配置,但宿主机与 guest 的直接通信行为、混杂模式和上游交换机能力需要单独验证。Open vSwitch 适合更复杂的 VLAN、隧道、策略和虚拟交换需求。云平台还可能使用 veth、软件交换机、eBPF、SR-IOV 或硬件卸载,数据路径不能套用单机 bridge 的假设。

8. 一个可观察的 virtio 网络数据路径

可以用 guest 和宿主机两侧的计数器交叉验证:

guest 内:

ip -s link show
ethtool -i eth0
ethtool -S eth0

宿主机:

ip -s link show tap0
bridge link
bridge fdb show

这些命令的含义不同:

  • guest 网卡计数器说明 virtio-net 驱动收发了多少包;
  • TAP 计数器说明包是否到达宿主机虚拟端口;
  • bridge FDB 说明二层转发学习到了哪些 MAC;
  • ethtool -S 的统计项由驱动决定,不能假设所有发行版都提供同名字段。

例如,guest 发送计数增加而 TAP 接收计数不增加,问题可能位于 virtio 后端、TAP 连接或 QEMU 配置;TAP 计数增加但 bridge 不转发,则应检查 bridge 端口状态、FDB、VLAN 和防火墙。计数器只能定位层次,不能单独证明包已经到达物理网络。

9. 中断、队列和并发

virtio 的性能不仅由“是否使用 virtio”决定,还取决于队列数量、vCPU、I/O 线程、中断和后端并发。

9.1 多队列的逻辑

单队列设备可以抽象为:

所有 guest 请求 → 一个 virtqueue → 一个主要处理路径

多队列设备则是:

guest CPU 0 → queue 0
guest CPU 1 → queue 1
guest CPU 2 → queue 2
...

这样可以减少多个 vCPU 争用同一队列锁,并让处理线程分布到多个 CPU。但队列数过多也会增加:

  • 中断数量;
  • 内存占用;
  • 后端线程或上下文切换;
  • NUMA 跨节点访问;
  • 调试复杂度。

多队列不是越大越好。应根据 guest 并发度、宿主机 CPU、网卡队列、存储后端和实际负载测试。

9.2 vCPU、I/O 线程和物理 CPU的绑定

假设宿主机有两个 NUMA 节点:

node 0: CPU 0-15,内存 64 GiB
node 1: CPU 16-31,内存 64 GiB

一个 8 vCPU guest 若其 vCPU 运行在 node 0,而 QEMU I/O 线程、网卡中断和 backing file 的内存主要位于 node 1,数据可能经历跨节点访问。跨 NUMA 节点访问不一定错误,但延迟和带宽会不同。

NUMA 关联应同时考虑:

  • vCPU 亲和性;
  • guest 内存绑定;
  • QEMU I/O 线程;
  • vhost 线程;
  • 物理网卡中断;
  • 存储控制器和设备所在节点。

可以用以下命令观察拓扑:

numactl --hardware
lscpu -e=CPU,NODE,SOCKET,CORE
cat /sys/devices/system/node/node0/cpulist

numactl --hardware 显示的是宿主机拓扑,不是 guest 内部拓扑。guest 看到的 vNUMA 拓扑可能由虚拟机配置显式暴露,也可能根本没有暴露真实节点结构。错误的 vNUMA 配置会让 guest 调度器做出不合理的内存放置决策。

10. 虚拟机隔离边界

10.1 VM 与容器的边界不同

容器通常共享宿主机内核,通过 namespaces 隔离视图,通过 cgroups 控制资源。容器中的进程仍执行宿主机内核代码。

KVM 虚拟机通常运行自己的 guest 内核:

虚拟机:
  guest 应用
      ↓
  guest 内核
      ↓
  虚拟设备驱动
      ↓
  KVM/QEMU
      ↓
  宿主机内核

这增加了一层内核边界。guest 普通进程不能直接调用宿主机内核系统调用,也不能直接读取宿主机进程地址空间。

但“有虚拟机就绝对安全”是错误的。隔离边界至少包括以下部分:

  • guest 内核到虚拟 CPU 的边界;
  • guest 到虚拟设备模型的边界;
  • QEMU 到宿主机内核的边界;
  • QEMU 进程权限和文件系统访问边界;
  • 虚拟网络和宿主机网络策略边界;
  • 直通设备带来的 IOMMU/DMA 边界。

10.2 QEMU 进程权限很重要

QEMU 若以 root 身份运行,QEMU 漏洞或配置错误的潜在影响远大于受限用户运行。实际部署通常通过 libvirt 为每个虚拟机使用受限用户、组、SELinux/AppArmor 标签和专用目录。

必须限制的对象包括:

  • 虚拟磁盘及其 backing file;
  • UEFI 变量文件;
  • QMP socket;
  • ISO、日志和临时文件;
  • /dev/kvm/dev/vhost-net 等设备;
  • 共享目录和宿主机网络控制接口。

QEMU 的管理接口也属于高权限接口。将 QMP socket 暴露给不可信用户,可能允许关机、挂载介质、改变设备状态甚至读取 guest 内存状态。Unix socket 的文件权限、目录权限和管理代理边界必须明确配置。

10.3 设备直通会改变边界

VFIO 设备直通允许 guest 直接驱动某个 PCI 设备,常用于高性能网卡、GPU 或存储控制器。典型路径变为:

guest 驱动
    ↓
VFIO / IOMMU
    ↓
物理 PCI 设备

这减少了 QEMU 模拟路径,但风险和约束增加:

  • 设备必须处于可隔离的 IOMMU group;
  • 设备 DMA 必须由 IOMMU 限制到 guest 分配的地址范围;
  • 设备可能无法同时供宿主机使用;
  • 迁移、快照和热插拔能力受设备限制;
  • 设备固件和 guest 驱动错误可能影响稳定性;
  • SR-IOV VF、PF 和同组设备的隔离质量取决于硬件设计。

不能把 VFIO 视为“只改变性能,不改变安全模型”。它恰恰改变了设备访问和 DMA 的信任边界。

10.4 共享机制会削弱隔离

以下功能需要特别审查:

  • virtiofs、9p 等宿主机目录共享;
  • 共享剪贴板;
  • guest agent;
  • 主机与 guest 之间的端口转发;
  • QEMU monitor/QMP;
  • 设备直通;
  • 共享内存和 vhost-user;
  • 将宿主机文件作为可写虚拟磁盘使用。

例如,virtiofs 不是“虚拟机读取一个普通磁盘”,而是通过宿主机文件系统服务向 guest 暴露目录。guest 中的用户可能借此读取或修改宿主机上被共享的内容。共享目录的最小权限必须小于宿主机根目录,不能依赖 guest 内部的文件权限替代宿主机权限控制。

11. cgroups、namespaces 与 KVM 的关系

11.1 cgroups 控制 QEMU 资源

QEMU 是宿主机进程,因此可以被 cgroups 管理。cgroups v2 常见控制器包括:

  • cpu:CPU 权重、带宽和周期限制;
  • cpuset:允许使用的 CPU 和 NUMA 节点;
  • memory:内存上限、回收和压力;
  • io:块设备 I/O 权重和带宽;
  • pids:进程数量限制。

需要区分“guest 内的资源视图”和“宿主机对 QEMU 的限制”:

guest 看到的 4 vCPU、8 GiB RAM
        ≠
宿主机保证给 QEMU 的 4 个物理 CPU、8 GiB 未竞争内存

例如,guest 配置 8 GiB 内存,但 QEMU 所在 memory cgroup 的 memory.max 只有 6 GiB,QEMU 可能被宿主机 OOM 终止,guest 看到的结果可能是突然断电或虚拟机消失,而不是一个优雅的内存错误。

使用 systemd 管理的 QEMU 可以检查其 cgroup:

systemctl status libvirtd
systemctl status virtqemud
systemd-cgls

不同发行版可能使用 libvirtd、拆分后的 virtqemud 或其他服务名称。不要根据某个发行版的服务名编写跨发行版自动化脚本。

11.2 namespaces 主要隔离宿主机视图

namespaces 可以隔离:

  • PID 视图;
  • mount 视图;
  • network 设备和地址;
  • IPC;
  • UTS 主机名;
  • user ID 映射;
  • cgroup 视图。

但 KVM 虚拟机不需要依赖 namespaces 才能获得 guest 内核隔离。libvirt 可能使用 namespaces 或其他沙箱手段限制 QEMU,但这属于宿主机侧加固,不等同于 guest 自身的虚拟化边界。

一个常见误解是:“把 QEMU 放进容器,就自动拥有双重隔离。”实际上,容器必须获得 /dev/kvm、网络设备、镜像文件和可能的 VFIO 设备访问;这些权限若配置过宽,容器边界可能被削弱。还要确认容器运行时的 seccomp、设备白名单、user namespace 和 cgroup 设置是否允许 QEMU 正常运行。

12. 启动失败与故障路径

12.1 /dev/kvm 不可用

表现可能是:

Could not access KVM kernel module: Permission denied
failed to initialize KVM: Permission denied

排查顺序:

test -e /dev/kvm
id
ls -l /dev/kvm
dmesg | grep -i kvm

可能原因包括:

  • 模块未加载;
  • BIOS/UEFI 关闭虚拟化;
  • 用户无权访问设备;
  • 当前运行在不支持嵌套虚拟化的云主机中;
  • 容器没有透传 /dev/kvm
  • SELinux/AppArmor 或设备策略拒绝访问。

不要只看到 QEMU 能启动就认为使用了 KVM。应检查 QEMU 日志或启动参数,确认没有意外进入 TCG 软件模拟。

12.2 guest 没有网络

按数据路径逐层检查:

guest 网卡是否存在
  → virtio 驱动是否加载
  → guest 地址、路由和 DNS
  → TAP 是否存在
  → bridge 端口是否 UP
  → VLAN/防火墙是否放行
  → 物理链路和上游交换机

guest 内:

ip link
ip addr
ip route
ping -c 3 网关地址

宿主机:

ip link
bridge link
ip -s link show tap0

如果 guest 连网卡都没有,问题还未到 bridge 层;如果 guest 能访问网关但不能访问外部地址,可能是 NAT、路由或 DNS 问题;如果只有某些 VLAN 不通,应检查 bridge VLAN 过滤和物理交换机配置。

12.3 虚拟磁盘启动失败

常见表现:

  • No bootable device
  • guest 能启动但找不到根盘;
  • 磁盘容量显示异常;
  • 启动后文件系统只读;
  • I/O error、timeout 或 guest 卡顿。

排查:

qemu-img info disk.qcow2
file disk.qcow2
ls -lh disk.qcow2

还应检查:

  • 镜像格式是否与 QEMU 参数一致;
  • guest 是否含 virtio-blk/virtio-scsi 驱动;
  • 固件是 BIOS 还是 UEFI;
  • UEFI 变量文件是否匹配;
  • backing file 是否存在;
  • 镜像是否仍被其他实例写入;
  • 宿主机文件系统和底层块设备是否报错。

不要在虚拟机运行期间对镜像执行 qemu-img 的修改性操作,也不要通过复制一个正在写入的 qcow2 文件来宣称获得了一致性备份。需要一致性备份时,应使用 guest 文件系统冻结、应用层备份、QEMU block job 或存储系统一致性快照,并验证恢复。

13. 性能诊断:不要只看 guest CPU 使用率

虚拟机性能问题至少可能来自五个位置:

  1. guest 应用和 guest 内核;
  2. vCPU 调度;
  3. QEMU 设备模拟或 I/O 线程;
  4. 宿主机 cgroup、NUMA 和内存压力;
  5. 真实存储或网络设备。

可用工具及其边界:

# 宿主机整体 CPU、I/O、内存
vmstat 1
iostat -xz 1
mpstat -P ALL 1

# QEMU 线程和 CPU
top -H -p "$QEMU_PID"
ps -L -p "$QEMU_PID" -o tid,psr,stat,pcpu,comm

# NUMA
numastat -p "$QEMU_PID"
numactl --hardware

# 内存压力
cat /proc/pressure/memory
cat /proc/pressure/io

判断示例:

  • guest %steal 较高,同时宿主机 CPU 饱和:vCPU 在等待宿主机调度;
  • guest I/O wait 高,宿主机 iostat 显示底层设备延迟高:瓶颈可能在真实存储;
  • guest I/O wait 高,但底层设备空闲、QEMU I/O 线程受限:问题可能在 cgroup、队列或后端线程;
  • guest 网络丢包,宿主机软中断集中在少数 CPU:可能需要检查 RSS、RPS、网卡队列和中断亲和性;
  • QEMU CPU 高但 guest 负载不高:可能是设备模拟、频繁 VM exit、忙轮询或异常 I/O。

这些只是推断入口,不是单指标结论。调整 vCPU、队列、CPU 绑定和缓存模式前,应先建立基线并记录 guest、QEMU、宿主机和设备四层指标。

14. 迁移、快照和时间语义

虚拟机迁移要求目标主机能够复现源主机提供的 CPU 和设备模型。-cpu host 通常能提供本机更完整的 CPU 特性,但会降低迁移兼容性;生产迁移通常使用经过验证的 CPU model,而不是无条件暴露全部 host 特性。

内存迁移的核心过程可简化为:

1. 复制 guest 内存
2. guest 继续运行并产生脏页
3. 重复复制新脏页
4. 脏页产生速度低于复制速度时短暂停机
5. 复制 vCPU 和设备状态
6. 目标端恢复运行

如果 guest 持续修改内存的速度大于网络复制速度,脏页永远追不上,迁移会变慢、暂停时间增加,甚至无法完成。存储若未共享,还必须迁移磁盘状态或使用外部共享存储。

快照也有不同含义:

  • 磁盘快照;
  • 内存快照;
  • 崩溃一致性快照;
  • guest 文件系统一致性快照;
  • 应用一致性备份。

保存 qcow2 外壳文件不代表保存了 guest 的全部运行状态,更不代表数据库处于一致状态。恢复测试比“快照命令成功”更能证明备份有效。

虚拟机时间还涉及:

  • guest 虚拟时钟;
  • KVM paravirtualized clock;
  • 宿主机时间;
  • vCPU 暂停与恢复;
  • 迁移和快照;
  • guest NTP/chrony。

时间漂移可能导致证书、分布式锁、日志排序和数据库事务出现问题。guest 应使用合适的时钟源,并由 guest 自身时间同步机制校正;不能把宿主机当前时间等同于所有 guest 的有效时间语义。

15. 常见误解与真实边界

误解一:vCPU 数量越多越快

vCPU 增加只有在 guest 工作负载能够并行、宿主机有足够 CPU、锁竞争可接受且调度延迟不恶化时才有帮助。过量 vCPU 会增加调度和中断开销,甚至降低单线程任务的稳定延迟。

误解二:virtio 一定比模拟设备快

virtio 减少了设备模拟开销,但最终性能仍受后端影响。一个位于拥塞 NFS 上的 virtio 磁盘,不会因为接口名称是 virtio 就拥有本地 NVMe 的延迟;一个被 CPU cgroup 严格限流的 virtio-net 后端,也不能保证高吞吐。

误解三:raw 一定安全,qcow2 一定慢

raw 的格式结构简单,但若位于宿主机文件系统上,仍会经过文件系统缓存和底层存储层。qcow2 提供快照和稀疏分配,但性能取决于镜像链、cluster size、后端和负载。应测量实际工作负载,而不是只按格式名称判断。

误解四:虚拟机与宿主机完全隔离

QEMU 漏洞、KVM 漏洞、设备模拟错误、共享目录、管理 socket、直通设备和侧信道都会影响边界。虚拟机提高了隔离强度,但不能替代补丁、最小权限、网络策略和备份恢复。

误解五:QEMU 是一个普通单线程进程

QEMU 可能包含多个 vCPU、I/O、vhost 和管理线程。只观察主线程可能漏掉真正的 CPU 或延迟瓶颈。

误解六:关闭虚拟机就是结束 QEMU 进程

有序关机让 guest 文件系统和应用完成 flush。强制终止 QEMU 类似突然断电,可能触发文件系统恢复、丢失缓存数据、破坏镜像一致性或使分布式系统误判节点状态。

16. 生产配置的判断顺序

针对一个具体虚拟机,配置和诊断应按依赖关系进行,而不是先盲目调参:

  1. 确认硬件和 KVM 可用性:CPU 虚拟化、/dev/kvm、内核模块和权限。
  2. 确认机器类型与固件:BIOS/UEFI、PCI 拓扑、guest 驱动可用性。
  3. 确认 vCPU 和内存模型:数量、CPU model、NUMA、内存上限和 cgroup。
  4. 确认设备模型:virtio 还是全模拟,队列和中断是否匹配负载。
  5. 确认存储语义:镜像格式、backing file、flush、discard、备份和恢复。
  6. 确认网络路径:user NAT、bridge、VLAN、OVS、TAP、vhost 和防火墙。
  7. 确认权限边界:QEMU 用户、SELinux/AppArmor、QMP、共享目录和设备访问。
  8. 确认故障恢复:guest 有序关机、宿主机重启、存储故障、迁移失败和备份恢复。
  9. 最后才做性能调优:CPU 亲和性、NUMA、队列、I/O 线程和后端参数。

这个顺序体现了虚拟化系统的因果关系:如果 /dev/kvm 不可用,调 vCPU 亲和性没有意义;如果 guest 没有 virtio 驱动,调 virtqueue 数量也不会产生预期效果;如果存储本身延迟很高,改变 QEMU 线程数通常只是把等待位置移动。

KVM 虚拟化的核心不是“启动一个带 -enable-kvm 参数的 QEMU”,而是理解一条完整路径:guest 指令由 vCPU 在线程和物理 CPU 之间被调度,guest 地址经过二阶段转换落到宿主机内存,virtio 通过共享队列降低设备虚拟化成本,存储和网络请求再经过 QEMU、vhost、TAP、bridge、文件系统和真实设备,最终受到 cgroups、namespaces、IOMMU、权限模型和内核漏洞边界的共同约束。只有把这些层次分别验证,性能、可靠性和隔离结论才有可证明的基础。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。