Linux io运行情况,Linux IO调度层分析

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

#define ELEVATOR_INSERT_BACK2

#define ELEVATOR_INSERT_SORT3

#define ELEVATOR_INSERT_REQUEUE4

在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加入到请求队列中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值