__make_request()函数分析

我们知道,每个块设备程序都有一个请求队列与之关联。在块设备初始化时,会分配并初始化请求队列。在这个时候,我们便可以为块设备驱动程序指定特定的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被插入;

可将bio合并到request中

在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之后的一些附加操作。

创建一个新request

如果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中。

获取合适的req进行处理

在任何情况下,请求队列中的请求都由块设备驱动程序的策略例程来完成。策略例程是块设备驱动程序中的一个函数或一组函数,他与硬件块设备之间相互作用以满足请求队列中所汇集的请求。通过请求队列描述符中的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加入到请求队列中。

你可能感兴趣的:(__make_request()函数分析)