IO调度层

来源:互联网 发布:雪河二少捏脸数据 编辑:程序博客网 时间:2024/06/05 08:34

在进行io调度层源码分析前,先来说说io调度层的主要作用:

  1. Bio 的合并问题以及request之间的合并问题。
  2. 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)。