前面介绍了块设备的相关概念、 buffer_head和bio结构体。接下来主要分析如何向通用块层提交IO。
1.1 分配bio
当向通用块层提交一个IO操作请求的时候,假设被请求的数据块在磁盘上是相邻的,并且内核要已经知道了他们的物理位置。那么首先第一步就是执行bio_alloc()函数 分配一个新的bio描述符,然后内核通过设置一些字段的值来初始化bio描述符。该函数主要做的工作如下:
.将bi_sector字段设置为数据的起始扇区号(如果块设备被分成了几个分区,那么扇区号是相对于分区的起始位置)。
.将bi_size字段设置为涵盖整个数据的扇区数目。
.将bi_bdev设置为块设备描述符的地址(这个是block_device的对象,代表的是一个分区或者是主设备)。
.将bi_io_vec设为bio_vec结构数组的其实地址,数组中的每个元素描述了io操作中的一个段(内存缓存),此外,将bi_vcnt设置为bio中总的段数。
.将bi_rw字段设置为被请求的操作的标志。
.将bi_end_io字段设置成为当bio上的IO操作完成时所执行的完成程序的地址。
1.2 提交bio
generic_make_request()函数会接手一个已经基本初始化好的bio,并使用make_request_fn将请求置于驱动程序的请求队列上。即把该bio传给设备对应的驱动程序。
blk_qc_t generic_make_request(struct bio *bio)
参数bio中bi_dev和bi_sector都已经设定为要进行IO的对应的设备的具体地址。其中在make_request_fn(每个设备会对应一个请求队列(request_queue),该请求队列上会有对应的make_request_fn指针)函数调用后就不应该再对bio有所改动。
但是make_request可能会递归调用generic_make_request,但是对于内核态来说,栈空间是有限的。所以为了保证递归深度不能超过1,这块做了个聪明的处理。
if(current->bio_list) { bio_list_add(current->bio_list,bio); gotoout; }
其中current->bio_list是记录了由make_request_fn提交的request组成的链表,一般情况下,如果是第一次调用generic_make_request,该current->bio_list为空的。所以这个if语句并不会执行。
BUG_ON(bio->bi_next); bio_list_init(&bio_list_on_stack);/* 初始化双向列表 */ current->bio_list= &bio_list_on_stack; do { structrequest_queue *q = bdev_get_queue(bio->bi_bdev); /* 获取当前设备的队列 */ if(likely(blk_queue_enter(q, false) == 0)) { /* 判断当前队列是否可有效响应请求 */ ret= q->make_request_fn(q, bio); /* 提交bio */ blk_queue_exit(q); bio= bio_list_pop(current->bio_list); } else{ structbio *bio_next = bio_list_pop(current->bio_list); bio_io_error(bio); bio= bio_next; } } while(bio);
上面说了current->bio_list为空,所以初始化一个链表用来存后来make_request_fn提交的request。接下来获取设备的请求队列(request_queue),然后本次提交的bio可以通过make_request_fn添加到请求队列上,但是在make_request_fn函数中可能会发生递归调用generic_make_request(比如软RAID的实现,把请求拆分到其他设备上)。为了防止这种递归调用层次太深,但是逻辑上又要递归调用,就需要使用到current->bio_list,比如说现在make_request_fn已经调用了generic_make_request,那么就会进入上面提到的if块中,此时只是把该bio加入到bio_list之后就返回没有再做处理了。
这时候会回到循环中make_request_fn的下一句代码(假设整个过程只调用了一次generic_make_request),可以看见该语句又会从bio_list拿出刚刚添加的bio进行处理。这是不是就把递归问题巧妙地转变成了循环同时又保证了调用递归的方便性?
下图是generic_make_request的整个处理流程,最主要的操作是获取请求队列,然后调用对应的make_request_fn方法处理bio,make_request_fn方法会将bio放入请求队列中进行调度处理,普通磁盘介质一个最大的问题是随机读写性能很差。为了提高性能,通常的做法是聚合IO,因此在块设备层设置请求队列,对IO进行聚合操作,从而提高读写性能,对于I/O的调度将单独进行分析。
此时,通用层的基本使命已经完成,从代码流程上来看,通用层主要完成的是将应用层的读写请求构成一个或者多个I/O请求而已,后面的主要工作就交给I/O调度层。