一块 RTX 4090 分时复用:KVM 虚拟机 GPU 直通实战
作者: 杨显钊
版本: 1.0
日期: 2026年10月27日
标签: KVM、GPU直通、VFIO、IOMMU、libvirt、NVIDIA、虚拟化
适合人群: 虚拟化运维、AI 环境搭建、Linux 系统工程师
摘要
本文记录一台安装 Kylin V11 的物理机,把机器上唯一的显示设备 NVIDIA RTX 4090 以 PCI 直通(VFIO)方式交给 KVM 虚拟机,并实现宿主机与虚拟机分时复用的完整过程。
具体做了四件事:宿主机侧开启 IOMMU 并确认显卡独占一个 IOMMU 组;不写死 vfio.conf,改用 libvirt 的 managed='yes' 让显卡驱动在 nvidia 与 vfio-pci 之间自动切换;在无外网的 CentOS 8 客户机里离线装上 580.95.05 驱动;写一个 gpu-switch 脚本,一条命令在"归宿主机"和"归虚拟机"之间切换。
最终效果:客户机里 nvidia-smi 认出 RTX 4090 / 24564MiB,CUDA 计算可用;要收回宿主机时关掉虚机、恢复桌面即可。
一、场景:一台机器一块 4090,两边都想用
需求很朴素,但卡得很死:
- 宿主机是办公/开发用的桌面系统,只有这一块 RTX 4090,图形界面本身就跑在它上面;
- 同时又要给 KVM 虚拟机里的 CentOS 8 提供 GPU 算力(跑深度学习环境);
- 同一块卡,同一时刻只能归一边。
为什么不用 vGPU、不用其他共享方案:
| 方案 | 为什么没选 |
|---|---|
| NVIDIA vGPU | 面向数据中心卡授权,GeForce 消费级卡拿不到 |
| 远程调用宿主机的 CUDA | 环境要做到客户机里,宿主机要能被随便折腾,隔离性不够 |
| 软件渲染 / 虚拟显卡 | 拿不到真正的 CUDA 算力,等于没做 |
所以走 PCI 直通:把整块显卡从宿主机"拔"下来交给虚拟机,用完再"插"回去。目标三个:
- 客户机里
nvidia-smi能正常输出,并能跑 CUDA 程序; - 宿主机桌面在不用 GPU 的时候不受影响;
- 切换动作要短、要可控,不能每次手敲一堆命令。
二、动手前先理清三个概念
这三个词网上到处都是,但真动手前最好先把它们的关系理顺,不然报错时不知道自己在哪一层。
2.1 IOMMU:直通的硬件前提
CPU 的 I/O 内存管理单元。它的作用是把设备发起的 DMA 地址做一层翻译,让"设备只能访问被授权的那块内存"。
对直通来说它是硬门槛:没有 IOMMU,虚拟机里的显卡驱动发起 DMA 时,用的是客户机的物理地址,落到真实内存上就是乱写,轻则花屏重则整机崩。开了 IOMMU,虚机才能安全地把物理设备当自己的用。
2.2 VFIO:把设备"让"出去的内核框架
VFIO(Virtual Function I/O)是内核里负责"把某个 PCI 设备从它的原生驱动上摘下来,交给用户态程序(也就是 QEMU)直接管"的框架。它由几个模块配合:
vfio:框架本体vfio_iommu_type1:把 IOMMU 的分组、DMA 映射能力暴露出来vfio_pci:PCI 设备的实现
只要设备被绑到 vfio-pci 上,它的原生驱动就碰不到它了------这就是"显卡归虚拟机"的技术状态。
2.3 显卡在宿主机上只有"一副面孔"
一块物理显卡,在宿主机上要么绑定 nvidia(宿主机自己用),要么绑定 vfio-pci(交给虚机)。不存在"两边都能用"。
这也解释了后面所有的切换动作:切换的本质就是重新绑定驱动,外加保证没有进程还攥着 /dev/nvidia* 不放。
#mermaid-svg-p9DQJ5MDWfXh4jnY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-p9DQJ5MDWfXh4jnY .error-icon{fill:#552222;}#mermaid-svg-p9DQJ5MDWfXh4jnY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-p9DQJ5MDWfXh4jnY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .marker.cross{stroke:#333333;}#mermaid-svg-p9DQJ5MDWfXh4jnY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-p9DQJ5MDWfXh4jnY p{margin:0;}#mermaid-svg-p9DQJ5MDWfXh4jnY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster-label text{fill:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster-label span{color:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster-label span p{background-color:transparent;}#mermaid-svg-p9DQJ5MDWfXh4jnY .label text,#mermaid-svg-p9DQJ5MDWfXh4jnY span{fill:#333;color:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .node rect,#mermaid-svg-p9DQJ5MDWfXh4jnY .node circle,#mermaid-svg-p9DQJ5MDWfXh4jnY .node ellipse,#mermaid-svg-p9DQJ5MDWfXh4jnY .node polygon,#mermaid-svg-p9DQJ5MDWfXh4jnY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .rough-node .label text,#mermaid-svg-p9DQJ5MDWfXh4jnY .node .label text,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape .label,#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape .label{text-anchor:middle;}#mermaid-svg-p9DQJ5MDWfXh4jnY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .rough-node .label,#mermaid-svg-p9DQJ5MDWfXh4jnY .node .label,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape .label,#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape .label{text-align:center;}#mermaid-svg-p9DQJ5MDWfXh4jnY .node.clickable{cursor:pointer;}#mermaid-svg-p9DQJ5MDWfXh4jnY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .arrowheadPath{fill:#333333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-p9DQJ5MDWfXh4jnY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p9DQJ5MDWfXh4jnY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster text{fill:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster span{color:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-p9DQJ5MDWfXh4jnY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY rect.text{fill:none;stroke-width:0;}#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape p,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape .label rect,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p9DQJ5MDWfXh4jnY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-p9DQJ5MDWfXh4jnY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-p9DQJ5MDWfXh4jnY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 同一时刻只能归一边
libvirt managed=yes 自动切换
物理机:Kylin V11
RTX 4090
01:00.0 显示 + 01:00.1 音频
绑定 nvidia 驱动
绑定 vfio-pci
宿主机桌面 / CUDA 任务
客户机内装 nvidia 驱动
07:00.0 → CUDA
三、宿主机侧准备
3.1 打开 IOMMU
先看当前内核参数里有没有开启:
bash
# cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.6.0-32.24... root=/dev/mapper/... ro resume=... rd.lvm.lv=... \
cgroup_disable=files apparmor=0 crashkernel=512M audit=0 console=ttyS0,115200 \
intel_iommu=on iommu=pt
关注最后两个:
intel_iommu=on:Intel 平台开启 IOMMU(AMD 平台写amd_iommu=on)iommu=pt:Pass-Through 模式,只对需要直通的设备做地址翻译,没直通的设备保持原样,能少掉一部分性能损耗
持久化写在 /etc/default/grub 里:
bash
# grep GRUB_CMDLINE_LINUX /etc/default/grub
GRUB_CMDLINE_LINUX="... console=ttyS0,115200 intel_iommu=on iommu=pt"
改完执行 grub2-mkconfig -o /boot/grub2/grub.cfg 再重启。验证是否真的开了:
bash
# dmesg | grep -i iommu | head -3
[ 0.005074] ACPI: DMAR 0x0000000078459000 000050 (v01 INTEL EDK2 00000002 01000013)
[ 0.012395] DMAR: IOMMU enabled
[ 1.913192] DMAR: Host address width 39
看到 DMAR: IOMMU enabled 就说明硬件和内核都准备好了。
3.2 显卡必须独占一个 IOMMU 组
这是能不能直通的决定性一步。IOMMU 是以"组"为单位隔离的,如果显卡所在的组里还混着别的设备(比如某块网卡、某个桥),那要么整组都直通,要么就得靠 ACS 补丁把组拆开------后者又依赖主板 BIOS 是否支持。
先看显卡本身:
bash
# lspci -nn | grep -i nvidia
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation AD102 [GeForce RTX 4090] [10de:2684] (rev a1)
01:00.1 Audio device [0403]: NVIDIA Corporation AD102 High Definition Audio Controller [10de:22ba] (rev a1)
一块显卡在 PCI 上通常是两个功能:显示功能(01:00.0)和 HDMI/DP 音频功能(01:00.1)。
再看它所在的组里有哪些设备:
bash
# IOMMU group = 13
# ls /sys/kernel/iommu_groups/13/devices/
0000:01:00.0
0000:01:00.1
组 13 里干干净净,就是显卡自己的两个功能------可以直接直通。
3.3 屏蔽 nouveau
开源的 nouveau 驱动会抢先认领 NVIDIA 显卡,必须提前屏蔽:
bash
# cat /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
options nouveau modeset=0
宿主机和客户机两边都要做(客户机后面在安装脚本里一并处理)。
四、把显卡交给虚拟机
4.1 没有写死 vfio-pci:这次交给 libvirt 托管
网上常见的做法是提前把设备 ID 写进 /etc/modprobe.d/vfio.conf:
text
options vfio-pci ids=10de:2684,10de:22ba
这样做等于开机就把显卡从宿主机手里拿走,宿主机再也用不了它------只适合"显卡永久归虚机"的场景。
本次要的是分时复用,所以反过来:不写 vfio 绑定,让 libvirt 在启动/关闭虚机时动态切换 。这台宿主机上 /etc/modprobe.d/ 里确实没有任何 vfio 配置,靠的是 XML 里 managed='yes' 这一个属性。
libvirt 的托管逻辑是:
- 虚机启动 → 若设备当前挂在原生驱动上,先解绑,再绑到
vfio-pci,然后交给 QEMU; - 虚机关闭 → 解绑
vfio-pci,重新绑定回原来的驱动(这里是nvidia)。
虚机运行时的实际状态:
bash
# lspci -k -s 01:00.0
01:00.0 VGA compatible controller: NVIDIA Corporation AD102 [GeForce RTX 4090] (rev a1)
Kernel driver in use: vfio-pci
Kernel modules: nouveau, nvidia_drm, nvidia
Kernel driver in use: vfio-pci,说明宿主机的驱动已经松手了。
4.2 XML 里的两段 hostdev
virsh edit centos8,在 <devices> 段里加两个主机设备(用 virt-manager 的"添加硬件 → PCI 主机设备"效果一样),实际内容如下:
xml
<hostdev mode='subsystem' type='pci' managed='yes'>
<driver name='vfio'/>
<source>
<address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
</source>
<alias name='hostdev0'/>
<address type='pci' domain='0x0000' bus='0x07' slot='0x00' function='0x0'/>
</hostdev>
<hostdev mode='subsystem' type='pci' managed='yes'>
<driver name='vfio'/>
<source>
<address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
</source>
<alias name='hostdev1'/>
<address type='pci' domain='0x0000' bus='0x08' slot='0x00' function='0x0'/>
</hostdev>
几个字段:
| 字段 | 含义 |
|---|---|
mode='subsystem' type='pci' |
直通的是 PCI 设备 |
managed='yes' |
驱动绑定/解绑由 libvirt 自动完成(分时复用的关键) |
<driver name='vfio'/> |
用 vfio 后端 |
<source> |
宿主机上的 PCI 地址:01:00.0 和 01:00.1 |
最后一个 <address> |
该设备在客户机里 的 PCI 地址:07:00.0 和 08:00.0 |
4.3 三个容易踩的点
① 音频功能要一起直通。 只给 01:00.0、不给 01:00.1,客户机里会看到一个"残缺"的显卡设备,容易出现驱动报错、设备唤醒异常之类的怪问题。反正它们在同一个 IOMMU 组里,本来就是绑在一起的。
② 显卡是虚机里的"第二块显卡",主显示仍然走 virtio。 这台虚机的显示配置是:
xml
<video>
<model type='virtio' heads='1' primary='yes'/>
</video>
<graphics type='vnc' port='5900' autoport='yes' listen='127.0.0.1'/>
主显示是 virtio + VNC,4090 只是个从设备。这样一来有两个好处:
- 虚机不需要从 4090 上引导,不用给显卡刷 vBIOS、不用指定
<rom file>、也不用-vga none之类的 QEMU 参数 (这台虚机的 XML 里也没有<qemu:commandline>段); - 装驱动时就算 X 起不来,VNC 照样能连上,不会把自己关在门外。
③ 客户机里的 PCI 地址和宿主机不一样,这是正常的。
#mermaid-svg-GBm9Cv3cojkLXOG2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GBm9Cv3cojkLXOG2 .error-icon{fill:#552222;}#mermaid-svg-GBm9Cv3cojkLXOG2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GBm9Cv3cojkLXOG2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .marker.cross{stroke:#333333;}#mermaid-svg-GBm9Cv3cojkLXOG2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GBm9Cv3cojkLXOG2 p{margin:0;}#mermaid-svg-GBm9Cv3cojkLXOG2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster-label text{fill:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster-label span{color:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster-label span p{background-color:transparent;}#mermaid-svg-GBm9Cv3cojkLXOG2 .label text,#mermaid-svg-GBm9Cv3cojkLXOG2 span{fill:#333;color:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .node rect,#mermaid-svg-GBm9Cv3cojkLXOG2 .node circle,#mermaid-svg-GBm9Cv3cojkLXOG2 .node ellipse,#mermaid-svg-GBm9Cv3cojkLXOG2 .node polygon,#mermaid-svg-GBm9Cv3cojkLXOG2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .rough-node .label text,#mermaid-svg-GBm9Cv3cojkLXOG2 .node .label text,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape .label,#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-GBm9Cv3cojkLXOG2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .rough-node .label,#mermaid-svg-GBm9Cv3cojkLXOG2 .node .label,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape .label,#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape .label{text-align:center;}#mermaid-svg-GBm9Cv3cojkLXOG2 .node.clickable{cursor:pointer;}#mermaid-svg-GBm9Cv3cojkLXOG2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .arrowheadPath{fill:#333333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GBm9Cv3cojkLXOG2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GBm9Cv3cojkLXOG2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster text{fill:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster span{color:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GBm9Cv3cojkLXOG2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape p,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape .label rect,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GBm9Cv3cojkLXOG2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GBm9Cv3cojkLXOG2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GBm9Cv3cojkLXOG2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} hostdev managed=yes
hostdev managed=yes
宿主机 0000:01:00.0
显示功能
客户机 0000:07:00.0
宿主机 0000:01:00.1
音频功能
客户机 0000:08:00.0
从宿主机 01:xx 变成客户机的 07:xx,只说明它在虚机自己的 PCI 拓扑里被挂到了另一条总线上,跟直通是否成功无关。客户机里认卡、装驱动,一律以 lspci 在客户机内部看到的地址为准。
五、客户机里装驱动(全程离线)
虚机里的系统是 CentOS Linux 8.2.2004(内核 4.18.0-193.el8.x86_64,SELinux Enforcing),而且没有外网,所以驱动和依赖都得本地解决。
5.1 先确认客户机认出了显卡
bash
# lspci -nn | grep -i nvidia
07:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2684] (rev a1)
08:00.0 Audio device [0403]: NVIDIA Corporation Device [10de:22ba] (rev a1)
# lspci -k -s 07:00.0
07:00.0 VGA compatible controller: NVIDIA Corporation Device 2684 (rev a1)
Kernel driver in use: nvidia
Kernel modules: nouveau, nvidia_drm, nvidia
(这里的 Kernel driver in use: nvidia 是驱动装好之后的复查,安装前这个字段是空的。只要前一条 lspci 能列出设备,就说明直通本身通了。)
5.2 把驱动包送进去
客户机没网,驱动包(NVIDIA-Linux-x86_64-580.95.05.run)需要人工送进去。这次的做法是做了一张临时数据 ISO,挂成虚机的光驱:
bash
# virsh domblklist centos8
目标 源
-------------------------------------------------
vda /var/lib/libvirt/images/centos8.qcow2
vdb /var/lib/libvirt/images/centos8-dvd.iso
vdc /var/lib/libvirt/images/gpu-stage.iso
gpu-stage.iso(379M,卷标 GPU-STAGE)里只放了两样东西:
text
install-gpu.sh
NVIDIA-Linux-x86_64-580.95.05.run
客户机里挂载:
bash
mkdir -p /mnt/gpustage
mount -L GPU-STAGE /mnt/gpustage
用 ISO 的好处是只读、不依赖网络、用完拔掉就行,比开临时共享目录省心。
5.3 依赖:内核头文件 + 编译工具链,也从本地装
驱动要现场编译内核模块,所以需要与当前运行内核完全一致 的 kernel-devel。虚机里另外挂了一张 CentOS 8 安装 DVD(centos8-dvd.iso),直接用 ISO 里的仓库当本地源。整理后的脚本如下:
bash
#!/bin/bash
# centos8 虚机内为「直通进来的 RTX 4090」安装 NVIDIA 驱动
# 全程离线:CentOS 8.2 安装 DVD 作本地 yum 源,驱动来自本 ISO
set -u
LOG=/var/log/nvidia-install.log
[ "$(id -u)" = 0 ] || { echo "请用 root 执行"; exit 1; }
# 图形终端里跑会被 systemctl isolate 杀掉 -> 用 systemd-run 换到独立 cgroup
if [ -n "${DISPLAY:-}" ] && [ -z "${INVOCATION_ID:-}" ] && [ "${1:-}" != "--bg" ]; then
systemd-run --unit=gpu-install --collect --description="NVIDIA driver install" \
bash "$0" --bg || exit 1
echo "已启动。看进度: tail -f $LOG ;完成后 reboot"
exit 0
fi
exec >>"$LOG" 2>&1
STAGE=/mnt/gpustage
mountpoint -q "$STAGE" || mount -L GPU-STAGE "$STAGE" 2>/dev/null
RUN=$(ls "$STAGE"/NVIDIA-Linux-x86_64-*.run 2>/dev/null | head -1)
[ -n "$RUN" ] || { echo "!! $STAGE 下找不到 NVIDIA-Linux-x86_64-*.run"; exit 1; }
echo "驱动包: $RUN"
# --- 0. 检查直通进来的 GPU ---
lspci -nn | grep -i nvidia || { echo "!! 没看到 NVIDIA PCI 设备,先确认虚机 XML 里有 hostdev"; }
# --- 1. 挂载 CentOS 安装 DVD 作本地 yum 源 ---
DVD=/mnt/dvd; mkdir -p "$DVD"
if ! mountpoint -q "$DVD"; then
while read -r dev; do
lab=$(blkid -o value -s LABEL "$dev" 2>/dev/null)
case "$lab" in CentOS*8*dvd*) mount -o ro "$dev" "$DVD" && break;; esac
done < <(ls /dev/sr* /dev/sd[a-z] /dev/vd[a-z] 2>/dev/null)
fi
cat > /etc/yum.repos.d/local-dvd.repo <<EOF
[dvd-baseos]
baseurl=file://$DVD/BaseOS
gpgcheck=0
enabled=1
[dvd-appstream]
baseurl=file://$DVD/AppStream
gpgcheck=0
enabled=1
EOF
for r in /etc/yum.repos.d/*.repo; do
[ "$r" = /etc/yum.repos.d/local-dvd.repo ] || sed -i 's/^enabled=1/enabled=0/' "$r"
done
# --- 2. 离线安装编译工具链 + 内核头文件 ---
dnf -y --disablerepo='*' --enablerepo='dvd-*' install gcc make binutils \
kernel-devel kernel-headers elfutils-libelf-devel pciutils 2>&1 | tail -8
# 头文件对齐当前运行内核
dnf -y --disablerepo='*' --enablerepo='dvd-*' install \
"kernel-devel-$(uname -r)" "kernel-headers-$(uname -r)" 2>&1 | tail -3
[ -e "/lib/modules/$(uname -r)/build" ] || { echo "内核头文件没装好"; exit 1; }
# --- 3. 屏蔽 nouveau ---
printf 'blacklist nouveau\noptions nouveau modeset=0\n' > /etc/modprobe.d/blacklist-nouveau.conf
modprobe -r nouveau 2>/dev/null
dracut -f --regenerate-all
# --- 4. 停图形界面(装驱动必须) ---
systemctl isolate multi-user.target 2>/dev/null || systemctl stop gdm 2>/dev/null
# --- 5. 安装 NVIDIA 驱动 ---
sh "$RUN" --silent --no-dkms --disable-nouveau --no-nouveau-check
装完的依赖可以复查:
bash
# rpm -q kernel-devel kernel-headers gcc make dkms
kernel-devel-4.18.0-193.el8.x86_64
kernel-headers-4.18.0-193.el8.x86_64
gcc-8.3.1-5.el8.0.2.x86_64
make-4.2.1-10.el8.x86_64
未安装软件包 dkms
第 4 步那个 systemctl isolate multi-user.target 有个隐藏问题:驱动安装是在图形终端里敲的,一停图形界面,自己也被停掉了 ,安装会半途夭折。脚本里用 systemd-run 把任务扔进独立的 systemd 单元(独立 cgroup),停图形界面时就杀不到它了。
脚本第 5 步写的是 --silent,最后落地那次用的是 --ui=none -q(下一节的日志里有记录)。两种写法目标一致:把安装器的交互界面关掉。
5.4 真正的安装命令与几个参数
安装器会把自己的命令行完整记进日志,最终成功那次是这样的:
bash
# sed -n '1,16p' /var/log/nvidia-installer.log
nvidia-installer log file '/var/log/nvidia-installer.log'
creation time: Mon Oct 26 23:24:10 2026
installer version: 580.95.05
nvidia-installer command line:
./nvidia-installer
--ui=none
-q
--no-dkms
--no-nouveau-check
--no-x-check
一条条说明:
| 参数 | 作用 | 为什么这次要用 |
|---|---|---|
--ui=none |
不用 ncurses 界面,改用内置的纯文本流界面 | 必须,见下面踩坑 |
-q |
安静模式,少打日志 | 日志太乱不好定位问题 |
--no-dkms |
不注册 DKMS | 这台的客户机内核版本固定,不需要 DKMS 自动重编 |
--no-nouveau-check |
跳过 nouveau 检查 | 前面已经手工屏蔽过了 |
--no-x-check |
安装期间不去检查/关闭 X | 虚机里 X 是开着的(VNC 桌面),不想被它干预 |
装完的收尾日志:
text
-> Installation of the NVIDIA Accelerated Graphics Driver for Linux-x86_64 (version: 580.95.05) is now complete.
然后 reboot。
5.5 验证
bash
# nvidia-smi
Tue Oct 27 03:22:41 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.95.05 Driver Version: 580.95.05 CUDA Version: 13.0 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 4090 Off | 00000000:07:00.0 Off | Off |
| 31% 57C P0 1W / 450W | 0MiB / 24564MiB | 0% Default |
+-----------------------------------------+------------------------+----------------------+
| Processes: |
| No running processes found |
+-----------------------------------------------------------------------------------------+
Bus-Id 是 00000000:07:00.0,正好对应 XML 里给它在客户机中分配的那个地址。驱动版本 580.95.05,与宿主机侧同版本。
设备节点和模块:
bash
# ls -l /dev/nvidia*
crw-rw-rw-. 1 root root 195, 0 10月 26 23:36 /dev/nvidia0
crw-rw-rw-. 1 root root 195, 255 10月 26 23:35 /dev/nvidiactl
crw-rw-rw-. 1 root root 195, 254 10月 26 23:36 /dev/nvidia-modeset
crw-rw-rw-. 1 root root 237, 0 10月 26 23:36 /dev/nvidia-uvm
crw-rw-rw-. 1 root root 237, 1 10月 26 23:36 /dev/nvidia-uvm-tools
# modinfo nvidia | grep -E '^(filename|version)'
filename: /lib/modules/4.18.0-193.el8.x86_64/kernel/drivers/video/nvidia.ko
version: 580.95.05
5.6 让 nvidia_uvm 开机自动加载
nvidia_uvm 是 CUDA 做统一内存管理要用的模块,不加载的话 CUDA 程序会在初始化阶段报错。写一个配置让它开机自动加载:
bash
# cat /etc/modules-load.d/nvidia.conf
nvidia
nvidia_uvm
六、分时切换:一条命令换边
6.1 切换脚本
需求就三件事:把 GPU 给虚机、把 GPU 收回宿主机、随时看当前归谁。脚本 /usr/local/bin/gpu-switch 全文如下:
bash
#!/bin/bash
# 4090 分时切换:宿主机 <-> 虚拟机 centos8
# gpu-switch vm 把 4090 交给虚机(停桌面 → 起虚机,libvirt 自动切驱动)
# gpu-switch vm -f 同上,但遇到占用进程直接强杀
# gpu-switch host 把 4090 收回宿主机(关虚机 → 回桌面)
# gpu-switch status 查看当前状态
set -u
VM=centos8
DEVS="0000:01:00.0 0000:01:00.1"
drv() { local d; d=$(readlink -f "/sys/bus/pci/devices/$1/driver" 2>/dev/null); [ -n "$d" ] && basename "$d" || echo "(未绑定)"; }
blk() { # 找占用 /dev/nvidia* 的进程
fuser -v /dev/nvidia0 /dev/nvidiactl /dev/nvidia-uvm 2>&1 | sed -n '2,$p'
}
case "${1:-}" in
vm)
echo "[1/4] 停图形界面,释放 4090 ..."
systemctl isolate multi-user.target 2>/dev/null
sleep 2
echo "[2/4] 检查残留占用 ..."
if pgrep -x -f "VLLM::EngineCore" >/dev/null 2>&1 || pgrep -x -f "vllm" >/dev/null 2>&1; then
echo " !! 仍有 vLLM 进程占用 GPU: $(pgrep -f 'VLLM::EngineCore|vllm' | tr '\n' ' ')"
if [ "${2:-}" = "-f" ]; then
echo " -f 已指定,强制结束 ..."
pkill -f "VLLM::EngineCore"; pkill -f "vllm"; sleep 3
else
echo " !! 请先手工停掉,或执行: $0 vm -f"; exit 1
fi
fi
h=$(blk); [ -n "$h" ] && { echo " !! 仍有进程持有 /dev/nvidia*:"; echo "$h"; exit 1; }
echo "[3/4] 启动虚机 $VM(libvirt 自动把 4090 切到 vfio-pci)..."
virsh start "$VM" || exit 1
echo "[4/4] 等待驱动切换 ..."; sleep 3
exec "$0" status
;;
host)
echo "[1/3] 关闭虚机 $VM ..."
[ "$(virsh domstate "$VM")" = "运行" ] && virsh shutdown "$VM"
for i in $(seq 1 90); do [ "$(virsh domstate "$VM" 2>/dev/null)" = "关闭" ] && break; sleep 2; done
echo " 虚机状态: $(virsh domstate "$VM")"
echo "[2/3] 4090 已交还宿主机 (nvidia 驱动)"
echo "[3/3] 恢复图形界面 ..."
systemctl isolate graphical.target 2>/dev/null
sleep 3
nvidia-smi --query-gpu=name,memory.used --format=csv 2>&1 | tail -2
echo "提示: 宿主上的 vLLM 需手工重新拉起"
;;
status)
echo "--- PCI 驱动绑定 ---"
for d in $DEVS; do printf " %s -> %s\n" "$d" "$(drv "$d")"; done
echo "--- 虚机 $VM: $(virsh domstate "$VM" 2>&1) ---"
echo "--- 目前的 GPU 归属 ---"
if [ "$(drv 0000:01:00.0)" = "vfio-pci" ]; then
echo " => 归 虚机 $VM"
elif [ "$(drv 0000:01:00.0)" = "nvidia" ]; then
echo " => 归 宿主机"
else
echo " => 未绑定/其他"
fi
;;
*)
echo "用法: $0 {vm [-f]|host|status}"; exit 1;;
esac
它把"三件麻烦事"都包进去了:
- 停图形界面 。这台机器只有 4090 一块显示设备,桌面本身就跑在它上面。要给虚机用,宿主机必须先退回
multi-user.target,否则 nvidia 驱动一直被 Xorg 攥着; - 检查占用 。宿主机上还有 CUDA 任务(脚本里针对 vLLM 做了检查)。有进程握着
/dev/nvidia0不放时,强行解绑会失败甚至把进程搞崩,所以先拦一道,要么人工清、要么加-f强制杀; - 等待状态收敛 。关虚机用
virsh shutdown走优雅关闭,然后每 2 秒轮询一次域名状态,最多等 90 次,等状态真的变成"关闭"再去恢复桌面。
切换流程:
#mermaid-svg-jev4JDcumPBgy4i8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jev4JDcumPBgy4i8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jev4JDcumPBgy4i8 .error-icon{fill:#552222;}#mermaid-svg-jev4JDcumPBgy4i8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jev4JDcumPBgy4i8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jev4JDcumPBgy4i8 .marker.cross{stroke:#333333;}#mermaid-svg-jev4JDcumPBgy4i8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jev4JDcumPBgy4i8 p{margin:0;}#mermaid-svg-jev4JDcumPBgy4i8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster-label text{fill:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster-label span{color:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster-label span p{background-color:transparent;}#mermaid-svg-jev4JDcumPBgy4i8 .label text,#mermaid-svg-jev4JDcumPBgy4i8 span{fill:#333;color:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .node rect,#mermaid-svg-jev4JDcumPBgy4i8 .node circle,#mermaid-svg-jev4JDcumPBgy4i8 .node ellipse,#mermaid-svg-jev4JDcumPBgy4i8 .node polygon,#mermaid-svg-jev4JDcumPBgy4i8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jev4JDcumPBgy4i8 .rough-node .label text,#mermaid-svg-jev4JDcumPBgy4i8 .node .label text,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape .label,#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-jev4JDcumPBgy4i8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jev4JDcumPBgy4i8 .rough-node .label,#mermaid-svg-jev4JDcumPBgy4i8 .node .label,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape .label,#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape .label{text-align:center;}#mermaid-svg-jev4JDcumPBgy4i8 .node.clickable{cursor:pointer;}#mermaid-svg-jev4JDcumPBgy4i8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jev4JDcumPBgy4i8 .arrowheadPath{fill:#333333;}#mermaid-svg-jev4JDcumPBgy4i8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jev4JDcumPBgy4i8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jev4JDcumPBgy4i8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jev4JDcumPBgy4i8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jev4JDcumPBgy4i8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jev4JDcumPBgy4i8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jev4JDcumPBgy4i8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster text{fill:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster span{color:#333;}#mermaid-svg-jev4JDcumPBgy4i8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jev4JDcumPBgy4i8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jev4JDcumPBgy4i8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape p,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape .label rect,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jev4JDcumPBgy4i8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jev4JDcumPBgy4i8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jev4JDcumPBgy4i8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有
没有
gpu-switch vm
isolate multi-user.target
停掉宿主机桌面
还有进程占用
/dev/nvidia* 吗
报错退出
或 -f 强制杀掉
virsh start centos8
libvirt 自动把 01:00.0/.1
从 nvidia 切到 vfio-pci
status 显示:归虚机
6.2 实测状态
虚机运行中执行 gpu-switch status:
text
--- PCI 驱动绑定 ---
0000:01:00.0 -> vfio-pci
0000:01:00.1 -> vfio-pci
--- 虚机 centos8: 运行 ---
--- 目前的 GPU 归属 ---
=> 归 虚机 centos8
同一时刻,在宿主机上敲 nvidia-smi 会是这个结果:
text
Failed to initialize NVML: No supported GPUs were found
Unable to determine the number of GPUs
这不是故障,正是"显卡已经不在宿主机手里"的证据。收回来(gpu-switch host)之后 nvidia-smi 就恢复正常了。
七、踩坑记录
① 安装器在非交互终端下自旋,把提问吞了
第一次跑 ./nvidia-installer 没带 --ui=none,安装器进了 ncurses 界面。因为是在脚本/串口这类非 tty 的 stdin 下执行的,界面在那儿空转,后面"是否跳过 sanity check"的提问也没人应答,直接失败。改成 --ui=none 用内置文本流界面,一次通过。远程/脚本里装驱动,--ui=none 是必备项。
② --no-dkms 的代价
省掉了 DKMS 注册,代价是:客户机内核一升级,kernel-devel 和已编译的模块就对不上了,得重装驱动。如果这个虚机会滚动更新内核,建议改成装 DKMS,或者干脆锁住内核版本。
③ 装驱动必须在非图形目标下进行
Xorg 开着的时候装 NVIDIA 驱动,安装器会尝试接管显示,很容易装一半失败。脚本里先 systemctl isolate multi-user.target;但要注意别把自己也一起停了 (见 5.3 的 systemd-run 处理)。
④ 只直通显示功能不够
只给 01:00.0 不给音频功能 01:00.1,客户机里会是个不完整的 NVIDIA 设备。它们在同一个 IOMMU 组,一起直通最省事。
⑤ 直通期间宿主机的 nvidia-smi 会"报错"
Failed to initialize NVML: No supported GPUs were found 是正常现象,别当成驱动坏了去重装。判断归属看 gpu-switch status 里的驱动绑定状态更准。
⑥ 切换失败十有八九是"有人还在用显卡"
宿主机上跑着 CUDA 服务(比如 vLLM)时,进程会攥着 /dev/nvidia0。这时候解绑驱动会失败。脚本先用 fuser -v /dev/nvidia0 ... 拦一道,确认真要强杀再上 -f。
⑦ 离线机器装依赖,ISO 就是最好的源
客户机没网,kernel-devel、gcc、make 这些都从 CentOS 8 安装 DVD 里取:把 ISO 挂上,写一个 file:// 的 repo 文件,再用 --disablerepo='*' --enablerepo='dvd-*' 把其他源全禁掉,避免 dnf 去连不存在的外网源、卡在超时上。
⑧ 装完依赖记得对齐内核版本
kernel-devel 装成了比当前运行内核更新的版本,编译照样失败。脚本里显式加了一步 dnf install "kernel-devel-$(uname -r)",并检查 /lib/modules/$(uname -r)/build 是否存在。
八、总结
整套方案没有动 vfio.conf,也没有写自定义的 QEMU 参数,配置量其实很小:
| 位置 | 改什么 |
|---|---|
| 宿主机内核参数 | intel_iommu=on iommu=pt |
宿主机 /etc/modprobe.d/ |
屏蔽 nouveau |
| 虚机 XML | 两段 hostdev(managed='yes') |
| 客户机 | 装 580.95.05 驱动 + modules-load.d/nvidia.conf |
| 日常操作 | gpu-switch vm / host / status |
回过头看,真正花时间的不是 XML,而是这几件"不在文档上"的事:确认显卡独占 IOMMU 组、驱动必须离线装、安装器在非交互终端下的表现、以及"切换前先确认没人在用卡"。
适用边界要说清楚:这是"分时复用",不是"同时使用"。同一时刻显卡只能归一边;而且因为这台机器只有一块显示设备,交给虚机时宿主机桌面必须停掉------如果你的机器有核显、桌面跑在核显上,那切换时就不用停图形界面,体验会好很多。
附录:环境信息
| 项目 | 宿主机 | 客户机(虚拟机) |
|---|---|---|
| 系统 | Kylin Linux Advanced Server V11 (Swan25) | CentOS Linux 8.2.2004 |
| 内核 | 6.6.0-32.24 | 4.18.0-193.el8 |
| 虚拟化 | libvirt 9.10.0 / QEMU 8.2.0,机型 pc-q35-8.2,CPU host-passthrough |
8 vCPU / 8 GiB |
| 显卡 | RTX 4090(10de:2684)+ 音频(10de:22ba),IOMMU 组 13 |
07:00.0 / 08:00.0 |
| 驱动 | 580.95.05(归虚机时解绑) | 580.95.05(CUDA 13.0) |
| 显示 | 无核显,桌面在 4090 上 | virtio 主显示 + VNC |