vitrio-blk请求发起
source code:3.19.3
较新的内核中(从哪个版本开始的没去考证)virtio-blk使用了blk-mq模型,先看一下virtio-blk初始化的时候的和blk-mq相关的设置(在virtblk_probe函数中):
1.blk-mq回调函数族:
vblk->tag_set.ops = &virtio_mq_ops;
ops是blk-mq模型中关键的一组回调函数,至少要保证设置其中的queue_rq和map_queue
queue_rq函数简单来说就是指明如何处理请求
map_queue函数负责sw queue到hw queue的映射
而virtio_mq_ops中将两者分别设置为* virtio_queue_rq函数和 blk_mq_map_queue函数*,前者是我们需要关注的,后者是blk-mq中提供的一种默认映射(目前基本上都是使用这种映射关系)
2.队列深度:
vblk->tag_set.queue_depth = virtblk_queue_depth;
queue_depth表示硬件队列的深度,这里将其设置为virtblk_queue_depth,而它又和virtqueue结构的num_free有关系(只是和初始化时期的num_free有关系,因为后面在请求处理过程中num_free会变化):
/* Default queue sizing is to fill the ring. */
if (!virtblk_queue_depth) {
virtblk_queue_depth = vblk->vqs[0].vq->num_free;
/* ... but without indirect descs, we use 2 descs per req */
if (!virtio_has_feature(vdev, VIRTIO_RING_F_INDIRECT_DESC))
virtblk_queue_depth /= 2;
}
3.“请求”额外所需大小:
vblk->tag_set.cmd_size =
sizeof(struct virtblk_req) +
sizeof(struct scatterlist) * sg_elems;
在blk-mq模型中,硬件队列对应了一个“请求数组”,该数组中的元素表示“请求”,而这个“请求”的大小是sizeof(request)+cmd_size。这里对cmd_size进行了设置,从这个设置中我们也可以意识到virtio-blk的“请求”结构是:首先是一个struct request,紧接着是struct virtblk_req,最后是一个聚散列表。如图所示:
4.硬件队列数量:
vblk->tag_set.nr_hw_queues = vblk->num_vqs;
nr_hw_queues表示硬件队列的数量,这里将它设置为virtqueue的数量。
函数调用关系
virtio_queue_rq函数
1.需要明确的是这个函数是在blk-mq对请求处理的流程中被调用的,执行到这里的时候struct request对象是存储在blk-mq的hardware queue中的。
2.这个函数有两个参数:hctx指向这个请求所在的hardware queue;bd指向了一个blk_mq_queue_data对象,该对象也是blk-mq层在调用queue_fn之前准备好了的。
主要流程:
1.初始化部分局部变量:
- 获得这个硬件队列对应的virtio_blk对象,该对象的指针存在request_queue结构的queuedata域中(在virtblk_probe函数中设置的:q->queuedata = vblk)。
- 从传进来的blk_mq_queue_data中取出我们要处理的请求放在req中。
- 得到virtblk_req的地址(从前面的“请求”图中看到可以轻松的计算出来)存放在vbr变量中。
2.设置vbr的out_hdr对象(该对象用来向后端描述这次请求),需要根据request对象的cmd_flags、cmd_type来决定如何设置out_hdr。
3.调用* blk_rq_map_sg函数*,根据request对象的信息来设置scattelist,该函数返回在scatterlist中设置了多少项存在局部变量num中。
4.如果num不为零,则说明这个request涉及到数据的传输,既然是有数据传输就势必需要确定传输的方向:如果是写操作就需要设置VIRTIO_BLK_T_OUT标志,表明是往外送数据;否则就设置VIRTIO_BLK_T_IN标志,表明需要从外部读入数据。
5.调用* __virtblk_add_req函数*将和此request相关的一些信息加到vring中。
6.如果需要的话调用virtquue_notify函数通知后端(QEMU),该函数主要是virtqueue中的notify回调函数,在virtio-blk中该回调函数对应的是vp_notify函数。
在vp_notify函数中通过io_write函数将virtqueue的编号写到相应的内存中(由virtqueue的priv域指定,该域在初始化的时候被设置为vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY,可以看出这个地址是virtio专门用来前端后端互相通知的地址),向后端表明该处理哪个虚拟队列了。这里通过MMIO的形式VM exit到host去处理。
blk_rq_map_sg函数
主要流程:
1.调用* __blk_bios_map_sg函数*根据这个request对象中隐含的bios链表中的bio来设置sglist。该函数会返回sglist中设置了的表项的数目,并且传递给它的参数sg会被设置为指向最后一个设置了的表项。
2.有的设备存在“excess DMA”的问题,如果是这样的话必须在散列表后面追加一个“抽干缓冲区”,用于存放那些无意义的数据。
3.调用sg_mark_end函数设置scatterlist的末尾表项的结束标志,最后返回scatterlist中设置了的表项的数目。
__blk_bios_map_sg函数
此函数负责从bio构建出一个scatterlist,因为一个request可以对应多个bio,所以这里处理的不仅仅是传入的bio,还会继续它的下一个bio直到末尾。
sglist:表示我们要设置的那张scatterlist。
sg:当这个函数执行完成之后,sg就是指向了最后一个有用scatterlist表项。
它返回scatterlist中实际设置了多少个项。
主要流程:
1.如果bio设置了REQ_WRITE_SAME标志,表示多次读写同一个块,那么只需要设置一个scatterlist表项(因为如果这个bio后面还跟随了其他bios,那么肯定也是写同一个位置的,这是merge操作决定的)并返回1。
2.对这个bio链中的* 每个bio的每个segment调用__blk_segment_map_sg函数*,逐段的设置scatterlist。
3.返回nsegs(scatterlist中使用了的表项数目)
__blk_segment_map_sg函数
bvec:当前要“转换”(根据bio_vec设置scatterlist表项)的bio_vec
sglist:表示我们要设置的那张聚散列表
bvprv:用来保存前一个segment
sg:用于存储前一个聚散表表项
nsegs:用来存储目前设置了多少个表项
主要流程:
1.如果 *sg 不为空(只有第一次才可能为空),说明前面存在scatterlist表项,尝试bvec能否和前一个合并:
- 合并之后的大小不能超过request_queue的limits限制的最大段大小
- 两者所描述的physical address必须能够衔接起来
- 合并后的段的地址必须符合limits中段界限要求
只有上面的三个条件都满足才能认为是可以合并的,此时只需修改 *sg 中的长度即可,然后跳至第4步;否则的则需要使用一个新段。
2.如果 *sg 为空,说明这是第一次进来设置sglist,那么就将它赋值为sglist;否则的话,将它赋值为下一个表项。这时,sg的作用有了改变,它现在指向这个bvec要使用的scatterlist表项。
3.调用sg_set_page函数设置该表项,并递加nsegs的值。
4.最后将bvprv设置为这个bvec(这样下次进入这个函数时bvprv就成了指向前一个段的指针)
__virtblk_add_req函数
data_sg:表示用于实际IO操作的scatterlist
主要流程:
1.根据情况(可能有的不需要)构建一张scatterlist,这张表中依次存储下列表项:前面提到过的out_hdr、request->cmd、data_sg、request->sense、in_hdr、status,可以看到这张scatterlist中将信息输出的放在前面、信息输入的放在后面。除此之外,用num_out记录信息输出的项数、num_in记录信息输入的项数。
2.调用* virtqueue_add_sgs函数:在该函数中先计算出总共有多少个scatterlist(不仅仅是钢num_out+num_in,因为其中的data_sg中包含的项数也会被计算进去),然后直接调用 virtqueue_add函数*来“写到”vring中。
virtqueue_add函数
- 参数sgs代表上面__virtblk_add_req函数中创建的那张最多包含6个表项的scatterlist(其中data_sg指向的是实际IO操作需要的scatterlist)
- 参数data,可能不同virtio设备有不同用处,针对virtio-blk,它是一个在virtio_queue_rq函数的第1步中构造的那个virtblk_req对象
- 此函数中主要对vring进行操作,里面涉及到得域比较多,下面重新梳理vring_virtqueue、vring、virtqueue三者的关系(图中只列出了此函数中会使用到的域):
struct vring_virtqueue:
此结构中包含了一个struct vring对象vring、一个struct virtqueue对象vq
free_head表示vring中的desc所指向的desc table中空闲descriptor链表的首元素的索引
num_added表示从上次后端处理了请求到现在guest向vring中提交了多少次请求
struct vring:
num表示的是最大支持多少个segment
struct virtqueue:
num_free表示vring的desc table中空闲descriptor的数目
主要流程:
1.如果支持indirect descriptor table,并且本次需要加入vring的有超过1个的scatterlist表项,并且vring中还有空闲的descriptor,则调用alloc_indirect函数分配一个indirect descriptor table。(indirect descriptor table的作用是来增加数据传输效率的)
2.如果使用了indirect,那么我们只需要占用vring中的desc table的一项就可以了(占用空闲中的首项),于是需要对该项赋值:addr等于indirect desc table的地址、len等于indirect desc table的大小(字节为单位);否则的话就只能使用vring中的desc table的total_sg个空闲项。
将所需要的vring中的desc table中空闲项的数目存储在局部变量descs_used中。
3.需要确保desc table中的空闲项数目不小于descs_used。
因为我们将要占用descs_used个空闲项,因此调整virtqueue的num_free,减去descs_used。
4.根据传递给此函数的scatterlist填充descriptor table:对于使用indirect的则填充indirect desc table;未使用indirect(即使用vring中desc table)的则填充vring中的desc table。
5.调整vring_virtqueue的free_head:根据是否使用indirect进行调整、调整的幅度肯定不同。
6.调整vring的avail:将这从请求在desc table中的入口索引记录在vring的avail域中。
7.递加virng_virtqueue的num_added,如果它的值等于(1<<16)-1,那么我们就需要调用virtqueu_kick函数痛处后端来处理了,因为已经提交了太多次了(如果太多次了还不kick后端的话可能会使vring中充满了请求数据而后续的请求没法加进来、进而影响了IO性能)
8.返回0表示成功
本文深入探讨virtio-blk在使用blk-mq模型下的请求处理流程,从virtio_queue_rq函数开始,详细解析了blk_rq_map_sg、__blk_bios_map_sg等函数的作用,涉及硬件队列设置、请求结构及scatterlist的构建。通过对内核源码3.19.3的分析,揭示了virtio-blk请求从发起到完成的内部机制。

1962





