用户态虚拟化IO通道实现概览及实践(上)(点击蓝字,阅读往期文章)中,我们介绍了virtio的用户态实现分析及实现验证。

 接下来,本文将从vfio-user的角度来对虚拟化IO通道的用户态实现方式进行简单的介绍。

vfio-user实现分析

在虚拟化应用中,借助内核vfio机制,hypervisor已经支持Host下的PCIe设备以pass-through的方式提供给VM使用,其中就包括了常见的NVMe设备。但同样由于在线迁移方面的限制以及无法扩展使用更多形态的后端资源,NVMe的pass-throgh使用比较有限。

vfio-user技术的提出,使得用户态模拟PCIe设备成为可能,给NVMe设备在虚拟化场景中的使用带来了新的应用形式。

vfio-user的思路和vhost-user有点类似,例如在VM所在的Qemu进程之外来模拟设备并提供给Qemu进程中的VM使用,但使用的协议是vfio-user对应的协议,遵循PCIe的相关语义。当然也可以提供模拟的设备给非VM的应用使用。

vfio-user机制

如同vhost-user的实现,vfio-user的实现也不需要内核态模块和接口的支持,可以完全在用户态完成。vfio-user的框架包含了两部分:第一,vfio-user client,通常存在于hypervisor中,其扩展了hypervisor中用户态通过ioctl与内核vfio-pci模块进行交互的接口,以unix socket方式和在用户态模拟的PCIe设备进行通信;第二,vfio-user server, 独立于VM所在的hypervisor进程之外的单独进程,主要是在用户态实现模拟的PCIe设备。

vfio-user server侧模拟设备的语义逻辑与内核vfio-pci模块的实现相似,这样hypervisor(如Qemu)中与vfio-pci模快交互的逻辑可以很大程度的复用,vfio-user client侧主要是要需要基于unix socket接口封装一套API来让hypervisor(如Qemu)可以像使用ioctl的接口一般来访问目标PCIe设备的资源。以Qemu多进程的方式为例,可以将vfio-user server与vfio-user client的关系表示如图所示(其中libvfio-user库和vfio client之间的接口通道是unix socket)。

Apple M1如何做虚拟化 io虚拟化_java

图1. vfio-user server和vfio-user client的关系示意

(以Qemu多进程实现为例)

在vfio-user server侧与vfio-user client进行交互的功能由libvfio-user库来提供(可以从https://github.com/nutanix/libvfio-user 获取源码 )。基于libvfio-user提供的接口,vfio-user server侧理论上可以对各种PCIe设备进行用户态的模拟实现。

vfio-user的协议正在标准化过程中,目前vfio-user client侧主已在Qemu中支持,对于vfio-user server在Qemu及SPDK中也都都有对应的实现。在这里主要基于在SPDK中实现的vfio-user 情况进行分析。

SPDK vfio-user集成

SPDK在2021年的版本中已经实现了基于vifo-user标准对NVMe设备的模拟实现,且为了对模拟的NVMe设备进行测试,也为了可能的在非Qemu使用场景(如container等)下的应用,在SPDK NVMe initiator的驱动中增加了vfio-user协议的支持,作为vfio-user client的角色。

SPDK vfio-user server实现

SPDK对 vfio-user server的实现是基于NVMf Target来做的 --- 在NVMf Target模块增加了一种transport(与rdma、tcp并列)来基于libvfio-user库提供的接口与vfio-user client进行通信,NVMe设备的语义模拟,仍由NVMf模块原有的逻辑实现。

SPDK NVMf Target通过vfio-user协议对vfio-client侧(如Qemu)提供服务的基本结构关系可见图2所示。

Apple M1如何做虚拟化 io虚拟化_java_02

图2. NVMf Target与Qemu对接的模块关系示意

【注:SPDK vfio-user的具体实现,可以参考之前的技术分享:vfio-user与NVMe虚拟设备(点击蓝字阅读往期文章)

NVMf Target新增的vfio-user transport的代码在 “SPDK/lib/nvmf”目录下,其中主要依赖libvfio-user的接口完成transport数据结构定义的各个接口函数的实现。vfio-user transport注册的操作在SPDK_NVMF_TRANSPORT_REGISTER(muser, &spdk_nvmf_transport_vfio_user)中完成,注册的transport名字为“VFIOUSER”。libvfio-user库的源码也以submodule方式下载到 “SPDK/libvfio-user”目录下,随SPDK代码一同进行编译。

前已说明SPDK中针对vfio-user server的实现以NVMf Target为载体进行,因而实际使用中需要依托NVMf Target的应用程序来配合使用。针对NVMf Target vfio-user transport的主要工作流程及相关函数可以简单总结如下。

1.创建NVMf tgt;

包括NVMf tgt创建、为tgt创建poll group等操作均与使用tcp、rdma transport时无异,均在nvmf_tgt程序启动后由nvmf_target_advance_state函数中的状态机逻辑进行实现。

2.创建vfio-user transport; 

可以通过json配置文件在nvmf_tgt启动时的初始化流程中完成,也可以在后续通过rpc命令来创建。执行过程无特别的地方,通过注册的vfio-user tansport的处理函数来进行操作。

3.创建vfio-user listener(或者叫endpoint); 

可以通过json配置文件或者rpc命令来进行操作。vfio-user的listener有几个特点需要注意:

1)每个listener会在libvfio-user库的中创建一个socket的server,并且注册了一个周期执行函数nvmf_vfio_user_accept来轮询是否有vfio-user client的进程发起链接。 

2)每个vfio-user listener 的endpoint与vfio-user client端是1:1的对应关系,即只支持一个vfio-user client进程来进行链接。

3)每个vfio-user listener 对应了一个对vfio-user client侧摸拟的PCIe设备,设配相关的配置空间的初始化操作在libvfio-user提供的vfu_xxx形态的库函数接口中完成。 

4.创建subsystem并为subsystem添加vfio-user transport; 

此步操作与其他transport时的情况一致,主要是在设定subsystem与transport层的关联关系,如让vfio-user client可以知道该subsystem可以通过什么方式进行connect,以及链接时ctrlr知道与subsystem的关联从而为后续命令处理流程服务等。

5.响应vfio-user client侧的建链请求并创建ctrlr描述结构;

该过程的处理可以概述为几个部分:

1)在nvmf_vfio_user_accept函数中查询到有vfio-user client的建链时,就会调用nvmf_vfio_user_create_ctrlr来创建vfio-user transport层的ctrlr并执行NVMf逻辑中的spdk_nvmf_tgt_new_qpair操作创建admin qpair。此操作完成后就会注销nvmf_vfio_user_accept的轮询函数。

2)在spdk_nvmf_tgt_new_qpair的调用流程中执行nvmf_vfio_user_poll_group_add函数,该函数中主动填充了一个SPDK_NVMF_FABRIC_COMMAND_CONNECT的fabric命令的请求交给spdk_nvmf_request_exec_fabrics来处理,并在命令完成后的回调函数handle_queue_connect_rsp中注册周期轮询的poller函数vfio_user_poll_vfu_ctx,处理针对模拟设备的配置空间相关访问请求。

3)在handle_queue_connect_rsp函数中还会将admin QPair对应的sq添加到transport poll group的sq链表当中。在vfio-user transport中每个NVMf QPair有其对应的sq和cq,sq 是NVMf QPair在libvfio-user库中的描述资源,具有一一对应的关系。多个sq可以对应相同的cq。

6.在nvmf_vfio_user_poll_group_poll中处理从vfio-user client侧发送过来的各个请求。

当vfio-user client已经完成了admin Qpair的建立后,nvmf_vfio_user_sq_poll函数会在各个CPU核上经由nvmf_poll_group_poll ---- > nvmf_vfio_user_poll_group_poll流程进行周期性地调用。各次调用会在handle_sq_tdbl_write中从admin QPair对应的sq中取出命令(如创建IO QPair的命令)并进行处理。

1)对于创建IO QPair的命令,就会在handle_create_io_sq中通过前边已经描述的spdk_nvmf_tgt_new_qpair的调用流程,实现将创建的QPair对应的sq添加到transport poll group的sq链表中由nvmf_vfio_user_sq_poll函数进行处理; 

2)对于已创建并添加到transport poll group的sq链表中的IO 类型sq,其中获取的command会在handle_cmd_req函数中最终交由spdk_nvmf_request_exec处理,已和后端的bdev设备进行交互。

SPDK vfio-user client 实现

SPDK NVMe driver中也添加了针对vfio-user的支持,即给NVMe initiator端增加了一种transport,充当vfio-user client的角色。这块的实现代码主要在 “SPDK/lib/nvme”目录下,其中部分实现的接口放到了“SPDK/lib/vfio_user”目录中。

在上述代码中,nvme_vfio_user.c文件主要是像NVMe驱动的transport层注册一个新的vfio类型的通道,如SPDK_NVME_TRANSPORT_REGISTER(vfio, &vfio_ops)所示。其中实现了NVMe语义相关的各个操作接口。该函数涉及PCIe的部分语义逻辑复用了nvme_pcie_common.c中的实现。

vfio_user_pci.c文件中主要是实现了vfio-user PCIe设备配置空间和资源相关的一些逻辑,其向上给nvme_vfio_user.c文件中的函数提供接口,向下调用vfio_user.c文件中的接口与vfio-user client端进行交互。

vfio_user.c(在SPDK/lib/vfio_user目录下的文件,注意与SPDK/lib/nvmf中的相应文件进行区分)文件中则主要是实现基本的通过unix socket与vfio-user server端进行链接并作为交互通道的功能。

 此种情况下的NVMe initiator 与vfio-user server端的关系可以由图3进行描述。

Apple M1如何做虚拟化 io虚拟化_linux_03

图3. NVMe initiator 与vfio-user server端的关系示意

用户态vfio虚拟化

实现验证实践

本章节将对前边介绍的vfio实现进行验证举例。涉及的具体命令以引号和斜体标识。

vfio-user用户态实现及验证

前边已经提到,基于SPDK实现的vfio-user server侧的逻辑主要集成在NVMf Target模块中,且对接的vfio-user client端可以是Qemu,也可以是SPDK 的NVMe驱动。在这个章节,将对者两种情况进行实际验证的描述。

通过Qemu访问以vfio-user 为Transport的NVMf Target

在此种场景以Malloc类型的bdev作为后端实际的块设备资源给VM访问。执行的步骤大致可以分成三个部分:其一创建NVMf Target subsystem并添加VFIOUSER类型的listener;其二,在Qemu侧链接NVMf Target并呈现NVMe设备给Guest系统;其三,在Guest系统下对NVMe设备跑IO。

创建NVMf Target并添加VFIOUSER类型的listener

要测试vfio-user,可以从github上下载当前最新的SPDK代码(理论而言,21.01版本之后的均可以)。configure操作时,加上 “--with-vfio-user” 参数,然后再编译。编译后 “./build/bin” 目录下的nvmf_tgt既可以用来进行测试。启动NVMf Target并添加vfio-user相关transport和listener的步骤可参见如下描述,具体过程可以通过json配置文件实现,也能够以rpc命令方式执行,此处以rpc命令方式进行举例。

1.清理计划给vfio-user server使用的unix socket句柄文件路径,如使用“/var/tmp”目录,则需要确保删除目录下的相关文件:“rm -f /var/tmp/{cntrl,bar0}”

2.启动nvmf_tgt应用程序:“./build/bin/nvmf_tgt -m 0x3”

3. 创建VFIOUSER nvmf transport:“./scripts/rpc.py nvmf_create_transport -t VFIOUSER”

4.创建NVMf subsystem:“./scripts/rpc.py nvmf_create_subsystem  nqn.2022-05.io.spdk:cnode0 -a -s SPDK0”

5.创建Malloc类型的bdev(用作NVMf Target背后的块设备资源):“./scripts/rpc.py bdev_malloc_create 64 512 -b Malloc0”

6.给NVMf Target添加bdev:“./scripts/rpc.py nvmf_subsystem_add_ns nqn.2022-05.io.spdk:cnode0 Malloc0”

7.给NVMf Target添加vfio-user类型的listener:“./scripts/rpc.py nvmf_subsystem_add_listener nqn.2022-05.io.spdk:cnode0 -t VFIOUSER -a /var/tmp -s 0”

以上步骤后,拥有vfio-user类型transport的NVMf subsystem就创建完成了,可以通过rpc命令“./scripts/rpc.py nvmf_get_subsystems” 命令来获取创建的NVMf subsystem的信息,如图4所示。

Apple M1如何做虚拟化 io虚拟化_python_04

图4. 添加了vfio-user类型transport的NVMf Target信息举例

Qemu链接NVMf Target给Guset呈现NVMe设备

为了测试vfio-user的功能,需要Qemu支持vfio-user-pci的设备类型,因而需要使用包含了对应功能的Qemu版本。可以从“https://github.com/oracle/qemu”  获取对应的版本,并且编译前的配置需要加上“--enable-multiprocess”参数。具体的操作命令可以简单罗列如下。

1.下载支持vfio-user-pci设备类型的Qemu代码并编译;

1)clone 代码: “git clone https://github.com/oracle/qemu qemu-or”

2)下载依赖的代码,进入qemu-or目录后执行:“git submodule update --init --recursive”

3)配置Qemu:“./configure --enable-multiprocess”

4)编译:“ninja -C build”。//编译后生成的文件在build目录中。

2. 给Qemu虚拟机指定vfio-user-pci类型的设备,并启动;

需要指定与前边创建vfio-user 类型listener对应的unix socket句柄文件。具体命令如下:

“./qemu-system-x86_64 -cpu host -smp 8 -m 4G -object memory-backend-file,id=mem,size=4G,mem-path=/dev/hugepages,share=on -numa node,memdev=mem -drive file=/home/zj/qemu-test/fedora_34.img,if=none,id=disk -device ide-hd,drive=disk,bootindex=0 -net user,hostfwd=tcp::10020-:22 -net nic -vnc :1 --enable-kvm -device vfio-user-pci,socket=/var/tmp/cntrl

命令中“-device vfio-user-pci”开始的部分是指定了Host下NVMf Target对应vfio-user server侧的unix socket句柄文件,并且表示设备类型为vfio-user-pci类型。

Guest系统下对NVMe设备跑IO测试

虚拟机启动后可以在Guest系统下看到NVMf Target通过vfio-user通道呈现的NVMe设备,如下图5中所示。

Apple M1如何做虚拟化 io虚拟化_大数据_05

图5. 通过vfio-user方式指定给虚拟机的NVMe设备在Guest系统下的形态

以fio对虚拟机下的NVMe设备跑性能可以看到具体的性能数据如下图6中所示。

Apple M1如何做虚拟化 io虚拟化_python_06

图6. 使用fio工具在Guest系统下对以vfio-user方式是定的NVMe设备跑IO测试

通过SPDK NVMe驱动访问以vfio-user 为Transport的NVMf Target

在此种场景下,除需要启动NVMf Target进程外,还需要启动一个SPDK进程通过VFIOUSER transport访问Target侧的资源。其主要的执行步骤也可以分为:其一创建NVMf Target subsystem并添加VFIOUSER类型的listener;其二,启动另外的SPDK进程,并链接NVMf Target侧的设备并跑IO。

创建NVMf Target并添加VFIOUSER类型的listener

此步骤中的操作与前边NVMf Target通过vfio-user通道对接Qemu进程时是一样的,具体可以参考前边的步骤。但注意编译SPDK时加上“--with-host”和“--with-fio=”选项,其中fio-path是指fio源码的路径,主要是为了使用fio的头文件和库(因而需要先下载fio代码)。

启动SPDK进程以VFIOUSER tranport链接NVMf Target并跑IO

启动另外的SPDK进程模拟bare-metal应用。仍然以bdevperf为例来进行介绍,如下测试步骤可以通过配置json文件来执行,此处以rpc命令方式执行。

1. 启动bdevperf进程:“./test/bdev/bdevperf/bdevperf -r /var/tmp/spdk1.sock -q 128 -o 4096 -w randread -t 200 -S 5 -z”。   //-r参数给新的SPDK进程指定新的rpc调用访问的socket句柄对应的文件,以和vhost-blk设备创建的SPDK进程区分开来,-g参数设置与vhost进程一致,-S参数指定多久显示一次测试结果,-z参数标识启动后先不进行测试,由rpc命令来启动测试。

2. 创建基于VFIOUSER transport的NVMe设备:“./scripts/rpc.py -s /var/tmp/spdk1.sock  bdev_nvme_attach_controller -t VFIOUSER -a /var/tmp -s 0 -b nv0n1”。// 创建的bdev为nv0n1的名字。

3. 此时,可以通过rpc命令来查询通过vfio-user通道attach的NVMe设备:“./scripts/rpc.py -s /var/tmp/spdk1.sock bdev_nvme_get_controllers”

Apple M1如何做虚拟化 io虚拟化_Apple M1如何做虚拟化_07

图7.  以SPDK NVMe驱动初始化通过VFIOUSER transport访问的NVMe设备信息

4. 运行bdevperf对创建的nvme设备跑测试:“PYTHONPATH=$PYTHONPATH:./scripts/ test/bdev/bdevperf/bdevperf.py -s /var/tmp/spdk1.sock -t 200 perform_tests”

Apple M1如何做虚拟化 io虚拟化_大数据_08

图8.  对以SPDK NVMe驱动初始化的通过VFIOUSER transport访问的NVMe设备跑IO测试

参考信息

1. https://www.redhat.com/en/blog/introduction-virtio-networking-and-vhost-net 

2. https://www.youtube.com/watch?v=paTvtJ6JdAc 

3. https://www.redhat.com/en/blog/journey-vhost-users-realm 

4. https://www.redhat.com/en/blog/how-deep-does-vdpa-rabbit-hole-go 

5. https://spdk.io/doc/vhost.html

6. http://doc.dpdk.org/guides/sample_app_ug/vdpa.html

7. https://github.com/tmakatos/qemu/blob/master/docs/devel/vfio-user.rst 

8. https://github.com/nutanix/libvfio-user/blob/master/docs/spdk.md