块设备驱动之I/O调度层之调度器

  通过generic_make_request提交请求给I/O调度层,这个函数最后调用到q->make_request_fn(q, bio),那么对于这个函数的调用就是I/O调度层的入口点,首先来看看这个make_request_fn在哪被赋于能量的

void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
{
    /*
     * set defaults
     */
    q->nr_requests = BLKDEV_MAX_RQ;    //最大的请求数为128  

    q->make_request_fn = mfn;       //完成bio所描述的请求处理函数
    blk_queue_dma_alignment(q, 511);   //该函数用于告知内核块设备DMA 传送的内存对齐限制
    blk_queue_congestion_threshold(q);  //主要做流控
    q->nr_batching = BLK_BATCH_REQ;

    blk_set_default_limits(&q->limits);   //块设备在处理io时会受到一些参数(设备的queue limits参数)的影响,如请求中允许的最大扇区数
                          //这些参数都可以在/sys/block//queue/下查看,块设备在初始化时会设置默认值

    /*
     * by default assume old behaviour and bounce for any highmem page
     */                          //BLK_BOUNCE_HIGH:对高端内存页使用反弹缓冲
    blk_queue_bounce_limit(q, BLK_BOUNCE_HIGH);  //此函数告知内核设备执行DMA时,可使用的最高物理地址dma_addr//
}

  从上面可以看出,这个函数是设置一些请求队列的参数,如请求数目,dma处理的时候的对齐,i/o参数和请求处理函数。下面需要层层剥丝,直到发现由哪个函数来处理我们的请求,还有使用怎么样的算法来处理这些请求队列。

struct request_queue *
blk_init_allocated_queue(struct request_queue *q, request_fn_proc *rfn,
             spinlock_t *lock)
{
    if (!q)
        return NULL;

    q->fq = blk_alloc_flush_queue(q, NUMA_NO_NODE, 0);    //申请blk_flush_queue
    if (!q->fq)
        return NULL;

    if (blk_init_rl(&q->root_rl, q, GFP_KERNEL))        //初始化request_list
        goto fail;

    q->request_fn        = rfn;                  //请求处理函数,当内核期望驱动程序执行某些动作时,就会使用这个函数
    q->prep_rq_fn        = NULL;
    q->unprep_rq_fn        = NULL;
    q->queue_flags        |= QUEUE_FLAG_DEFAULT;

    /* Override internal queue lock with supplied lock pointer */
    if (lock)
        q->queue_lock        = lock;

    /*
     * This also sets hw/phys segments, boundary and size
     */
    blk_queue_make_request(q, blk_queue_bio);        //设置bio所描述的请求处理函数

    q->sg_reserved_size = INT_MAX;

    /* Protect q->elevator from elevator_change */
    mutex_lock(&q->sysfs_lock);

    /* init elevator */
    if (elevator_init(q, NULL)) {              //初始化调度算法
        mutex_unlock(&q->sysfs_lock);
        goto fail;
    }

    mutex_unlock(&q->sysfs_lock);

    return q;

fail:
    blk_free_flush_queue(q->fq);
    return NULL;
}

如果被访问的设备是一个有queue的块设备,那么系统会调用blk_queue_bio函数进行bio的调度合并。

static 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;

    /*
     * 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)
     *//* 为了建立bounce buffer,以防止不适合这次I/O操作的时候利用bounce buffer*/    blk_queue_bounce(q, &bio);                    

    if (bio_integrity_enabled(bio) && bio_integrity_prep(bio)) {    //数据完整性校验
        bio_endio(bio, -EIO);
        return;
    }

    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.
     */
    if (!blk_queue_nomerges(q) &&                  //请求队列不允许合并请求
        blk_attempt_plug_merge(q, bio, &request_count))    //将bio合并到当前plugged的请求队列中
        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);
            if (!attempt_back_merge(q, req))
                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))
                elv_merged_request(q, req, el_ret);
            goto out_unlock;
        }
    }
/* 无法找到对应的请求实现合并 */
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);          //获取一个empty request请求
    if (IS_ERR(req)) {
        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.
     */
    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) {
        /*
         * 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);            //请求数量达到队列上限值,进行unplug操作
                trace_block_plug(q);
            }
        }
        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);
        __blk_run_queue(q);
out_unlock:
        spin_unlock_irq(q->queue_lock);
    }
}

对于 blk_queue_bio函数主要做了三件事情:

1) 进行请求的后向合并操作

2) 进行请求的前向合并操作

3) 如果无法合并请求,那么为 bio 创建一个 request ,然后进行调度

在 bio 合并过程中,最为关键的函数是 elv_merge 。该函数主要工作是判断 bio 是否可以进行后向合并或者前向合并。

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.
     */                             //last_merge指向最近进行合并操作的request,并成功合并    if (q->last_merge && elv_rq_merge_ok(q->last_merge, bio)) {  
        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_iter.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;
}

elevator_merge_fn是特定于I/O调度器的方式,涉及到调度的算法,留待下一章来分析。通过elv_merge,得到了bio是向前还是向后走到相应的处理接口中,下面来分别看看向前或者向后的处理方式。

1. elv_bio_merged

void elv_bio_merged(struct request_queue *q, struct request *rq,
            struct bio *bio)
{
    struct elevator_queue *e = q->elevator;

    if (e->type->ops.elevator_bio_merged_fn)
        e->type->ops.elevator_bio_merged_fn(q, rq, bio);        //调用调度算法的处理函数,这个只是针对cfq的算法提供
}

2. elv_merged_request

void elv_merged_request(struct request_queue *q, struct request *rq, int type)
{
    struct elevator_queue *e = q->elevator;

    if (e->type->ops.elevator_merged_fn)
        e->type->ops.elevator_merged_fn(q, rq, type);        //调用调度算法的合并函数

    if (type == ELEVATOR_BACK_MERGE)
        elv_rqhash_reposition(q, rq);

    q->last_merge = rq;
}

由上面来看,对于合并和调度都会用到一些算法的回调接口,下章主要针对调度算法来看看内核支持那些调度算法,各有什么优缺点。

时间: 2024-08-26 00:25:26

块设备驱动之I/O调度层之调度器的相关文章

【深入Linux块设备驱动层次之一】整体层次

最近在做文件系统和linux块设备相关的工作,需要对文件系统和底层块设备之间的关系和交互有比较深入的了解.笔者参考的书籍有敖青云所著的<存储技术原理分析-基于Linux2.6内核原代码>,同时参考的还有Jonathan Corbet.Alessandro Rubini和Greg Kroah-Hartman 一起著作的linux设备经典书籍<Linux设备驱动Edition 3 >.陈学松写的<深入Linux设备驱动内核机制>.对比了一下,还是敖青云写的很存储结合更紧密一

LINUX块设备驱动&lt;1&gt;

转自:http://blog.chinaunix.net/uid-15724196-id-128139.html 第1章 +---------------------------------------------------+|                 写一个块设备驱动                  |+---------------------------------------------------+| 作者:赵磊                               

LINUX块设备驱动&lt;3&gt;

转自:http://blog.chinaunix.net/uid-15724196-id-128141.html 第3章 +---------------------------------------------------+|                 写一个块设备驱动                  |+---------------------------------------------------+| 作者:赵磊                               

块设备驱动程序设计

一.块设备简介 1.块设备 块设备将数据存储在固定大小的块中,每个块的大小通常在512字节到32768字节之间.磁盘.SD卡都是常见的块设备. 2.块设备VS字符设备 # 块设备和字符设备最大的区别在于读写数据的基本单元不同.块设备读写数据的基本单元为块,例如磁盘通常为一个sector,而字符设备的基本单元为字节. # 块设备能够随机访问,而字符设备则只能顺序访问. 块设备体系架构: VFS是对各种具体文件系统的一种封装,为用户程序访问文件提供统一的接口. Disk Cache 当用户发起文件访

【转】写一个块设备驱动(1)

原文地址:写一个块设备驱动 一直对块设备驱动似懂非懂,这次发现了这个介绍块设备驱动很好的系列,打算把这套东西弄懂,一起跟着作者学习一遍 作者写这个系列的初衷如下,我觉得很好,网上搜到的大部分都是介绍一些玄乎的东西,看完似懂非懂的~ 在这套教程中,我们通过写一个建立在内存中的块设备驱动,来学习linux内核和相关设备驱动知识. 选择写块设备驱动的原因是: 1:容易上手 2:可以牵连出更多的内核知识 3:像本文这样的块设备驱动教程不多,所以需要一个 概述 在开始赵磊的教程之前,先对块IO子系统进行一

linux块设备驱动---相关结构体(转)

上回最后面介绍了相关数据结构,下面再详细介绍 块设备对象结构 block_device 内核用结构block_device实例代表一个块设备对象,如:整个硬盘或特定分区.如果该结构代表一个分区,则其成员bd_part指向设备的分区结构.如果该结构代表设备,则其成员bd_disk指向设备的通用硬盘结构gendisk 当用户打开块设备文件时,内核创建结构block_device实例,设备驱动程序还将创建结构gendisk实例,分配请求队列并注册结构block_device实例. 块设备对象结构blo

块设备驱动架构分析

1. 块设备概念:块设备是指只能以块为单位进行访问的设备,块的大小一般是512个字节的整数倍.常见的块设备包括硬件,SD卡,光盘等.</span> 上边是通过一个编写好的块设备驱动,然后安装块设备驱动以及一些相关操作来体会块设备驱动!(此处省略) 2. 块设备驱动的系统架构 2.1 系统架构---VFS VFS是对各种具体文件系统的一种封装,用户程序访问文件提供统一的接口. 2.2 系统架构---Cache 当用户发起文件访问请求的时候,首先回到Disk Cache中寻址文件是否被缓存了,如果

linux块设备驱动---概念与框架(转)

基本概念   块设备(blockdevice) --- 是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区. 字符设备(Character device) ---是一个顺序的数据流设备,对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据流.他不具备缓冲区,所以对这种设备的读写是实时的. 扇区(Sectors):任何块设备硬件对数据处理的基本单位.通常,1个扇区的大小为512byt

写一个块设备驱动1,2

http://blogold.chinaunix.net/u3/108239/showart.php?id=2144624 第1章 +---------------------------------------------------+|                 写一个块设备驱动                  |+---------------------------------------------------+| 作者:赵磊                          

Linux块设备驱动详解

<机械硬盘> a:磁盘结构 -----传统的机械硬盘一般为3.5英寸硬盘,并由多个圆形蝶片组成,每个蝶片拥有独立的机械臂和磁头,每个堞片的圆形平面被划分了不同的同心圆,每一个同心圆称为一个磁道,位于最外面的道的周长最长称为外道,最里面的道称为内道,通常硬盘厂商会将圆形蝶片最靠里面的一些内道(速度较慢,影响性能)封装起来不用:道又被划分成不同的块单元称为扇区,每个道的周长不同,现代硬盘不同长度的道划分出来的扇区数也是不相同的,而磁头不工作的时候一般位于内道,如果追求响应时间,则数据可存储在硬盘的