Linux IO请求处理流程-bio和request
说明
从这里开始,我们要深入进入每个IO请求内部,探测它的生命轨迹。
数据结构
与块设备层IO相关的主要数据结构有以下两个:
struct bio {
sector_t bi_sector;
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned long bi_flags; /* status,command,etc */
unsigned long bi_rw;
unsigned short bi_vcnt; /* how many bio_vec's */
unsigned short bi_idx;
unsigned int bi_phys_segments;
......
// bio完成时的回调函数
bio_end_io_t *bi_end_io;
void *bi_private;
bio_destructor_t *bi_destructor;
struct bio_vec bi_inline_vecs[0];
};
struct request {
struct list_head queuelist;
struct call_single_data csd;
struct request_queue *q;
unsigned int cmd_flags;
enum rq_cmd_type_bits cmd_type;
unsigned long atomic_flags;
......
}
将linux io相关最重要的两个数据结构列在这里,不作过多分析,都很简单。以后需要再仔细分析吧。
提交bio请求
请求的提交是由上层文件系统发起的,文件系统准备好所需读写参数(初始化bio),接下来调用Linux块设备层提供的submit_bio接口提交读写请求:
void submit_bio(int rw, struct bio *bio)
{
bio->bi_rw |= rw;
......
// 根据bio构造request,接下来的下层主要与request打交道
generic_make_request(bio);
}
void generic_make_request(struct bio *bio)
{
struct bio_list bio_list_on_stack;
if (!generic_make_request_checks(bio))
return;
// 保证每个进程同时只有一个线程在执行make_request_fn
// 否则在某些情况下有可能出错
// 但是为什么这里不加锁呢?加入线程A和B同时进来
// 不是就出错了嘛?
if (current->bio_list) {
bio_list_add(current->bio_list, bio);
return;
}
bio_list_init(&bio_list_on_stack);
current->bio_list = &bio_list_on_stack;
do {
// bio->bi_bdev指向bio请求所属块设备描述符
// 进而可以找到gendisk,找到其request_queue
// 最后调用request_queue->make_request_fn方法
// 这个方法被初始化为__make_request
struct request_queue *q = bdev_get_queue(bio->bi_bdev);
q->make_request_fn(q, bio);
bio = bio_list_pop(current->bio_list);
} while (bio);
current->bio_list = NULL; /* deactivate */
}
注意:到这里开始出现了OO编程思想:调用了request_queue的make_request_fn()方法,我们不作过多纠结,在后面会仔细梳理request_queue内容。但是我们必须知道,对于scsi磁盘,该request_queue的make_request_fn被初始化为__make_request。
bio处理-合并
经过上面的处理,每个bio到达了磁盘设备的request_queue,接下来需要对该bio进行深加工,为什么需要深加工,提高IO效率,谁叫你是巨人的阿喀琉斯之踵呢?
static int __make_request(struct request_queue *q, struct bio *bio)
{
struct request *req;
int el_ret;
spin_lock_irq(q->queue_lock);
// 尝试进行请求合并,将bio合并至请求req中
// 返回值el_ret表明了该bio需要合并的方向:前向合并还是后向合并
el_ret = elv_merge(q, &req, bio);
switch (el_ret) {
// 可后向合并
case ELEVATOR_BACK_MERGE:
......
// 可前向合并
case ELEVATOR_FRONT_MERGE:
// 无法做合并
default:
}
......
}
这里的关键在于将bio合并至已存在request内,所谓的合并指的是该bio所请求的io是否与当前已有request在物理磁盘块上连续,如果是,无需分配新的request,直接将该请求添加至已有request,这样一次便可传输更多数据,提升IO效率,这其实也是整个IO系统的核心所在。
根据elv_merge()的判断结果,可能会出现以下三种情况:
1. 可以后向合并:该bio可以合并至某个request的尾部;
2. 可以前向合并:该bio可以合并至某个request的头部;
3. 无法合并:该bio无法与任何request进行合并。
我们以“后向合并”和“无法合并”为例阐述具体实现逻辑。
后向合并
case ELEVATOR_BACK_MERGE:
BUG_ON(!rq_mergeable(req));
// 检查该req是否由于硬件限制而无法再进行合并
// elv_merge()只能判断是否可以进行合并
if (!ll_back_merge_fn(q, req, bio))
break;
// 执行后向合并,将bio链接到req的bio链表的尾部
req->biotail->bi_next = bio;
req->biotail = bio;
req->__data_len += bytes;
req->ioprio = ioprio_best(req->ioprio, prio);
if (!blk_rq_cpu_valid(req))
req->cpu = bio->bi_comp_cpu;
drive_stat_acct(req, 0);
// 尚不知道这个函数到底干啥?
// 目前好像只有CFQ算法用到了
elv_bio_merged(q, req, bio);
// 后向合并完成后检查req是否与下一个req可以继续合并
// 如果可以则将两个request再作一次合并,好复杂
if (!attempt_back_merge(q, req))
// 因为做了request合并,可能需要调整request在具体
// 调度算法中的位置,对于deadline算法来说,实现是
// deadline_merged_request()
// 它对于前向合并过的request,调整了其在RB树中的位置
elv_merged_request(q, req, el_ret);
goto out;
如果某个request可以做后向合并,那么:
1. 调用ll_back_merge_fn()判断是否可以真的合并,因为request内的数据大小可能受限于硬件;
2. 将bio添加到request的尾部,因为是后向合并;
3. 判断合并后的request是否可以与其他的request再做一次合并,调用了attempt_back_merge
4. 因为request做过合并,可能需要调整其在调度算法中的位置,调用了elv_merged_request,因为不同的调度算法可能内部实现不同,因此,内部实现其实是一个接口,由具体调度算法实现。
无法合并
get_rq:
rw_flags = bio_data_dir(bio);
if (sync)
rw_flags |= REQ_SYNC;
// 这里可能会陷入sleep
req = get_request_wait(q, rw_flags, bio);
// 根据bio初始化req
init_request_from_bio(req, bio);
spin_lock_irq(q->queue_lock);
if (test_bit(QUEUE_FLAG_SAME_COMP, &q->queue_flags) ||
bio_flagged(bio, BIO_CPU_AFFINE))
req->cpu = blk_cpu_to_group(smp_processor_id());
if (queue_should_plug(q) && elv_queue_empty(q))
blk_plug_device(q);
/* insert the request into the elevator */
drive_stat_acct(req, 1);
// 将req添加到调度算法队列之中
// 这个函数需要处理判断req是否可以合并至现有req逻辑
// 因为在get_request_wait返回后,这个bio已经可以合并了
__elv_add_request(q, req, where, 0);
无法合并的bio请求的处理逻辑就相对简单:为bio分配一个request结构,注意:这里可能会阻塞。接下来初始化该request,将request添加到queue(__elv_add_request)。
需要注意的一点就是:由于在申请request的时候可能会阻塞,在此期间,其他进程提交的bio可能与本次bio在物理位置上连续,因此在__elv_add_request()内必须判断该request是否可合并,而不仅仅将其添加到request_queue中就完事。
由于前向合并和后向合并的逻辑极其相似,我们就不再此赘述了,感兴趣的读者可自行分析。
总结
在这里,我们着重分析了上层请求bio是如何被下发并添加到系统的request中。接下来我们要来仔细分析相关request是如何被下发到具体的物理设备执行。