状态:草稿 (2026-10-17),源码 + 只读实测。 基于 KubeVirt
v1.9.0(提交79d34c17)。连接了 GPU 的虚拟机处于关闭状态, 其域 XML 是通过代码绘制的 预测值(未实测)。实测部分读取了 KubeVirt CR、节点的 allocatable 和 virt-handler 日志。
KubeVirt 源码阅读 7:设备直通
目录
1. 直观理解:三个地方必须了解同一设备
要将 PCI 设备传递给虚拟机,三个地方必须了解同一事实。
- 宿主机内核:必须将该设备从原有驱动程序中剥离并绑定到
vfio-pci。只有这样,用户空间 (QEMU) 才能在 IOMMU 的保护下直接操作设备。 - Kubernetes:设备必须显示为“节点的资源”,调度程序才能将 Pod 调度到该节点,并且 kubelet 会将设备文件注入 Pod 中。这座桥梁就是设备插件。
- libvirt:域 XML 中必须包含
<hostdev>,QEMU 才能将该 PCI 地址附加到客户机。
KubeVirt 不做第 1 点(由管理员负责)。连接第 2 点和第 3 点的工作由四个组件分摊。
2. 图解:七个步骤
3. 代码路径
① 允许列表 — KubeVirt CR
在 KubeVirt CR 的 spec.configuration.permittedHostDevices.pciHostDevices[] 中编写 pciVendorSelector(벤더:장치 (十六进制)和 resourceName 。还必须启用功能门控 HostDevices 。即使 VMI 写了名称,这里没有的设备也无法使用。virt-controller 在绘制 Pod 之前通过 validatePermittedHostDevices()
(renderresources.go:493进行检查。
② 设备管理器扫描 sysfs — virt-handler
virt-handler 中的 DeviceController.Run()(device_controller.go:481每次 KubeVirt 配置更改时都会注册回调以调用 refreshPermittedDevices() (:497)。此函数通过
updatePermittedHostDevicePlugins()(:208调用 discoverPermittedHostPCIDevices()(pci_device.go:258。此函数遍历 /sys/bus/pci/devices 并执行三项操作:
- 检查设备的
벤더:장치是否在允许列表中。 - 检查驱动程序是否为
vfio-pci((:278)。否则,shouldPermitNvidia()(nvidia.go:23会再次检查。即使是绑定到 NVIDIA 驱动程序的设备,如果是带有 vGPU 配置文件的虚拟功能 (VF) 也会被接受,否则会记录日志并丢弃。 - 读取设备的 IOMMU 组号和 NUMA 节点。
③ 设备插件 — 向 kubelet 注册资源
每个资源名称都会启动一个 PCIDevicePlugin (Start, pci_device.go:62)并向 kubelet 的设备插件套接字注册。向 kubelet 提供的设备 ID 不是 PCI 地址,而是 IOMMU 组号(constructDPIdevices, :132)。由于 VFIO 按组单位传递设备,因此不能将同一组的设备分配给不同的虚拟机。注册完成后,它会像 status.allocatable 一样出现在节点的
nvidia.com/<GPU 이름>: 1 中。同一设备管理器也会以相同的方式暴露诸如 devices.kubevirt.io/kvm·tun·vhost-net 等通用设备。
④ Pod 请求资源 — virt-controller
如果 VMI 中存在 spec.domain.devices.gpus[].deviceName: nvidia.com/<GPU 이름> ,则第 3 部分中的模板使用资源渲染器选项
WithGPUsDevicePlugins()(renderresources.go:149将该资源逐个添加到
compute 容器的 limits 中。内存开销会增加 1Gi(hypervisorbackend.go:130)。注释中写道,这是因为 VFIO 会为 DMA 锁定整个客户机 RAM。判断标准位于 vmitrait.HasVFIO()
(vmitrait.go:33— 即宿主机设备、GPU 或 SR-IOV 中存在任意一个时。
⑤ Allocate — 传入设备文件和地址
当调度员将 Pod 安排到资源有剩余的节点上时,kubelet 会调用设备插件的 Allocate()
(pci_device.go:152)。答案有两个:
- 设备文件:
/dev/vfio/vfio和/dev/vfio/<그룹 번호>(formatVFIODeviceSpecs, common.go:275),如果是使用 iommufd 的内核,还会加入每个设备专属的 cdev。 - 环境变量:
PCI_RESOURCE_前缀(types.go:3672)将资源名称转换为大写,并将/·.替换为_拼接后的名称(ResourceNameToEnvVar, util.go:89), 值是用逗号连接的 PCI 地址。例如:PCI_RESOURCE_NVIDIA_COM_<GPU>=0000:01:00.0.
这个环境变量是连接节点(handler)和 Pod(launcher)的唯一纽带。launcher 不会查看 sysfs,只相信这个值。
⑥ launcher <hostdev> 的创建者
5篇 generateConverterContext() 的 getGPUDevices()(manager.go:1138)并调用, gpu.CreateHostDevices()(gpu/hostdev.go:39)负责创建地址池。该池会通过读取⑤中的环境变量来填充(AddressPool.load,
addresspool.go:46)。针对每个 GPU 提取一个地址, createPCIHostDevice()(hostdev.go:141)会创建
api.HostDevice ,接着 converter 的 HostDeviceDomainConfigurator
(compute/host_device.go:42)会将其挂载到虚拟机域上。如果相同的 GPU 列表是 vGPU(mediated device),则会走向 MDEV_PCI_RESOURCE_… 变量与 CreateMDEVHostDevices 方向。在 v1.9.0 中,通过 DRA(Dynamic Resource Allocation)而非设备插件来接收的方式(hostdevice/dra/)也已在功能门控(Feature Gate)之后引入。
⑦ 预期 XML(未实际测量)
根据代码,单个 GPU(名称为 gpu1)的虚拟机域 XML 应该类似于这样。
<hostdev mode='subsystem' type='pci' managed='no'>
<source>
<address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
</source>
<alias name='ua-gpu-gpu1'/>
</hostdev>
managed='no' 至关重要。如果是 managed='yes' ,当 libvirt 启动虚拟机时,会将设备从宿主机驱动中分离并绑定到 vfio-pci,但 Pod 内的非 root libvirt 没有这样的权限。因此, 绑定的工作必须在宿主机上提前 完成(第 1 节的 1),而 KubeVirt 只会统计已经绑定好的设备(②)。别名使用用户自定义别名,前缀为 ua- 的 GPU 前缀 gpu- 命名。此外,带有这种设备的 VMI 会被 virt-handler 标记为不可进行实时迁移(vmiContainsNonMigratablePCIHostDevices, vm.go:1304).
4. 实测:允许列表已启用但资源为 0
读取了为将小型集群中的一个 GPU 节点专用于 VM 而进行的配置状态(2026-10-17)。
-
① KubeVirt CR 中有一个功能门控
HostDevices,以及一个pciHostDevices(pciVendorSelector: 10DE:xxxx,resourceName: nvidia.com/<GPU 이름>)。请求此 GPU 的一个 VM 通过runStrategy: Halted关闭了。 -
③ 然而该节点的 allocatable 是
nvidia.com/<GPU 이름>: 0。在同一个节点上,用于容器的 GPU 资源(nvidia.com/gpu,由其他设备插件暴露)显示为 1。在同一个节点上,KubeVirt 的通用设备(devices.kubevirt.io/kvm·tun·vhost-net)分别为 1k。 -
② 该节点的 virt-handler 日志中不断重复这一行(
pos: nvidia.go:33).Skipping non-vfio-pci device without vGPU profile 0000:01:00.0 (10de:xxxx, driver: nvidia)设备的驱动程序不是
vfio-pci而是宿主机的nvidia,并且没有 vGPU 配置文件,因此在第 ② 步被过滤掉了。所以第 ③ 步的设备数量为 0。在这种状态下启动虚拟机,Pod 将因资源不足而停留在 Pending 状态(我没有实际去启动它)。 解决方法不是在 KubeVirt 中,而是在宿主机上将 绑定到vfio-pci(通过内核参数或驱动程序绑定),同时还需要确保用于容器的 GPU 设备插件不再暴露同一个 GPU。 -
这行日志在 2026-10-16 当天(UTC)就出现了 641 次,在该 virt-handler 重新启动后更是出现了 7,889 次。这意味着平均每两分钟多就会重新扫描一次设备。至于是什么导致如此频繁地调用 ②,我们这次没有深入追踪。
-
⑤ 的痕迹。 在同一个 virt-handler 日志中,在创建其他 VM 的 Pod 的瞬间(2026-10-14T14:38:34Z),通用设备插件的
Generic Allocate: resourceName: kvm·tun·vhost-net三行(generic_device.go:240)以 1ms 的间隔被打印出来。这就是 kubelet 在部署 Pod 并调用设备插件 Allocate 时的样子。
5. 局限性
- 由于没有启动挂载了 GPU 的 VM,因此 ⑤ 的环境变量、⑥ 和 ⑦ 的 XML 以及客户机内的
lspci均处于 未实测状态。在宿主机上将设备切换到 vfio-pci 属于修改节点配置的操作,超出了本文的范围(仅限阅读)。 - 关于如何处理 IOMMU 组中存在多个设备(如同一组中的音频功能等)的情况,我们仅通过阅读代码进行了了解。
- 关于 vGPU、SR-IOV 和 DRA 的路径,文中仅提及了名称。
TT 的一句话总结。 如果纳闷“明明加进白名单了,为什么还是看不到?”,请在 virt-handler 日志中搜索
Skipping。KubeVirt 只会统计已经绑定到 vfio-pci 的设备。绑定设备是宿主机的事情!
参考资料
所有资料均于 2026-10-17 查阅。
device-manager: https://github.com/kubevirt/kubevirt/tree/v1.9.0/pkg/virt-handler/device-managerhostdevice: https://github.com/kubevirt/kubevirt/tree/v1.9.0/pkg/virt-launcher/virtwrap/device/hostdevice- KubeVirt 用户指南,宿主机设备分配: https://kubevirt.io/user-guide/compute/host-devices/
- Kubernetes 设备插件: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
- Linux 内核 VFIO 文档: https://docs.kernel.org/driver-api/vfio.html
- libvirt 域 XML,宿主机设备: https://libvirt.org/formatdomain.html#host-device-assignment