KVM虚拟化

Virtio and QEMU storage stack

2017-11-03  本文已影响0人  goldhorn

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包括(按照执行的先后顺序):

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

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 */

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

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:

写完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之间传输的数据:

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

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

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

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


如果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承载。

上一篇下一篇

猜你喜欢

热点阅读