Elevator子系统介绍
Elevator子系统是IO 路径上非常重要的组成部分,前面已经分析过,elevator中实现了多种类型的调度器,用于满足不同应用的需求。那么,从整个IO路径的角度来看,elevator这层主要解决IO的QoS问题,通常需要解决如下两大问题:
1)Bio的合并问题。主要考虑bio是否可以和scheduler中的某个request进行合并。因为从磁盘的角度来看,临近的请求需要合并,所有的IO需要顺序化处理,这样磁头才能往一个方向运行,避免无规则的乱序运行。
2)Request的调度问题。request在何时可以从scheduler中取出,并且送入底层驱动程序继续进行处理?不同的应用可能需要不同的带宽资源,读写请求的带宽、延迟控制也可以不一样,因此,需要解决request的调度处理,从而可以更好的控制IO的QoS。
通过上面分析,一个IO在经过块设备层处理之后,终于来到了elevator层。我们熟知,一个request在送往设备之前会被放入到每个设备所对应的request queue。其实,通过分析一个IO在elevator层其实会经过很多request queue,不同的request queue会有不同的作用。如下图所示,一个IO在经历很多层queue的调度处理之后,最后才能达到每个设备的request queue。Linux中各个request queue之间的关系如下图所示:
在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的类型也就是我们通常所说的Noop、deadline以及CFQ方法。最后,Elevator中的request会在一定的策略控制下被送入每个设备的request queue。从这个结构中,我们可以看出,只要控制住了elevator的调度器,那么我们就可以控制每个设备IO的优先级,从而达到IO QoS的目的。
通过分析,我们已经知道Request在三类request queue中被调度处理,其主要处理时机点可以描述如下:
在一般的请求处理过程中,request被创建并且会被挂载到unplug request queue中,然后通过flush request方法将request从unplug 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子系统。