在进行io调度层源码分析前,先来说说io调度层的主要作用:
- Bio 的合并问题以及request之间的合并问题。
Request 的调度问题。 request 在何时可以从调度器中取出,并且送入底层驱动程序继续进行处理?不同的应用对处理延时以及io带宽的要求是不同的。因此根据需要选择合适的调度器。
我们熟知,一个 request 在送往设备之前会被放入到每个设备所对应的 request queue 。其实,通过分析一个 IO 在 elevator 层其实会经过很多 request queue ,不同的 request queue 会有不同的作用。新内核引入了plug队列,每个线程都有一个自己的plug队列,当产生一个新的request请求时,如果这个线程没有 plug 请求队列,那么 IO request 直接被送入 elevator 。否则加入plug队列,在 plug 请求队列中等待的 request 会在请求 unplug 的过程中被送入 elevator 的请求队列。
在一般的请求处理过程中, request 被创建并且会被挂载到plug request queue 中,然后通过 flush request 方法将 request 从 plug request queue 中移至 elevator request queue 中。当一个新的 BIO 需要被处理时,其可以在 plug request queue 或者 elevator request queue 中进行合并。当需要将请求发送到底层设备时,可以通过调用 run_queue 的方法将 elevator 分类处理过的 request 转移至 device request queue 中,最后由设备对应的驱动去处理 device request queue 中的每个request请求。
当一个 IO 刚来到通用块层时,首先需要判断该 IO 是否可以和正在等待处理的 request 进行合并。这一步主要是通过 elv_merge() 函数来实现的,需要注意的是,在调用 elv_merge 进行合并操作之前,首先需要判断 plug request queue 是否可以进行合并,如果不能合并,那么才调用 elv_merge 进行 elevator request queue 的合并操作。一旦 bio 找到了可以合并的 request ,那么,这个 IO 就会合并放入对应的 request 中,否则需要创建一个新的 request ,并且如果存在plug队列则放入到 plug request queue 中。
下面将用具体的代码解释上述过程:
void blk_queue_bio(struct request_queue *q, struct bio *bio)
{
const bool sync = !!(bio->bi_rw & REQ_SYNC);
struct blk_plug *plug;
int el_ret, rw_flags, where = ELEVATOR_INSERT_SORT;
struct request *req;
unsigned int request_count = 0;
blk_queue_bounce(q, &bio);
/* 为了建立bounce buffer,以防止不适合这次I/O操作的时候利用bounce buffer*/
if (bio_integrity_enabled(bio) && bio_integrity_prep(bio)) {
//数据完整性校验
bio_endio(bio, -EIO);
return;
}
if (bio->bi_rw & (REQ_FLUSH | REQ_FUA)) {
//FLUSH的bio,直接申请新的request
spin_lock_irq(q->queue_lock);//加锁
where