在多显卡或双显卡(如核显+独显)的场景下,显卡直通(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替换为你实际创建的虚拟机的精确名称(例如win10或arch-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
当虚拟机启动时,该脚本需要执行以下操作:
- 暂停/关闭显示管理器(Display Manager)。
- 解绑控制台终端(VT consoles),防止控制台占用显卡。
- 临时解绑 EFI Framebuffer。
- 卸载显卡驱动(Nvidia/AMD/Intel)。
- 将显卡(包括其 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 中配置虚拟机时,还需要确保几项硬性要求:
- 添加显卡 PCI 设备:在虚拟机硬件配置中,添加你的显卡 PCI 设备(VGA 和 Audio 两个设备都要添加)。
- 移除虚拟显示设备:移除默认的
Spice 服务器、QXL 显卡以及Tablet等虚拟外设,否则可能导致虚拟机识别不到物理显卡输出。 - 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>
调试与排错指南
如果启动虚拟机后,屏幕黑屏且宿主机未正常拉起虚拟机:
查看 Hook 运行日志:
利用另一台设备通过 SSH 连入宿主机,运行以下命令查看脚本执行细节:cat /var/log/libvirt/custom_hooks.log日志中会清晰显示哪一步
modprobe -r或者virsh nodedev-detach报错。显卡驱动无法卸载(Module is in use):
如果日志显示nvidia is in use by...,说明还有进程在强行使用 GPU 运行。- 可以尝试在
start.sh中卸载驱动前,加入以下命令强制杀死占用 GPU 的进程:fuser -v /dev/nvidia* -k
- 可以尝试在
手动恢复系统:
如果 VM 启动失败导致桌面丢失,直接在 SSH 终端中手动运行恢复脚本:sudo /etc/libvirt/hooks/qemu.d/YOUR_VM_NAME/release/end/revert.sh你的宿主机桌面环境应该会重新亮起。