• virtio简介(五)—— virtio_blk设备分析


    一: 创建过程关键函数

    1. virtblk_probe

      虚拟机在启动过程中,virtio bus上检测到有virtio块设备,就调用probe函数来插入这个virtio block设备(前端创建的virtio设备都是PCI设备,因此,在对应的virtio设备的probe函数调用之前,都会调用virtio-pci设备的probe函数,在系统中先插入一个virtio-pci设备)。

      初始化设备的散列表,从简介(一)的流程图我们知道,系统的 IO请求会先映射到散列表中。

      

      virtio_find_single_vq为virtio块设备生成一个vring_virtqueue。

      

      这个函数通过调用在virtio-pci中为virtio device定义的OPS,find_vqs就跳转到vp_find_vqs,之后调用vp_try_to_find_vqs。

      

       

      在函数vp_try_to_find_vqs中,setup_vq为virtio设备创建vring_virtqueue队列。

     

    2. vring_virtqueue

      每个virtio设备都有一个virtqueue接口,它提供了一些对vring进行操作的函数,如add_buf,get_buf,kick等,而vring_virtqueue是virtqueue及vring的管理结构。我们在virtio设备中保存virtqueue指针,当要使用它操作vring时,通过to_vvq来获得其管理结构vring_virtqueue。

     

      Vring_virtqueue的数据结构如下所示:

       

      Vring,是IO请求地址的真正存放空间(virtio block设备在初始化的时候,会申请了两个页大小的空间)。num_free,表示vring_desc表中还有多少项是空闲可用的;free_head,表示在vring_desc中第一个空闲表项的位置(并且系统会将其余空闲表项通过vring_desc的next串联成一个空闲链表);num_added,表示我们在通知对端进行读写的时候,与上次通知相比,我们添加了多少个新的IO请求到vring_desc中;last_used_idx,表示vring_used表中的idx上次IO操作之后被更新到哪个位置(与当前的vring_used->idx相减即可获得本次QEMU处理了多少个vring_desc表中的数据)。

    3. setup_vq

      

      通过to_vp_device将virtio_device转换成virtio-pci,我们在前端虚拟机内创建的virtio设备都是一个pci设备,因此可以利用PCI设备的配置空间来完成前后端消息通知,vp_dev->ioaddr就指向配置空间的寄存器集合的首地址。

      

      iowrite写寄存器VIRTIO_PCI_QUEUE_SEL来通知QEMU端,当前初始化的是第index号vring_virtqueue;ioread则从QEMU端读取vring_desc表,共有多少项(virtio block设备设置为128项)。

      

      根据之前ioread获得的表项数来确定vring共享区域的大小,并调用alloc_pages_exact在虚拟机里为vring_virtqueue分配内存空间。

       

      virt_to_phys(info->queue) >> VIRTIO_PCI_QUEUE_ADDR_SHIFT将虚拟机的虚拟机地址转换成物理地址,偏移VIRTIO_PCI_QUEUE_ADDR_SHIFT(12位)得到页号。

      iowrite将vring_virtqueue在虚拟机内的物理页号写到寄存器VIRTIO_PCI_QUEUE_PFN,产生一个kvm_exit,QEMU端会捕获这个exit,并根据寄存器的地址将这个物理页号赋值给QEMU端维护的virtqueue。

      

    4. Vring_new_virtqueue 

       

      参数pages,是在setup_vq中给vring_virtqueue申请的物理内存页地址;参数num,是在setup_vq中通过ioread获得的vring_desc表的表项数目;vring_align,为4096,表示一个页的大小;notify,是virtio-pci注册的函数vp_notify,当要通知qemu端取vring中的数据时,就调用notify函数;callback,是qemu端完成IO请求返回后,前端处理的回调函数,virtio-blk的回调函数就是blk_done。

    二: QEMU获取VRING地址

      在1.3节中,提到了virtio_map函数注册了对PCI配置空间寄存器的监听函数,当虚拟机产生kvm_exit时,会根据exit的原因将退出数据分发,IO请求会被发送到这些监听函数,他们会调用virtio_ioport_write/read确定前端读/写了哪个寄存器,触发何种动作。

    virtio_ioport_write对应前端的iowrite操作,virtio_ioport_read对应前端的ioread操作。

    1. virtio_ioport_write

      virtio_ioport_write函数根据我们iowrite的地址来区分写了哪个寄存器,要执行之后的哪些操作。

      对于vring这个数据区域的共享,前端虚拟机在分配物理页之后,调用

      

      来通知后端QEMU进程

      因此,我们可以看到在函数中:

      从配置空间对应的位置获得前端写入的物理页号(客户机的物理页)。

    2. virtio_queue_set_addr

      该函数将QEMU进程维护的virtio设备的virtqueue地址初始化,使得前端和后端指向同一片地址空间。(由于KVM下虚拟机是一个QEMU进程,因此虚拟机的内存是由QEMU进程来分配的,并且在QEMU进程内有mem_slot链表进行维护, QEMU进程知道了虚拟机创建的VRING的GPA就可以通过简单的转换在自己的HVA地址中找到VRING的内存地址)。

    3. virtqueue_init

      

      将QEMU进程内的vring中的三个表的地址初始化。

    三:完整的读写流程

    四. 前端写请求(Guest kernel)

    1. do_virtblk_request

      

      内核将IO request队列传递给该函数来处理,在while循环中将队列里的request取出,递交给do_req来做具体的请求处理,每处理一个请求issued就加1。

      从while循环中退出时,若issued不等于0,就表示有IO request被处理过,调用virtqueue_kick通知对端QEMU。

     2. do_req

      该函数主要动作如下:

      1)        获取IO request中的磁盘扇区信息;

      

      2)        第一次调用sg_set_buf,将磁盘请求的扇区信息存在virtio device的散列表中;

       

      3)        调用blk_rq_map_sg,将request中的数据地址存至散列表里;

      

      4)        第二次调用sg_set_buf,将本次请求的状态等额外信息存至散列表末尾;

      

      5)        调用virtqueue_add_buf,将散列表中存储的信息映射至vring数据结构内。

      

     3. blk_rq_map_sg

      将以此IO request的地址映射到virtio device的散列表(scatterlist),返回值是散列表被填充的数目。

      Linux系统中磁盘IO请求的数据结构是如下所示的:

      

      因此,IO请求的具体内容是存储在bio_vec->bv_page这个页的bio_vec->bv_offset位置。

       

      这个函数是一个循环,从request中依次取出bio_vec,并保存在bvec变量中。

      之后调用sg_set_page将bio_vec结构体中的数据赋值给virtio device的散列表。

     

      该函数的具体操作如下:

      

      在散列表中设置一次请求的结尾标志:

      

      因此,virtio块设备的散列表组织是这样的,第一项是IO请求的磁盘扇区信息,中间是具体的IO请求在内存中的地址,最后一项为结束标记位。

    4.  virtqueue_add_buf_gfp

      之前,我们通过blk_rq_map_sg将request中的请求地址存至散列表scatterlist中,通过add_buf可以将散列表中的请求地址存至vring数据结构中。

      

      通过to_vvq,获得virtqueue(这个virtqueue是每个virtio device的成员)的管理结构vring_virtqueue。

      有了vring_virtqueue后我们的vring数据结构如下所示:

      

      freehead指向了空闲链表的头结点。

      在virtqueue_add_buf_gfp中对vq加锁,防止其他线程来操作vring这个数据结构。

      

      

      判断vring_desc中是否有足够的空闲空间来保存本次IO  request中需要添加的数据项(out+in),如果没有足够的空间就调用notify函数来通知对端,进行读写操作消耗掉vring中的数据。

       

      num_free域进行更新,减去本次要添加到vring_desc表中的数据项,head指向当前表项中的第一个空闲位置。

       

      在这个for循环中,sg_phys函数将散列表中的sg->page_link和sg->offset相加,获得IO请求的一个具体物理内存地址并存至vring.desc.addr中。同时设置flags为VRING_DESC_F_NEXT表示本次IO请求的数据还未结束,每个IO请求都有可能在vring_desc表中占据多项,通过next域将他们连接成一个链表的形式。prev指向vring_desc添加的最后一项表项,i指向下一个空闲表项。

       

      循环退出后,prev指向的最后一项表项的flags域置为非next,表示这次IO请求到该项结束。更新free_head为第一个空闲表项,即i。

       

      vring->avail是与vring_desc对应的一张表,它指示vring_desc中有哪些表项是新添加的数据项。

      vring.avail->ring中依次存放着每个IO请求在vring_desc组成的链表的表头位置,在for循环开始之前我们将head赋值为free_head,然后在for循环中我们从free_head所指向的位置开始添加数据,因此本次IO请求在vring_desc中组成的链表的表头就是head所指向的位置。

      vring.avail->idx是16位长的无符号整数,它指向的是vring->avail.ring这个数组下一个可用位置,将在virtqueue_kick函数中更新这个域。

      对vring_virtqueue更新完之后,用END_USE(vq)对其锁进行释放。

    5. virtqueue_kick

      

      对vring_virtqueue加锁,之后更新vring.avail->idx域,它指向的是vring.avail->ring数组中下一个可用位置(即第一个空闲位置)。对端qemu程序可以取得vring这个数据结构,然后从vring.avail->ring[]中获得每一个request的链表头位置,而vring.avail->idx指示了当前哪些数据是前端设备驱动新更新的。

      

      调用vring_virtqueue的notify函数来通知对端,这个函数在vring_virtqueue初始化的时候用virtio-pci定义的vp_notify对其注册的,因此调用切换到vp_notify:

      

      调用iowrite向PCI配置空间的相应寄存器写入通知前端的消息。

      queue_index表示前端将IO请求的数据存在哪个vring中(对于磁盘设备只有一个vring,因此是0;对于virtio-net设备有2-3个vring,这就要区分是读的vring还是写的vring)。

      vdev->ioaddr是virtio设备初始化的时候赋值,指向PCI配置空间寄存器集的首地址位置,VIRTIO_PCI_QUEUE_NOTIFY是对应寄存器的偏移位置,配置空间的寄存器如下图所示分布:

       

      iowrite会引发kvm_exit,这样通过kvm这个内核模块退出到qemu进程做下一步处理。

    五.  QEMU端写请求代码流程(QEMU代码)

      前端产生kvm_exit后,kvm模块会根据EXIT的原因,如果是IO操作的EXIT,就会退出到用户空间,由QEMU进程来完成具体的IO操作。

      QEMU进程的kvm_main_loop_cpu循环等待KVM_EXIT的产生。当有退出产生时,调用KVM_RUN函数来确定退出的原因,对于IO操作的退出,KVM_EXIT为KVM_EXIT_IO。

      KVM_RUN对于IO退出的操作就是调用我们在 1.3 virtio_map中注册的virtio_ioport_write函数。

    1. virtio_ioport_write

      对前端kick函数的iowrite操作,在virtio_ioport_write中有对应操作如下:

      

      virtio_queue_notify调用各个virtio设备在初始化的时候注册的handle函数(virtio-blk设备注册了virtio_blk_handle_output)。

     2. virtio_blk_handle_output

      

      在这个while循环中virtio_blk_get_request会从vring中将数据取出(这个过程是通过virtqueue_pop这函数来实现的),放入VirtQueueElement这个数据结构的in_sg[]和out_sg[]这两个数组中。

      函数virtio_blk_handle_request根据读或写或其他请求来将这些IO请求数据进一步处理。

     3. virtqueue_pop

      这个函数主要任务是将数据从vring中取出并保存在QEMU维护的数据结构VirtQueueElement中,所执行的操作如下所示:

      

      

      要获取vring_desc中的IO请求数据,就要先获取vring_avail这个数组,它保存了vring_desc表中每个IO请求链表的起始位置。virtqueue_get_head返回的就是第一个可用的vring_avail.ring[]的值,根据i这个值就可以从vring_desc[i]中获取数据,并通过next指针在链表中递增查找。同时我们对last_avail_idx递增加1,下次再调用这个函数的时候,就取到了下一个vring_avail.ring[]的值,也就是存在vring_desc表中的另一个IO请求链表的起始位置。

       

      函数virtqueue_pop中通过这个do..while循环来遍历整个vring_desc表。elem指向的是VirtQueueElement数据结构,in_addr直接存vring中取到的数据是前段virtio设备存放的IO请求的GPA地址,sg指向VirtQueueElement.in_sg,并将每个vring_desc[i].len保存在in_sg.iov_len中。vring_desc_addr(desc_pa, i)返回desc_pa[i].addr;vring_desc_len(desc_pa, i)返回desc_pa[i].len;virtqueue_next_desc(desc_pa, i, max)返回的是desc表中的next值。

      这样我们在vring_desc中的保存的IO请求地址和长度都取了出来:

    •   VirtQueueElement.in_addr[]= GPA地址
    •   VirtQueueElement.in_sg[].io_len = 长度

      我们调用virtqueue_map_sg这个函数对VirtQueueElement.in_addr[]中保存的GPA地址转换成QEMU进程的HVA地址,并保存到VirtQueueElement.in_sg[].iov_base中。

      

     4. virtio_blk_handle_request

      

      req->elem.out_sg[0].iov_base是IO请求的第一个数据,这数据值在前文中提到过,它是通过第一次调用sg_set_buf获取到的要执行写操作磁盘扇区信息。

      

      type为该请求的类型

      

      函数qemu_iovec_init_external是对VirtQueueElement进一步封装成VirtIOBlockReq,然后调用virtio_blk_handle_write,这个函数将请求组成链表,到达32个请求就往下层递交,然后调用virtio_blk_rw_complete—>virtio_blk_req_complete。

      virtio_blk_req_complete:

      

     5. virtqueue_push

      virtqueue_push函数主要完成对vring_used表的更新。

      调用了两个函数:

      l  virtqueue_fill:

      

      更新vring_used表:vring_used_idx返回当前指向vring_used的第一个空闲位置;vring_used_ring_id在idx指向的空闲位置填入vring_desc表中被处理完成的IO请求链表的头结点的位置;vring_used_ring_len在idx指向的空闲位置填入被处理完成的IO请求的链表长度。

      l  virtqueue_flush

       

      vring_used_idx_increment更新vring_used表中的idx域,使他指向表中的下一个空闲位置。

    6. virtio_notify

      当虚拟机中的virtio设备注册了回调函数,我们就调用qemu在初始化virtio_pci 时注册的绑定函数

      

      如果前端的virtio pci设备打开了MSIX中断机制,就采用msix_notify;若没开通就用普通的中断,由qemu发起:

      

    六.  前端回调函数后续处理(内核代码)

      QEMU进程对数据处理完成后,通过中断返回到前端,前端在virtio设备初始化virtqueue时注册了一个callback回调函数,对于块设备,回调函数即blk_done。

    1. blk_done

      这个函数是前端virtio设备初始化vring_virtqueue时,设置的回调函数。

      

       

      处理过程如下所示,通过while循环对每一个处理过的IO请求进行状态检查:正常完成的IO请求,返回0,否贼返回<0。

      

      将返回的状态值通知给系统,并将处理完成的请求从请求队列中删除,并释放virtio block request的内存空间:

      

     2.virtqueue_get_buf

      根据vring_used表在vring_desc中查找被后端qemu进程处理过的IO请求表项,并将这些表项重新设置为可用。

      

      last_used_idx指向qemu在这次IO处理中所更新的vring_used表的第一项,从vring_used表中取出一个被qemu已处理完成的IO请求链(该链的头结点在vring_desc中的位置并赋值给i),同时获得这个链表的长度len。

       

      ret为void指针,vq->data[i]在virtqueue_add_buf_gfp中将其赋值为virtio block request,在这里用ret指向它并返回ret,可以在blk_done中通过ret->status来获得该virtio块请求的状态等信息。Last_used_idx递增,指向vring_used表的下一项。

     3. detach_buf

      这个函数根据vring_usd表获得的值i,将vring_desc表中从i开始的请求链表添加到free链表中。

      

      

      while循环从链表头结点遍历整个链表,递增num_free的值。

      

      vq的free_head指向的是vring_desc表中空闲表项组成的链表的表头,我们将现在释放的vring_desc表中的内容插入到这个空闲链表的表头位置。这样就将vring的空间释放,等待下次IO请求。

      vq的free_head指向的是vring_desc表中空闲表项组成的链表的表头,我们将现在释放的vring_desc表中的内容插入到这个空闲链表的表头位置。这样就将vring的空间释放,等待下次IO请求。

    七. 磁盘设备下发discard参数流程

    virtio-blk设备如果要支持discard参数下发,需要在执行mount时添加上discard参数,discard参数在ext4文件系统生效的过程如下图所示:

    jbd2收到discard事件后,分发给blk设备并传递到后端QEMU的过程如下图所示:

    discard事件后端处理涉及的数据结构和对应的注册及调用流程如下图:

  • 相关阅读:
    ES6常用语法简介
    webpack核心概念
    前端模块化规范详解
    使用Node.js原生代码实现静态服务器
    Node.js脚手架express与前段通信【socket】
    临门一脚- Node.js
    redis缓存穿透和雪崩
    redis哨兵模式
    redis主从复制
    redis发布订阅
  • 原文地址:https://www.cnblogs.com/edver/p/16255243.html
Copyright © 2020-2023  润新知