Elevator子系统介绍

Elevator子系统是IO 路径上非常重要的组成部分,前面已经分析过,elevator中实现了多种类型的调度器,用于满足不同应用的需求。那么,从整个IO路径的角度来看,elevator这层主要解决IOQoS问题,通常需要解决如下两大问题:

1)Bio的合并问题。主要考虑bio是否可以和scheduler中的某个request进行合并。因为从磁盘的角度来看,临近的请求需要合并,所有的IO需要顺序化处理,这样磁头才能往一个方向运行,避免无规则的乱序运行。

2)Request的调度问题。request在何时可以从scheduler中取出,并且送入底层驱动程序继续进行处理?不同的应用可能需要不同的带宽资源,读写请求的带宽、延迟控制也可以不一样,因此,需要解决request的调度处理,从而可以更好的控制IOQoS

通过上面分析,一个IO在经过块设备层处理之后,终于来到了elevator层。我们熟知,一个request在送往设备之前会被放入到每个设备所对应的request queue。其实,通过分析一个IOelevator层其实会经过很多request queue,不同的request queue会有不同的作用。如下图所示,一个IO在经历很多层queue的调度处理之后,最后才能达到每个设备的request queueLinux中各个request queue之间的关系如下图所示:

一个IO的传奇一生(8) -- elevator子系统_elevator

Linux-3.2中,已经采用新的unplug机制对请求进行批量unplug处理,相对于2.6.23 kernel这是新的一层。在老Kernel中,没有这层unplug机制,request请求可以直接进入elevator,然后通过内核中的unplug定时器对elevator中的request进行unplug调度处理。在新kernel中,每个线程可以对自己的request进行unplug调度处理。例如,ext3文件系统的writeback线程可以主动unplug自己的request,这种application awareness的方法可以最大限度的减少请求处理的延迟时间。

从上图可以看出,一个IO请求首先进入每个线程域所在的unplug请求队列。如果这个线程没有unplug请求队列,那么IO request直接被送入elevator。在unplug请求队列中等待的request会在请求unplug的过程中被送入elevator的请求队列。每个设备可以采用不同类型的IO调度方法,因此,在elevator中的IO分类方法会有所不同。这里Elevator的类型也就是我们通常所说的Noopdeadline以及CFQ方法。最后,Elevator中的request会在一定的策略控制下被送入每个设备的request queue。从这个结构中,我们可以看出,只要控制住了elevator的调度器,那么我们就可以控制每个设备IO的优先级,从而达到IO QoS的目的。

通过分析,我们已经知道Request在三类request queue中被调度处理,其主要处理时机点可以描述如下:

一个IO的传奇一生(8) -- elevator子系统_elevator_02

在一般的请求处理过程中,request被创建并且会被挂载到unplug request queue中,然后通过flush request方法将requestunplug request queue中移至elevator request queue中。当一个新的BIO需要被处理时,其可以在unplug request queue或者elevator request queue中进行合并。当需要将请求发送到底层设备时,可以通过调用run_queue的方法将elevator分类处理过的request转移至device request queue中,最后调用scsi_dispatch_cmd方法将请求发送到HBA。在这个过程有一些问题需要处理:底层设备可能存在故障;HBA的处理队列是有长度限制的。因此,如何连续调度device request queue及重新调度request成了一个需要考虑的问题。在Linux中,如果scsi层需要重新调度一个request,可以通过blk_requeue_request接口来完成。通过该接口,可以把request重新放回到device request queue中。另外,在一个request结束之后的回调函数中,需要通过scsi_run_queue函数来再次调度处理device request queue中的剩余请求,从而可以保证批量处理device request queue中的请求,HBA也一直运行在最大的queue depth深度。

Elevator层关键函数分析

Elv_merge

当一个IO离开块设备层,需要发送到底层设备时,首先需要判断该IO是否可以和正在等待处理的request进行合并。这一步主要是通过elv_merge()函数来实现的,需要注意的是,在调用elv_merge进行合并操作之前,首先需要判断unplug request queue是否可以进行合并,如果不能合并,那么才调用elv_merge进行elevator request queue的合并操作。一旦bio找到了可以合并的request,那么,这个IO就会合并放入对应的request中,否则需要创建一个新的request,并且放入到unplug request queue中。

Elevator层提供的bio合并函数分析如下:

int elv_merge(struct request_queue *q, struct request **req, struct bio *bio)
{
struct elevator_queue *e = q->elevator;
struct request *__rq;
int ret;
/*
* Levels of merges:
*  nomerges:  No merges at all attempted
*  noxmerges: Only simple one-hit cache try
*  merges:    All merge tries attempted
*/
if (blk_queue_nomerges(q))
return ELEVATOR_NO_MERGE;
/*
* First try one-hit cache.
* 尝试和最近的request进行合并
*/
if (q->last_merge) {
ret = elv_try_merge(q->last_merge, bio);
if (ret != ELEVATOR_NO_MERGE) {
/* 可以和last_merge进行合并 */
*req = q->last_merge;
return ret;
}
}
if (blk_queue_noxmerges(q))
return ELEVATOR_NO_MERGE;
/*
* See if our hash lookup can find a potential backmerge.
* 查找elevator中的后向合并的hash table,获取可以合并的request
*/
__rq = elv_rqhash_find(q, bio->bi_sector);
if (__rq && elv_rq_merge_ok(__rq, bio)) {
*req = __rq;
return ELEVATOR_BACK_MERGE;
}
/* 查找scheduler检查是否可以进行前向合并,如果可以,那么进行前向合并 */
if (e->ops->elevator_merge_fn)
return e->ops->elevator_merge_fn(q, req, bio);
return ELEVATOR_NO_MERGE;
}

__elv_add_request

需要将一个request加入到request queue中时,可以调用__elv_add_request函数。通过该函数可以将request加入到elevator request queue或者device request queue中。该函数的实现如下:

void __elv_add_request(struct request_queue *q, struct request *rq, int where)
{
trace_block_rq_insert(q, rq);
rq->q = q;
if (rq->cmd_flags & REQ_SOFTBARRIER) {
/* barriers are scheduling boundary, update end_sector */
if (rq->cmd_type == REQ_TYPE_FS ||
(rq->cmd_flags & REQ_DISCARD)) {
q->end_sector = rq_end_sector(rq);
q->boundary_rq = rq;
}
} else if (!(rq->cmd_flags & REQ_ELVPRIV) &&
(where == ELEVATOR_INSERT_SORT ||
where == ELEVATOR_INSERT_SORT_MERGE))
where = ELEVATOR_INSERT_BACK;
switch (where) {
case ELEVATOR_INSERT_REQUEUE:
case ELEVATOR_INSERT_FRONT:
/* 将request加入到device request queue的队列前 */
rq->cmd_flags |= REQ_SOFTBARRIER;
list_add(&rq->queuelist, &q->queue_head);
break;
case ELEVATOR_INSERT_BACK:
/* 将request 加入到device request queue的队列尾 */
rq->cmd_flags |= REQ_SOFTBARRIER;
elv_drain_elevator(q);
list_add_tail(&rq->queuelist, &q->queue_head);
/*
* We kick the queue here for the following reasons.
* - The elevator might have returned NULL previously
*   to delay requests and returned them now.  As the
*   queue wasn't empty before this request, ll_rw_blk
*   won't run the queue on return, resulting in hang.
* - Usually, back inserted requests won't be merged
*   with anything.  There's no point in delaying queue
*   processing.
*/
__blk_run_queue(q);
break;
case ELEVATOR_INSERT_SORT_MERGE:
/* 尝试对request进行合并操作,如果无法合并将request加入到elevator request queue中 */
/*
* If we succeed in merging this request with one in the
* queue already, we are done - rq has now been freed,
* so no need to do anything further.
*/
if (elv_attempt_insert_merge(q, rq))
break;
case ELEVATOR_INSERT_SORT:
/* 将request加入到elevator request queue中 */
BUG_ON(rq->cmd_type != REQ_TYPE_FS &&
!(rq->cmd_flags & REQ_DISCARD));
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);
break;
case ELEVATOR_INSERT_FLUSH:
rq->cmd_flags |= REQ_SOFTBARRIER;
blk_insert_flush(rq);
break;
default:
printk(KERN_ERR "%s: bad insertion point %d\n",
__func__, where);
BUG();
}
}

Elv_dispatch_sort

elevator request queue中的request需要发送到device request queue中时,可以调用elv_dispatch_sort函数,通过该函数可以对request进行排序,插入到合适的位置。Elv_dispatch_sort函数的实现如下:

void elv_dispatch_sort(struct request_queue *q, struct request *rq)
{
sector_t boundary;
struct list_head *entry;
int stop_flags;
if (q->last_merge == rq)
q->last_merge = NULL;
elv_rqhash_del(q, rq);
q->nr_sorted--;
boundary = q->end_sector;
stop_flags = REQ_SOFTBARRIER | REQ_STARTED;
list_for_each_prev(entry, &q->queue_head) {
struct request *pos = list_entry_rq(entry);
if ((rq->cmd_flags & REQ_DISCARD) !=
(pos->cmd_flags & REQ_DISCARD))
break;
if (rq_data_dir(rq) != rq_data_dir(pos))
break;
if (pos->cmd_flags & stop_flags)
break;
if (blk_rq_pos(rq) >= boundary) {
if (blk_rq_pos(pos) < boundary)
continue;
} else {
if (blk_rq_pos(pos) >= boundary)
break;
}
if (blk_rq_pos(rq) >= blk_rq_pos(pos))
break;
}
list_add(&rq->queuelist, entry);
}

Elevator子系统小结

Elevator子系统是实现IO调度处理的框架,功能不同的scheduler可以做为一种elevator type加入到这个框架中来。所以,如果需要设计实现一个自定义的scheduler,那么首先必须需要了解elevator子系统。