• QEMU中VIRTIO实现


    http://39.107.46.219/qemu%E8%99%9A%E6%8B%9F%E5%8C%96%E5%AE%89%E5%85%A8%EF%BC%88%E4%BA%8C%EF%BC%89/

    VIRTIO设备

    ​ 了解QEMU和KVM交互的知道,客户机的IO操作通过KVM处理后再交由QEMU,反馈也如此。这种纯软件的模拟IO设备,增加了IO的延迟。

    ​ 而Virtio却为虚拟化的IO提供了另一种解决方案:

    Virtio在虚拟机系统内核安装前端驱动,在QEMU中实现后端驱动。前后端驱动通过Virtqueue直接通信,从而绕过了KVM内核模块处理,提高了IO操作性能。

    QEMU中VIRTIO实现

    启动配置设备
    -device virtio-scsi-pci
    

    在虚拟机里查看scsi设备lspci

    Bm5Sf0.png

    可以看到Virtio-pci设备的相关信息:IO/PORT: 0xc040 (size=64),MemoryAddress: 0xfebf1000(size=4k)

    Virtqueue

    ​ Virtio使用Virtqueue实现IO机制,每个Virtqueue就是承载大量数据的queue。vring是Virtqueue实现的具体方式;virtio_ring是virtio传出机制的实现,vring引入ving buffer作为数据的载体。

    struct VirtQueue
    {
        VRing vring;
        /* Next head to pop */
        uint16_t last_avail_idx;
    
        /* Last avail_idx read from VQ. */
        uint16_t shadow_avail_idx;
    
        uint16_t used_idx;
    
        /* Last used index value we have signalled on */
        uint16_t signalled_used;
    
        /* Last used index value we have signalled on */
        bool signalled_used_valid;
    
        /* Notification enabled? */
        bool notification;
    
        uint16_t queue_index;
    
        int inuse;
    
        uint16_t vector;
        void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq);   // handle output
        void (*handle_aio_output)(VirtIODevice *vdev, VirtQueue *vq);
        VirtIODevice *vdev;
        EventNotifier guest_notifier;
        EventNotifier host_notifier;
        QLIST_ENTRY(VirtQueue) node;
    };
    
    vring
    typedef struct VRing
    {
        unsigned int num;       // 
        unsigned int num_default;
        unsigned int align;
        hwaddr desc;            // 关联描述符数组 (buffer的描述)
        hwaddr avail;           // 表示客户机可用的描述符
        hwaddr used;            // 表示宿主机已经使用的描述符
    } VRing;
    Vring Descriptor
    typedef struct VRingDesc
    {
        uint64_t addr;  // 指向guest端的物理地址, 一组buffer列表
        uint32_t len;   // buffer长度
        uint16_t flags; // 包含 3 个值,分别是 VRING_DESC_F_NEXT(1)、
                        // VRING_DESC_F_WRITE(2)、VRING_DESC_F_INDIRECT(4);
        uint16_t next;  //指向下一个描述符的index(链表结构)
    } VRingDesc;
     

    ​ 由一组描述符构成描述符表

    Available Vring
    typedef struct VRingAvail
    {
        uint16_t flags;
        uint16_t idx;  // 指向下一描述符表的入口
        uint16_t ring[0]; // 每一个值是一个索引,指向描述符表中的一个可用描述符
    } VRingAvail;
    
    VRingUsedElem
    typedef struct VRingUsedElem
    {
        uint32_t id;
        uint32_t len;
    } VRingUsedElem;
    VRingUsed
    typedef struct VRingUsed
    {
        uint16_t flags;
        uint16_t idx;
        VRingUsedElem ring[0];
    } VRingUsed;
    Virtqueue初始化(在Qemu端实现)
    VirtQueue *virtio_add_queue(VirtIODevice *vdev, int queue_size,
                                void (*handle_output)(VirtIODevice *, VirtQueue *))
    {                           //每个Device 维护一组Virtqueue
        int i;
    
        for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {    
            if (vdev->vq[i].vring.num == 0)
                break;
        }
    
        if (i == VIRTIO_QUEUE_MAX || queue_size > VIRTQUEUE_MAX_SIZE) 
            abort();                        // 每个Device最多1024Virtqueue
                                            // 每个Virtqueue最多1024 vring
        vdev->vq[i].vring.num = queue_size; // 初始化vring.num
        vdev->vq[i].vring.num_default = queue_size; // 初始化vring.num_default
        vdev->vq[i].vring.align = VIRTIO_PCI_VRING_ALIGN; //初始化vring.align
        vdev->vq[i].handle_output = handle_output;  // 初始化handle_output
        vdev->vq[i].handle_aio_output = NULL;   // handle_aio_output
    
        return &vdev->vq[i];
    }
     

    ​ 在Guest端,virtio驱动中vm_setup_vq建立与queue对应的Virtqueue

    num = readl(vm_dev->base + VIRTIO_MMIO_QUEUE_NUM_MAX);// 获取vring.num
    
    // vring_create_virtqueue
    queue = vring_alloc_queue(vdev, vring_size(num, vring_align),
                          &dma_addr, GFP_KERNEL|__GFP_ZERO);// 分配Virtqueue空间
    
    //vring_size计算方式
    static inline unsigned vring_size(unsigned int num, unsigned long align)
    {
        return ((sizeof(struct vring_desc) * num + sizeof(__virtio16) * (3 + num)
             + align - 1) & ~(align - 1))
            + sizeof(__virtio16) * 3 + sizeof(struct vring_used_elem) * num;
    }

    ​ 从这里可以看出来vring的内存布局

    Bm59pV.png

    ​ 接着Guest virtio驱动通知Qemu Queue的vring.num

    writel(virtqueue_get_vring_size(vq), vm_dev->base + VIRTIO_MMIO_QUEUE_NUM);
    
    unsigned int virtqueue_get_vring_size(struct virtqueue *_vq)
    {
        struct vring_virtqueue *vq = to_vvq(_vq);
        return vq->vring.num;
    }
    
     
    Guest向虚拟设备提供buffer

    在virtio驱动virtqueue_add实现

    // buffer空间 DMA方式分配
    dma_addr_t addr = vring_map_one_sg(vq, sg, DMA_TO_DEVICE);
    // 填充desc表 flags addr len
    desc[i].flags = cpu_to_virtio16(_vq->vdev, VRING_DESC_F_NEXT);
    desc[i].addr = cpu_to_virtio64(_vq->vdev, addr);
    desc[i].len = cpu_to_virtio32(_vq->vdev, sg->length);
    
    //更新可用ring头
    /* Put entry in available array (but don't update avail->idx until they
         * do sync). */
    avail = vq->avail_idx_shadow & (vq->vring.num - 1);
    vq->vring.avail->ring[avail] = cpu_to_virtio16(_vq->vdev, head);
    
    //更新可用ring  index
    vq->avail_idx_shadow++;
    vq->vring.avail->idx = cpu_to_virtio16(_vq->vdev, vq->avail_idx_shadow);
    
    //当Virtqueue添加次数达到64k时,flush vring内容到QEMU
    if (unlikely(vq->num_added == (1 << 16) - 1))
        virtqueue_kick(_vq);
    
    bool virtqueue_kick(struct virtqueue *vq)
    {
        if (virtqueue_kick_prepare(vq))
            // 修改 virtqueue notify 寄存器
            return virtqueue_notify(vq);
        return true;
    }
    
     
    虚拟设备使用Buffer
        offset = 0;
    while (offset < size) { 
            //从desc表中寻找available ring中添加的buffers,映射内存
            elem = virtqueue_pop(vrng->vq, sizeof(VirtQueueElement));
    
            if (!elem) {
                break;
            }
            // 读取内容
            len = iov_from_buf(elem->in_sg, elem->in_num,
                               0, buf + offset, size - offset);
            // 更新读取光标
            offset += len;
            virtqueue_push(vrng->vq, elem, len);
            trace_virtio_rng_pushed(vrng, len);
            g_free(elem);
        }
    
    void virtqueue_push(VirtQueue *vq, const VirtQueueElement *elem,
                        unsigned int len)
    {   // 取消内存映射,跟新usedVring字段
        virtqueue_fill(vq, elem, len, 0);
        virtqueue_flush(vq, 1);
    }
     
    QEMU-GUEST交互

    所有设备的i/o操作都经由virtio_ioport_write处理

    static void virtio_ioport_write(void *opaque, uint32_t addr, uint32_t val){
        .....
        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:  // addr = 8
            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_SEL:  // addr = 14
            if (val < VIRTIO_QUEUE_MAX)
                vdev->queue_sel = val;  // 更新Virtqueue handle_output 序号 
            break;
        case VIRTIO_PCI_QUEUE_NOTIFY:   // addr = 16
            if (val < VIRTIO_QUEUE_MAX) {
                virtio_queue_notify(vdev, val); //根据val序号 触发Virtqueue的描述符表
            }
            break;
            }
    }
     

    其中addr是相对于ioport端口地址的偏移, val是写入的数据。

    ​ 在该函数下断点,运行到vdev被初始化后

    outl(0xaa, 0xc040+0x10)     // module_init执行
    

    ​ 断下的状态

    Bm5km4.png

    可以看到有三种handle_output:ctrl, event, cmd

    而我们handle_output被触发的路径

    virtio_ioprt_write ==> virtio_queue_notify(vdev, val) ==> virtio_queue_notify_vq(&vdev->vq[n]) ==> 
    
    // 触发VirtQueue->Handle_Output
    static void virtio_queue_notify_vq(VirtQueue *vq)
    {
        if (vq->vring.desc && vq->handle_output) {
            VirtIODevice *vdev = vq->vdev;
    
            trace_virtio_queue_notify(vdev, vq - vdev->vq, vq);
            vq->handle_output(vdev, vq);
        }
    }
     
    简单的I/O交互示例
      VRingDesc *desc1;
        req * buffer;
        VRingAvail *avail;
        VRingUsed *used1;
        unsigned long mem;
        mem = kmalloc(0x3000, GFP_KERNEL);//align
        memset(mem,0,0x3000);
        //vring的内存布局
        desc1 = mem;
        // 因为设备默认最大有0x80个描述符表,一个描述符的大小为0x10
        // qemu实现中把avail表接在了描述符表之后,因此avail表=desc+0x80*0x10;`
        avail = mem + 0x800;
        // 而一个avail结构体为0x2*0x80+4=>0x104,而qemu做了一个4k对齐操作,因此变成了+0x1000
        used1 = mem + 0x1000;
    
        // 初始化desc
        desc1[0].addr = (u64)virt_to_phys(buffer);
        desc1[0].len = (u32)0x33;   // buffer的大小
        desc1[0].flags = (u16)0x2;  // VRING_DESC_F_WRITE,因为没有VRING_DESC_F_NEXT标志,表示没有                                // 下一个描述符
        desc1[0].next = (u16)0x2;   //这个字段无效了
    
        // buffer为scsi定义的结构体,详见virtio-scsi.h的99行
        buffer = kmalloc(sizeof(req) * SIZE, GFP_KERNEL);
        buffer->cmd.cdb[0] = 0x28;
        buffer->cmd.lun[0] = 0x0;   //0x1
        buffer->cmd.lun[1] = 0x0;
        buffer->cmd.lun[2] = 0x0;   //0x40
    
        //初始一个avail表
        avail->idx = 0;
        avail->ring[0] = 0x0;
    
        // I/O 交互
        queue_sel(2);// 设定命令类型为2,代表 virtio_scsi_handle_cmd
        queue_pfn(mem>>12);// 设定描述符表
        queue_notify(2);// 触发virtio_scsi_handle_cmd函数.
     
  • 相关阅读:
    mysql 慢查询分析工具
    php+redis实现消息队列
    Mysql数据库千万级数据查询优化方案.....
    windows下安装docker详细步骤
    Git基础使用教程
    redis实现消息队列&发布/订阅模式使用
    macos上改变输入法顺序
    ssh动态转发小记
    ubuntu上runsv/runit小记
    使用libcurl下载https地址的文件
  • 原文地址:https://www.cnblogs.com/dream397/p/14386208.html
Copyright © 2020-2023  润新知