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函数.
 
原文地址:https://www.cnblogs.com/dream397/p/14386208.html