我们知道,每个块设备程序都有一个请求队列与之关联。在块设备初始化时,会分配并初始化请求队列。在这个时候,我们便可以为块设备驱动程序指定特定的IO调度算法,默认情况下是强制使用系统默认的调度算法。
熟悉块设备驱动的人知道,内核是通过generic_make_request函数来不断转发bio,直到该bio被挂载到物理设备的请求队列中。generic_make_request函数会获取bio所指向bdev的请求队列,并通过请求队列的q->make_request_fn方法来下发bio。如果该bdev指向的是物理设备时,make_request_fn是由内核的__make_request函数来实现,通常IO调度也就是在该函数中发生。该函数过程分析如下(只列出与IO调度有关系的部分):
int __make_request(request_queue_t *q, struct bio *bio)
{
……
el_ret = elv_merge(q, &req, bio);
switch (el_ret) {
//前两种可以合并
case ELEVATOR_BACK_MERGE:
if (!ll_back_merge_fn(q, req, bio))
break;
……
//插入到链表尾部
req->biotail->bi_next = bio;
req->biotail = bio;
……
if (!attempt_back_merge(q, req))
elv_merged_request(q, req, el_ret);
goto out;
case ELEVATOR_FRONT_MERGE:
……
//插入到链表头
bio->bi_next = req->bio;
req->bio = bio;
……
if (!attempt_front_merge(q, req))
elv_merged_request(q, req, el_ret);
goto out;
//不能合并,需要新创一个request。
/* ELV_NO_MERGE: elevator says don't/can't merge. */
default:
;
}
……
}
elv_merge函数相当重要,它试图在请求队列中找到一个能够合并该bio的request,函数返回三个可能值:
ELEVATOR_NO_MERGE:队列已经存在的请求中不能包含bio结构,需要创建一个新请求。
ELEVATOR_BACK_MERGE:bio结构可作为末尾的bio而插入到某个请求中;
ELEVATOR_FRONT_MERGE:bio结构可作为某个请求的第一个bio被插入;
在elv_merge函数中,首先试图将bio合并到上一次被合并的req中。如果可以合并的话,返回结果。q->last_merge保存上一次合并的请求的指针。
if (q->last_merge) {
ret = elv_try_merge(q->last_merge, bio);
if (ret != ELEVATOR_NO_MERGE) {
*req = q->last_merge;
return ret;
}
}
否则,通过elv_rqhash_find函数在调度算法的hash表(即elevator_queue中的hash字段)中查找可以将bio插入到某个req末尾的请求是否存在,如果存在,则返回ELEVATOR_BACK_MERGE,表明bio可作为末尾的bio而插入到某个请求中。
__rq = elv_rqhash_find(q, bio->bi_sector);
if (__rq && elv_rq_merge_ok(__rq, bio)) {
*req = __rq;
return ELEVATOR_BACK_MERGE;
}
如果hash表中不存在这样的req的话,则调用调度算法的elevator_merge_fn函数将bio合并到req表头中。
if (e->ops->elevator_merge_fn)
return e->ops->elevator_merge_fn(q, req, bio);
仍以deadline算法为例,deadline算法中的elevator_merge_fn函数是由deadline_merge函数实现。该函数试图将bio插入到请求的链表头。在deadline_merge函数中通过elv_rb_find函数在读写的排序队列sort_list中(通过红黑二叉树来实现)查找可以把bio插入到请求的链表头的req(这里只是找到可以插入的req,究竟bio是否可以插入到此req中是在执行插入的时候才做判断)如果找到,则返回ELEVATOR_FRONT_MERGE,否则返回ELEVATOR_NO_MERGE。
static int deadline_merge(request_queue_t *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->bi_sector + bio_sectors(bio);
__rq = elv_rb_find(&dd->sort_list[bio_data_dir(bio)], sector);
if (__rq) {
BUG_ON(sector != __rq->sector);
if (elv_rq_merge_ok(__rq, bio)) {
ret = ELEVATOR_FRONT_MERGE;
goto out;
}
}
}
return ELEVATOR_NO_MERGE;
out:
*req = __rq;
return ret;
}
之后会__make_request函数会根据elv_merge函数的返回值进行相应的处理。这里以插入到req链表末尾为例,插入到req链表头的与此相似。首先ll_back_merge_fn函数判断bio是否可以插入到req请求链表的末尾。
req->biotail->bi_next = bio;
req->biotail = bio;
req->nr_sectors = req->hard_nr_sectors += nr_sectors;
然后调用attempt_back_merge试图将req和req下一个请求合并。
static inline int attempt_back_merge(request_queue_t *q, struct request *rq)
{
struct request *next = elv_latter_request(q, rq);
if (next)
return attempt_merge(q, rq, next);
return 0;
}
该函数首先通过elv_latter_request函数从队列中取出当前rq的后一个请求next。elv_latter_request函数会调用调度算法的elevator_latter_req_fn方法。在Deadline算法中,该函数由elv_rb_latter_request实现。由于Deadline算法的排序队列使用红黑树来实现,所以这个函数很简单,就是调用rb_next在红黑树中查找当前rq的下个请求,并返回之。
如果当前请求rq的后一个请求next存在的话,则通过attempt_merge将next合并到当前请求req中。attempt_merge函数首先判断这两个请求是否可以合并。如果可以合并的话,首先将后一个请求的bio链接到前一个请求的bio尾部,更新前一个请求的biolist指针,之后调用elv_merge_requests函数将两个请求合并。elv_merge_requests函数会调用IO调度算法的elevator_merge_req_fn方法,在Deadline算法中该方法由deadline_merged_requests函数来实现。
在deadline_merged_requests中,首先在rq和next中选择最小的“Deadline值”作为rq的“Deadline”值。然后从排序队列rbtree和fifo中移除next请求。
之后回到attempt_merge中,合并请求之后还需更新该请求在hash表中的索引(elv_rqhash_reposition(q, rq)),从hash表中删除被合并的请求(elv_rqhash_del(q, next)),保存最后一个被合并请求的指针q->last_merge。
如果无法将当前req与其后面一个next合并的话,但是确实有bio加入当前req链表尾,那么会调用elv_merged_request函数。elv_merged_request会调用具体的IO调度算法中的elevator_merged_fn函数,执行插入bio之后的一些附加操作。
如果bio无法插入到当前请求队列中任何一个request,那么内核会创建一个新的request:
req = get_request_wait(q, rw_flags, bio);
然后使用bio初始化该req:
init_request_from_bio(req, bio);
之后将该req加入到请求队列中:
add_request(q, req);
在add_request函数中,会调用__elv_add_request将req以一种特定的方式加入到请求队列中。一共有4种插入方式:
#define ELEVATOR_INSERT_FRONT 1
#define ELEVATOR_INSERT_BACK 2
#define ELEVATOR_INSERT_SORT 3
#define ELEVATOR_INSERT_REQUEUE 4
在add_request中使用的是ELEVATOR_INSERT_SORT,表示使用elevator方式加入到队列中,而不是fifo。
static inline void add_request(request_queue_t * q, struct request * req)
{
drive_stat_acct(req, req->nr_sectors, 1);
/*
* elevator indicated where it wants this request to be
* inserted at elevator_merge time
*/
__elv_add_request(q, req, ELEVATOR_INSERT_SORT, 0);
}
__elv_add_request函数会根据req中cmd_flag字段修改插入到队列的方式。之后会调用elv_insert将请求插入到队列中。按照我们现在的流程, req会以ELEVATOR_INSERT_SORT的方式加入到队列中,在elv_insert中,即会执行下面分支:
case ELEVATOR_INSERT_SORT:
BUG_ON(!blk_fs_request(rq));
rq->cmd_flags |= REQ_SORTED;
q->nr_sorted++;
if (rq_mergeable(rq)) {
elv_rqhash_add(q, rq);
if (!q->last_merge)
q->last_merge = rq;
}
/*
* Some ioscheds (cfq) run q->request_fn directly, so
* rq cannot be accessed after calling
* elevator_add_req_fn.
*/
q->elevator->ops->elevator_add_req_fn(q, rq);
可以看到,如果req可以合并的话,它会被加入到hash表中,其中hash键值为req的起始扇区号加上req的请求扇区数。之后会调用IO调度算法的elevator_add_req_fn方法。在Deadline算法中,该函数由deadline_add_request方法实现。
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);
/*
* set expire time (only used for reads) and add to fifo list
*/
rq_set_fifo_time(rq, jiffies + dd->fifo_expire[data_dir]);
list_add_tail(&rq->queuelist, &dd->fifo_list[data_dir]);
}
可以看到,Deadline算法将req加入到相应读写的排序队列中,然后设置req的最后期限值,并加入到相应读写的fifo中。
在任何情况下,请求队列中的请求都由块设备驱动程序的策略例程来完成。策略例程是块设备驱动程序中的一个函数或一组函数,他与硬件块设备之间相互作用以满足请求队列中所汇集的请求。通过请求队列描述符中的request_fn方法就可以调用策略例程。
现在很多块设备驱动都采用如下策略:
策略例程处理队列中第一个请求并设置块设备控制器,以便在数据传送完成时可以产生一个中断。然后策略例程就中止。
当磁盘控制器产生中断时,中断控制器重新调用策略例程。策略例程要么为当前请求在启动一次数据传送,要么当请求的所有数据块已经传送完成时,把该请求从调度对立中删除然后开始处理下一个请求。
对于SCSI块设备,队列的策略函数request_fn方法是由scsi_request_fn来实现。scsi_request_fn方法通过调用elv_next_request()函数从请求队列中获取一个合适的请求。elv_next_request函数是一个循环,它实质上通过__elv_next_request函数从请求队列中找到下一个要处理的req。
static inline struct request *__elv_next_request(request_queue_t *q)
{
struct request *rq;
while (1) {
while (!list_empty(&q->queue_head)) {
rq = list_entry_rq(q->queue_head.next);
if (blk_do_ordered(q, &rq))
return rq;
}
if (!q->elevator->ops->elevator_dispatch_fn(q, 0))
return NULL;
}
}
在__elv_next_request函数中,首先如果请求队列不为空,则从请求队列中取出req并返回。如果请求队列中不存在请求了,这时会调用IO调度算法的elevator_dispatch_fn方法获取要处理的req并将其加入到请求队列中。在Deadline算法中,该函数由deadline_dispatch_requests方法来实现。
l 函数首先确定读写的方向。如果处于batching中,就意味着调度程序需要连续处理同一方向的请求。因此,根据batching的方向,可以确定当前处理请求的方向
if (dd->next_rq[WRITE])
rq = dd->next_rq[WRITE];
else
rq = dd->next_rq[READ];
if (rq) {
/* we have a "next request" */
if (dd->last_sector != rq->sector)
/* end the batch on a non sequential request */
dd->batching += dd->fifo_batch;
if (dd->batching < dd->fifo_batch)
/* we are still entitled to batch */
goto dispatch_request;
}
如果next req存在的话,则判断该req是否和上一个req相连。如果相连,并且batching的request数没有超过fifo_batch,则当前这个req就是我们要分发的req,所以直接将request分发到设备请求队列中。此时将忽略写饥饿和超时的处理。如果不连续,则要结束batching。
如果没有处于batching中,优先处理读请求。但在处理过程中考虑到了写饥饿。如果此时还有写请求,则写饥饿计数+1。如果此时写饥饿次数大于了writes_starved,则该写请求已经不能再被放弃了,因此直接跳到dispath_writes去处理写请求。否则,则继续处理读请求。
if (reads) {
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;
}
l 根据读写的方向,选择最合适的请求。
if (deadline_check_fifo(dd, data_dir)) {
/* An expired request exists - satisfy it */
dd->batching = 0;
rq = rq_entry_fifo(dd->fifo_list[data_dir].next);
} else if (dd->next_rq[data_dir]) {
/*
* 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];
} else {
struct rb_node *node;
/*
* The last req was the other direction or we have run out of
* higher-sectored requests. Go back to the lowest sectored
* request (1 way elevator) and start a new batch.
*/
dd->batching = 0;
node = rb_first(&dd->sort_list[data_dir]);
if (node)
rq = rb_entry_rq(node);
}
首先调用deadline_check_fifo在最后期限队列中检查第一个请求是否超时,如果超时,则处理这个请求。如果没有超时,则判断相同请求方向的下一个请求是否存在(根据secotr号来排序的)。如果存在,则处理该请求。否则说明了扫描到了电梯的末尾,则返回排序队列的第一个req(即sector最小的req)进行处理。
l 将找到的请求分发到请求队列中。
static void deadline_move_request(struct deadline_data *dd, struct request *rq)
{
const int data_dir = rq_data_dir(rq);
struct rb_node *rbnext = rb_next(&rq->rb_node);
dd->next_rq[READ] = NULL;
dd->next_rq[WRITE] = NULL;
if (rbnext)
dd->next_rq[data_dir] = rb_entry_rq(rbnext);
dd->last_sector = rq->sector + rq->nr_sectors;
/*
* take it off the sort and fifo list, move
* to dispatch queue
*/
deadline_move_to_dispatch(dd, rq);
}
首先保存当前req的下一个req指针,然后更新last_sector值,调用deadline_move_to_dispatch()将req从红黑树和FIFO队列中删除,然后将req加入到请求队列中。