针对新型硬件设备(GPU/FPGA),为同时实现高性能和共享的需求,其最适合的虚拟化方式是直通共享,即设备支持SR-IOV扩展功能,包含多个功能接口VF,结合硬件辅助虚拟化技术VT-d(Intel)/IOMMU(AMD),使每个接口VF通过直通的方式单独分配给一个虚拟机,以便虚拟机直接和设备通信,提高I/O性能。
在虚拟化环境中,实现设备的热插拔,可与设备直通技术正交互补。在设备不支持SR-IOV扩展的情况下,此时直通的是完整的设备(或者说PF),借助于热插拔可在多个虚拟机间实现设备的共享。若设备支持SR-IOV,则透传给虚拟机的是VF,则可使设备支持更多的虚拟机。
1.KVM下设备直通方式
无论透传的是PF还是VF,其透传给虚拟机的方式是没区别的。针对KVM虚拟机环境下,有两种直通方式:pci-stub和vfio。另外,设备能透传有一个前提,即主板芯片必须支持硬件辅助虚拟化技术VT-d(Intel)/IOMMU(AMD),目前应该都是支持的。在这里,简单介绍大致流程,详情参见[1]。
第一步,使能VT-d(Intel)/IOMMU(AMD)
第二步,主机端安装对应的驱动模块
- 针对pci-stub: modprobe pci_stub
- 针对vfio:
modprobe vfio
modprobe vfio-pci
第三步,找到主机端待透传的PCI设备(以BDF=0000:01:00.0,vendor&device ID =8086:10b9为例),在主机端先与原始驱动进行解绑,后与pci-stub或vfio绑定。 - 针对pci-stub:
echo “8086 10b9” > /sys/bus/pci/drivers/pci-stub/new_id
echo 0000:01:00.0 > /sys/bus/pci/devices/0000:01:00.0/driver/unbind
echo 0000:01:00.0 > /sys/bus/pci/drivers/pci-stub/bind - 针对vfio:
echo 0000:01:00.0 > /sys/bus/pci/devices/0000:01:00.0/driver/unbind
echo 8086 10b9 > /sys/bus/pci/drivers/vfio-pci/new_id
第四步,向虚拟机添加设备 - 针对pci-stub:
静态:-device pci-assign,host=01:00.0
动态:device_add pci-assign,host=01:00.0,id=mydevice - 针对vfio:
静态:-device vfio,host=01:00.0
动态:device_add vfio,host=01:00.0,id=mydevice
2. 热插拔机制
上节最后谈到了“动态”向虚拟机添加设备的命令,device_add,这是QEMU-KVM向用户暴露的接口。QEMU-KVM提供了两个用户管理虚拟机的协议,HMP(Human Monitor Protocol)和QMP(Qemu Monitor Protocol)。 上述dvice_add命令可直接在qemu-command-monitor窗口(在虚拟机环境下,ctrl+alt+f2打开)手写输入,此时使用的是HMP协议进行解析。另外,也可借助于qga。
qga是一个运行在虚拟机内部的普通应用程序(可执行文件名称默认为qemu-ga,服务名称默认为qemu-guest-agent),其目的是实现一种宿主机和虚拟机进行交互的方式,这种方式不依赖于网络,而是依赖于virtio-serial(默认首选方式)或者isa-serial,而QEMU则提供了串口设备的模拟及数据交换的通道,最终呈现出来的是一个串口设备(虚拟机内部)和一个unix socket文件(宿主机上)。 qga通过读写串口设备与宿主机上的socket通道进行交互,宿主机上可以使用普通的unix socket读写方式对socket文件进行读写,最终实现与qga的交互,交互的协议与qmp(QEMU Monitor Protocol)相同(简单来说就是使用JSON格式进行数据交换)。
无论上述哪种方式,最后都有函数qmp_device_add()负责设备热插拔的实现,下图是通过qemu-command-monitor方式(即HMP协议)得到的函数调用关系。
针对QMP,参见$(qemu-src)/monitor.c 绑定的是qmp_device_add
void monitor_init_qmp_commands(void)
{
…
qmp_register_command(&qmp_commands, “device_add”, qmp_device_add,
QCO_NO_OPTIONS);
…
}
在这里先做一下说明,因QEMU-KVM模拟了多种机器架构,所以存在多种PCI设备热插拔模型,如下所示:
- ACPI(Advanced Configuration and Power Interface)
- SHPC(Standard Hot Plug Controller)
- PCIe native hotplug
- sPAPR/pSeries Dynamic Reconfiguration
无论采取哪种模型,其基本思路都在模拟物理机器实现热插拔的过程:
- Hot Plug:手动安插设备->手动通知OS(通常支持热插拔的主板,在插槽附近有按钮)->操作系统Enable/Probe/Configure设备->呈现设备是否安插成功(插槽附近有信号灯)。
- Hot Unplug:手动通知OS->操作系统Unconfigure/disable device ->信号表明设备可移除->手动移除设备
QEMU-KVM实现设备热插拔的基本模型如下所示:
-
虚拟机启动时,为支持热插拔的总线设置对应的hotplug回调函数:
qbus_set_hotplug_handler(BusState *bus, DeviceState *handler) -
收到device_add命令时,调用:handler->plug(bus,pcidev)
-
收到device_del命令时,调用:
handler->request_unbus(bus,pcidev);
handler->unplug(bus,pcidev);针对x86架构,使用的是ACPI方式,因此本文分析在qemu-kvm中如何实现ACPI-Based PCI设备热插拔。在这里,先简单对Linux中的ACPI Hot-Plug PCI工作流程做一个简单说明,详情参见[2]。
- 系统上电后,在系统初始化过程中针对每个设备插槽都有响应的“方法节点”Method(Lxx){…}以描述设备相对应的具体执行操作,比如安插CD ROM设备时所对应的方法:
Method(_Lxx) { // 某个GPEx_STS的状态位发生变化时的响应处理方法节点(以下为插入//事件的执行方法)
Sleep(250) // 延迟250微秒
If (InsertionEvent) {//检查是否为插入事件
PWRN() // 如果是设备插入,就给设备上电
}
Notify(CDRM, 1)//向上层(操作系统的驱动系统层)通告检测到了CDRM设备插入,那么需要唤醒相应的驱动系统层的PCI设备初/始化程序。
2. 用户插入PCI热拔插设备到Slot内
3. HPPC(南桥中热插拔控制器)生成GPE事件
4. ACPI核心控制芯片组产生SCI中断
5. ACPI硬件会清除这个GPE事件的响应位,并且运行对应GPE位在名字空间中的_LXX控制方法
6. _LXX控制方法将根据HPPC的相应控制位来决定当前事件是否为一个设备插入事件,以及哪个插槽有设备插入
7. _LXX控制方法将向驱动层发出通告(Notify)表示当前的PCI总线上的某一个插槽有设备插入
8. ACPI驱动层运行_STA方法获得第7步所知的设备的当前状态,如果运行_STA方法返回0x0a表示当前设备不能使能。
9. ACPI驱动层告诉PCI层准备对新插入的设备进行枚举。
10. PCI设备层读入相关设备(PCI功能模块)的配置信息,此时这个设备就被OS识别可用了。
最后,我们回到qmp_device_add函数,看qemu-kvm是如何操作的。下图是qmp_device_add的函数调用关系:
上述过程通过分析参数,找到bus、device,并分别完成类别初始化和对象实例化。最后找到对应的热插拔回调函数acpi_pcihp_device_plug_cb,执行ACPI-based 热插拔。
接下来分析acpi_pcihp_device_plug_cb,其主要是遵循ACPI hotplug的机制,配置相关寄存器,并产生SCI中断。寄存器配置原理,可参见acpi_pci_hotplug.txt[5]。
把SCI中断注入虚拟机后,此时就有虚拟机操作系统对应的ACPI驱动程序进行处理,检测到有设备插入,则执行对应的设备Enable/Probe/Configuration过程,对设备初始化。如下图所示:
//在执行device_add之前
//执行device_add之后