HOOOS

保姆级教程:单显卡(Single GPU)如何通过 Libvirt Hook 完美直通 KVM 虚拟机

0 15 VFIO折腾家 显卡直通KVMLibvirt
Apple

在多显卡或双显卡(如核显+独显)的场景下,显卡直通(GPU Passthrough)相对简单。但在**单显卡(Single GPU)**的宿主机上,直通意味着在 VM 启动时,宿主机必须动态地释放唯一的显卡,将其绑定给 VFIO 驱动;在 VM 关闭时,再将显卡还给宿主机,并重新拉起桌面环境。

这一过程如果纯手动操作极易导致系统崩溃。通过 Libvirt Hook 脚本,我们可以实现这一系列操作的自动化。


⚠️ 运行前的绝对红线:准备 SSH 调试环境

在配置和测试单显卡直通时,你的屏幕极大概率会黑屏。如果在没有准备好后路的情况下进行调试,一旦脚本出错,你将无法操作宿主机,只能强行关机,甚至可能导致系统文件损坏。

  • 必须准备第二台设备(手机、平板、另一台电脑)
  • 宿主机务必开启 sshd 服务,并在测试前确保能够通过 SSH 成功登录宿主机命令行。
  • 一旦发生黑屏或卡死,通过 SSH 登录宿主机,手动运行恢复脚本,或者安全重启。

第一步:理解 Libvirt Hook 的工作原理

Libvirt 允许在虚拟机的不同生命周期(如启动前、启动后、停止后、销毁后)自动执行宿主机上的特定脚本。

对于 QEMU/KVM 虚拟机,Libvirt 默认会寻找 /etc/libvirt/hooks/qemu 这个主入口脚本。我们通过在该脚本中编写分流逻辑,使其在虚拟机启动和关闭时,分别调用外部的 start.sh(解绑)和 revert.sh(绑定)脚本。


第二步:创建 Hook 目录结构

首先,在宿主机上建立标准的 Libvirt Hook 目录结构:

sudo mkdir -p /etc/libvirt/hooks/qemu.d/YOUR_VM_NAME/prepare/begin
sudo mkdir -p /etc/libvirt/hooks/qemu.d/YOUR_VM_NAME/release/end

⚠️ 注意:请将 YOUR_VM_NAME 替换为你实际创建的虚拟机的精确名称(例如 win10arch-vm)。


第三步:编写主分流脚本 /etc/libvirt/hooks/qemu

这个脚本是 Libvirt 调用的主入口。它会捕获传入的参数,并根据虚拟机的状态,自动去对应的目录下寻找并执行子脚本。

创建并编辑文件:

sudo nano /etc/libvirt/hooks/qemu

写入以下内容:

#!/bin/bash

# 获取虚拟机名称、操作类型及子操作
VM_NAME="$1"
ACTION="$2"
PHASE="$3"

# 拼接子脚本存放的路径
SYS_HOOK_DIR="/etc/libvirt/hooks/qemu.d/${VM_NAME}/${ACTION}/${PHASE}"

# 如果对应目录下存在可执行脚本,则循环执行
if [ -d "$SYS_HOOK_DIR" ]; then
    for file in $(find "$SYS_HOOK_DIR" -maxdepth 1 -type f -executable | sort); do
        "$file" "$@"
    done
fi

赋予其执行权限:

sudo chmod +x /etc/libvirt/hooks/qemu

第四步:编写启动解绑脚本 start.sh

当虚拟机启动时,该脚本需要执行以下操作:

  1. 暂停/关闭显示管理器(Display Manager)。
  2. 解绑控制台终端(VT consoles),防止控制台占用显卡。
  3. 临时解绑 EFI Framebuffer。
  4. 卸载显卡驱动(Nvidia/AMD/Intel)。
  5. 将显卡(包括其 HDMI 音频设备)绑定到 vfio-pci 驱动上。

创建并编辑文件:

sudo nano /etc/libvirt/hooks/qemu.d/YOUR_VM_NAME/prepare/begin/start.sh

写入以下模板(请根据注释修改你的 PCI 地址和显卡类型):

#!/bin/bash
# 调试模式:记录运行日志到 /var/log/libvirt/custom_hooks.log
exec 1>>/var/log/libvirt/custom_hooks.log 2>&1
set -x

# 1. 变量定义(根据你的实际情况修改!)
# 使用 lspci -nn | grep -i vga 查看你的显卡 PCI 地址
GPU_PCI="0000:01:00.0"
GPU_AUDIO_PCI="0000:01:00.1" 

# 2. 停止显示管理器(根据你的桌面环境选择其一,解开注释)
# systemctl stop gdm        # Gnome
# systemctl stop sddm       # KDE Plasma
# systemctl stop lightdm    # XFCE/Cinnamon
systemctl stop display-manager.service

# 3. 避免控制台死锁
echo 0 > /sys/class/vtconsole/vtcon0/bind
echo 0 > /sys/class/vtconsole/vtcon1/bind

# 4. 解绑 EFI Framebuffer
echo efi-framebuffer.0 > /sys/bus/platform/drivers/efi-framebuffer/unbind

# 防止资源竞争,稍微等待
sleep 2

# 5. 卸载显卡驱动(以 NVIDIA 为例,如果是 AMD 请替换为 amdgpu)
# 注意:如果加载了其他相关模块,需要一并卸载
modprobe -r nvidia_drm
modprobe -r nvidia_modeset
modprobe -r nvidia_uvm
modprobe -r nvidia
# 如果是 AMD 显卡,请使用:
# modprobe -r amdgpu

# 6. 将 GPU 绑定至 vfio-pci
virsh nodedev-detach pci_${GPU_PCI//:_}
virsh nodedev-detach pci_${GPU_AUDIO_PCI//:_}

赋予其执行权限:

sudo chmod +x /etc/libvirt/hooks/qemu.d/YOUR_VM_NAME/prepare/begin/start.sh

第五步:编写释放恢复脚本 revert.sh

当虚拟机关闭后,该脚本需要反向执行操作,将显卡还给宿主机,并重新拉起桌面,让你无缝回到 Linux 桌面环境。

创建并编辑文件:

sudo nano /etc/libvirt/hooks/qemu.d/YOUR_VM_NAME/release/end/revert.sh

写入以下内容:

#!/bin/bash
# 调试模式:记录运行日志
exec 1>>/var/log/libvirt/custom_hooks.log 2>&1
set -x

# 1. 变量定义
GPU_PCI="0000:01:00.0"
GPU_AUDIO_PCI="0000:01:00.1"

# 2. 将 GPU 从 vfio-pci 重新绑定回宿主机驱动
virsh nodedev-reattach pci_${GPU_PCI//:_}
virsh nodedev-reattach pci_${GPU_AUDIO_PCI//:_}

# 3. 重新加载显卡驱动(以 NVIDIA 为例)
modprobe nvidia
modprobe nvidia_uvm
modprobe nvidia_modeset
modprobe nvidia_drm
# 如果是 AMD 显卡,请使用:
# modprobe amdgpu

# 4. 重新绑定控制台与 EFI Framebuffer
echo 1 > /sys/class/vtconsole/vtcon0/bind
echo 1 > /sys/class/vtconsole/vtcon1/bind
echo "efi-framebuffer.0" > /sys/bus/platform/drivers/efi-framebuffer/bind

# 5. 重启显示管理器
systemctl start display-manager.service

赋予其执行权限:

sudo chmod +x /etc/libvirt/hooks/qemu.d/YOUR_VM_NAME/release/end/revert.sh

第六步:重启 Libvirt 服务以应用 Hook

由于 Libvirt 只在启动时加载一次 Hook 脚本,因此每次修改 /etc/libvirt/hooks/ 目录结构后,都必须重启 libvirtd 服务:

sudo systemctl restart libvirtd

第七步:虚拟机 XML 配置(直通关键步骤)

在 Virt-Manager 中配置虚拟机时,还需要确保几项硬性要求:

  1. 添加显卡 PCI 设备:在虚拟机硬件配置中,添加你的显卡 PCI 设备(VGA 和 Audio 两个设备都要添加)。
  2. 移除虚拟显示设备:移除默认的 Spice 服务器QXL 显卡 以及 Tablet 等虚拟外设,否则可能导致虚拟机识别不到物理显卡输出。
  3. ROM 镜像(VBIOS)注入
    单显卡直通通常需要向虚拟机提供该显卡的 VBIOS 镜像。如果不提供,启动时可能直接黑屏。
    • 你可以从 TechPowerUp 下载对应显卡的 ROM 文件(或者通过 nvflash 提取)。
    • 使用 virsh edit YOUR_VM_NAME 编辑虚拟机 XML。
    • <hostdev> 显卡设备的 <address> 标签上方加入 <rom file='/var/lib/libvirt/roms/your_gpu.rom'/>。例如:
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <source>
        <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
      </source>
      <rom file='/var/lib/libvirt/roms/patched_gpu.rom'/>
      <address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x0'/>
    </hostdev>
    

调试与排错指南

如果启动虚拟机后,屏幕黑屏且宿主机未正常拉起虚拟机:

  1. 查看 Hook 运行日志
    利用另一台设备通过 SSH 连入宿主机,运行以下命令查看脚本执行细节:

    cat /var/log/libvirt/custom_hooks.log
    

    日志中会清晰显示哪一步 modprobe -r 或者 virsh nodedev-detach 报错。

  2. 显卡驱动无法卸载(Module is in use)
    如果日志显示 nvidia is in use by...,说明还有进程在强行使用 GPU 运行。

    • 可以尝试在 start.sh 中卸载驱动前,加入以下命令强制杀死占用 GPU 的进程:
      fuser -v /dev/nvidia* -k
      
  3. 手动恢复系统
    如果 VM 启动失败导致桌面丢失,直接在 SSH 终端中手动运行恢复脚本:

    sudo /etc/libvirt/hooks/qemu.d/YOUR_VM_NAME/release/end/revert.sh
    

    你的宿主机桌面环境应该会重新亮起。

点评评价

captcha
健康