IO调度层
来源:互联网 发布:雪河二少捏脸数据 编辑:程序博客网 时间:2024/06/05 08:34
在进行io调度层源码分析前,先来说说io调度层的主要作用:
- Bio 的合并问题以及request之间的合并问题。
Request 的调度问题。 request 在何时可以从调度器中取出,并且送入底层驱动程序继续进行处理?不同的应用对处理延时以及io带宽的要求是不同的。因此根据需要选择合适的调度器。
我们熟知,一个 request 在送往设备之前会被放入到每个设备所对应的 request queue 。其实,通过分析一个 IO 在 elevator 层其实会经过很多 request queue ,不同的 request queue 会有不同的作用。新内核引入了plug队列,每个线程都有一个自己的plug队列,当产生一个新的request请求时,如果这个线程没有 plug 请求队列,那么 IO request 直接被送入 elevator 。否则加入plug队列,在 plug 请求队列中等待的 request 会在请求 unplug 的过程中被送入 elevator 的请求队列。
在一般的请求处理过程中, request 被创建并且会被挂载到plug request queue 中,然后通过 flush request 方法将 request 从 plug request queue 中移至 elevator request queue 中。当一个新的 BIO 需要被处理时,其可以在 plug request queue 或者 elevator request queue 中进行合并。当需要将请求发送到底层设备时,可以通过调用 run_queue 的方法将 elevator 分类处理过的 request 转移至 device request queue 中,最后由设备对应的驱动去处理 device request queue 中的每个request请求。
当一个 IO 刚来到通用块层时,首先需要判断该 IO 是否可以和正在等待处理的 request 进行合并。这一步主要是通过 elv_merge() 函数来实现的,需要注意的是,在调用 elv_merge 进行合并操作之前,首先需要判断 plug request queue 是否可以进行合并,如果不能合并,那么才调用 elv_merge 进行 elevator request queue 的合并操作。一旦 bio 找到了可以合并的 request ,那么,这个 IO 就会合并放入对应的 request 中,否则需要创建一个新的 request ,并且如果存在plug队列则放入到 plug request queue 中。
下面将用具体的代码解释上述过程:
void blk_queue_bio(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; unsigned int request_count = 0; blk_queue_bounce(q, &bio); /* 为了建立bounce buffer,以防止不适合这次I/O操作的时候利用bounce buffer*/ if (bio_integrity_enabled(bio) && bio_integrity_prep(bio)) {//数据完整性校验 bio_endio(bio, -EIO); return; } if (bio->bi_rw & (REQ_FLUSH | REQ_FUA)) {//FLUSH的bio,直接申请新的request spin_lock_irq(q->queue_lock);//加锁 where = ELEVATOR_INSERT_FLUSH; goto get_rq; } if (blk_attempt_plug_merge(q, bio, &request_count))//尝试将bio合并到当前plug请求队列中 return; spin_lock_irq(q->queue_lock);//加锁 el_ret = elv_merge(q, &req, bio);//elv_merge是核心函数,找到bio前向或者后向合并的请求 if (el_ret == ELEVATOR_BACK_MERGE) {//进行后向合并操作 if (bio_attempt_back_merge(q, req, bio)) { elv_bio_merged(q, req, bio);//调度器合并bio到req请求 if (!attempt_back_merge(q, req))//继续尝试将请求req和其后面的请求合并,可以合并返回1,无法合并返回0 elv_merged_request(q, req, el_ret);//无法合并请求后的处理 goto out_unlock; } } else if (el_ret == ELEVATOR_FRONT_MERGE) {// 进行前向合并操作,与向后合并流程差不多 if (bio_attempt_front_merge(q, req, bio)) { elv_bio_merged(q, req, bio); if (!attempt_front_merge(q, req))//继续尝试将请求req与其前面请求合并 elv_merged_request(q, req, el_ret); goto out_unlock; } }/* 该bio无法找到对应的请求实现合并,需创建一个新的request */get_rq: /* * 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. */ req = get_request(q, rw_flags, bio, GFP_NOIO);//获取一个新的request请求 if (unlikely(!req)) { bio_endio(bio, -ENODEV); /* @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. */ init_request_from_bio(req, bio);//采用bio对request请求进行初始化 if (test_bit(QUEUE_FLAG_SAME_COMP, &q->queue_flags)) req->cpu = raw_smp_processor_id(); plug = current->plug; if (plug) {//查看当前plug请求队列是否存在,存在则将request加入该队列 /* * If this is the first request added after a plug, fire * of a plug trace. */ if (!request_count) trace_block_plug(q); else { if (request_count >= BLK_MAX_REQUEST_COUNT) { blk_flush_plug_list(plug, false); trace_block_plug(q); //plug队列请求数量达到队列上限值,进行unplug操作 } } list_add_tail(&req->queuelist, &plug->list);//将请求加入到plug队列 blk_account_io_start(req, true); } else { /* 在新的内核中,如果用户没有调用start_unplug,那么,在IO scheduler中是没有合并的,一旦加入到request queue中,马上执行unplug操作,这个地方个人觉得有点不妥,不如以前的定时调度机制。对于ext3文件系统,在刷写page cache的时候,都需要首先执行start_unplug操作,因此都会进行request/bio的合并操作。 */ spin_lock_irq(q->queue_lock);//怎么又加一遍? add_acct_request(q, req, where);/* 将request加入到调度器中 */ __blk_run_queue(q);/* 调用底层函数执行unplug操作 */out_unlock: spin_unlock_irq(q->queue_lock); }}
对于blk_queue_bio函数主要做了三件事情:
1)进行请求的后向合并操作
2)进行请求的前向合并操作
3)如果无法合并请求,那么为bio创建一个request,然后进行调度
blk_queue_bio仅仅是一个上层函数,最主要完成后向合并、调用调度器方法进行前向合并以及初始化request准备调度。
在bio合并过程中,最为关键的函数是elv_merge。该函数主要工作是判断bio是否可以进行后向合并或者前向合并。对于所有的调度器,后向合并的逻辑都是相同的。在系统中维护了一个request hash表,然后通过bio请求的起始地址进行hash寻址。Hash表的生成原理比较简单,就是将所有request的尾部地址进行分类,分成几大区间,然后通过hash函数可以寻址这几大区间。我们向后合并bio时需要找到一个request尾部结束的地方恰好是bio起始的地方,通过查hash表可快速找到。hash表的key值就是各个request结束的位置。
采用hash方式维护request,有一点需要注意:当一个request进行合并处理之后,需要对该request在hash表中进行重新定位。这主要是因为request的尾地址发生了变化。
如果后向合并失败,那么调度器会尝试前向合并。不是所有的调度器支持前向合并,如果调度器支持这种方式,那么需要注册elevator_merge_fn函数实现前向调度功能。
int elv_merge(struct request_queue *q, struct request **req, struct bio *bio){ struct elevator_queue *e = q->elevator; struct request *__rq; int ret; /* * Levels of merges: * nomerges: No merges at all attempted * noxmerges: Only simple one-hit cache try * merges: All merge tries attempted */ if (blk_queue_nomerges(q))//请求队列不允许合并请求,则返回NO_MERGE return ELEVATOR_NO_MERGE; /* * First try one-hit cache. */ if (q->last_merge && elv_rq_merge_ok(q->last_merge, bio)) { //last_merge指向最近进行合并操作的request ret = blk_try_merge(q->last_merge, bio); if (ret != ELEVATOR_NO_MERGE) { *req = q->last_merge; return ret; } } if (blk_queue_noxmerges(q)) return ELEVATOR_NO_MERGE; /* * See if our hash lookup can find a potential backmerge. */ __rq = elv_rqhash_find(q, bio->bi_sector);//根据bio的起始扇区号,通过rq的哈希表寻找一个request,可以将bio合并到request的尾部 if (__rq && elv_rq_merge_ok(__rq, bio)) { *req = __rq; return ELEVATOR_BACK_MERGE; }/*如果以上的方法不成功,则调用特定于io调度器的elevator_merge_fn函数寻找一个合适的request*/ if (e->type->ops.elevator_merge_fn) return e->type->ops.elevator_merge_fn(q, req, bio); return ELEVATOR_NO_MERGE;}
bool bio_attempt_back_merge(struct request_queue *q, struct request *req, struct bio *bio){ const int ff = bio->bi_rw & REQ_FAILFAST_MASK; if (!ll_back_merge_fn(q, req, bio)) return false; //查看是否超过请求request的最大字节,或是request的最大段数 trace_block_bio_backmerge(q, req, bio); if ((req->cmd_flags & REQ_FAILFAST_MASK) != ff) blk_rq_set_mixed_merge(req); req->biotail->bi_next = bio;//将bio添加到req请求的尾部 req->biotail = bio; req->__data_len += bio->bi_size; req->ioprio = ioprio_best(req->ioprio, bio_prio(bio)); blk_account_io_start(req, false); return true;}
对blk_queue_bio的总结:
(1)当收到上层发下来的bio请求时,并不是急于将bio与块设备请求队列上的待处理的请求描述符进行合并,而是先尝试和plug队列合并(blk_attempt_plug_merge(q, bio, &request_count) ),因为plug队列是每个线程都有自己的专属plug队列的,所以合并是不需要加锁,而块设备的请求队列不同,所有线程共享,因此在请求队列上进行合并需要加锁,大大影响了并发度。
因此先尝试在线程自己的plug队列对bio进行合并。
(2)如果在plug队列没有找到合适的request进行合并,此时才在请求队列上进行合并,当然要加锁。
(3)如果请求队列上也没有找到合适的request可以合并,那么只能申请一个新的request并把bio加入该请求中,然后加入plug队列。
plug队列上的请求毕竟不是在块设备的请求队列中,因此不会被驱动处理,要想被处理,必须把plug队列的请求加入到请求队列中才行,这个过程就是blk_flush_plug_list要做的, 它把plug上的请求经过调度器排序重新加入到请求队列中合适的位置。当线程将要睡眠时或者pulg队列请求数超过最大值时会触发blk_flush_plug_list操作,blk_flush_plug_list最终还会进行unplug操作(也就是调用块设备驱动的处理例程request_fn)。
- IO调度层
- IO调度层
- [IO系统]14 IO调度层
- Linux IO调度层分析-1
- Linux IO调度层分析-2
- Linux IO调度层分析-3
- Linux IO调度层分析-1
- 通用块层IO调度算法之deadline算法
- IO队列和IO调度
- IO队列和IO调度
- IO队列和IO调度
- IO队列和IO调度
- linux io调度
- IO调度选择
- Linux IO调度
- 磁盘IO调度策略
- linux IO调度器
- IO调度器
- 每天laravel[002]-自动加载函数流程之bootstrap autoload.php
- ACM:A: 二叉树结点公共祖先
- Android运行项目出现UnsupportedMethodException
- 在C / C ++中清除输入缓冲区
- js 捕获型事件、冒泡型事件、DOM事件流、事件委托
- IO调度层
- 揭秘微信小程序
- Android产品研发(三)-->基类Activity
- POJ 1778 All Discs Considered 笔记
- Fckeditor使用方法
- 【飘红】继承了公共类中已经有的方法,再在实体类中写就是重写了
- 删除IIS Express上的网站
- 算法设计与应用基础:第十五周(2)
- 红黑树