OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 140

在 NixOS 声明 KVM 宿主能力

通过固定版本的模块接口配置 libvirt,同时管理网络、固件和 VM 外部运行状态。

所在专题KVM 高级能力与平台选择 ↗
约 31 分钟阅读开始阅读

本文是 KVM 虚拟化专题的第 6 篇子文档,聚焦 NixOS 作为 KVM 宿主机的声明式配置与排障。通用概念(QEMU/KVM 原理、libvirt 架构、virt-manager 操作)请先阅读 通用安装指南。


1. NixOS KVM 配置全景#

1.1 为什么 NixOS 特别适合跑 KVM

NixOS 的声明式配置模型与 KVM 虚拟化的契合度极高,主要体现在四个方面:

优势 说明
单文件全量配置 内核模块、libvirtd、QEMU、virt-manager、网络、防火墙全部聚合在一个 configuration.nix 或 flake 中,不再分散于 /etc/libvirt/、/etc/modprobe.d/、systemd unit 手动编写等多处
可复现性 换机、重装、多机部署时,应固定 nixpkgs 与模块输入并准备宿主硬件/网络差异;VM 磁盘、libvirt 运行态、NVRAM 和 TPM 另行恢复
声明式 VM 定义 搭配社区项目 NixVirt,甚至可以用 Nix 语法声明式定义虚拟机(domain XML 的 nix 表达),做到 "VM as Code"
原子升级与回滚 升级内核 / QEMU / libvirt 后如果 VM 出问题,直接从 bootloader 选择上一代配置回滚;不能回滚 VM 磁盘、NVRAM 和 TPM 中的外部状态

1.2 两步启用

在 NixOS 上获得一个可用的 KVM 环境,最少只需要两行:

virtualisation.libvirtd.enable = true;
programs.virt-manager.enable = true;

加上把用户加入 libvirtd 组:

users.users.user.extraGroups = [ "libvirtd" ];

先按项目规则评估与构建;activation/reboot 由管理员单独决定。应用配置后重新登录使组成员身份生效,再检查 libvirt URI、KVM 和网络;本文没有 rebuild 或运行 VM。

libvirtd.enable = true 会自动拉入 QEMU/KVM 用户态组件,无需单独声明 pkgs.qemu_kvm。但如果需要特定版本的 QEMU 或额外的 QEMU 功能(如 virtiofsd),则需要通过 qemu.package 和 qemu.vhostUserPackages 显式指定。


2. 声明式配置全解#

以下是一份面向 NixOS 配置模型,并以本次核对的 nixos-unstable 模块为接口快照(实际项目应以 flake.lock 固定的 nixpkgs 为准) 的完整 KVM 宿主机配置,按功能分组,每组附解释。

2.1 内核模块与启动参数

{ config, pkgs, lib, ... }:
{
  # ===== 内核模块 =====
  # AMD CPU: "kvm-amd";Intel CPU: "kvm-intel"
  # 按宿主 CPU 选择模块,下面只演示 AMD 分支
  boot.kernelModules = [ "kvm-amd" ];

  # ===== IOMMU 开启(PCI 直通前提) =====
  # AMD IOMMU 通常由固件开启后自动初始化;不要套用不存在的 amd_iommu=on 值
  # Intel: "intel_iommu=on"
  # iommu=pt 可让宿主 DMA 使用 passthrough 默认域;需评估隔离与性能,非通用默认
  boot.kernelParams = [
    "iommu.strict=1"
    # "iommu=pt"  # 仅在明确评估宿主 DMA 隔离后启用
  ];

  # ===== 嵌套虚拟化(可选) =====
  # 允许在 VM 内部再运行 KVM(比如在 VM 里跑 minikube 或测试 nested KVM)
  boot.extraModprobeConfig = ''
    options kvm_amd nested=1
  '';
}

各参数说明:

  • kvm-amd:AMD-V 硬件虚拟化内核模块。加载后可检查:lsmod | grep kvm,应看到 kvm_amd 和 kvm。
  • iommu.strict=1:控制 DMA unmap 的同步失效,不是启用 AMD IOMMU 的开关。AMD 通常由固件和内核自动初始化;验证 IOMMU group、interrupt remapping 与 VFIO。SVM 是 CPU 虚拟化开关,AMD-Vi 是 IOMMU 功能。
  • iommu=pt:可改变宿主设备的默认 DMA 域,不能宣称总有性能收益;它也改变隔离边界,应按威胁模型选择。
  • nested=1:允许 VM 内部使用 svm 指令,即嵌套虚拟化。

验证 IOMMU 是否生效:dmesg | grep -i iommu,应看到 AMD-Vi: Interrupt remapping enabled 或类似输出。验证 IOMMU 分组:find /sys/kernel/iommu_groups/ -type l | sort -V。

2.2 libvirtd 主配置

  virtualisation.libvirtd = {
    enable = true;

    # ===== QEMU 子配置 =====
    qemu = {
      # QEMU 包选择:pkgs.qemu_kvm 内置 KVM 加速支持
      package = pkgs.qemu_kvm;

      # system libvirt 与 QEMU 运行身份分开。此接口快照默认 true。
      # true: QEMU 以 root 运行;false: 使用 qemu-libvirtd 用户/组。
      # 迁移现有镜像时检查读写、目录遍历与安全标签;这并非整个 daemon rootless。
      runAsRoot = false;

      # Software TPM 2.0 模拟(满足 Windows 11 TPM 条件的一种后端)
      swtpm.enable = true;

      # vhost-user 后端包:virtiofsd 用于宿主机-虚拟机共享目录
      vhostUserPackages = with pkgs; [ virtiofsd ];
    };

    # ===== 宿主关机时 libvirt-guests 的 VM 处理 =====
    # "shutdown":发送 ACPI 关机信号
    # "suspend":尝试保存 VM 状态到磁盘;直通等域可能不支持,必须验证
    onShutdown = "shutdown";
  };

  # ===== SPICE USB 重定向 =====
  # 允许将宿主机 USB 设备热插拔到 VM 中
  virtualisation.spiceUSBRedirection.enable = true;

重要变更:OVMF 配置 本次核对的 nixos-unstable 模块已移除旧 qemu.ovmf.enable/packages 配置接口,固件元数据来自选择的 QEMU 包;实际迁移以项目固定 revision 的 option 定义为准。旧配置在此接口中会触发 assertion。

固件路径也发生了变化:

旧路径 新路径
/run/libvirt/nix-ovmf/OVMF_CODE.fd /run/libvirt/nix-ovmf/edk2-x86_64-code.fd
/run/libvirt/nix-ovmf/OVMF_VARS.fd /run/libvirt/nix-ovmf/edk2-i386-vars.fd
/run/libvirt/nix-ovmf/OVMF_CODE.ms.fd /run/libvirt/nix-ovmf/edk2-x86_64-secure-code.fd

新建域可使用 <os firmware="efi"> 自动选择固件。已有 VM 必须先备份 NVRAM/TPM 与加密恢复资料,再核对 CODE/VARS 兼容性;不要直接删除 <nvram> 重置已有状态。表中路径是历史示例,实际从当前固件 metadata 与 domcapabilities 确认。

2.3 GUI 与客户端工具

  # ===== virt-manager GUI =====
  programs.virt-manager.enable = true;

  # ===== 宿主机 QEMU Guest Agent 与 SPICE Agent =====
  # qemuGuest: 宿主机作为 VM 时可被 hypervisor 控制
  # spice-vdagentd: 宿主机上的 SPICE agent daemon(剪贴板共享等)
  # 仅当这个 NixOS 系统本身是需要此集成的 guest 时启用:
  # services.qemuGuest.enable = true;
  # services.spice-vdagentd.enable = true;

  # ===== 额外系统包 =====
  environment.systemPackages = with pkgs; [
    virt-viewer          # SPICE/VNC 独立控制台
    spice-gtk            # SPICE GTK 客户端库
    win-virtio           # Windows 客户机 virtio 驱动 ISO
    win-spice            # Windows 客户机 SPICE guest tools
  ];

  # ===== 用户组 =====
  users.users.user.extraGroups = [ "libvirtd" ];

win-virtio 包提供 virtio-win.iso,实际路径由安装 profile 与包布局决定;systemPackages 应从 /run/current-system/sw/share/ 或当前 store 包中定位,不能假定在用户 profile 中。创建 Windows VM 时将此 ISO 作为第二个 CDROM 挂载,即可在安装过程中加载 virtio 存储和网络驱动。

2.4 防火墙配置

不要把 trustedInterfaces = [ "virbr0" ] 当默认必要条件;它会放宽客体对宿主服务的入站访问。先检查 libvirt 网络的 nftables/iptables 规则及 NixOS 防火墙,按实际需求只允许 DHCP/DNS 等服务。例如确有需要时:

  networking.firewall.interfaces.virbr0 = {
    allowedUDPPorts = [ 53 67 ];
    allowedTCPPorts = [ 53 ];
  };
}

这是 DNS/DHCP 入站示例,不是所有转发/ NAT 问题的统一修复;以实际后端和规则验证流量。


3. 网络配置#

3.1 默认 NAT 网络自动激活

默认网络的存在、active 与 autostart 状态是 libvirt 运行态。此接口快照会在首次配置时复制包内默认网络定义而保留已有文件;仍需显式查看 net-list --all、net-info default 与网段冲突。对于已有持久网络,可以在授权管理操作中设置 net-autostart default;这不是 Nix 对全部 domain/network 状态的声明式所有权。

若确实需要服务来激活已存在的网络,下面示例先检测 active 状态,避免重复 net-start;不在服务停止时 net-destroy,以免 rebuild/restart 中断客体网络。

systemd.services.libvirt-default-network = {
  description = "Activate existing libvirt default NAT network";
  after = [ "libvirtd.service" ];
  requires = [ "libvirtd.service" ];
  wantedBy = [ "multi-user.target" ];
  path = [ pkgs.libvirt pkgs.gawk ];
  environment.LC_ALL = "C";
  script = ''
    active=$(virsh -c qemu:///system net-info default | awk '/^Active:/ {print $2}')
    if [ "$active" != "yes" ]; then
      virsh -c qemu:///system net-start default
    fi
  '';
  serviceConfig = { Type = "oneshot"; RemainAfterExit = true; };
};

若项目改用模块化 daemon,依赖需对应实际服务;不要同时宣称单体和模块化都默认启用。网络 XML 若要求完全声明式,可评估 NixVirt,并明确它对已有资源的接管范围。

3.2 桥接网络(Bridged Mode)

如果希望 VM 直接接入宿主机所在的物理网络(与宿主机同级 IP),使用桥接模式替代默认 NAT:

networking.bridges.br0 = {
  interfaces = [ "enp3s0" ];  # 替换为你的物理网卡名
};
networking.interfaces.br0.useDHCP = true;
networking.interfaces.enp3s0.useDHCP = false;  # 物理网卡不再独立获取 IP

然后用 virsh net-define 创建一个指向 br0 的 libvirt 网络,或在 virt-manager 中为 VM 网卡选择 "Bridge device" 并填入 br0。

桥接模式下宿主机将失去对物理网卡的直接控制(IP 移到 br0 上)。不要在远程 SSH 连接的机器上直接操作,配置错误会导致断连。先在本地验证。

3.3 网络拓扑总览

flowchart LR
    VM_A[VM A / 192.0.2.x] --> virbr0[virbr0 NAT]
    VM_B[VM B / 192.0.2.x] --> virbr0
    virbr0 --> Host[宿主机网络栈]
    Host --> enp3s0[物理网卡 enp3s0]

    VM_C[VM C / 198.51.100.x] --> br0[br0 Bridge]
    Host --> br0
    br0 --> enp3s0

4. dconf 配置:消除首次连接延迟#

预设 URI/autoconnect 可以减少手动连接步骤,但不能保证消除所有启动延迟。NixOS dconf profile、Home Manager dconf.settings 和 GUI 用户状态的优先级不同;只设默认值时不要意外锁定。下面 lockAll 若启用会限制用户改写,应按需要选择。

部分用户在首次启动 virt-manager 时会遇到界面卡在 "Connecting..." 转圈的情况。原因是 virt-manager 没有预设的 libvirt 连接 URI,需要手动输入 qemu:///system 才能连接。

通过 dconf 预先写入 autoconnect 配置可解决此问题:

programs.dconf = {
  enable = true;
  profiles.user.databases = [
    {
      lockAll = false; # 这里只提供默认值;需要管理锁定时另行决定
      settings = with lib.gvariant; {
        "org/virt-manager/virt-manager/connections" = {
          autoconnect = [ "qemu:///system" ];
          uris = [ "qemu:///system" ];
        };
      };
    }
  ];
};

programs.dconf.profiles 是 NixOS 系统模块接口;Home Manager 使用 dconf.settings 写入用户配置。两者作用域和优先级不同,不应将系统 profile 称为 Home Manager 风格。只有实际遇到连接配置问题才预设相关项,不能仅凭 Wayland 或打包方式推定故障。Home Manager 示例:

# Home Manager 版
dconf.settings = {
  "org/virt-manager/virt-manager/connections" = {
    autoconnect = [ "qemu:///system" ];
    uris = [ "qemu:///system" ];
  };
};

5. AMD GPU 直通与显示方案#

5.1 宿主能力清单

配置依赖设备能力而非某台机器的型号。先记录 CPU 虚拟化、固件 AMD-Vi/IOMMU、内核 driver、IOMMU group、可独立复位的 GPU/音频功能,以及替代显示或带外管理路径。

能力 检查方向
CPU AMD-V/SVM;嵌套环境还需宿主开放
GPU 目标设备功能与 driver、ROM、reset 支持
内核驱动 常见 AMD 显示驱动为 amdgpu
IOMMU AMD-Vi、interrupt remapping、实际 group 隔离
恢复 独立显示/控制路径及正常启动 generation

5.2 IOMMU 与 VFIO 基础配置

boot.kernelParams = [
  "iommu.strict=1"
  "iommu=pt"
  # vfio-pci.ids 按需添加,格式:vendor:device
  # 示例占位符:VGA 与相关音频功能须从实际设备清单确认:
  # "vfio-pci.ids=<VENDOR_DEVICE_ID>,<VENDOR_DEVICE_ID>"
];

# 提前加载 VFIO 模块(在显卡驱动之前抢占设备)
boot.initrd.kernelModules = [
  "vfio_pci"
  "vfio"
  "vfio_iommu_type1"
];

获取 PCI 设备的 vendor:device ID:

lspci -nn | grep -i vga    # 显卡
lspci -nn | grep -i audio  # 声卡(通常需要一起直通)

使用 <GPU_VENDOR_DEVICE_ID>、<GPU_AUDIO_VENDOR_DEVICE_ID> 占位符;不能复制别人的 ID。ID 匹配可能包含多块同型号设备,必要时按 BDF 与 driver_override 设计绑定。

5.3 AMD GPU Reset Bug 与 vendor-reset

部分 AMD GPU(尤其是 dGPU,iGPU 较少遇到)存在 "reset bug":VM 关闭后 GPU 无法被干净重置,导致第二次启动 VM 时报错或死机。

仅对受支持设备考虑的方案:先核对 vendor-reset 的支持列表与内核兼容,再决定加载:

boot.extraModulePackages = with config.boot.kernelPackages; [ vendor-reset ];
boot.kernelModules = [ "kvm-amd" "vendor-reset" ];

vendor-reset 只支持特定设备及内核组合。出现 reset failure 时先核对项目支持列表,不应将“较新 GPU”视为自动支持,也不能保证模块解决 reset。

5.4 单 GPU 直通(Specialisation 方案)

本节是需要单独验收的引导配置示意。绑定唯一 GPU 或黑名单 amdgpu 可能让宿主失去全部本地显示;提前准备已验证的恢复 generation 与独立管理路径。video=efifb:off 不保证覆盖现代 simpledrm/固件 framebuffer;设备 reset 与宿主重新绑定也必须测试。specialisation 合并语义与实际 kernelParams 要在构建前检查。

如果你希望将 iGPU 直通给 Windows VM 玩游戏,而宿主机 NixOS 在直通期间失去显示,可以使用 NixOS 的 specialisation 功能创建一个"VFIO 模式"引导项:

specialisation.VFIO.configuration = {
  system.nixos.tags = [ "VFIO" ];
  boot.kernelParams = [
    "iommu.strict=1"
    # "iommu=pt"  # 仅在明确评估宿主 DMA 隔离后启用
    "vfio-pci.ids=<VENDOR_DEVICE_ID>,<VENDOR_DEVICE_ID>"  # 替换为实际 ID
    "video=efifb:off"                     # 禁用 EFI 帧缓冲
  ];
  boot.initrd.kernelModules = [ "vfio_pci" "vfio" "vfio_iommu_type1" ];
  boot.blacklistedKernelModules = [ "amdgpu" ];
};

重启后在 systemd-boot 菜单中会多出一个 "VFIO" 选项,选择它即可进入 VFIO 直通模式。常规 generation 可作为恢复选择,但磁盘/固件外部状态不会随 generation 自动回滚。

5.5 virtio-gpu + virgl(3D 加速替代方案)

如果不做完整 GPU 直通,但仍需要 Linux 客户机的 3D 加速,可以使用 virtio-gpu 配合 virgl(OpenGL);Vulkan/Venus 是需另外验证的版本与配置路径:

在 virt-manager 中为 VM 的 Video 设备选择:

  • Model: Virtio
  • 勾选 3D acceleration

或者直接在域 XML 中:

<video>
  <model type="virtio" heads="1" primary="yes">
    <acceleration accel3d="yes"/>
  </model>
</video>

virtio-gpu/virgl 与 QXL/SPICE 对应不同显示路径;适用性和性能按客体桌面、驱动、GL支持与实际负载验证。


6. NixOS 常见问题与排障#

6.1 问题速查表

问题 症状 解决方案
用户组未生效 "polkit agent unavailable" / "permission denied connecting to libvirtd" 确认 extraGroups = ["libvirtd"],然后注销重新登录(不是 newgrp)
default 网络未激活 virsh net-list 输出为空 / "network 'default' is not active" 添加 3.1 节 的 systemd oneshot 服务
virt-manager 卡在连接 界面持续显示 "Connecting..." 转圈 添加 第 4 节 的 dconf autoconnect 配置
OVMF 固件找不到 "Unable to complete install: 'Cannot access firmware'" 确保 libvirtd.enable = true(OVMF 会自动包含);旧 VM 需删除 <loader> 和 <nvram> 标签
QEMU 图形性能差 VM 界面卡顿、鼠标漂移 lsmod | grep kvm 确认 kvm_amd 已加载;VM 内使用 virtio 驱动;Wayland 上尝试 GDK_BACKEND=x11 virt-manager
嵌套虚拟化不可用 VM 内 lsmod | grep kvm 无输出 添加 boot.extraModprobeConfig = "options kvm_amd nested=1" 并重启
KVM 模块未加载 modprobe: ERROR: could not insert 'kvm_amd' 确认 BIOS/UEFI 中 SVM Mode 已启用(AMD)/ VT-x 已启用(Intel)
SPICE 在 Wayland 下渲染异常 窗口闪烁、输入延迟 尝试 GDK_BACKEND=x11 virt-manager;或在 virt-manager 中为 VM 切换为 QXL Video + X11 客户机
USB 重定向不工作 SPICE 菜单中无 USB 设备 确认 virtualisation.spiceUSBRedirection.enable = true;确认用户已安装 spice-gtk
VM 关机后 GPU 无法释放 第二次启动 GPU 直通 VM 报错 核对 reset 日志、设备支持和 vendor-reset 适用性(5.3 节)

6.2 诊断命令速查

# KVM 模块状态
lsmod | grep kvm

# IOMMU 是否启用
dmesg | grep -i "iommu\|amd-vi"

# IOMMU 分组(验证设备隔离)
find /sys/kernel/iommu_groups/ -type l | sort -V

# libvirtd 状态
systemctl status libvirtd

# 网络状态
virsh net-list --all

# VFIO 设备是否被正确绑定
dmesg | grep vfio

# AMD-V / SVM 是否可用
grep -o 'svm' /proc/cpuinfo | head -1

6.3 Wayland 宿主机的特殊注意事项

Wayland 宿主上运行 virt-manager 和 SPICE 客户端时,应按 compositor、客户端、guest agent 与客体桌面的版本分别验证:

  • virt-manager 启动:如果 GTK 主题缺失导致启动失败,通过 Home Manager 设置指针主题:
    home.pointerCursor = {
      gtk.enable = true;
      package = pkgs.vanilla-dmz;
      name = "Vanilla-DMZ";
    };
  • SPICE 客户端性能:先区分客户端渲染、客体驱动和远程带宽问题;virtio-gpu/virgl 可供本地受支持组合评估,SPICE GL 通常要求本地 Unix socket
  • 剪贴板共享:集成取决于客体 agent 与 compositor 的协议支持;分别测试宿主和客体会话,不能将所有 Wayland 组合一概视为不支持,也不能保证切换 X11 就解决全部问题

7. 与 NixOS VM 构建系统的关系#

本节讨论两个互补但不重叠的 NixOS 虚拟化能力:

维度 KVM 宿主机(本文档) NixOS VM 构建
角色 运行通用 VM(Linux/Windows/macOS)的 hypervisor 使用 Nix 表达式声明式构建 NixOS VM 镜像
配置方式 virtualisation.libvirtd.* + virt-manager / virsh nixos-build-vm 或 nix-build '<nixpkgs/nixos>' -A vm
典型用途 跑 Windows 游戏 / 测试其他发行版 / 多租户隔离 CI 测试 NixOS 配置、构建云镜像、快速验证 nix 表达式
产物 磁盘镜像 (qcow2/raw) + libvirt domain XML 可直接启动的 result/bin/run-*-vm 脚本

互补关系 你可以在本文档搭建的 KVM 宿主机上运行通过 nixos-build-vm 构建的 NixOS VM 镜像。两者使用相同的 qemu_kvm 后端,只是管理和构建方式不同。详细内容参见 NixOS 虚拟化与部署 MOC。

7.1 快速示例:在 KVM 宿主机上启动 NixOS 构建 VM

# 在任意 NixOS 配置目录下构建 VM
nixos-build-vm

# 构建产物是一个可执行脚本,直接运行
./result/bin/run-nixos-vm

此命令会启动一个 QEMU VM,使用你当前的 NixOS 配置,自动分配网络和显示输出。


8. 推荐阅读路径#

  1. KVM 专题内:

  2. 跨专题链接:

    • NixOS 虚拟化与部署 MOC — NixOS 作为 VM 客户端的构建与部署


参考与延伸

  1. nixpkgs libvirtd 模块接口快照 ↗
  2. NixOS 手册 ↗
  3. NixVirt ↗
  4. libvirt firmware auto-selection ↗
  5. vendor-reset 支持范围 ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

KVM 高级能力与平台选择

  1. 01KVM 的固件、VFIO、virtiofs 与镜像链
  2. 02在 NixOS 声明 KVM 宿主能力
  3. 03KVM 管理工具与自动化入口选型
CONTINUE READING / 下一篇KVM 管理工具与自动化入口选型

比较桌面、Web 和自动化工具,并区分 libvirt 前端、独立平台与离线镜像构建。