一直以来,I/O顺序问题一直困扰着我。其实这个问题是一个比较综合的问题,它涉及的层次比较多,从VFS page cache到I/O调度算法,从i/o子系统到存储外设。而Linux I/O barrier就是其中重要的一部分。可能很多人认为,在做了文件写操作后,调用fsycn就能保证数据可靠地写入磁盘。大多数情况下,确实如此。但是,由于缓存的存在,fsycn这些同步操作,并不能保证存储设备把数据写入非易失性介质。如果此时存储设备发生掉电或者硬件错误,此时存储缓存中的数据将会丢失。这对于像日志文件系统中的日志这样的数据,其后果可能是非常严重的。因为日志文件系统中,数据的写入和日志的写入存在先后顺序。如果顺序发生错乱,则可能破坏文件系统。因此必须要有一种方式,来知道写入的数据是否真的被写入到外部存储的非易失性介质,比便文件系统根据写入情况来进行下一步的操作。如果把fsycn理解成OS级别同步的话,那么对于Barrier I/O,我的理解就是硬件级别的同步。具体Linux Barrier I/O的介绍,参考”Linux Barrier I/O”。本文主要分析Linux Barrier I/O的实现以及其他块设备驱动对它的影响。
Barrier I/O的目的是使其之前的I/O在其之前写入存储介质,之后的I/O需要等到其写入完成后才能得到执行。为了实现这个要求,我们最多需要执行2次flush(刷新)操作。(注意,这儿所说的flush,指的是刷新存储设备的缓存。但并不是所有存储设备都支持flush操作,所以不是所有设备都支持barrier I/O。支持根据这个要求,需要在初始化磁盘设备的请求队列时,显式的表明该设备支持barrier I/O的类型并实现prepare flush 方法,参见”Linux Barrier I/O”。)第一次flush是把barrier I/O之前的所有数据刷新,当刷新成功,也就是这些数据被存储设备告知确实写入其介质后,提交Barrier I/O所在的请求。然后执行第二次刷新,这次刷新的是Barrier I/O所携带的数据。当然,如果Barrier I/O没有携带任何数据,则第二次刷新可以省略。此外,如果存储设备支持FUA,则可以在提交Barrier I/O所携带的数据时,使用FUA命令。这样可以直接知道Barrier I/O所携带的数据是否写入成功,从而省略掉第二次刷新。
通过对Barrier I/O的处理过程,我们可以看到,其中最核心的是两次刷新操作和中间的Barrier I/O。为了表示这两次刷新操作以及该Barrier I/O,在Linux Barrier I/O的实现中,引入了3个辅助request: pre_flush_rq, bar_rq, post_flush_rq. 它们包含在磁盘设备的request_queue中。每当通用块层接收到上面发下来的Barrier I/O请求,就会把该请求拷贝到bar_rq,并把这3个请求依次加入请求队列,形成flush-》barrier-》flush请求序列。这样,在处理请求时,便能实现barrier I/O所要求的功能。当然,并不是所有设备都必须使用以上序列中的所有操作,具体要使用那些操作,是有设备自身特点决定的。为了标示设备所需要采取的操作序列,Linux Barrier I/O中定义了以下标志:
QUEUE_ORDERED_BY_DRAIN= 0x01,
QUEUE_ORDERED_BY_TAG= 0x02,
QUEUE_ORDERED_DO_PREFLUSH= 0x10,
QUEUE_ORDERED_DO_BAR= 0x20,
QUEUE_ORDERED_DO_POSTFLUSH= 0x40,
QUEUE_ORDERED_DO_FUA= 0x80,
QUEUE_ORDERED_NONE= 0x00,
QUEUE_ORDERED_DRAIN= QUEUE_ORDERED_BY_DRAIN |
QUEUE_ORDERED_DO_BAR,
QUEUE_ORDERED_DRAIN_FLUSH= QUEUE_ORDERED_DRAIN |
QUEUE_ORDERED_DO_PREFLUSH |
QUEUE_ORDERED_DO_POSTFLUSH,
QUEUE_ORDERED_DRAIN_FUA= QUEUE_ORDERED_DRAIN |
QUEUE_ORDERED_DO_PREFLUSH |
QUEUE_ORDERED_DO_FUA,
QUEUE_ORDERED_TAG= QUEUE_ORDERED_BY_TAG |
QUEUE_ORDERED_DO_BAR,
QUEUE_ORDERED_TAG_FLUSH= QUEUE_ORDERED_TAG |
QUEUE_ORDERED_DO_PREFLUSH |
QUEUE_ORDERED_DO_POSTFLUSH,
QUEUE_ORDERED_TAG_FUA= QUEUE_ORDERED_TAG |
QUEUE_ORDERED_DO_PREFLUSH |
QUEUE_ORDERED_DO_FUA,
不同的标志决定了不同的操作序列。此外,为了标示操作序列的执行状态,Linux Barrier I/O中定义了以下标志,它们表示了处理Barrier I/O过程中,执行操作序列的状态:
QUEUE_ORDSEQ_STARTED= 0x01,/* flushing in progress */
QUEUE_ORDSEQ_DRAIN= 0x02,/* waiting for the queue to be drained */
QUEUE_ORDSEQ_PREFLUSH= 0x04,/* pre-flushing in progress */
QUEUE_ORDSEQ_BAR= 0x08,/* original barrier req in progress */
QUEUE_ORDSEQ_POSTFLUSH= 0x10,/* post-flushing in progress */
QUEUE_ORDSEQ_DONE= 0x20,
整个Barrier I/O的处理流程,就是根据操作序列标志确定操作序列,然后执行操作序列并维护其状态的过程。下面将具体分析其代码实现。
1.提交Barrier I/O
提交Barrier I/O最直接的方法是设置该i/o的标志为barrier。其中主要有两个标志:WRITE_BARRIER和BIO_RW_BARRIER。WRITE_BARRIER定义在fs.h,其定义为:#define WRITE_BARRIER((1 《《 BIO_RW) | (1 《《 BIO_RW_BARRIER)),而BIO_RW_BARRIER定义在bio.h。这两个标志都可以直接作用于bio。此外,在更上一层,如buffer_header,它有个BH_Ordered位,如果该位设置,并且为去写方式为WRITE,则在submit_bh中会初始化bio的标志为WRITE_BARRIER。其中,在buffer_head.h中定义了操作BH_Ordered位的函数:set_buffer_ordered,buffer_ordered。
if (buffer_ordered(bh) && (rw & WRITE))
rw |= WRITE_BARRIER;
带有barrier i/o标志的bio通过submit_bio提交后,则需要为其生成request。在生成request的过程中,会根据该barrier i/o来设置request的一些标志。比如在__make_request-》init_request_from_bio中有:
if (unlikely(bio_barrier(bio)))
req-》cmd_flags |= (REQ_HARDBARRIER | REQ_NOMERGE);
这两个标志告诉elevator,该request包含barrier i/o,不需要合并。因此内核elevator在加入该request的时候会对其做专门的处理。
2.barrier request加入elevator调度队列
我们把包含barrier i/o的request叫做barrier request。Barrier request不同于一般的request,因此在将其加入elevator调度队列时,需要做专门处理。
void __elv_add_request(struct request_queue *q, struct request *rq, int where,
int plug)
{
if (q-》ordcolor)
rq-》cmd_flags |= REQ_ORDERED_COLOR;
if (rq-》cmd_flags & (REQ_SOFTBARRIER | REQ_HARDBARRIER)) {
/*
* toggle ordered color
*/
if (blk_barrier_rq(rq))
q-》ordcolor ^= 1;
/*
* barriers implicitly indicate back insertion
*/
if (where == ELEVATOR_INSERT_SORT)
where = ELEVATOR_INSERT_BACK;
/*
* this request is scheduling boundary, update
* end_sector
*/
if (blk_fs_request(rq)) {
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_BACK;
…
elv_insert(q, rq, where);
}
为了标明调度队列中两个barrier request的界限,request引入了order color。通过这两句话,把两个barrier request之前的request“填上”不同的颜色:
if (q-》ordcolor)
rq-》cmd_flags |= REQ_ORDERED_COLOR;
if (blk_barrier_rq(rq))
q-》ordcolor ^= 1;
比如:
Rq1re2barrrier1 req3req4barrier2
000111
因为之后,在处理barrier request时,会为其生成一个request序列,其中可能包含3个request。通过着色,可以区分不同barrier request的处理序列。
另外,对barrier request的特殊处理就是设置其插入elevator调度队列的方向为ELEVATOR_INSERT_BACK。通常,我们插入调度队列的方向是ELEVATOR_INSERT_SORT,其含义是按照request所含的数据块的盘块顺序插入。在elevator调度算法中,往往会插入盘块顺序的红黑树中,如deadline调度算法。之后调度算法在往设备请求队列中分发request的时候,大致会按照这个顺序分发(有可能发生回扫,饥饿,操作等)。因此这这种插入方式不适合barrier request。Barrier request必须插到所有request的最后。这样,才能把之前的request 都”flush”下去。
选择好插入方向后,下面就是调用elv_insert来具体插入一个barrier request:
void elv_insert(struct request_queue *q, struct request *rq, int where)
{
rq-》q = q;
switch (where) {
case ELEVATOR_INSERT_FRONT:
rq-》cmd_flags |= REQ_SOFTBARRIER;
list_add(&rq-》queuelist, &q-》queue_head);
break;
case ELEVATOR_INSERT_BACK:
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_remove_plug(q);
q-》request_fn(q);
break;
case ELEVATOR_INSERT_SORT:
…
case ELEVATOR_INSERT_REQUEUE:
/*
* If ordered flush isn‘t in progress, we do front
* insertion; otherwise, requests should be requeued
* in ordseq order.
*/
rq-》cmd_flags |= REQ_SOFTBARRIER;
/*
* Most requeues happen because of a busy condition,
* don’t force unplug of the queue for that case.
*/
if (q-》ordseq == 0) {
list_add(&rq-》queuelist, &q-》queue_head);
break;
}
ordseq = blk_ordered_req_seq(rq);
list_for_each(pos, &q-》queue_head) {
struct request *pos_rq = list_entry_rq(pos);
if (ordseq 《= blk_ordered_req_seq(pos_rq))
break;
}
list_add_tail(&rq-》queuelist, pos);
break;
。.
}
if (unplug_it && blk_queue_plugged(q)) {
int nrq = q-》rq.count[READ] + q-》rq.count[WRITE]
– q-》in_flight;
if (nrq 》= q-》unplug_thresh)
__generic_unplug_device(q);
}
}
前面分析了,正常情况下,barrier request的插入方向是ELEVATOR_INSERT_BACK。在把barrier request加入设备请求队列末尾之前,需要调用elv_drain_elevator把调度算法中的请求队列中的request都“排入”设备的请求队列。注意,elv_drain_elevator调用的是while (q-》elevator-》ops-》elevator_dispatch_fn(q, 1));。它设置了force dispatch,因此elevator调度算法必须强制把所有缓存在自己调度队列中的request都分发到设备的请求队列。比如AS调度算法,如果在force dispatch情况下,它会终止预测和batching。这样,当前barrier request必然是插入设备请求队列的最后一个request。不然,如果AS可能出于预测状态,它可能延迟request的处理,即缓存在调度算法队列中的request排不干净。现在,barrier request和之前的request都到了设备的请求队列,下面就是调用设备请求队列的request_fn来处理每个请求。(blk_remove_plug(q)之前的注视不是很明白,需要进一步分析)
上面是request barrier正常的插入情况。但是,如果在barrier request的处理序列中,某个request可能出现错误,比如设备繁忙,无法完成flush操作。这个时候,通过错误处理,该barrier request处理序列中的request需要requeue: ELEVATOR_INSERT_REQUEUE。因为一个barrier request的处理序列存在preflush,barrier,postflush这个顺序,所以当其中一个request发生requeue时,需要考虑barrier request处理序列当前的顺序,看究竟执行到了哪一步了。然后根据当前的序列,查找相应的request并加入队尾。
3.处理barrier request
现在,barrier request以及其前面的request都被排入了设备请求队列。处理过程具体是在request_fn中,调用__elv_next_request来进行处理的。
static inline struct request *__elv_next_request(struct request_queue *q)
{
struct request *rq;
while (1) {
while (!list_empty(&q-》queue_head)) {
rq = list_entry_rq(q-》queue_head.next);
if (blk_do_ordered(q, &rq))
return rq;
}
if (!q-》elevator-》ops-》elevator_dispatch_fn(q, 0))
return NULL;
}
}
__elv_next_request每次取出设备请求队列中队首request,并进行blk_do_ordered操作。该函数具体处理barrier request。blk_do_ordered的第二个参数为输入输出参数,它表示下一个要执行的request。Request从设备队列头部取出,交由blk_do_ordered判断,如果是一般的request,则该request会被直接返回给设备驱动,进行处理。如果是barrier request,则该request会被保存,rq会被替换成barrier request执行序列中相应的request,从而开始执行barrier request的序列。
int blk_do_ordered(struct request_queue *q, struct request **rqp)
{
struct request *rq = *rqp;
const int is_barrier = blk_fs_request(rq) && blk_barrier_rq(rq);
if (!q-》ordseq) {
if (!is_barrier)
return 1;
if (q-》next_ordered != QUEUE_ORDERED_NONE) {
*rqp = start_ordered(q, rq);
return 1;
} else {
…
}
}
/*
* Ordered sequence in progress
*/
/* Special requests are not subject to ordering rules. */
if (!blk_fs_request(rq) &&
rq != &q-》pre_flush_rq && rq != &q-》post_flush_rq)
return 1;
if (q-》ordered & QUEUE_ORDERED_TAG) {
/* Ordered by tag. Blocking the next barrier is enough. */
if (is_barrier && rq != &q-》bar_rq)//the next barrier i/o
*rqp = NULL;
} else {
/* Ordered by draining. Wait for turn. */
WARN_ON(blk_ordered_req_seq(rq) 《 blk_ordered_cur_seq(q));
if (blk_ordered_req_seq(rq) 》 blk_ordered_cur_seq(q))
*rqp = NULL;
}
return 1;
}
blk_do_ordered分为两部分。首先,如果barrier request请求序列还没开始,也就是还没有开始处理barrier request,则调用start_ordered来初始化barrier request处理序列。此外,如果当前正处于处理序列中,则根据处理序列的阶段来处理当前request。
初始化处理序列start_ordered
static inline struct request *start_ordered(struct request_queue *q,
struct request *rq)
{
q-》ordered = q-》next_ordered;
q-》ordseq |= QUEUE_ORDSEQ_STARTED;
/*
* Prep proxy barrier request.
*/
blkdev_dequeue_request(rq);
q-》orig_bar_rq = rq;
rq = &q-》bar_rq;
blk_rq_init(q, rq);
if (bio_data_dir(q-》orig_bar_rq-》bio) == WRITE)
rq-》cmd_flags |= REQ_RW;
if (q-》ordered & QUEUE_ORDERED_FUA)
rq-》cmd_flags |= REQ_FUA;
init_request_from_bio(rq, q-》orig_bar_rq-》bio);
rq-》end_io = bar_end_io;
/*
* Queue ordered sequence. As we stack them at the head, we
* need to queue in reverse order. Note that we rely on that
* no fs request uses ELEVATOR_INSERT_FRONT and thus no fs
* request gets inbetween ordered sequence. If this request is
* an empty barrier, we don‘t need to do a postflush ever since
* there will be no data written between the pre and post flush.
* Hence a single flush will suffice.
*/
if ((q-》ordered & QUEUE_ORDERED_POSTFLUSH) && !blk_empty_barrier(rq))
queue_flush(q, QUEUE_ORDERED_POSTFLUSH);
else
q-》ordseq |= QUEUE_ORDSEQ_POSTFLUSH;
elv_insert(q, rq, ELEVATOR_INSERT_FRONT);
if (q-》ordered & QUEUE_ORDERED_PREFLUSH) {
queue_flush(q, QUEUE_ORDERED_PREFLUSH);
rq = &q-》pre_flush_rq;
} else
q-》ordseq |= QUEUE_ORDSEQ_PREFLUSH;
if ((q-》ordered & QUEUE_ORDERED_TAG) || q-》in_flight == 0)
q-》ordseq |= QUEUE_ORDSEQ_DRAIN;
else
rq = NULL;
return rq;
}
start_ordered为处理barrier request准备整个request序列。它主要完成以下工作(1)保存原来barrier request,用设备请求队列中所带的bar_rq来替换该barrier request。其中包括从该barreir request复制request的各种标志以及bio。(2)根据设备声明的所支持的barrier 序列,初始化request序列。该序列最多可能包含三个request:pre_flush_rq, bar_rq, post_flush_rq。它们被倒着插入对头,这样就可以一次执行它们。当然,这三个request并不是必须得。比如,如果设备支持的处理序列仅为QUEUE_ORDERED_PREFLUSH,则只会把pre_flush_rq和bar_qr插入队列。又比如,如果barrier reques没有包含任何数据,则post flush可以省略。因此,也只会插入上面两个request。注意,pre_flush_rq和post_flush_rq都是在插入之前,调用queue_flush初始化得。除了插入请求序列包含的request,同时还需要根据请求序列的设置来设置当前进行的请求序列:q-》ordseq。现在请求序列还没有开始处理,怎么在这儿设置当前的请求序列呢?那是因为,根据设备的特性,三个request不一定都包括。而每个request代表了一个序列的处理阶段。这儿,我们根据设备的声明安排了整个请求序列,因此知道那些请求,也就是那些处理阶段不需要。在这儿把这些阶段的标志置位,表示这些阶段已经执行完毕,是为了省略相应的处理阶段。现在,设备请求队列中的请求以及处理序列如下所示:
Rq1re2 pre_flush_rqbar_rqpost_flush_rq
00000
QUEUE_ORDSEQ_DRAINQUEUE_ORDERED_PREFLUSH QUEUE_ORDSEQ_BARQUEUE_ORDERED_POSTFLUSH