Virtio and QEMU storage stack

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容