基于virtio的virtio-blk是KVM-Qemu虚拟化生态中的虚拟化块存储的一种实现方式,利用了virtio共享内存的机制,提供了一种高效的块存储挂载的方法。Guest OS内核通过加载virtio-blk驱动,实现块存储的读写,无需额外的厂家专用驱动。Virtio-blk设备在虚拟机以一个磁盘的方式呈现,是目前应用最广泛的虚拟存储控制器。如下是qemu所模拟的PC(基于intel i440fx主板架构)的组成结构图。
qemu模拟实现的virtio-blk设备的组成结构如下图所示:
从图中可见,virtio-blk设备“内嵌”在一块PCI设备板(即virtio-blk-pci设备)上,其内部通过一条virtio总线连接PCI接口和virtio-blk设备。为何要将virtio-blk设备设计成这样呢?
qemu模拟的所有设备都通过总线相连,总线下可挂接若干设备,桥接设备又可生成子总线;整个PC只有一条根总线(即Main System Bus,对应前端总线FSB)。因此,qemu内模拟的所有设备构成一棵总线与设备交替衍生的树。virito-blk是一种什么样的设备?又该连接在什么总线上呢?虽然virtio-blk仅在虚拟化环境下存在,但如果完全凭空创造一种新的设备类型,那前端驱动开发将是一个很大的挑战。PCI设备是PC中最为常见的一种设备类别,且有较为完善的规范说明,因此可将virtio-blk设备模拟成一种PCI设备,这样可复用虚拟机内部已有的PCI驱动。
virtio-blk设备从功能上来看,核心功能就是实现虚拟机内外的事件通知和数据传递:虚拟机内部的前端驱动准备好待处理的IO请求和数据存放空间并通知后端;虚拟机外部的后端程序获取待处理的请求并交给真正的IO子系统处理,完成后将处理结果通知前端。实际上,除了虚拟磁盘,虚拟网卡也完全可以复用这套机制,从而实现半模拟的网络前后端(virtio-net)。如果将virtio-blk或virtio-net设计成不同类型的PCI设备,那么前端驱动中会存在大量关于事件通知和数据传递的重复代码。
综上分析,virtio-blk首先是PCI设备;其次为了复用半模拟中通用的事件通知和数据传递机制,抽象出一类virtio-pci设备,其内部通过virtio总线连接不同的virtio设备。这样virtio-blk设备就通过virtio总线连接到virtio-blk-pci设备的PCI接口上,virtio-net也通过virtio总线连接到virtio-net-pci设备的PCI接口上。可能有的人会问,为何通过设备的抽象就能复用前端驱动的代码?在virtio-blk-pci或virtio-net-pci前端驱动加载时,最初识别到的都是virtio-pci设备,这样都可调用virtio-pci驱动进行事件通知和数据传递的初始化,后续也可使用virtio-pci中相关函数进行事件通知和数据传递。
因此virtio-blk完全是基于通用的virtio框架实现的磁盘前后端,virtio框架中最为核心的就是事件通知和数据传递机制。
基于vDPA架构的virtio-blk硬件卸载
由于virtio机制通过硬件实现加速已经是通用做法,所以利用这个优势,virtio-blk卸载到硬件,已经是必然趋势。在智能网卡中,将virtio-blk到后端映射到如NVMe-oF的远端磁盘上,这样相比较当前virtio-blk的用法,不需要在主机系统中挂载很多的远端NVMe磁盘,由智能网卡直接完成映射,更加安全。
在2021年KVM论坛会议中,Redhat提出统一软硬件卸载virtio-blk方案,正式将virtio-blk加入vDPA框架,同virtio-net公用相同的框架,来完成硬件卸载控制平面。
virtio-blk和virtio-scsi的用户态驱动代码在 “SPDK/module/bdev/virtio” 目录中,其主要处理对象是 “Virtio block device” 和 “Virtio SCSI” 的pci controller,将virtio pci controller初始化成spdk中的spdk_bdev设备对外提供服务。virtio-blk和virtio-scsi的设备的代码流程和前述的virtio-net的驱动流程类似。具体以virtio-blk的流程梳理概述如下:
响应rpc bdev_virtio_attach_controller命令pci/blk类型操作的入口函数是bdev_virtio_pci_blk_dev_create。该函数执行中调用了spdk_pci_device_attach(其中主要是通过DPDK的rte_eal_hotplug_add接口来实现)来执行pci设备匹配。当给定pci地址的设备扫描添加成功后,会回调指定的回调函数virtio_pci_dev_probe来完成设备合法性判断、bar空间映射、基本操作函数集添加等公共初始化步骤。
2. 创建virtio_blk描述设备,并完成virtio设备层级和blk层级的初始化;
在virtio_pci_dev_probe函数执行的末尾,会回调在bdev_virtio_pci_blk_dev_create中设置的回调函数 bdev_virtio_pci_blk_dev_create_cb,并在其中完成virtio_blk设备的创建和初始化。这个初始化的过程也基本可以分成两个部分:
1) 在virtio_pci_dev_init中完成virtio设备的基本初始化,如feature协商、virtio设备基本操作函数集(如modern_ops)添加、virtqueue创建及与back-end设备同步等;
2) 在virtio_blk_dev_init中创建并注册bdev到SPDK框架,让virtio设备对外呈现为bdev提供服务。其中指定的bdev的操作函数集virtio_fn_table可以响应SPDK应用层的读写及配置命令与back-end设备交互。
SPDK应用主要通过呈现的bdev对virtio 设备进行访问,IO读写及控制类接口与其它普通bdev无异,其后台由前述的virtio_fn_table中的函数来进行转化处理。
需要注意的是,在上述流程中,会以virtio_blk描述结构为io_device通过spdk_io_device_register函数进行注册,以便当SPDK应用程序执行IO操作时能够通过设置的回调函数bdev_virtio_blk_ch_create_cb,在各个使用的CPU核上将bdev的io_channel和virtqueue(vring)关联起来。
同时,在BSC(Big Spring Canyon , 一种基于FPGA实现的Intel Smart NIC)上实现的blk-net的设备本质上也是virtio_blk设备的一种特殊形态,其代码逻辑和上述描述的过程大体对应。
对于virtio-blk的用户态front-end驱动的使用,可以通过SPDK的test程序bdevperf来进行。比如可以通过json配置文件或者rpc命令指定要使用的virtio block device的pci addr信息及对应创建的spdk_bdev设备名称,然后对该bdev设备跑测试。具体的使用在本文后续章节会进行描述。
virtio-blk-user/virtio-scsi-user的驱动代码存在于 “SPDK/module/bdev/virtio” 目录中。也即在SPDK代码中,处理pci controller和直接对接vhost-user的virtio front-end 驱动的实现是在一起的,以不同的分支存在。若以virtio-blk-user驱动为例,可以将其相关流程概述如下:
响应rpc bdev_virtio_attach_controller命令user/blk类型操作的入口函数是bdev_virtio_user_blk_dev_create。该函数执行中通过调用virtio_user_blk_dev_create来创建virtio_blk_dev并进行初始化。主要的初始化步骤由virtio_user_dev_init和virtio_blk_dev_init这两部分完成,前者执行virtio 相关的初始化如与back-end设备建立unix socket链接等,后者完成blk设备相关的初始化如创建并注册bdev设备。
2. virtio_user_dev_init函数中初始化了virtio back-end设备相关的操作函数集virtio_user_ops以及用户态驱动和back-end进行交互的virtio_user_backend_ops的函数集ops_user,并与back-end设备建立链接;
3. virtio_blk_dev_init完成了virtqueue的初始化,创建并注册bdev到SPDK框架,让virtio设备对外呈现为bdev提供服务。其中指定的bdev的操作函数集virtio_fn_table可以响应SPDK应用层的读写及配置命令与back-end设备交互。
4. SPDK应用程序通过bdev块设备层的访问接口对块设备进行IO及控制类操作。
SPDK应用主要通过呈现的bdev对virtio 设备进行访问,IO读写及控制类接口与其它普通bdev无异,其后台由前述的virtio_fn_table中的函数来进行转化处理。
virtio-blk-user/virtio-scsi-user驱动可以通过SPDK的test程序bdevperf来进行验证,其实用方式与前述pci controller模式的virtio-blk/virtio-scsi用户态驱动类似,只是注意指定-t的参数为user即可。具体步骤可以参考后续验证章节的例子描述。
vhost-blk/vhost-scsi back-end设备的实现代码在 “SPDK/lib/vhost” 目录下。其主要是以spdk中可见的bdev作为实际载体,将其attach到vhost blk/scsi controller共同作为virtio back-end设备对外提供服务。以vhost-blk为例,可以将其实现流程基本概述如下:
1.响应rpc命令创建并初始化vhost_blk 设备;
SPDK中vhost-blk的功能以rpc方式显示调用,响应的函数为spdk_vhost_blk_construct。在其中,会创建spdk_vhost_blk_dev,并获取指定的spdk_bdev设备的访问句柄供后续响应、处理virtio front-end驱动的请求时使用。同时在在vhost_blk 设备 vhost_dev_register -> vhost_user_dev_register -> vhost_register_unix_socket的调用中执行前述的DPDK库提供的vhost的3个基本的初始化函数。
2.接收来自virtio front-end驱动的消息,建立front-end和back-end的关联;
当virtio front-end驱动的建链请求被DPDK库中的函数处理后,vhost-blk侧即可以接收并处理控制消息。如前述,函数vhost_user_msg_handler在处理接收到的消息时,第一次当对应的front-end的virtio_net设备(vhost-blk基于DPDK的vhost框架实现,在DPDK库中back-end 设备与每个front-end驱动的链接对应一个virtio_net的数据结构,并以此跟踪标识后续的消息)还未置位VIRTIO_DEV_RUNNING标志,则会执行vhost_user_msg_handler -> start_device -> vhost_blk_start -> vhost_blk_start_cb的调用流程的调用流程,并在其中分配用于IO处理的task_pool及注册poller用于处理virtqueue中的数据(poller的处理函数为vdev_worker)。该过程执行完成后即会置位VIRTIO_DEV_RUNNING标志。
3.在注册的poller函数中处理virtqueue的队列数据;
对于每个从virtqueue的vring中取出的IO数据会被归集在spdk_vhost_blk_task数据结构中,由函数spdk_vhost_blk_task提交到vhost-blk关联的spdk bdev设备进行处理。
SPDK代码中,vhost的app程序由 “SPDK/app/vhost” 目录下的文件编译,默认即会编译。其使用参考见SPDK: vhost Target。
在DPDK代码 “DPDK/examples/vhost-blk” 目录下,同样有一个vhost-blk的逻辑实现,其工作流程与SPDK中的vhost-blk的基本一致,主要的区别点在于在后端实际处理IO数据的对应bdev设备实现时没有经过SPDK bdev的框架,且目前代码中仅实现了一种通过申请的内存模拟的bdev类型。DPDK代码中vhost-blk的测试程序可以通过meson配置编译时加上“-Dexpamples=vhost_blk”参数来生成,使用说明可以参考35. Vhost_blk Sample Application — Data Plane Development Kit 22.07.0-rc1 documentation。
virtio-blk简介_sdulibh的博客-CSDN博客
DPU应用场景系列(二) 存储功能卸载-中科驭数(北京)科技有限公司-电子发烧友网
SPDK virtio 驱动模块介绍及使用_weixin_37097605的博客-CSDN博客
用户态虚拟化IO通道实现概览及实践(上)_weixin_37097605的博客-CSDN博客
用户态虚拟化IO通道实现概览及实践(下)_weixin_37097605的博客-CSDN博客
virtio-blk原理_whutyuxinghai的博客-CSDN博客
virtio-scsi和virtio-blk的理解_yongwan5637的博客-CSDN博客_virtio-blk virtio-scsi