一个IO传奇的一生-6

来源:互联网 发布:sql 求平均值 编辑:程序博客网 时间:2024/05/01 16:50

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进行合并处理之后,需要对该requesthash表中进行重新定位。这主要是因为request的尾地址发生了变化,有可能会超过一个hash区间的范围。

如果后向合并失败,那么调度器会尝试前向合并。不是所有的调度器支持前向合并,如果调度器支持这种方式,那么需要注册elevator_merge_fn函数实现前向调度功能。例如deadline算法采用了红黑树的方式实现前向调度。如果前向调度无法完成合并。那么调度器认为该合并失败,需要产生一个新的request,并且采用现有bio对其进行初始化,然后加入到request queue中进行调度处理。

IO利用generic_make_request来到块设备层之后,对其进行处理的重要函数blk_queue_bio主要任务是合并IO。由于不同的调度器有不同的合并方法、IO分类方法,所以,具体调度器的算法会采用函数注册的方式实现。blk_queue_bio仅仅是一个上层函数,最主要完成后向合并、调用调度器方法进行前向合并以及初始化request准备调度。

<待续>

本文出自 “存储之道” 博客,转载请与作者联系!

0 0
原创粉丝点击