美文网首页KVM虚拟化
Virtio and QEMU storage stack

Virtio and QEMU storage stack

作者: goldhorn | 来源:发表于2017-11-03 23:33 被阅读0次

    virtio

    Virtio是IO虚拟化中的一个优化方案,属于para-virtulization的一种实现,即Guest OS中需要运行virtio的驱动程序,通过virtio设备和后端(KVM/QEMU)进行交互。

    Virtio设备可以视为QEMU为Guest模拟的一个PCI设备,因此可以像普通PCI设备一样配置、使用中断和DMA机制,这对设备驱动开发者来说很方便。

    Virtio 使用 virtqueue 来实现其 I/O 机制,每个 virtqueue 就是一个承载大量数据的 queue。vring 是virtqueue的具体实现方式,后面会详细介绍vring的实现。

    Virtio-blk

    QEMU为虚拟机指定一个Virtio-blk设备 ,使得Guest中能看到一个”/dev/vda”设备

    -drive file=../sdb.img,cache=none,if=virtio
    

    Virtio-blk前端驱动

    Guest系统中涉及的Virtio-blk drivers包括(按照执行的先后顺序):

    • virtio.c
      • 注册virtio_bus
    • virtio_pci.c
      • 注册pci_driver到pci总线(pci_bus_type)
      • probe函数会根据pci_dev创建virtio_pci_device,并将virtio_pci_device添加到virtio_bus
    • virtio_blk.c
      • 注册virtio_driver到virtio_bus下
      • probe函数完成virtio-blk设备具体的初始化:
        • 创建块设备"/dev/vda"及其request_queue
        • 创建和Host通信需要的virtqueue和vring

    从Linux设备驱动的框架来看,virtio-blk涉及到:

    • 两个bus:pci_bus_type, virtio_bus
    • 两个driver:virtio_pci_driver, virtio_blk
    • 两个device:pci_dev, virtio_pci_device

    Virtio-blk前端IO流程

    virtblk_probe函数中为gendisk分配了request_queue,内核从v3.13开始,virtio开始使用multi-queue。(multi-queue的设计牺牲了全局范围的request合并;认为大部分相邻的访问都集中在同一个进程,所以request只在本CPU的软件队列处理,因而不需要加锁。)


    virtio_blk

    “/dev/vda”和读写普通的磁盘一样,VFS的读写请求在到达块设备之前会经过一个漫长的旅程

    user memory  -->  page -->  buffer_head  -->  bio  -->  request
    

    最终构造成request提交给块设备的请求队列:

    submit_bh(write_op, bh);
        submit_bio(rw, bio);
            generic_make_request 
                q->make_request_fn(q, bio);  /* blk_sq_make_request */
                    blk_mq_run_hw_queue 
                        __blk_mq_run_hw_queue 
                            q->mq_ops->queue_rq   /* virtio_queue_rq */
    

    对于一个读写请求,最终需要交给后端的信息有:

    • page/offset/len Guest的物理内存地址
    • sector 虚拟块设备的地址
    • type 读还是写
    virtio_queue_rq()
        blk_rq_map_sg
            __blk_bios_map_sg
        __virtblk_add_req(vblk->vqs[qid].vq, vbr, vbr->sg, num);
            sg_init_one(&hdr, &vbr->out_hdr, sizeof(vbr->out_hdr))
            sgs[num_out + num_in++] = data_sg;
                virtqueue_add_sgs(vq, sgs, num_out, num_in, vbr, GFP_ATOMIC)
                    virtqueue_add            /* 将sg填入到vring中去 */
                        desc[i].addr = sg_phys(sg);
                        desc[i].len = sg->length;
        virtqueue_kick_prepare
        virtqueue_notify(vblk->vqs[qid].vq);
    

    我们可以看到向vring中写了多个scatterlist:

    • out_hdr 用来向后端描述这次请求,包括type, sector, ioprio
    • Data 一个或者多个Guest OS的一个物理地址
    • Status Guest OS准备好的一个字节,后端在IO完成后填写


      image.png

    写完vring之后通过virtqueue_notify来通知QEMU

    virtqueue_notify
        vq->notify(_vq)     <--  vp_notify  
        iowrite16(vq->index, vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY)
    

    其实质是Guest写io寄存器,从而触发VM exit到KVM中处理,KVM检查退出的返回值,无法处理就一步步返回到最初的入口kvm_vcpu_ioctl,然后返回到用户态也就是QEMU进程空间。

    Vring

    vring

    Vring由一个freelist和两个ring组成:

    desc数组构造了一个freelist,每一片里存放着Guest和Host之间传输的数据:

    • addr/len Guest的物理地址和长度
    • flags next是否有效?读 or 写? INDIRECT ?
    • next

    avail->ring[]是发送端(Guest)维护的环形队列,指向需要host处理的desc(一次用了多片desc,但ring[]里只写入了一个idx;这多片desc通过链表组织起来)

    used->ring[]是接收端(Host/QEMU)维护的环形队列,指向自己已经处理过了的desc

    • 发送端(Guest)更新
      • vring.avail->idx
      • vring_virtqueue.free_head,它指向desc数组里freelist的头
      • vring_virtqueue.last_used_idx,它表示Guest下一次检查used ring[]的位置
    • Host更新
      • vring.used->idx
      • VirtQueue.last_avail_idx,它表示Host下一次检查avail ring[]的位置
    • 这四个计数会一直递增下去

    QEMU

    KVM退出到QEMU之后进入kvm_handle_io函数,通过write eventfd将等待在ppoll系统调用上的QEMU的主线程唤醒

    int kvm_cpu_exec(CPUArchState *env)
    {
        do {
            run_ret = kvm_vcpu_ioctl(env, KVM_RUN, 0);
            switch (run->exit_reason) { /* Qemu根据退出的原因进行处理 */
            case KVM_EXIT_IO:
                kvm_handle_io();
                ...
    

    main线程处理vring的主要流程:调用vq的回调函数,从vring中读取Guest的物理地址,并转化为自己的虚拟地址后构造成QEMU的request

    main()  main_loop() main_loop_wait ()
        os_host_main_loop_wait()
            glib_pollfds_poll()
                g_main_context_dispatch () 
                    aio_ctx_dispatch    aio_dispatch
                        virtio_queue_host_notifier_read
                            virtio_queue_notify_vq 
                                virtio_blk_handle_output
    

    Vring的处理函数

    Vring注册的处理函数virtio_blk_handle_output,从vring中读取请求,然后构造成QEMU的request,然后创建协程,在协程中完成IO的提交。


    处理vring

    QEMU协程

    如果指定了aio=native

    -drive if=none,id=drive0,cache=none,aio=native,format=qcow2,file=path/to/disk.img \
    -device virtio-blk,drive=drive0,scsi=off
    

    那么IO主流程和协程的交互过程大致如下图所示:


    协程

    要理解协程,上图有几个关键跳转需要注意:

    1. 原线程调用qemu_coroutine_enter进入协程;
    2. 协程submit_io后通过qemu_coroutine_yield直接“退出”协程,返回到原线程调用enter处,而不是“返回”到调动yield处,此时协程的代码逻辑是没有执行完的;原线程可以继续在循环中创建新的协程来不断的提交io;
    3. io完成后main_loop中再次调用qemu_coroutine_enter再次进入协程,协程的代码逻辑好像是调用yield返回一样,然后开始执行yield之后的代码,一步步返回到上层函数;
    4. 协程调用blk_aio_complete

    QEMU block driver

    上图协程的部分里的回调函数需要关注

    • 在协程的IO栈里bdrv_aligned_preadv被调用了两次,但两次调用drv->bdrv_co_readv是不一样的,第一次的drv是bdrv_qcow2,第二次的drv是bdrv_file
    • 对于本例中的块设备IO,QEMU协程中实际上分了两步:QCOW2处理和file处理,分别对应两个struct BlockDriverState,它们有不同的drv
    • bs->drv->bdrv_aio_readv,这是不同drv提交IO的函数,对于本地文件系统就是raw_aio_submit,最终选择io_submit或者pread/pwrite系统调用;而对于其它类型的存储,比如Ceph rbd就参考bdrv_rbd中的实现。

    如果qemu参数没有指定aio=native,那么协程中将会使用线程池来模拟异步IO,paio_submit会从线程池中找一个worker线程,然后在worker线程中调用pread/pwrite:

    | start_thread 
    |     worker_thread 
    |         req->func(req->arg)        /*  aio_worker  */
    |             handle_aiocb_rw
    |                 handle_aiocb_rw_linear
    |                     pwrite/pread      /* syscall */
    |         qemu_bh_schedule
    |             aio_notify(ctx)            /* 写main_loop中阻塞的fd */
    

    main_loop线程被qemu_bh_schedule唤醒之后:

    | main_loop  -- > glib_pollfds_poll -- > thread_pool_completion_bh -- > ...
    |     bdrv_co_io_em_complete        < -- 调用drv->bdrv_aio_readv时指定的回调函数
    |         qemu_coroutine_enter(co->coroutine, NULL)
    |             qemu_coroutine_switch        /* 再次进入协程 */
    

    对于不同的BlockBackend,其对应的BlockDriver也不相同,我们需要的就是实现自己的BlockDriver中的各种函数,比如. bdrv_file_open和.bdrv_aio_readv

    Vhost

    Virtio-vring实现了一套Guest和Host之间基于PCI设备的标准接口,同时将原来多次的IO寄存器的访问改为vring的读写,从而减少了VM Exit和Resume的次数。

    但是Virtio避免不了Host上内存的拷贝:
    QEMU仍然是一个普通的进程,QEMU也需要通过syscall发起IO请求,Host内核正常情况下会将数据读/写到内核的page中,然后从内核page拷贝到QEMU的虚拟地址中。

    Vhost可以实现Guest和Host Kernel直接进行数据交换,从而避免syscall和数据拷贝的性能消耗。

    vhost和kvm是两个独立的运行模块,用户态程序通过“/dev/vhost-net”来访问,对于Guest来说,vhost并没有模拟一个完整的PCI适配器。它内部只涉及了virtqueue-vring的操作,而virtio设备的适配模拟仍然由Qemu来负责。

    vhost与kvm的事件通信通过eventfd机制来实现,主要包括两个方向的event,一个是Guest到Vhost方向的kick event,通过ioeventfd承载;另一个是Vhost到Guest方向的call event,通过irqfd承载。

    相关文章

      网友评论

        本文标题:Virtio and QEMU storage stack

        本文链接:https://www.haomeiwen.com/subject/eegxmxtx.html