一个IO传奇的一生-6

  • Post author:
  • Post category:其他




IO


调度器










IO


旅行到调度器的时候,发现自己受到的待遇竟然很不一样,有些


IO


倚仗着特权很快就放行了;有些


IO


迟迟得不到处理,甚至在有些系统中居然饿死!面对这样的现状,


IO


显然是很不高兴的,凭什么别人就能被很快送到下一个旅程,自己需要在调度器上耗费青春年华?这里是拼爹的时代,人家出身好,人家是读请求,人家就可以很快得到资源。咱们是写请求,出生贫寒�”>* may now be mergeable after it had proven unmergeable (above).


* We don't worry about that case for efficiency. It won't happen

* often, and the elevators are able to handle it.

*/

/* 采用bio对request请求进行初始化 */

init_request_from_bio(req, bio);

if


(test_bit(QUEUE_FLAG_SAME_COMP, &q->queue_flags))

req->cpu = raw_smp_processor_id();

plug = current->plug;

if


(plug) {


/*

* If this is the first request added after a plug, fire

* of a plug trace. If others have been added before, check

* if we have multiple devices in this plug. If so, make a

* note to sort the list before dispatch.

*/

if


(list_empty(&plug->list))

trace


_block_plug(q);

else


{


if


(!plug->should_sort) {


struct request *__rq;

__rq = list_entry_rq(plug->list.prev);

if


(__rq->q != q)

plug->should_sort =


1


;

}

if


(request_count >= BLK_MAX_REQUEST_COUNT) {


/* 请求数量达到队列上限值,进行unplug操作 */

blk_flush_plug_list(plug,


false


);

trace


_block_plug(q);

}

}

/* 将请求加入到队列 */

list_add_tail(&req->queuelist, &plug->list);

drive_stat_acct(req,


1


);

}


else


{


/* 在新的内核中,如果用户没有调用start_unplug,那么,在IO scheduler中是没有合并的,一旦加入到request queue中,马上执行unplug操作,这个地方个人觉得有点不妥,不如以前的定时调度机制。对于ext3文件系统,在刷写page cache的时候,都需要首先执行start_unplug操作,因此都会进行request/bio的合并操作。 */

spin_lock_irq(q->queue_lock);

/* 将request加入到调度器中 */

add_acct_request(q, req, where);

/* 调用底层函数执行unplug操作 */

__blk_run_queue(q);

out_unlock:

spin_unlock_irq(q->queue_lock);

}

}


对于


blk_queue_bio


函数主要做了三件事情:

1)

进行请求的后向合并操作

2)

进行请求的前向合并操作

3)

如果无法合并请求,那么为


bio


创建一个


request


,然后进行调度





bio


合并过程中,最为关键的函数是


elv_merge


。该函数主要工作是判断


bio


是否可以进行后向合并或者前向合并。对于所有的调度器,后向合并的逻辑都是相同的。在系统中维护了一个


request hash


表,然后通过


bio


请求的起始地址进行


hash


寻址。


Hash


表的生成原理比较简单,就是将所有


request


的尾部地址进行分类,分成几大区间,然后通过


hash


函数可以寻址这几大区间。


Hash


函数是:


hash_long(ELV_HASH_BLOCK((sec)), elv_hash_shift)


一旦通过


hash


函数找到所有位于这个区间的


request


之后,通过遍历的方式匹配到所需要的


request


。具体该过程的实现函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

static


struct request *elv_rqhash_find(struct request_queue *q, sector_t offset)

{


struct elevator_queue *e = q->elevator;

/* 通过hash函数找到区间内的所有request */

struct hlist_head *hash_list = &e->hash[ELV_HASH_FN(offset)];

struct hlist_node *entry, *next;

struct request *rq;

/* 遍历地址区间内的所有request */

hlist_for_each_entry_safe(rq, entry, next, hash_list, hash) {


BUG_ON(!ELV_ON_HASH(rq));

if


(unlikely(!rq_mergeable(rq))) {


__elv_rqhash_del(rq);

continue


;

}

/* 如果地址匹配,那么找到所需的request */

if


(rq_hash_key(rq) == offset)

return


rq;

}

return


NULL;

}


采用


hash


方式维护


request


,有一点需要注意:当一个


request


进行合并处理之后,需要对该


request





hash


表中进行重新定位。这主要是因为


request


的尾地址发生了变化,有可能会超过一个


hash


区间的范围。


如果后向合并失败,那么调度器会尝试前向合并。不是所有的调度器支持前向合并,如果调度器支持这种方式,那么需要注册


elevator_merge_fn


函数实现前向调度功能。例如


deadline


算法采用了红黑树的方式实现前向调度。如果前向调度无法完成合并。那么调度器认为该合并失败,需要产生一个新的


request


,并且采用现有


bio


对其进行初始化,然后加入到


request queue


中进行调度处理。





IO


利用


generic_make_request


来到块设备层之后,对其进行处理的重要函数


blk_queue_bio


主要任务是合并


IO


。由于不同的调度器有不同的合并方法、


IO


分类方法,所以,具体调度器的算法会采用函数注册的方式实现。


blk_queue_bio


仅仅是一个上层函数,最主要完成后向合并、调用调度器方法进行前向合并以及初始化


request


准备调度。

<待续>

本文出自 “

存储之道

” 博客,转载请与作者联系!