块设备驱动之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;
}

其实从这个函数来看,这个函数就是进入了真正调度的几个关键函数,下面来看看函数elevator_init,其主要用来为请求队列分配一个I/O调度器

int elevator_init(struct request_queue *q, char *name)
{
    struct elevator_type *e = NULL;
    int err;

    /*
     * q->sysfs_lock must be held to provide mutual exclusion between
     * elevator_switch() and here.
     */
    lockdep_assert_held(&q->sysfs_lock);        //检查当前进程是否已经设置了调度标志

    if (unlikely(q->elevator))
        return 0;

    INIT_LIST_HEAD(&q->queue_head);           //初始化请求队列的相关元素
    q->last_merge = NULL;
    q->end_sector = 0;
    q->boundary_rq = NULL;
/*下面根据情况在elevator全局链表中来寻找适合的调度器分配给请求队列*/  
    if (name) {
        e = elevator_get(name, true);      //如果指定了name,则寻找与name匹配的调度器
        if (!e)
            return -EINVAL;
    }

/*如果没有指定io调度器,并且chosen_elevator存在,则寻找其指定的调度器*/
    if (!e && *chosen_elevator) {
        e = elevator_get(chosen_elevator, false);
        if (!e)
            printk(KERN_ERR "I/O scheduler %s not found\n",
                            chosen_elevator);
    }
/*依然没获取到调度器的话则使用默认配置的调度器*/
    if (!e) {
        e = elevator_get(CONFIG_DEFAULT_IOSCHED, false);
        if (!e) {
            printk(KERN_ERR
                "Default I/O scheduler not found. "                 "Using noop.\n");
            e = elevator_get("noop", false);        //获取失败则使用最简单的noop调度器
        }
    }

    err = e->ops.elevator_init_fn(q, e);
    if (err)
        elevator_put(e);
    return err;
}

所有的I/O调度器类型都会通过链表链接起来(通过struct elevator_type中的list元素),elevator_get()函数便是通过给定的name,在链表中寻找与name匹配的调度器类型。当确定了I/O调度器的类型后,就会调用对应的elevator_init_fn,由于每个调度器根据自身算法的不同,都会拥有不同的队列结构,在elevator_init_fn()中会调用特定于调度器的初始化函数针对这些队列进行初始化,并且返回特定于调度器的数据结构,现在内核提供有三种调度的算法

1). noop-iosched

2). cfq-iosched.c

3). deadline-iosched.c

那么我们针对这三种调度算法来看看其实现的基本原理

1. noop调度算法

看看其初始化是通过elv_register注册了一个elevator_noop结构,下面来看看noop的初始化都做了一些什么?

static int noop_init_queue(struct request_queue *q, struct elevator_type *e)
{
    struct noop_data *nd;
    struct elevator_queue *eq;

    eq = elevator_alloc(q, e);                   //为等待队列分配一个调度器的实例
    if (!eq)
        return -ENOMEM;

    nd = kmalloc_node(sizeof(*nd), GFP_KERNEL, q->node);   //从通用缓冲区中分配一个属于指定 NUMA 节点的对象         if (!nd) {
        kobject_put(&eq->kobj);
        return -ENOMEM;
    }
    eq->elevator_data = nd;                    

    INIT_LIST_HEAD(&nd->queue);                  //加入链表

    spin_lock_irq(q->queue_lock);          
    q->elevator = eq;                        //将调度器赋值给等待队列,以方便后续使用
    spin_unlock_irq(q->queue_lock);
    return 0;
}

这个只是主要是将分配的调度器赋予等待队列,那么看看这个调度方法最核心的数据结构

struct noop_data {
    struct list_head queue;
};

从结构来看只有一个成员queue,其实就noop中维护的一个fifo(先进先出)链表的链表头,猜想noop对于调度的处理一个对于基本链表的处理方式,就是一个链表的当io请求过来了,就会被加入到这个链表的后面,在链表前面的就会被移到系统的请求队列(request_queue)中。下面结合代码看看,整个处理流程。

static void noop_add_request(struct request_queue *q, struct request *rq){   struct noop_data *nd = q->elevator->elevator_data;
   list_add_tail(&rq->queuelist, &nd->queue);}

当调度器需要发送request时,会调用noop_dispatch。该函数会直接从调度器所管理的request queue中获取一个request,然后调用elv_dispatch_sort函数将请求加入到设备所在的request queue中,最后

static int noop_dispatch(struct request_queue *q, int force)
{
    struct noop_data *nd = q->elevator->elevator_data;

    if (!list_empty(&nd->queue)) {
        struct request *rq;
        rq = list_entry(nd->queue.next, struct request, queuelist);    //从调度器的队列头中获取一个request
        list_del_init(&rq->queuelist);                     //将获取到的节点(node)从链表中删掉        elv_dispatch_sort(q, rq);                        //刚取出的rq放入到系统的请求队列
        return 1;
    }
    return 0;
}

由此可见,noop调度器的实现是很简单的,仅仅实现了一个调度器的框架,用一条链表把所有输入的request管理起来,简单方便,不会陷入极端,并且也不会损失多少性能,还能带来一定额实时性,但是缺点也非常明显,没有对io进行排序,没有参与调度,对于一些机械式的访问有明显的不足之处。

2.  deadline调度算法

从noop的调度来看,缺少优化和调度,那么deadline是如何来处理调度算法呢?从noop的分析过程来看,其数据结构决定了其方法,那么首先来看看deadline数据结构

struct deadline_data {
    struct rb_root sort_list[2];            //采用红黑树管理所有的request,请求地址作为索引值
    struct list_head fifo_list[2];         //采用FIFO队列管理所有的request,所有请求按照时间先后次序排列

    struct request *next_rq[2];           //批量处理请求过程中,需要处理的下一个request
    unsigned int batching;                 //统计当前已经批量处理完成的request
    sector_t last_sector;                  /* head position */
    unsigned int starved;                  /* times reads have starved writes */

    int fifo_expire[2];               //读写请求的超时时间值
    int fifo_batch;                  //批量处理的request数量
    int writes_starved;               //写饥饿值
    int front_merges;
};

从其内容来看,这个比noop的调度复杂好几倍,还引入了红黑树缩短查找时间,通过noop的elevator_init_fn我们大致可以看出会做些什么操作?基本都类似,对于deadline会多一些数据结构初始化的操作,所以没有分析的必要,我们主要关注其差异,那么deadline是如何处理请求,加入到队列中呢?

static void
deadline_add_request(struct request_queue *q, struct request *rq)
{
    struct deadline_data *dd = q->elevator->elevator_data;
    const int data_dir = rq_data_dir(rq);

    deadline_add_rq_rb(dd, rq);                        //请求加入到deadline调度器的sort_list红黑树中

    /*
     * set expire time and add to fifo list
     */
    rq->fifo_time = jiffies + dd->fifo_expire[data_dir];        //设置请求超时的时间,这个请求在这个时间到了必须得到响应
    list_add_tail(&rq->queuelist, &dd->fifo_list[data_dir]);      //将请求加入deadline调度器的list_fifo链表中
}

一种是采用红黑树(RB tree)的方式将所有request组织起来,通过request的访问地址作为索引;另一种方式是采用队列的方式将request管理起来,所有的request采用先来后到的方式进行排序,即FIFO队列。各个请求被放入到队列后,那么该轮到合并出场了。

static int
deadline_merge(struct request_queue *q, struct request **req, struct bio *bio)
{
    struct deadline_data *dd = q->elevator->elevator_data;
    struct request *__rq;
    int ret;

    /*
     * check for front merge
     */
    if (dd->front_merges) {
        sector_t sector = bio_end_sector(bio);                  //取bio的最后一个扇区 

        __rq = elv_rb_find(&dd->sort_list[bio_data_dir(bio)], sector);   //从红黑树中查找起始扇区号与sector相同的request
        if (__rq) {
            BUG_ON(sector != blk_rq_pos(__rq));

            if (elv_rq_merge_ok(__rq, bio)) {                  //各项属性的检查,确定bio可以插入
                ret = ELEVATOR_FRONT_MERGE;
                goto out;
            }
        }
    }

    return ELEVATOR_NO_MERGE;
out:
    *req = __rq;
    return ret;
}

那么通过上面的函数可能改变红黑树的结构,deadline_merged_request进行bio插入的善后工作,所以要将节点删除再重新进行插入

static void deadline_merged_request(struct request_queue *q,
                    struct request *req, int type)
{
    struct deadline_data *dd = q->elevator->elevator_data;

    /*
     * if the merge was a front merge, we need to reposition request
     */
    if (type == ELEVATOR_FRONT_MERGE) {
        elv_rb_del(deadline_rb_root(dd, req), req);            //将request从红黑树中删除 
        deadline_add_rq_rb(dd, req);                     //重新添加至红黑树
    }
}

上面完成队列的合并后,该轮到调度登场了,deadline_dispatch_requests完成这份工作。

static int deadline_dispatch_requests(struct request_queue *q, int force)
{
    struct deadline_data *dd = q->elevator->elevator_data;
    const int reads = !list_empty(&dd->fifo_list[READ]);        //确定读fifo的状态  
    const int writes = !list_empty(&dd->fifo_list[WRITE]);       //确定读fifo的状态
    struct request *rq;
    int data_dir;

    /*
     * batches are currently reads XOR writes
     *//* 如果批量请求处理存在,并且还没有达到批量请求处理的上限值,那么继续请求的批量处理 */  if (dd->next_rq[WRITE])
        rq = dd->next_rq[WRITE];
    else
        rq = dd->next_rq[READ];

    if (rq && dd->batching < dd->fifo_batch)
        /* we have a next request are still entitled to batch */
        goto dispatch_request;

    /*
     * at this point we are not running a batch. select the appropriate
     * data direction (read / write)
     */
/* 优先处理读请求队列 */
    if (reads) {                            //读请求fifo不为空
        BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[READ]));

        if (writes && (dd->starved++ >= dd->writes_starved))  //如果写请求队列存在饿死的现象,那么优先处理写请求队列
            goto dispatch_writes;

        data_dir = READ;

        goto dispatch_find_request;
    }

    /*
     * there are either no reads or writes have been starved
     */

    if (writes) {/* 没有读请求需要处理,或者写请求队列存在饿死现象 */
dispatch_writes:
        BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE]));

        dd->starved = 0;

        data_dir = WRITE;

        goto dispatch_find_request;
    }

    return 0;

dispatch_find_request:
    /*
     * we are not running a batch, find best request for selected data_dir
     */
    if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) {
/* 如果请求队列中存在即将饿死的request,或者不存在需要批量处理的请求,那么从FIFO队列头获取一个request */        /*
         * A deadline has expired, the last request was in the other
         * direction, or we have run out of higher-sectored requests.
         * Start again from the request with the earliest expiry time.
         */
        rq = rq_entry_fifo(dd->fifo_list[data_dir].next);
    } else {
/* 继续批量处理,获取需要批量处理的下一个request */        /*
         * The last req was the same dir and we have a next request in
         * sort order. No expired requests so continue on from here.
         */
        rq = dd->next_rq[data_dir];
    }

    dd->batching = 0;
/* 将request从调度器中移出,发送至设备 */
dispatch_request:
    /*
     * rq is the selected appropriate request.
     */
    dd->batching++;
    deadline_move_request(dd, rq);

    return 1;
}

deadline调度算法相对noop要复杂一点,其设计目标是,在保证请求按照设备扇区的顺序进行访问的同时,兼顾其它请求不被饿死,要在一个最终期限前被调度到,同时增加了读操作的具有有限度。

3. CFQ调度算法

暂不分析,待续....

时间: 2024-10-26 17:06:55

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

块设备驱动之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_MA

【深入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