Linux Block模块之IO合并代码解析

1 IO路径从内核角度看 , 进程产生的IO路径主要有三条:

  • 缓存IO:系统绝大部分IO走的这种形式 , 充分利用文件系统层的page cache所带来的优势 。应用程序产生的IO经系统调用落入page cache之后便可以直接返回 , page cache中的缓存数据由内核回写线程在适当时机同步到底层的存储介质上 , 当然应用程序也可以主动发起回写过程(如fsync系统调用)来确保数据尽快同步打扫存储介质上 , 从而避免系统崩溃或者掉电带来的数据不一致性;
  • 非缓存IO(带蓄流):这种IO绕过文件系统层的cache 。用户在打开要读写的文件的时候需要加上 O_DIRECT 标记 , 意为直接IO , 不让文件系统的page cache介入 。从用户角度而言 , 应用程序能直接控制的IO形式除了上面提到的缓存IO , 剩下的IO都走这种形式 , 就算打开文件时加上了 O_SYNC 标记 , 最终产生的IO也会进入蓄流链表 。如果应用程序在用户空间自己做了缓存 , 那么就可以使用这种IO方式 , 常见的如数据库应用;
  • 非缓存IO(不带蓄流):内核通用快层的蓄流机制只给内核空间提供了接口来控制IO请求是否蓄流 , 用户空间进程无法控制提交的IO请求进入通用快层的时候是否蓄流 。严格的说 , 用户空间直接产生的IO都会走蓄流路径 , 哪怕是提交IO的时候加上了 O_DIRECTO_SYNC 标记 。用户空间间接产生的IO , 如文件系统日志数据、元数据 , 有的不会走蓄流路径而是直接进入调度队列尽快得到调度 。注意一点 , 通用块层的蓄流只是提供机制和接口而不是策略 , 也就是需不需要蓄流、何时蓄流完全由内核中的IO派发者决定;
2 合并方式应用程序无论使用图中哪条IO路径 , 内核都会想方设法对IO进行合并 。内核为促进这种合并 , 在IO栈上设置了三种合并方式以对应以上三种IO路径:
  • Cache(页高速缓存)
  • Plug list(蓄流链表)
  • Elevator Queue(调度队列)
其中Cache属于文件系统层的内容 , 本文中不讨论 , 我们主要分析另外两种IO合并方式 。
2.1 逻辑入口blk_queue_io() 函数是通用块层进行IO合并的入口函数 , 该函数内先尝试Plug合并 , 合并成功返回 , 合并失败则继续尝试Elevator合并 , 合并成功返回 , 合并失败则为bio创建一个新的request插入到调度队列中 , 尝试IO合并部分的代码如下:
void blk_queue_io(struct request_queue *q, struct bio *bio){ const bool sync = !!(bio->bi_rw & REQ_SYNC); struct blk_plug *plug; int el_ret, rw_flags, where = ELEVATOR_INSERT_SORT; struct request *req, *free; unsigned int request_count = 0; /** low level driver can indicate that it wants pages above a* certain limit bounced to low memory (ie for highmem, or even* ISA dma in theory)** 做DMA时的相关地址限制 , 可能该bio只能访问低端内存 , * 因此需要将高端内存中的bio数据拷贝到低端内存中*/ blk_queue_bounce(q, &bio); /* 完整性校验 */ if (bio_integrity_enabled(bio) && bio_integrity_prep(bio)) {bio_endio(bio, -EIO);return; } /* FLUSH和FUA直接生成新的请求处理 */ if (bio->bi_rw & (REQ_FLUSH | REQ_FUA)) {spin_lock_irq(q->queue_lock);where = ELEVATOR_INSERT_FLUSH;goto get_rq; } /** Check if we can merge with the plugged list before grabbing* any locks.** 先尝试plug合并 , plug中为当前进程的req链表 , 合并成功直接返回*/ if (!blk_queue_nomerges(q)) {if (blk_attempt_plug_merge(q, bio, &request_count, NULL))return; } elserequest_count = blk_plug_queued_count(q); /* 不管是与队列中的请求合并还是插入新的请求都需要加锁 */ spin_lock_irq(q->queue_lock); /* 这里记录下电梯哈希和调度算法调度队列* bio生成新的请求后 , 会插入两个地方* 1.电梯的通用哈希表 , 以请求的结束为止为哈希值进行哈希 , *便于查找可以向后合并的请求*q->elevator->hash* 2.具体调度算法的调度队列 , 用于调度算法进行调度 , 以deadline为例*q->elevator->elevator_data->sort_list/fifo_expire** 调度算法派发请求后 , 请求会进入q的派发队列 , *同时从哈希和调度队列中移除*/ /* 在请求队列的哈希表中查找可以向后合并的请求 , 在调度算法* 的调度队列中查找可以合并的请求(deadline算法中只有向前合并)*/ el_ret = elv_merge(q, &req, bio); if (el_ret == ELEVATOR_BACK_MERGE) {/* 尝试进行bio与req的合并 */if (bio_attempt_back_merge(q, req, bio)) {/* 合并成功的话 , 调用具体调度算法的后续处理(deadline中并没有实现接口) */elv_bio_merged(q, req, bio);/* 这次合并的bio可能会弥补两个bio之间的空隙 , 所以这里查找根据* 给定req后边的一个请求 , 判断能否进行合并*/free = attempt_back_merge(q, req);if (!free)/* 如果上边没有检测到可以合并的request , 则调用接口处理* bio合并到request之后的处理:*1.调用调度算法的回调函数处理调度算法需要执行的操作*2.如果是向后合并还需要更新电梯哈希*/elv_merged_request(q, req, el_ret);else__blk_put_request(q, free);goto out_unlock;} } else if (el_ret == ELEVATOR_FRONT_MERGE) {if (bio_attempt_front_merge(q, req, bio)) {elv_bio_merged(q, req, bio);free = attempt_front_merge(q, req);if (!free)elv_merged_request(q, req, el_ret);else__blk_put_request(q, free);goto out_unlock;} }get_rq: /* bio无法合并 , 为其申请一个新的request */ /** This sync check and mask will be re-done in init_request_from_bio(),* but we need to set it earlier to expose the sync flag to the* rq allocator and io schedulers.*/ rw_flags = bio_data_dir(bio); if (sync)rw_flags |= REQ_SYNC; /** Grab a free request. This is might sleep but can not fail.* Returns with the queue unlocked.*/ blk_queue_enter_live(q); /* 获取空闲的请求 */ req = get_request(q, rw_flags, bio, 0); if (IS_ERR(req)) {blk_queue_exit(q);bio_endio(bio, PTR_ERR(req)); /* @q is dead */goto out_unlock; } /** After dropping the lock and possibly sleeping here, our request* may now be mergeable after it had proven unmergeable (above).* We don't worry about that case for efficiency. It won't happen* often, and the elevators are able to handle it.** 根据bio初始化request*/ init_request_from_bio(req, bio); if (test_bit(QUEUE_FLAG_SAME_COMP, &q->queue_flags))req->cpu = raw_smp_processor_id(); /* 如果当前进程有plug_list , 首先插入到plug_list中 */ plug = current->plug; if (plug) {/** If this is the first request added after a plug, fire* of a plug trace.** @request_count may become stale because of schedule* out, so check plug list again.*/if (!request_count || list_empty(&plug->list))trace_block_plug(q);else {struct request *last = list_entry_rq(plug->list.prev);if (request_count >= BLK_MAX_REQUEST_COUNT ||blk_rq_bytes(last) >= BLK_PLUG_FLUSH_SIZE) {blk_flush_plug_list(plug, false);trace_block_plug(q);}}/* 插入到plug链表 */list_add_tail(&req->queuelist, &plug->list);blk_account_io_start(req, true); } else {spin_lock_irq(q->queue_lock);/* 插入到电梯及调度算法中 */add_acct_request(q, req, where);/* 执行q->request_fn(q) , 调用底层驱动的策略例程处理请求 。* 以SCSI为例 , request_fn初始化为scsi_request_fn* scsi_request_fn->blk_peek_request->__elv_next_request* __elv_next_request中会使用调度具体算法的* dispatch回调函数取出请求进行处理*/__blk_run_queue(q);out_unlock:spin_unlock_irq(q->queue_lock); }}

经验总结扩展阅读