首页 / 资讯中心 / 文章详情

VFIO 与 mdev 实战:从物理设备直通到多虚拟机共享

VFIO 与 mdev 实战:从物理设备直通到多虚拟机共享 ★ FEATURED ARTICLE
搞虚拟化的人早晚会撞上设备直通这道坎。刚开始我遇到的问题是业务方要求虚拟机里必须跑硬件加速纯软件模拟怎么压都顶不住CPU开销于是我把一张物理卡直接塞给某台虚拟机性能确实上来了可一台宿主机只有一两张卡虚拟机却有十几台最后又把卡撤了回来。后来我把 Linux 内核的 VFIO 和 mdev 这套组合彻底啃透才找到平衡办法既保证接近原生的性能又让一个物理设备能被多台虚拟机安全共享。这篇就把我对 VFIO mdev 的完整理解从设计思路到实操命令再到排障记录一次性讲明白。适合正在折腾虚拟化资源池、GPU虚拟化、云平台底层设备的同学参考。1. 为什么绕不开 mdev设备共享的三条技术路线1.1 软件模拟通用但性能天花板太低先看最原始的做法。虚拟机里要有一块网卡、显卡或者磁盘控制器传统虚拟化靠的是软件模拟。QEMU这类虚拟化程序会模拟出设备寄存器、DMA和中断处理guest里的驱动每访问一次硬件都要经过一次“虚拟机退陷入模拟器 → 模拟器翻译成真实硬件操作 → 再进虚拟机”的路径。这条路径非常费CPUIO频率一高宿主机负载跟着暴涨。你可以把它理解成一个小卖部只开了一个窗口所有顾客都挤在同一个柜台前业务员再熟练也快不起来。并非说软件模拟没用。对于管理网络、低速磁盘这类IO不密集的场景它完全够用而且兼容性最好任何设备都可以模拟出来不需要硬件支持。但到了GPU、高速网卡、NVMe这类追求带宽和低延迟的设备软件模拟基本顶不住。高帧率图形渲染、大规模并行计算场景里虚拟机的IO模型一旦被模拟层卡住性能损耗可以达到百分之几十甚至更多。所以才有后两条路线。1.2 VFIO 全量直通性能漂亮分配成本太高第二条路线是把真实物理设备整个直通给一台虚拟机。这套机制的内核基础就是 VFIOVirtual Function I/O。做法是把物理设备从宿主机驱动里解绑改绑到 vfio-pci 驱动通过 /dev/vfio/N 把设备暴露给用户态QEMU 拿到这个文件描述符后guest 驱动可以直接编程真实设备寄存器可以发起 DMA可以安装自己的中断处理经过IOMMU的地址翻译和访问隔离后几乎等于直接用裸机硬件。关键原因是IOMMU。没有IOMMU的话guest 驱动一旦能在宿主机地址空间里随意做 DMA等于握着真实钥匙却连防盗门都不设guest 可以读写宿主机任意物理内存。IOMMU 做的就是给每个设备限定一张“地址翻译表”guest 产生的 DMA 地址被映射到指定物理页面之外其他内存它看不到。这也是为什么开直通必须在 BIOS 开启 IOMMU 功能并且内核里要有 CONFIG_VFIO_IOMMU_TYPE1。VFIO直通的性能非常接近原生但缺点也明显一个物理设备一次只能给一个虚拟机独享。假如你有10台虚拟机想共用一块GPU直通路线最多服务1台想加卡成本立刻上去。而且直通时设备所在IOMMU group里的设备最好都交给同一个虚拟机否则组内别的设备可能会带来DMA越界风险group不干净时就会碰见各种磨人的问题。1.3 mdev 的定位让驱动来“切分”物理设备mdevmediated device中介设备就是想解决直通“独占”问题的框架。它不把整个物理设备交给一台虚拟机而是让物理设备对应的厂商驱动先把资源切成几份每一份封装成一个虚拟设备。从 guest 视角看这个虚拟设备依然是一块完整的PCI设备有自己的PCI配置空间、BAR、中断驱动照常加载。但从宿主机视角看它只是一个mdev实例底层共享同一个父设备的显存、计算单元、队列等资源并由父驱动负责仲裁。典型例子就是GPU虚拟化。某个GPU厂商在驱动里实现了mdev parent ops把一块真实GPU按不同模板划分出多个虚拟GPU每个虚拟GPU有独立显存空间或受限算力guest里加载对应驱动后就能工作。多个虚拟机可以同时访问同一块真实GPU但不能互相看到对方的显存安全性由厂商驱动和VFIO共同保证。这种“切分”能力是软件模拟和全量直通都没有的。1.4 三种方案对比与选型思路下面这张表我整理项目时经常拿来当决策参考方案性能共享能力配置复杂度依赖条件软件模拟中低可以任意共享低无需特殊硬件VFIO全量直通接近原生不共享中需要IOMMU设备组要干净mdev接近原生可多VM共享中高需要厂商驱动实现mdev接口选型上没有绝对答案。如果只是网卡多队列SR-IOV硬件级的单根I/O虚拟化往往更贴合因为网卡天然支持硬件VF但如果对象是GPU、FPGA这类SR-IOV支持弱、且需要软件划分资源的设备mdev基本是最优解。有些智能网卡和存储控制器也会用mdev做细粒度虚拟化。我在实际项目里的默认判断是硬件有VF就优先试VF硬件没有VF、但厂商驱动提供了mdev支持就用mdev两者都不满足才考虑软件模拟或者干脆不虚拟化。2. mdev 内核框架拆解从父设备到虚拟设备2.1 四个概念先对齐写内核代码的人看mdev其实就是一套“基于物理父设备的虚拟设备管理框架”。里面四个概念必须分清父设备parent、mdev类型type、mdev实例instance、device_api。父设备是真实存在的硬件比如PCI总线上的一块GPU它有自己的BDF地址挂在PCI子系统的设备树里。mdev类型是一组资源模板由厂商驱动定义比如“类型A”表示分配4GB显存、最多4个实例“类型B”表示分配8GB显存、最多2个实例。每种类型在sysfs里都有对应目录目录下能看到设备接口、剩余可创建实例数等属性。mdev实例就是根据某个类型模板创建的虚拟设备每个实例有唯一UUID内核会给它在mdev总线上建立一个设备节点。device_api则是这个实例对外宣称的“访问方式”最常见是 vfio-pci意思是用VFIO的PCI语义来访问它有些新框架里的vDPA类型会走别的总线语义比如 vdpa 这类。2.2 厂商驱动如何注册自己mdev框架不是凭空感知父设备的需要厂商驱动主动接入。驱动初始化时调用注册函数把自己实现的 parent ops 交给内核退出时再注销。parent ops 里最关键的就是create和remove回调创建mdev时内核调用create厂商驱动核对剩余实例数、分配硬件资源、初始化虚拟设备上下文销毁时调用remove回收资源。另外还要提供设备接口描述、剩余实例数展示这些钩子让sysfs里的属性有数据可读。代码示意如下字段名会随内核版本调整核心逻辑不变// 逻辑示意不是某个具体内核版本的精确结构 struct mdev_parent_ops { int (*create)(struct mdev_device *mdev); int (*remove)(struct mdev_device *mdev); ssize_t (*available_instances)(struct mdev_type *type); ssize_t (*show_device_api)(struct mdev_type *type); };如果你去看新版内核会发现接口形态换成了 mdev_driver、mdev_class 这一套但你要抓的本质还是厂商驱动在父设备上声明了“我能创建哪些虚拟设备、每个长什么样、怎么创建销毁”。至于新版里 mdev 设备创建后是走 vfio-mdev 这类VFIO子驱动还是走独立mdev总线都由 device_api / class 决定这个抽象是这套机制后来越用越广的原因。注意编译内核时除了保留基础的 VFIO 支持还要确认 CONFIG_VFIO_MDEV 和 CONFIG_VFIO_MDEV_DEVICE 已经打开否则即便厂商驱动写了parent ops系统里也没有 vfio_mdev 驱动来承接。2.3 一次mdev创建内核发生了哪些事手动创建一个mdev实例本质是在sysfs里触发内核走一遍设备生命周期。假设父设备类型目录下有 create 这个文件向它写入一个UUID接下来发生几步内核根据UUID在mdev总线上创建 mdev_device 结构并挂到 /sys/bus/mdev/devices/ 。调用厂商驱动里的create回调厂商驱动检查该类型实例余量按模板分配显存等资源。mdev设备把 device_api 暴露出来内核依据它决定后续绑定到哪个驱动。绑定vfio_mdev后VFIO框架看到该设备把它加入一个VFIO group最终通过 /dev/vfio/N 暴露给用户态。guest通过QEMU拿到这个VFIO设备后就可以像访问真实PCI设备一样访问它。销毁时往mdev设备目录里的remove属性写下1内核触发remove回调回收资源并移除设备节点。这个生命周期和PCI设备本身的hotplug模型有点像只不过mdev不是真实的物理hotplug而是完全由软件和厂商驱动协同管理。上面这段写清楚后你在排障时心里就有一幅地图是父设备驱动没起来还是类型资源不够还是vfio_mdev没承接还是VFIO group出了问题。接下来第三章就照这个地图做一次真实操作。3. 实操记录把 mdev 设备串给虚拟机3.1 前置检查与内核参数动手之前先把地基打好。BIOS里确认IOMMU开关打开不同平台叫法不同常见是 VT-d 或 AMD-Vi。然后在内核引导参数里开启IOMMU和直通模式。我通常在grub里这么配以你熟悉的发行版配置方式为准intel_iommuon iommupt如果是AMD平台把 intel_iommuon 替换成 amd_iommuon 这类参数。iommupt 的意思是“直通模式”让直通设备尽量不做额外地址转换把IOMMU对性能的损耗压到最低。生产环境我基本都加这个参数。进入系统后先确认三件事# 1. 内核是否识别IOMMU dmesg | grep -i -e DMAR -e IOMMU # 2. IOMMU group是否已经创建 ls /sys/kernel/iommu_groups/ | head # 3. VFIO用户态接口是否存在 ls -l /dev/vfio/vfio如果 /sys/kernel/iommu_groups 下面没有内容或者 /dev/vfio/vfio 不存在多半是BIOS没开、内核参数没生效或者模块没加载。先补这块再看mdev省得一步错步步错。3.2 找到父设备并确认可用类型用 lspci 找到目标设备地址。以一块GPU为例lspci -nnk | grep -i -A3 VGA记下类似 0000:00:02.0 的BDF地址接着查看它是否带有 mdev_supported_types 目录ls /sys/devices/pci0000:00/0000:00:02.0/mdev_supported_types/这个目录下每一个子目录就是一种mdev类型。进入类型目录重点看三个属性cat /sys/devices/pci0000:00/0000:00:02.0/mdev_supported_types/某类型/device_api cat /sys/devices/pci0000:00/0000:00:02.0/mdev_supported_types/某类型/available_instances cat /sys/devices/pci0000:00/0000:00:02.0/mdev_supported_types/某类型/descriptiondevice_api 会告诉你这个类型是走 vfio-pci 还是别的总线available_instances 是还能创建几个实例description 通常描述这个类型的资源规格。假设某GPU上注册了两种类型我用一个示意表格展示它们的差异类型device_apiavailable_instances资源规格示意类型Avfio-pci44GB显存适合普通桌面类型Bvfio-pci28GB显存适合图形渲染/训练如果 available_instances 是0说明父设备资源已经被占满了需要先释放其他实例或者换个资源小的类型。这里也提醒一句实际设备上类型目录的名字可能很长别手打用TAB补全或复制粘贴最稳。3.3 创建实例并确认绑定类型选好后用uuidgen生成一个UUID写入对应create文件UUID$(uuidgen) echo $UUID /sys/devices/pci0000:00/0000:00:02.0/mdev_supported_types/某类型/create创建完确认设备节点和device_apils -l /sys/bus/mdev/devices/$UUID cat /sys/bus/mdev/devices/$UUID/device_api这个阶段新版内核通常会把带 vfio-pci 语义的mdev自动绑定到 vfio_mdev 驱动不需要你手动操作。如果查看 /sys/bus/mdev/devices/$UUID/driver 发现是空的就手动补一刀echo vfio_mdev /sys/bus/mdev/devices/$UUID/driver_override echo $UUID /sys/bus/mdev/drivers/vfio_mdev/bind如果你不确定驱动路径可以用这个命令搜索ls /sys/bus/mdev/drivers/正常会出现 vfio_mdev 或其他厂商提供的mdev驱动。如果内核版本比较老sysfs路径可能会出现在 /sys/class/mdev_bus/devices/ 下面原理是一样的。绑定完成后设备就已经在VFIO的管辖范围里了接下来交给QEMU/libvirt即可。3.4 在 libvirt/QEMU 里透传 mdev用libvirt管理虚拟机的同学直接编辑虚拟机XML加一段 hostdevhostdev modesubsystem typemdev modelvfio-pci source address uuid12345678-1234-1234-1234-123456789abc/ /source /hostdev保存后 virsh define 或直接从图形管理界面添加这张“中介设备”。libvirt会翻译成QEMU命令行本质上就是下面这行qemu-system-x86_64 \ -device vfio-pci,sysfsdev/sys/bus/mdev/devices/12345678-1234-1234-1234-123456789abc \ ...注意这里用的是 sysfsdev 参数不是普通直通里的 host。mdev设备没有真实的PCI BDF只能用sysfs路径或mdev uuid来定位。有些QEMU版本也支持用 /dev/vfio/N 直接传但用 sysfsdev 是最稳妥、文档最齐全的方式。虚拟机启动后在guest里 lspci 应该能看到对应设备并且能加载驱动。如果是GPUguest里跑一次图形测试或者对应厂商的监控工具确认设备正常计数、显存正确。我按这个流程在一台带GPU的测试机上操作从写UUID到虚拟机里看到设备前后不到三分钟前提是前置检查和厂商驱动都已经就位。3.5 用 mdevctl 把设备管理起来手动echo创建有一个大坑重启失效。设备不在任何配置库里宿主机一重启mdev就消失了虚拟机再开机也找不到设备。mdevctl 这个工具就是干这个的把mdev设备定义为持久化配置系统启动时可以自动恢复。基本用法# 列出当前运行中的设备 mdevctl list # 把设备保存为定义指定父设备、类型、uuid mdevctl define --parent 0000:00:02.0 --type 某类型 --uuid 12345678-1234-1234-1234-123456789abc # 手动启动一个已定义的设备 mdevctl start --uuid 12345678-1234-1234-1234-123456789abc # 查看已定义列表 mdevctl list --defined把 define 做完后这台设备在重启后就能自动恢复。如果你所在的环境没有 mdevctl可以自己写一个 systemd 单元来在开机时 echo UUID但我强烈建议优先用发行版自带的 mdevctl因为它还会处理依赖顺序和配置冲突。4. 常见问题排查与避坑指南4.1 /dev/vfio 不存在或IOMMU group为空现象进系统后没有 /dev/vfio/vfiols /sys/kernel/iommu_groups/ 为空。绝大多数情况是BIOS里IOMMU开关没开或者内核参数没生效。先重查 BIOS 设置再检查 grub 引导参数是否真的应用到当前内核。有时候多内核引导参数只加到旧内核上了用 uname -r 和 /proc/cmdline 对一下就知道。另外别忘了加载 vfio 模块modprobe vfio modprobe vfio_iommu_type1 modprobe vfio_pci部分发行版把这些模块做进 initramfs改完内核参数需要刷新 initramfs不然重启后模块照样加载不上。这个坑我踩过不止一次。4.2 类型目录不存在或创建返回失败现象父设备 sysfs 里根本没有 mdev_supported_types或者 available_instances 是0echo UUID 到 create 时报错。先说没有目录八成是厂商驱动没加载或者该硬件不支持mdev。确认驱动模块是否正常加载dmesg里有没有对应的mdev注册信息。如果是GPU还要确认硬件已经被厂商虚拟化驱动接管而不只是普通显示驱动。available_instances 为0说明类型资源配额满了。把现网里占用资源的实例列出来能删的先删掉腾出配额再创建。还要注意一种隐蔽情况多个类型共用父设备的总资源某个大类型占用的空间可能影响到其他类型的剩余实例数。在 dmesg 里能看到具体资源不足的报错逐条读。dmesg | grep -i -e mdev -e vfio -e mediated4.3 虚拟机启动报 “VFIO group is not viable”这个报错在直通和mdev场景都可能出现。意思是IOMMU group里有设备没有正确进入VFIO管控导致整组不可安全使用。排查顺序一般是先确认除了要透传的设备外组里其他设备也被绑到 vfio_pci或者让它们保持在驱动正常状态但组要干净再检查是不是有几个设备必须作为一个整体一起给guest不能只给其中一部分。有个测试环境常用的内核参数 pcie_acs_overridedownstream,multifunction 可以强行拆分IOMMU group。但要提醒这个参数会改变PCI ACS隔离语义等于削弱了硬件对DMA隔离的保证我只建议在测试环境用生产环境不要碰。真到生产宁可去调整设备插槽让目标设备和它所在的组变干净。4.4 mdev 设备重启后消失这个我在上一章讲了 mdevctl 的持久化做法这里再补充一个实操里的坑如果 mdevctl define 报父设备类型不对先确认父设备BDF在重启后有没有变化。PCI地址在某些拓扑下会变最稳妥的办法是在define时用完整的父设备路径并确认和 mdevctl list 里的一致。另外libvirt里如果已经定义了虚拟机引用这个mdevmdevctl启动失败会让虚拟机整体无法启动日志通常会停在设备构建阶段顺着报错去查父设备资源和驱动状态。4.5 guest 里设备无法驱动或初始化失败这种情况常见原因有三类device_api 不匹配、guest需要额外的厂商驱动、mdev类型资源过小导致初始化失败。先用 cat 确认 device_api 是不是 vfio-pci不是的话说明这个类型本来就不该用普通guest驱动访问再进 guest 看 dmesg 报错判断是注册BAR失败、显存访问异常还是中断分配失败最后可以尝试切到资源更大的类型排除资源不足的干扰。厂商驱动的兼容性也要注意。mdev 的实现高度依赖厂商驱动和内核版本的配对宿主机内核升级后原有mdev类型可能失效或可用实例数变化。我项目里会先把内核和厂商驱动的版本固定下来升级前单独做回归测试不在生产环境直接升。4.6 性能达不到预期的几个检查点mdev性能通常已经接近原生但如果和裸机对比差距明显按这个顺序检查首先是 iommupt 有没有加没加的话IOMMU做完整地址翻译会有额外开销其次是guest里中断是否用了MSI/MSI-X传统INTx中断在高IO场景下损耗不小还有CPU是否绑定正确特别是大流量网卡和GPU场景vCPU不绑核会导致跨NUMA访问延迟情况差别很大。最后别忽视mdev共享父设备意味着资源总分有限多个guest同时跑高负载单guest看到的性能自然会下降这是共享方案的物理边界不是配置错误。5. 一些实践体会与避坑心得如果让我总结mdev这套机制最值得记住的一点那就是“资源如何切分”永远比“如何透传”更重要。你要先花时间弄清父设备能提供什么类型、每种类型消耗什么资源、余量怎么变化再考虑QEMU和libvirt的配置。我在几次生产交付里发现真正出问题的并不是命令敲错而是资源规划和持久化管理没做好。启动一个mdev只要一条echo但忘记 mdevctl define或者类型资源没预留好第二天就能让整个资源池出现连锁故障。我个人的习惯是把所有mdev设备清单纳入版本管理。每台宿主机上定义了哪些UUID、对应哪个类型、服务哪台虚拟机都写下来留档。排查问题时不至于对着一个UUID发呆。如果你刚接触这套东西建议先在一台测试机里故意制造几次错误关掉IOMMU再创建一次、把available_instances占满再创建一次、拔掉父设备再启动虚拟机把这些错误日志都看一眼。这样落地生产时你再看到同样的报错基本不用翻文档就能定位到问题。
阅读完成 · 觉得有帮助?
咨询建站