virtio-blk后端处理-请求接收、解析、提交

来源:互联网 发布:淘宝店铺刷信誉多少钱 编辑:程序博客网 时间:2024/05/16 15:54

在“virtio-blk后端处理”这一系列中将分析Qemu对guest中发送过来的请求是如何进行处理的。大致想了下。这个系列分成如下几个部分:
- Qemu接收、解析、提交请求
- 执行请求
- 请求完成
- dataplane情况下的处理

这个文件是这系列的第一部分,主要分析Qemu收到请求到将其派发到线程的过程。这是不考虑dataplane的情况
现在Qemu对virtio请求的接收都是通过ioeventfd来触发的(详见《kvm ioevent创建触发调用关系》),下图是Qemu上接收到virtio-blk的eventfd后的calltrace:
这里写图片描述
可以看到Qemu后端对virtio-blk的请求处理入口是virito_blk_handle_output函数,下面的分析也是从这个函数开始的。

virtio_blk_handle_output函数

这个函数中进行对请求的处理
主要流程
1.如果使用了dataplane,那么就调用virtio_blk_data_start函数来处理,并返回(采用dataplane的情况放在后面分析
2.在while循环中首先通过virtio_blk_get_request函数来取得请求,然后调用virtio_blk_handle_request函数来处理请求,这个函数中根据不同的情况会对作出不同的处理,具体后面分析
3.某些情况下(可能是多数?)上面virtio_blk_handle_request函数不会将请求提交出去,因此如果有序要提交的请求的话那么就调用virtio_blk_submit_multireq函数提交它(们)。

virtio_blk_get_request函数

主要流程
1.首先调用了virtio_blk_alloc_request函数分配了一个VirtIOBlockReq对象req
2.调用virtqueue_pop函数从vring中取出一个请求,这个请求中的信息将传递到req的elem域中

virtqueue_pop函数

主要流程
1.调用virtqueue_num_heads函数得到当前有多少个head(每个head对应了一个sg列表,这里需要和guest对vring的结构、使用方式联系起来就好理解了,也可以说是一个head就对应了一个请求),该函数中会做正确性检查(如果head的数目超过了vring的容量,那么肯定出错误了、终止运行)。如果该函数返回0,说明没有要处理的请求,所以直接返回0。
head的数量是怎么计算的呢?首先需要明白guest中vring的avail的idx域的含义,其次在qemu端VirtQueue的last_avail_idx域表示上一次处理请求时候的最后一个head位置、每次处理一个head之后都会递加该值,这样只需要两者相减就可以得到这一次有多少个head要处理。
2.取出要处理的第一个head,同时递加last_avail_idx
3.如果设置了VRING_DESC_F_INDIRECT,即使用了indirect descriptor table,那么就相应的调整我们取desc的地址(在Guest中virtio-blk请求发起过程中也可以看到:其实每个desc是由一个相应的sg表项而来)。
4.下面是一个do…while循环,循环的作用是将desc table的所有项填充到elem(传进来的参数)的in_addr、out_addr中。desc描述的是一段内存片段,该片段用来做IO数据传输的,desc也是从scatterlist中的片段演变而来的。
如果设置了VRING_DESC_F_WRITE,则对于guest来说这是一片用来接收数据的区域(比如说read时候指定一些内存片段用来接收数据),将该片段的地址(此处应该是转换了之后的Guest的物理地址gpa)存放在in_addr中、该片段的长度放在in_sg中;否则将该片段的地址放在out_addr中、长度放在out_sg中。
5.分别调用virtqueue_map_sg函数将设置in_sg、out_sg的iov_base。此时放在iov_base中的地址是Qemu的虚拟地址(hva),该函数中会调用cpu_physical_memory_map函数从上面的gpa得到对应的hva。
6.返回总共有多少个desc。

virtio_blk_handle_request函数

这个函数中根据不同情况,对请求有不同的处理方式。
主要流程
1.vring的“协议”中规定了需要有一个头、一个尾,因此首先需要确保符合此条件
2.根据guest传过来的请求类型type做不同的处理。判断的条件是type清空最低位(表示传输方向)和最高位(表示是否是barrier),然后根据这个“新type”来处理:
- 新值是“VIRTIO_BLK_T_IN”:说明这是数据读写的请求,但是因为最低位被清空了无法得知是读还是写,因此重新用type判断是读还是写、这关系到是使用out_sg还是in_sg,确定好之后根据sg设置req的QEMUIOVector对象。这个函数的第二个参数mrb是一个MultiReqBuffer对象(应该是用来缓存同一类型(r/w)的req的),如果它含有的请求数量达到了VIRTIO_BLK_MAX_MERGE_REQS或者当前这个请求因为类型不同而无法放入它或者配置成了不合并请求,就调用virtio_blk_submit_multireq函数先提交mrb中的请求。否则的话可以将这个请求放在mrb中线缓存起来。
- 新值是“VIRTIO_BLK_T_FLUSH/VIRTIO_BLK_T_SCSI_CMD/VIRTIO_BLK_T_GET_ID”,是一些特定 的操作,赞不关心。

virtio_blk_submit_multireq函数

这个函数负责将闯入的参数mrb中缓存的请求提交出去
主要流程
1.如果mrb中只缓存了一个请求,那么直接调用submit_requests函数将它提交了。
2.如果有多个的话就首先将它们按照请求的起始扇区号由小到大排序
3.就这是一个for循环,它的作用是将mrb中的请求(经过第2步排序了的)通过submit_requests函数尽可能一次性较多的提交
4.对于mrb的最后的部分,上面的for循环没有提交出去,所以最后调用submit_requests函数将最后一部分提交了。

submit_requests函数

start表示要提交的请求在mrb中的起始位置
num_reqs表示要连续提交多少个请求
niov表示这num_reqs个请求总共涉及到多少个io vector
主要流程
1.如果要提交的连续请求的个数超过一个,那么需要将每个请求的io vector这些信息合并到一个QEMUIOVector对象中。合并的处理方式是:重新分配一个io vector数组,将start号请求的io vector通过qemu_iovec_init函数加入到其中,然后依次将剩下的num_reqs-1个请求的io vector通过qemu_iovec_concat函数拼接到它的后面。
2.对于写操作使用blk_aio_writev函数来进行异步的写操作,对于读操作则是调用blk_aio_readv函数(我们以读操作为例),这个函数又涉及一连串单一的函数调用:
blk_aio_readv –> bdrv_aio_readv –> bdrv_co_aio_rw_vector 因此后面就直接去分析bdrv_co_aio_rw_vector函数
这里要注意的是传递的IO完成的回调函数是virtio_blk_rw_complete函数

bdrv_co_aio_rw_vector函数

sector_num表示本次要处理请求的起始扇区
qiov表示相应的io vector信息
nb_sectors表示这次请求涉及到的扇区数目
cb表示请求完成的回调函数
is_write表示是否是写操作
主要流程
1.根据参数构建一个BlockAIOCoroutine对象acb
2.通过qemu的协程机制启动一个子协程,执行函数是bdrv_co_do_rw函数,上面构建的acb是它的参数。

bdrv_co_do_rw函数

这个函数在协程中被调用
主要流程
1.如果是读操作则调用bdrv_co_do_readv函数进行处理;如果是写操作则调用bdrv_do_do_writev函数进行处理
2.通过aio_bh_new函数创建一个bottom half结构
3.调用qemu_bh_schedule函数添加该bh
bdrv_co_do_readv函数直接调用了bdrv_co_do_preadv函数,所以下面分析bdrv_co_do_preadv函数

bdrv_co_do_preadv函数

offset表示从磁盘的哪个地方开始读写数据
bytes表示此次操作想要读写多少字节的数据
qiov是和此次操作相关的io vector
主要流程
1.获得这块虚拟磁盘对请求大小对齐的要求,存放在align中
2.进行必要的一些检查,确保请求的有效性
3.如果这个虚拟磁盘设置了“copy on read”,那么就设置相应的BDRV_REQ_COPY_ON_READ标志
copy on read是Qemu中为性能提升提供的一个机制,它会将数据存储在本地image中,这样将来使用到这些数据的时候就不用从共享存储中读取了。
4.根据align,检查前(offset)后(offset+bytes)是否按照align大小对齐了,如果没有对齐的话就需要将它们扩充一下、让它们对齐,在这种情况下会使用local_qiov,并将use_local_qiov设置为true
5.调用bdrv_aligned_preadv函数处理对齐后的请求
6.如果在第4步中补齐了、即use_local_qiov位true,则需要将在第4步额外分配的内存释放掉

bdrv_aligned_preadv函数

这个函数处理对齐后的请求
offset是对齐后的要访问的起始位置
bytes是对齐后的要访问的大小
align是对齐的粒度
qiov是经过对齐处理后的io vector
主要流程
1.计算请求的山区起始号、请求的扇区数目
2.如果设置了BDRV_REQ_COPY_ON_READ标志,那么调用bdrv_is_allocated函数判断是否已经分配了(分配什么?是指分配local image吗?),如果没有分配或者分配了的还不足以覆盖此次请求的范围,那么调用bdrv_co_do_copy_on_readv函数处理并退出。
3.zero_beyond_eof表示如果访问的范围超过了virtual disk的大小,那么是否用0来填充对超出部分的访问。如果没设置它就直接调用bdrv_co_readv回调函数进行与virtual disk格式相关的读操作;如果设置了zero_beyond_eof,那么就又要做些额外的处理:
- 首先计算出从本次请求的起始扇区到虚拟磁盘最后共有多少个山区、存在max_nb_sectors中
- 然后这次请求的扇区数目不超过max_nb_sectors的话、即不需要补零,那么直接调用bdrv_co_readv回调函数进行处理
- 否则、即需要补零,先另外分配local_qiov,只是它要读取内容的大小是按照max_nb_sectors来计算的了,而不是传入的bytes,接着调用bdrv_co_readv回调函数处理,处理完成之后使用qemu_iovec_memset函数将“想多请求”的部分填0。

对于RAW格式的virtual disk,上面的bdrv_co_readv回调函数对应的是bdrv_co_readv_em函数,而该函数又是bdrv_co_io_em函数的直接前驱,所以下面分析bdrv_co_io_em函数

bdrv_co_io_em函数

主要流程
1.根据读写的类型调用不同的回调函数,对于读操作调用的是bdrv_aio_readv回调函数,这里需要注意的是它将协程完成IO的回调函数设置的是bdrv_co_io_em_complete函数。
对于RAW格式的虚拟磁盘,bdrv_aio_readv回调函数对应的是raw_aio_readv函数,而在该函数中又是以QEMU_AIO_READ的方式直接调用了raw_aio_submit函数
2.调用qemu_coroutine_yield函数来挂起这个协程,这样的话可以调度其他协程

raw_aio_submit函数

现在越来越接近于本文的终点了,这里即将把“被各种折磨”过的请求发给AIO去处理了。
这里有两种AIO接口:linux aio和posix aio,分别对应laio_submit和paio_submit
我们以paio_submit为例

paio_submit函数

fd表示虚拟磁盘的文件fd
secotr_num表示请求的起始扇区号
qiov表示请求对应的io vector
nb_sectors表示此次请求涉及多少个扇区
cb表示AIO完成的回调函数
主要流程
1.分配一个RawPosixAIOData对象,并根据传入的参数初始化它,这个对象将作为io thread的参数
2.通过aio_get_thread_pool函数获得对应的thread pool
3.调用thread_pool_submit_aio函数提交。这里传入了一个aio_worker函数,它将作为线程中具体的功能函数。
thread_pool_submit_aio函数中主要构造了一个ThreadPoolElement对象,该对象在线程具体功能函数中将被作为其参数,然后将这个对象加入到pool的相关链表中,就有机会被执行了

总结

这里虽然函数涉及非常多,但是大概走了下这部分流程之后发现功能还是非常明了的。这部分的重点是将Vring中的descriptor table转换成io vector,转换的过程比较简单,但是需要注意在流程中涉及到gpa到hva的变换,还涉及到copy on read、zero_of_eof、align等等,所以细节比较难。

0 0
原创粉丝点击