VIRTIO后端框架QEMU与VHOST分析

        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<----继承device
    VirtIONet 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));
        }
        else
            virtio_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: 0xc0000000
         guest physical addr: 0x0
         guest virtual  addr: 0x7fff00000000
         host  virtual  addr: 0x2aaac0000000
         mmap addr : 0x2aaac0000000
         mmap size : 0xc0000000
         mmap align: 0x40000000
         mmap 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

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值