VIRTIO设备的前端是GUEST的内核驱动,后端由QEMU或者DPU实现。不论是原来的QEMU-VIRTIO框架还是现在的DPU,VIRTIO的控制面和数据面都是相对独立的设计。本文主要针对QEMU的VIRTIO后端进行分析。

控制面负责GUEST前端和VIRTIO设备的协商流程,主要用于前后端的feature协商匹配、向GUEST提供后端设备信息、建立前后端之间的数据通道。等控制面协商完成后,数据面启动前后端的数据交互流程;后面的流程中控制面负责一些配置信息的下发和通知,比如block设备capacity配置、net设备mac地址动态修改等。

QEMU负责设备控制面的实现,而数据面由VHOST框架接管。VHOST又分为用户态的vhost-user和内核态的vhost-kernel路径,前者是由用户态的dpdk接管数据路径,将数据从用户态OVS协议栈转发,后者是由内核态的vhost驱动接管数据路径,将数据从内核协议栈发送出去。本文主要针对vhost-user路径,以net设备为例进行描述。

一、QEMU后端驱动

如果要顺利的看懂QEMU后端VIRTIO驱动框架,需要具备QEMU的QOM基础知识,在这个基础上将数据结构、初始化流程理清楚,就可以更快的上手。如果只是对VIRTIO相关的设计感兴趣,可直接看下一章原理性的内容。

QEMU设备管理是非常重要的部分,后来引入了专门的设备树管理机制。而其参照了C++的类、继承的一些概念,但又不是完全一致,对于非科班出身的作者阅读起来有些吃力。因为框架相关的代码中时常使用内部数据指针cast的一些宏定义,非常影响可读性。

1、VIRTIO设备创建流程

从实际的命令行示例入手,查看设备是如何创建的。

1、virtio-net-pci设备命令行

首先从QEMU的命令行入手,创建一个使用virtio设备的虚拟机,可使用如下命令行:

gdb --args ./x86_64-softmmu/qemu-system-x86_64 \-machine accel=kvm -cpu host -smp sockets=2,cores=2,threads=1 -m 3072M \-object memory-backend-file,id=mem,size=3072M,mem-path=/dev/hugepages,share=on \-hda /home/kvm/disk/vm0.img -mem-prealloc -numa node,memdev=mem \-vnc 0.0.0.0:00 -monitor stdio --enable-kvm \-netdev type=tap,id=eth0,ifname=tap30,script=no,downscript=no -device e1000,netdev=eth0,mac=12:03:04:05:06:08 \-chardev socket,id=char1,path=/tmp/vhostsock0,server \-netdev type=vhost-user,id=mynet3,chardev=char1,vhostforce,queues=$QNUM -device virtio-net-pci,netdev=mynet3,id=net1,mac=00:00:00:00:00:03,disable-legacy=on

其中,创建一个虚拟硬件设备,都是通过-device来实现的,上面的命令行中创建了一个virtio-net-pci设备

-device virtio-net-pci,netdev=mynet3,id=net1,mac=00:00:00:00:00:03,disable-legacy=on

这个硬件设备的构造依赖于qemu框架里的netdev设备(并不会独立的对guest呈现)

-netdev type=vhost-user,id=mynet3,chardev=char1,vhostforce,queues=$QNUM

上面的netdev设备又依赖于qemu框架里的字符设备(同样不会独立的对guest呈现)

-chardev socket,id=char1,path=/tmp/vhostsock0,server

2、命令行解析处理

QEMU的命令行解析在main函数进行,解析后按照qemu标准格式存储到本地。然后通过qemu_find_opts(“”)接口可以获取本地结构体中具有相应关键字的所有命令列表,对解析后的命令列表使用qemu_opts_foreach依次执行处理函数。

常用的用法,比如netdev的处理,qemu_find_opts找到所有的netdev的命令列表,qemu_opts_foreach则对列表里的所有元素依次执行net_init_netdev,初始化相应的netdev结构。

int net_init_clients(Error **errp)
{QTAILQ_INIT(&net_clients);if (qemu_opts_foreach(qemu_find_opts("netdev"),net_init_netdev, NULL, errp)) {return -1;}return 0;
}

net_init_netdev初始化函数中,根据type=vhost-user,执行相应的net_init_vhost_user函数进行初始化,并为每个队列创建一个NetClientState结构,用于后续socket通信。

对于"-device"参数的处理也是采用同样的方式,依次执行device_init_func,初始化相应的DeviceState结构。

    if (qemu_opts_foreach(qemu_find_opts("device"),device_init_func, NULL, NULL)) {exit(1);}

-device后跟的第一个参数qemu称为driver,其实就是根据不同的设备类型(我们的场景为“virtio-net-pci")匹配不同的处理。而device采用的是通用的设备类,根据驱动的名字在device_init_func函数里调用qdev_device_add()接口,然后匹配到相应的DeviceClass(就是virtio-net-pci对应的DeviceClass)。

匹配到DeviceClass后,调用class里的instance_init接口,创建相应的实例,即DeviceState。

备注:看到了DeviceClass和DeviceState,这个是QEMU设备管理框架里的重要元素。在QEMU设备管理框架中,

1)Class后缀表示一类方法实现,是相应设备类型的一类实现,对于同一设备类型的多个设备是通用的,不管创建几个virtio-pci-net设备,只需要一份VirtioPciClass。

2)State后缀表示具体的instance实体,每创建一个设备都要实例化一个instance结构。创建和初始化这个结构是由object_new()接口完成的,初始化还会调用相应的类定义的instance_init()接口。

Breakpoint 2, virtio_net_pci_instance_init (obj=0x5555575b8740) at hw/virtio/virtio-pci.c:3364
3364    {
(gdb) bt
#0  0x0000555555ab0c10 in virtio_net_pci_instance_init (obj=0x5555575b8740) at hw/virtio/virtio-pci.c:3364
#1  0x0000555555b270bf in object_initialize_with_type (data=data@entry=0x5555575b8740, size=<optimized out>, type=type@entry=0x5555563c3070) at qom/object.c:384
#2  0x0000555555b271e1 in object_new_with_type (type=0x5555563c3070) at qom/object.c:546
#3  0x0000555555b27385 in object_new (typename=typename@entry=0x5555563d2310 "virtio-net-pci") at qom/object.c:556
#4  0x000055555593b5c5 in qdev_device_add (opts=0x5555563d22a0, errp=errp@entry=0x7fffffffddd0) at qdev-monitor.c:625
#5  0x000055555593db17 in device_init_func (opaque=<optimized out>, opts=<optimized out>, errp=<optimized out>) at vl.c:2289
#6  0x0000555555c1ab6a in qemu_opts_foreach (list=<optimized out>, func=func@entry=0x55555593daf0 <device_init_func>, opaque=opaque@entry=0x0, errp=errp@entry=0x0) at util/qemu-option.c:1106
#7  0x00005555557d85d6 in main (argc=<optimized out>, argv=<optimized out>, envp=<optimized out>) at vl.c:4593

3、设备实例初始化

在qdev_device_add函数中,首先会调用object_new,创建object(object是所有instance实例的根结构),最终是通过调用每个virtio-pci-net相应DeviceClass里的instance_init创建实例。

static void virtio_net_pci_instance_init(Object *obj)
{VirtIONetPCI *dev = VIRTIO_NET_PCI(obj);virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev),TYPE_VIRTIO_NET);object_property_add_alias(obj, "bootindex", OBJECT(&dev->vdev),"bootindex");
}

VirtioNetPci结构体中包含其父类的实例VirtIOPCIProxy,其拥有的设备框架自定义的结构是VirtIONet的实例。对于netdev来说,它也利用了qemu的class和device框架,但netdev不像-device一样通过框架的qdev_device_add接口调用object_new完成。他的数据空间跟随在virtio_net_pci的自定义结构里,然后通过virtio_instance_init_com接口显式的调用object_initialize()函数实现“virtio-net-device”的instance初始化。

struct VirtIONetPCI {VirtIOPCIProxy parent_obj;  //virtio-pci类<----继承pci-device<----继承deviceVirtIONet vdev;    //virtio-net<----继承virtio-device<----继承device
};

4、virtio-net-pci设备realize流程

qdev_device_add接口中,还会调用realize接口,前面的instance_init只是实例的简单初始化,真实的设备相关的具体初始化动作都是从设备realize之后进行的。也就是相应class的realize接口。

首先在qdev_device_add()接口中,置位设备的realized属性,进而调用每一层class的realize函数。大家想一下,类似于内核驱动,设备肯定按照协议的分层从下向上识别的,先识别pci设备,然后是virtio,进而识别到virtio-net设备。所以qemu的识别过程也是这样,从最底层的realize层层调用至上层的realize接口。

参照VirtIO的Class结构,整个realize的流程整理如下:

在初始化的过程中,对数据结构进行一一初始化。

在pci设备的realize之前插入virtio_pci_dc_realize函数的原因是,如果是modern模式的pci设备必须是pci-express协议,所以需要置位PCIDevice里的pcie-capability标识,强行令pci设备为pcie类型。然后再进行pci层的设备初始化,初始化一个pcie设备。

virtio_pci_realize接口对VirtioPCIProxy数据结构进行了初始化,是virtio+pci需要的初始化。所以初始化了virtio设备的bar空间以及需要的pcie-capability。

virtio_net_pci_realize接口主要是触发VirtIONet里的VirtIODevice的realize流程,这才是virtio设备的realize流程(virtio_device_realize接口)。

virtio_device_realize接口实现调用了virtio_net_device_realize,对于特定virtio设备(net类型)的初始化都是在这里进行的。所以这部分是对VirtIONet及其包裹的VirtIODevice数据结构进行初始化,包括VirtIODevice结构里的vq指针就是在这里根据队列个数动态申请空间的。

virtio_device_realize接口还执行了virtio_bus_device_plugged接口,这是virtio总线上的virtio设备的plugged接口,这部分内容脱离了virtio-pci框架,进入到更上层的virtio框架。但virtio_bus派生了virtio_pci_bus,virtio_pci_bus将继承的virtio_bus的接口都设置成了自己的接口。所以最终还是调用了virtio-pci下的virtio_pci_device_plugged函数。

virtio_pci_device_plugged接口是最核心的初始化接口,modern模式初始化pci设备的bar空间读写操作接口,因为分多块读写,所以还引入了memory_region,然后添加相应的capability;legacy模式初始化pci的bar空间读写操作接口。

至此virtio设备的初始化流程完成,等待与host的接口操作。

2、VIRTIO设备实现

上一节结合qemu的设备框架模型,分析了qemu从命令行到virtio设备创建的处理流程。现在设备创建出来了,是如何与host进行交互和操作的。本节主要讲述这部分内容,明确一点,所有的数据和操作接口都会汇聚到一个结构体,设备创建过程中的instance实例就承载了我们这个设备的所有数据和ops,所以分析这个VirtIONetPCI结构及其衍生辐射的其他数据就可以了。

struct VirtIONetPCI {VirtIOPCIProxy parent_obj;    //VIRTIO-PCI数据VirtIONet vdev;    //VIRTIO-NET数据
};

对于VirtIOPCIProxy,选取比较重要的部分摘抄如下:

struct VirtIOPCIProxy {PCIDevice pci_dev;MemoryRegion bar; struct {VirtIOPCIRegion common;VirtIOPCIRegion isr;VirtIOPCIRegion device;VirtIOPCIRegion notify;};MemoryRegion modern_bar;MemoryRegion io_bar;uint32_t msix_bar_idx;bool disable_modern;uint32_t nvectors;uint32_t guest_features[2];VirtIOPCIQueue vqs[VIRTIO_QUEUE_MAX];VirtIOIRQFD *vector_irqfd;
};

其实控制面主要就是用于协商操作,而这部分操作是通过bar空间的读写实现的。所以对virtio-pci来说,bar空间的读写操作接口是主线的流程,其余的数据结构都是围绕这组读写操作接口展开。

对于legacy设备,bar0用于virtio设备协商的空间。bar0在VirtIOPCIProxy中对应的是bar结构。bar0的读写接口对应virtio_pci_config_ops。

memory_region_init_io(&proxy->bar, OBJECT(proxy),&virtio_pci_config_ops,proxy, "virtio-pci", size);static const MemoryRegionOps virtio_pci_config_ops = {.read = virtio_pci_config_read,.write = virtio_pci_config_write,.impl = {.min_access_size = 1,.max_access_size = 4,},.endianness = DEVICE_LITTLE_ENDIAN,
};

对于modern设备,采用了更灵活的方式,将virtio设备协商的空间分成4个(common、isr、device、notify)区间,每个区间的bar index和bar内offset由pci设备的capability指定。VIRTIO前端驱动解析capability识别不同的区间,进而与设备同步地址区间。

    memory_region_init_io(&proxy->common.mr, OBJECT(proxy),&common_ops,proxy,"virtio-pci-common",proxy->common.size);memory_region_init_io(&proxy->isr.mr, OBJECT(proxy),&isr_ops,proxy,"virtio-pci-isr",proxy->isr.size);memory_region_init_io(&proxy->device.mr, OBJECT(proxy),&device_ops,virtio_bus_get_device(&proxy->bus),"virtio-pci-device",proxy->device.size);memory_region_init_io(&proxy->notify.mr, OBJECT(proxy),&notify_ops,virtio_bus_get_device(&proxy->bus),"virtio-pci-notify",proxy->notify.size);

如上述代码所示,common区间、isr区间、device区间、notify区间的操作接口分别是common_ops、isr_ops、device_ops、notify_ops。

注册完上述ops后,GUEST对设备的bar空间进行访问,会进入相应的操作接口。

VIRTIO控制面的初始化流程具体可以参见协议或者作者其他文章,文中主要是对代码的实现框架梳理,摘抄legacy模式的write接口部分内容示例说明。后端设备根据前端驱动写入的地址和数据进行相应的处理。

static void virtio_ioport_write(void *opaque, uint32_t addr, uint32_t val)
{VirtIOPCIProxy *proxy = opaque;VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);hwaddr pa;switch (addr) {case VIRTIO_PCI_GUEST_FEATURES:/* Guest does not negotiate properly?  We have to assume nothing. */if (val & (1 << VIRTIO_F_BAD_FEATURE)) {val = virtio_bus_get_vdev_bad_features(&proxy->bus);}virtio_set_features(vdev, val);break;case VIRTIO_PCI_QUEUE_PFN:pa = (hwaddr)val << VIRTIO_PCI_QUEUE_ADDR_SHIFT;if (pa == 0) {virtio_pci_reset(DEVICE(proxy));}elsevirtio_queue_set_addr(vdev, vdev->queue_sel, pa);break;case VIRTIO_PCI_QUEUE_NOTIFY:if (val < VIRTIO_QUEUE_MAX) {virtio_queue_notify(vdev, val);}break;case VIRTIO_PCI_STATUS:if (!(val & VIRTIO_CONFIG_S_DRIVER_OK)) {virtio_pci_stop_ioeventfd(proxy);}virtio_set_status(vdev, val & 0xFF);if (val & VIRTIO_CONFIG_S_DRIVER_OK) {virtio_pci_start_ioeventfd(proxy);}if (vdev->status == 0) {virtio_pci_reset(DEVICE(proxy));}break;default:error_report("%s: unexpected address 0x%x value 0x%x",__func__, addr, val);break;}
}

VIRTIO_PCI_QUEUE_PFN字段是写入queue地址的,对于legacy模式,avail/used ring以及descriptor的空间连续,所以传入连续空间的首地址即可,实际传入的是页帧号。virtio_queue_set_addr接口负责将前端驱动传入的GPA记录到VirtioDevice->vq结构。

重点关注一下VIRTIO_PCI_STATUS,该位置置位VIRTIO_CONFIG_S_DRIVER_OK,说明前端驱动操作完成,可启动virtio-net设备的数据面操作。所以这个位置很重要,在virtio_set_status()接口中,会启动数据面的一系列操作,包括对VHOST的配置也是由这里触发。

上图中就是从pcie---->virtio---->virtio-net----->vhost_net的调用关系。在设备初始化完成后,后续的操作都是由guest操作bar空间来触发,尤其是流程的推动都是对bar空间的写操作触发的。比如VIRTIO_COFNIG_S_DRIVER_OK的写入。

二、QEMU与VHOST接口描述

前一节末尾已经看到了pci-->virtio--->virtio-net---->vhost_net的调用关系。本节首先对QEMU里VHOST的部分进行介绍。

1、vhost-user类型netdev设备创建

上一章描述了virtio-net-pci设备的创建过程,本节就不再对设备的创建流程进行详细分析。在1.1节我们知道指定vhost设备的命令行如下,是根据type=vhost-user定义:

-netdev type=vhost-user,id=mynet3,chardev=char1,vhostforce,queues=$QNUM

设备的命令行处理流程是统一的,也是在main函数里首先会解析命令行参数到本地的数组,然后进入标准的设备创建流程(具体可见1.2节):

    if (qemu_opts_foreach(qemu_find_opts("netdev"),net_init_netdev, NULL, errp)) {return -1;}

所以netdev的设备创建入口就是net_init_netdev函数,在net_init_netdev函数里,根据type=vhost-user类型,执行net_init_vhost_user()接口创建设备。

该接口首先执行net_vhost_claim_chardev(),匹配其依赖的chardev,也就是命令行中的char1,找到相应的Chardev*设备。

然后调用net_vhost_user_init()接口进行真实的初始化操作。最终,vhost-user类型的netdev设备生成的结构实体是什么呢?

最终NetClientState被挂载到了全局变量net_clients链表中。另外,还有一组数据结构是在检测到vhost后端socket连接之后创建的。socket连接之后会调用上图中的事件处理函数net_vhost_user_event,进而调用vhost_user_start()接口。

上面就是vhost-user的net设备申请的数据结构和框架,其中vhost_net中vhost_dev结构有一个重要的vhost_ops指针,对后端的接口都在这里。前面提到过vhost的后端有用户态和内核态两种,所以VhostOps的实现也有两种,因为我们指定了vhost_user类型的后端,实际vhost_ops会初始化为user_ops。VhostOps是与后端类型相关的不同的处理方式。

2、QEMU与VHOST通信

qemu与vhost的通信是通过socket的方式。具体的实现都在vhost-user的backend接口里,以VhostOps的封装形式提供给virtio_net层使用。

1、通信接口

上一章最后提到,VIRTIO_PCI_STATUS标识控制面协商的状态,当VIRTIO_CONFIG_S_DRIVER_OK置位时,说明控制面协商完成,此时启动数据面的传输。所以VHOST层面的交互是从这个状态启动的。而该状态通过virtio_set_status()接口调用到virtio_net_set_status()和virtio_net_vhost_status(),判断是第一次VIRTIO_CONFIG_S_DRIVER_OK标识置位,则此时认为需要启动数据面了,会调用vhost_net_start()接口启动与VHOST后端的交互,对于数据面来说,最主要的是队列相关的信息。如下图示。

其实VhostOps里的每个接口根据名字都可以直观的推断出实现的作用,比如

vhost_set_vring_num是设置队列的大小,

vhost_set_vring_addr是设置ring的基地址(GPA),

vhost_set_vring_base设置last_avail_index,

vhost_set_vring_kick是设置队列kick需要的eventfd信息。

这里重点说一下vhost_set_mem_table接口,我们知道qemu里获取的guest的地址都是GUEST地址空间的,一般是GPA,那么qemu在创建虚拟机的时候是知道GPA到HVA的映射关系的,所以根据GPA可以轻松获得HVA,进而操作进程地址空间的VA即可。但作为独立的进程,VHOST-USER拥有独立的地址空间,是不可以使用QEMU进程的VA的。所以QEMU需要将自己记录的一组或多组[GPA,HVA,size,memfd]通知给VHOST-USER。VHOST_USER收到这几个信息后将这片内存空间通过mmap映射到本地进程的地址空间,然后一并记录到记录到本地的memory table。

mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,MAP_SHARED | populate, reg->fd, 0);

对于为何可以使用qemu里的文件句柄fd,具体原理没有深究,看起来内核是允许这样一种共想文件句柄的方式的。注意需要在qemu的命令行里指定memory share=on的方式就可以。

 -object memory-backend-file,id=mem,size=3072M,mem-path=/dev/hugepages,share=on

等后续接收到vring address的配置时,VHOST-USER就通过传入的GPA,查表找到memory table里的表项,获取自有进程空间的VA,就可以访问真实的地址空间了。

2、数据通信格式

具体的数据通信格式来说,QEMU与VHOST-USER的消息通过socket的方式,QEMU和VHOST-USER一个作为client,另一个作为server。socket文件路径是在qemu命令行里传入的。

数据通信的格式是固定的,都是一个标准的header后面跟着payload。

typedef struct {VhostUserRequest request;uint32_t flags;uint32_t size; /* the following payload size */
} QEMU_PACKED VhostUserHeader;typedef union {uint64_t u64;struct vhost_vring_state state;struct vhost_vring_addr addr;VhostUserMemory memory;VhostUserLog log;struct vhost_iotlb_msg iotlb;VhostUserConfig config;VhostUserCryptoSession session;VhostUserVringArea area;
} VhostUserPayload;typedef struct VhostUserMsg {VhostUserHeader hdr;VhostUserPayload payload;
} QEMU_PACKED VhostUserMsg;

其中request字段是一个union类型,标识具体的命令类型,所有的命令类型见下图。

typedef enum VhostUserRequest {VHOST_USER_NONE = 0,VHOST_USER_GET_FEATURES = 1,VHOST_USER_SET_FEATURES = 2,VHOST_USER_SET_OWNER = 3,VHOST_USER_RESET_OWNER = 4,VHOST_USER_SET_MEM_TABLE = 5,VHOST_USER_SET_LOG_BASE = 6,VHOST_USER_SET_LOG_FD = 7,VHOST_USER_SET_VRING_NUM = 8,VHOST_USER_SET_VRING_ADDR = 9,VHOST_USER_SET_VRING_BASE = 10,VHOST_USER_GET_VRING_BASE = 11,VHOST_USER_SET_VRING_KICK = 12,VHOST_USER_SET_VRING_CALL = 13,VHOST_USER_SET_VRING_ERR = 14,VHOST_USER_GET_PROTOCOL_FEATURES = 15,VHOST_USER_SET_PROTOCOL_FEATURES = 16,VHOST_USER_GET_QUEUE_NUM = 17,VHOST_USER_SET_VRING_ENABLE = 18,VHOST_USER_SEND_RARP = 19,VHOST_USER_NET_SET_MTU = 20,VHOST_USER_SET_SLAVE_REQ_FD = 21,VHOST_USER_IOTLB_MSG = 22,VHOST_USER_SET_VRING_ENDIAN = 23,VHOST_USER_GET_CONFIG = 24,VHOST_USER_SET_CONFIG = 25,VHOST_USER_CREATE_CRYPTO_SESSION = 26,VHOST_USER_CLOSE_CRYPTO_SESSION = 27,VHOST_USER_POSTCOPY_ADVISE  = 28,VHOST_USER_POSTCOPY_LISTEN  = 29,VHOST_USER_POSTCOPY_END     = 30,VHOST_USER_MAX
} VhostUserRequest;

三、VHOST-USER框架设计

VHOST-USER是DPDK一个重要的功能,主要的代码在lib/librte_vhost下。

VHOST的框架还是比较直接的,整体看起来DPDK的代码框架比QEMU的简洁很多,更加容易理解。DPDK主要是作为一个接口库使用,所以lib下面也是对外提供操作接口,供调用方使用。

1、VHOST初始化流程

VHOST模块初始化流程的关键接口有三个:

1、rte_vhost_driver_register(const char *path, uint64_t flags)

1)申请并初始化vhost_user_socket结构,vsocket指针存入全局数组vhost_user.vsockets[]中;

2)打开path对应的socket文件,fd存入vsocket->socket_fd中。

2、rte_vhost_driver_callback_register(const char *path, struct vhost_device_ops const * const ops)

1)注册vhost_device_ops到vsocket->ops中。

3、rte_vhost_driver_start(const char *path)

根据VHOST是client或者server,选择vhost_user_start_client()/vhost_user_start_server()两种路径,看一下vhost作为client的路径。vhost_user_start_client():

1)vhost_user_connect_nonblock,连接socket;

2)vhost_user_add_connection(int fd, struct vhost_user_socket *vsocket)

a、申请vhost_user_connection结构体;

b、申请virtio_net结构体,存储到全局数组vhost_devices,返回其在数组中的索引vid;

c、conn配置,conn->fd =fd; conn->vsocket=vsocket; conn->vid =vid;

d、注册socket接口的处理函数

fdset_add(&vhost_user.fdset, fd, vhost_user_read_cb, NULL, conn);

整个初始化流程到这里就结束了,后面就开始等待qemu的消息,对应的消息处理函数就是vhost_user_read_cb函数。

2、VHOST与QEMU通信流程

初始化流程创建了vsocket结构(与qemu的socket实体),创建了virtio_net结构(设备实体),并注册了socket消息的处理函数,后面就可以监听socket的信息,作为qemu的后端完善和建立整个流程。作为virtio设备的backend,vhost在初始化完成后,所有的动作都是由前端的socket消息触发的。

所有的消息处理函数以MSG的request请求类型字段作为索引,记录在vhost_message_handlers数组中。具体的处理不详细描述,主要就是将接收到的信息记录到本地,供数据面启动后使用。

本文贴一篇实际的VHOST和QEMU通信的数据流日志,1queue pair。

VHOST_CONFIG: /tmp/vhostsock0: connected
VHOST_CONFIG: new device, handle is 0
VHOST_CONFIG: read message VHOST_USER_GET_FEATURES
VHOST_CONFIG: read message VHOST_USER_GET_PROTOCOL_FEATURES
VHOST_CONFIG: read message VHOST_USER_SET_PROTOCOL_FEATURES
VHOST_CONFIG: negotiated Vhost-user protocol features: 0xcbf
VHOST_CONFIG: read message VHOST_USER_GET_QUEUE_NUM
VHOST_CONFIG: read message VHOST_USER_SET_SLAVE_REQ_FD
VHOST_CONFIG: read message VHOST_USER_SET_OWNER
VHOST_CONFIG: read message VHOST_USER_GET_FEATURES
VHOST_CONFIG: read message VHOST_USER_SET_VRING_CALL
VHOST_CONFIG: vring call idx:0 file:53
VHOST_CONFIG: read message VHOST_USER_SET_VRING_CALL
VHOST_CONFIG: vring call idx:1 file:54
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG: set queue enable: 1 to qp idx: 0
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG: set queue enable: 1 to qp idx: 1
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG: set queue enable: 1 to qp idx: 0
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG: set queue enable: 1 to qp idx: 1
VHOST_CONFIG: read message VHOST_USER_SET_FEATURES
VHOST_CONFIG: negotiated Virtio features: 0x140408002
VHOST_CONFIG: read message VHOST_USER_SET_MEM_TABLE
VHOST_CONFIG: guest memory region 0, size: 0xc0000000guest physical addr: 0x0guest virtual  addr: 0x7fff00000000host  virtual  addr: 0x2aaac0000000mmap addr : 0x2aaac0000000mmap size : 0xc0000000mmap align: 0x40000000mmap off  : 0x0
VHOST_CONFIG: read message VHOST_USER_SET_VRING_NUM
VHOST_CONFIG: read message VHOST_USER_SET_VRING_BASE
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ADDR
VHOST_CONFIG: reallocate vq from 0 to 1 node
VHOST_CONFIG: reallocate dev from 0 to 1 node
VHOST_CONFIG: read message VHOST_USER_SET_VRING_KICK
VHOST_CONFIG: vring kick idx:0 file:56
VHOST_CONFIG: read message VHOST_USER_SET_VRING_CALL
VHOST_CONFIG: vring call idx:0 file:57
VHOST_CONFIG: read message VHOST_USER_SET_VRING_NUM
VHOST_CONFIG: read message VHOST_USER_SET_VRING_BASE
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ADDR
VHOST_CONFIG: reallocate vq from 0 to 1 node
VHOST_CONFIG: read message VHOST_USER_SET_VRING_KICK
VHOST_CONFIG: vring kick idx:1 file:53
VHOST_CONFIG: virtio is now ready for processing.
VHOST_DATA: (0) device has been added to data core 1
VHOST_CONFIG: read message VHOST_USER_SET_VRING_CALL
VHOST_CONFIG: vring call idx:1 file:58
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG: set queue enable: 1 to qp idx: 0
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG: set queue enable: 1 to qp idx: 1
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG: set queue enable: 1 to qp idx: 0
VHOST_CONFIG: read message VHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG: set queue enable: 1 to qp idx: 1
VHOST_DATA: liufeng TX virtio_dev_tx_split: dev(0) queue_id(1), soc_queue(1)

从上面的日志中找到一条消息“virtio is now ready for processing”,说明从这里开始VHOST启动数据面的收发。查看上面的日志,是在两个队列的地址信息配置完成之后。对应到QEMU的流程,就是QEMU里的VIRTIO设备device_status的VIRTIO_CONFIG_S_DRIVER_OK状态标志置位时,调用virtio_net_start()接口做了这个动作。

至此,整个数据通道完全建立。

一般的使用场景是在OVS添加一个端口,关联到刚刚分析的VHOST-USER(指定与qemu的socket-path),实现对GUEST网口的数据收发。VHOST-USER作为client的场景,可以使用如下的命令行建立一个端口。

ovs-vsctl add-port br0 vhost-client-1 \-- set Interface vhost-client-1 type=dpdkvhostuserclient \options:vhost-server-path=$VHOST_USER_SOCKET_PATH

VIRTIO后端框架QEMU与VHOST分析相关推荐

  1. virtio后端驱动详解

    virtIO是一种半虚拟化驱动,广泛用于在XEN平台和KVM虚拟化平台,用于提高客户机IO的效率,事实证明,virtIO极大的提高了VM IO 效率,配备virtIO前后端驱动的情况下,客户机IO效率 ...

  2. virtio虚拟化框架

    文章目录 virtio虚拟化 virtio基本原理 virtio架构层次 virtio前端驱动 virtio层 virtio-ring层 virtio后端驱动 virtio数据流交互机制 参考 vir ...

  3. 2022CTF培训(十三)虚拟化QEMU架构分析QEMU CVE示例分析

    附件下载链接 虚拟化技术基本概念 硬件虚拟化 全虚拟化 提供可以完全模拟基础硬件的VME 可以在VM中运行任何能够在物理硬件上执行的软件,并且可以在每个单独的VM中运行基础硬件支持的任何OS 为每个V ...

  4. qemu 线程 vhost

    [root@localhost cloud_images]# lsmod | grep vhost_net vhost_net 262144 0 vhost 262144 1 vhost_net ta ...

  5. 应用服务器与WSGI协议以及flask后端框架总结(后端接收请求返回响应的整个流程)...

    上次遗留了两个问题,先说一下自己的看法 问题: 1.明明一个线程只能处理一个请求,那么栈里的元素永远是在栈顶,那为什么需要用栈这个结构?用普通变量不行吗. 2._request_ctx_stack和_ ...

  6. 视频教程-RPC服务框架(Dubbo)源码分析-Java

    RPC服务框架(Dubbo)源码分析 鲁班学院-子路老师曾就职于谷歌.天猫电商等多家互联网公司,历任java架构师.研发经理等职位,参与并主导千万级并发电商网站与后端供应链研发体系搭建,多次参与电商大 ...

  7. WebApi后端框架Token身份认证,Api接口Token验证

    令牌概述(Token) 在以用户账号体系作为安全认证的信息系统中,对用户身份的鉴定是非常重要的事情. 令牌机制是软件系统安全体系中非常重要的部分,在计算机身份认证中是令牌的意思,一般作为邀请.登录以及 ...

  8. 一文搞定7大流行后端框架:Spring、Netty、MyBatis、Hibernate、Dubbo...

    框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法:另一种定义认为,框架是可被应用开发者定制的应用骨架.前者是从应用方面而后者是从目的方面给出的定义. 可 ...

  9. 若依前后端分离版源码分析-前端头像上传后传递到后台以及在服务器上存储和数据库存储设计

    场景 使用若依前后端分离版本时,分析其头像上传机制. 可作为续参考学习. 注: 博客: https://blog.csdn.net/badao_liumang_qizhi 关注公众号 霸道的程序猿 获 ...

最新文章

  1. 新书预告《网络规划设计师考试考点分析与真题详解》
  2. 光纤收发器的分类介绍
  3. 目前个人计算机使用的电子电路主要是,计算机基础知识理论题库.xls
  4. R语言学习笔记(二)处理函数与基本图形绘制
  5. 全球最大语音识别公司Nuance的衰落与自我救赎
  6. 【Hadoop Summit Tokyo 2016】中型组织的数据基础设施架构:收集、存储和分析的技巧...
  7. 用putty玩linux的时候由于以前用window 习惯写完东西按一下ctrl+s 保存
  8. An App ID with Identifier 'com.XXX.XXX’ is not available. Please enter a different string.报错
  9. php unset引用变量后不会删除值
  10. mips 系统桥_系统功能调用实现简单输入输出(mips)
  11. 用java处理speex编码/解码
  12. 计讯物联山海步道安全管理与运维解决方案
  13. 使用useState因异步导致数据不能及时更新的问题
  14. 聊聊并发 生产者消费者模式
  15. spd耗材管理流程图_医用耗材SPD管理模式详解
  16. 荣耀电脑怎样更改计算机名称,王者荣耀荣耀战区怎么修改别的地区?手机电脑改荣耀战区不封号操作方法[多图]...
  17. 解决stackoverflow打开慢的问题
  18. ICV:2022上半年全球车用CMOS图像传感器出货量超18亿美元
  19. WIN10出现“PL2303HXA自2012已停产,请联系供货商“解决办法
  20. 矩形波如何傅立叶展开_音箱固定钣金焊接件如何制作图纸及展开

热门文章

  1. CDH大数据平台搭建之FLUME安装
  2. node插件rimraf的使用和扩展,批量删除多个项目中的node_modules
  3. 有7克、2克砝码各一个,天平一只,如何只用这些物品三次将140克的盐分成50、90克各一份
  4. 黄油刀 butterKnife
  5. 元宇宙星系粒子小圆点穿梭logo片头pr模板
  6. 今晚直播!池建强、二爷邱岳、左耳朵耗子、朱赟的技术夜话
  7. C++11花括号初始化
  8. 图标网站收集(ICO、PNG)
  9. 好业绩与好心态爱相随
  10. 华为IPsec预共享密钥配置命令汇总