通过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调度算法
暂不分析,待续....