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


  上一节主要梳理了下调度器的流程,其中对于blk_init_allocated_queue有一个elevator_init的初始化函数,下面从elevator_init为突破口,来看看内核中有哪些的调度算法。对于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调度算法

暂不分析,待续....


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM