linux的barrier
I/O barrier请求用来保证I/O请求的顺序。其主要是针对那些需要保证顺序的写请求,比如日志的checkpoint。在请求队列中,那些排在barrier请求前的请求,必须在barrier请求开始之前完成。(这里所说的完成指数据写入物理介质,而不是保存在OS或者设备缓存中)。而那些排在barrier请求后的请求,只有在barrier请求完成后才能开始(这儿所说的完成,同样是指barrier请求的捎带数据写入物理介质)。
总的来说, I/O barrier请求拥有一下两个性质:
1.请求顺序
非barrier请求不能跨越barrier请求。barrier请求之前的请求必须先于barrier请求进行处理,barrier请求之后的请求必须在barrier请求完成后进行处理。
根据磁盘驱动器的特性,以上条件可以用以下三种方式来实现:
i.对于设备队列深度大于1(TCQ设备)并且支持ordered
tag的设备,块设备层只需要发送一个标为ordered的请求来作为barrier,底层驱动,控制器和磁盘驱动器负责确保请求的顺序。现在,大多数SCSI控制器/磁盘驱动器都应该支持这个特性。
ii. 对于设备队列深度大于1但是不支持ordered
tag的设备,块设备层确保barrier请求往设备分发前,之前的请求将先被处理完。块设备层也会延迟barrier请求之后的请求,直到barrier请求完成。老的SCSI控制器/磁盘驱动器以及SATA磁盘驱动器属于这类设备。
iii.
对于设备队列深度为1的设备,这种设备相当于ii类设备的特例。只要保证分发的顺序就够了(保证i/o调度器不打乱顺序)。较老的SCSI控制器/磁盘驱动器和IDE驱动器属于这类设备。
2. 强制刷新数据到物理介质
使用I/O
barrier的原因主要是保护文件系统的完整性。意外掉电或者其他事件使得磁盘驱动器无法正常工作,将造成磁盘缓存中数据的丢失。所以,I/Obarrier需要保证i/o请求真正被顺序写入了非易失性介质上。
这儿有四种情况:
i.无write-back缓冲,保证请求自身的顺序就足够了。
ii.有write-back缓存但没有刷新缓存的操作。这种情况下,无法保证物理介质的写入顺序。这种类型的设备不能支持I/Obarrier。
iii.有write-bach缓存,有刷新缓存的操作但无FUA(forced unit
access),这种情况下,我们需要两次缓存刷新操作:分别在barrier请求前后。
iv.有write-back缓存,刷新缓存操作和FUA。这是,我们只需要一次刷新操作来确保barrier请求之前的请求被写入物理介质。而barrier请求之后的刷新操作可以省略。因为我们可以指定barrier请求为FUA写,这样确保了barrier请求自身能被真正地写入物理介质。从而避免了第二次刷新。
怎样在驱动中支持barrier请求
---------------------------
所有barrier的处理都是在通用块层内部进行的。所有底层驱动需要实现自己的prepare_flush_fn函数,并且使用以下两个函数之一来说明自己支持哪种barrier类型以及怎样准备用于刷新缓存的请求。注意,“ordered”用来说明处理barrier请求的整个过程顺序,包括请求的分发和缓存的刷新。
typedef void (prepare_flush_fn)(struct request_queue *q, struct request *rq);
int blk_queue_ordered(struct request_queue *q, unsigned ordered,
prepare_flush_fn *prepare_flush_fn);
@q : the queue in question
@ordered : the ordered mode the driver/device supports
@prepare_flush_fn : this function should prepare @rq such that it
flushes cache to physical medium when executed
比如,SCSI磁盘驱动的prepare_flush_fn如下:
static void sd_prepare_flush(struct request_queue *q, struct request *rq)
{
memset(rq->cmd, 0, sizeof(rq->cmd));
rq->cmd_type = REQ_TYPE_BLOCK_PC;
rq->timeout = SD_TIMEOUT;
rq->cmd[0] = SYNCHRONIZE_CACHE;
rq->cmd_len = 10;
}
当前Linux中,支持以下7中顺序模式。以下表格显示了根据不同设备/驱动的特点需要采用哪种模式。在表格的最左边,QUEUE_ORDERED_前缀被省略了,以节约空间。
表格后面是各种模式的解释。注意,在QUEUE_ORDERED_DRAIN*中的描述使用了“=>”,而在QUEUE_ORDERED_TAG*的描述中使用了“->”。前者表示,前面的步骤必须在后面的步骤开始之前完成。而后者表示后面的步骤只要在前面步骤开始后就可以开始。
write-back cache ordered tag flush FUA
-----------------------------------------------------------------------
NONE yes/no N/A no N/A
DRAIN no no N/A N/A
DRAIN_FLUSH yes no yes no
DRAIN_FUA yes no yes yes
TAG no yes N/A N/A
TAG_FLUSH yes yes yes no
TAG_FUA yes yes yes yes
QUEUE_ORDERED_NONE
I/O barrier不需要或者不被支持
Sequence: N/A
QUEUE_ORDERED_DRAIN
请求以请求队列中分发的顺序进行排序,不需要刷新缓存操作。
Sequence: drain => barrier
QUEUE_ORDERED_DRAIN_FLUSH
请求以请求队列中分发的顺序进行排序,需要在barrier请求前和之后刷新缓存。
Sequence: drain => preflush => barrier => postflush
QUEUE_ORDERED_DRAIN_FUA
请求以请求队列中分发的顺序进行排序,之需要在barrier请求前刷新缓存。在barrier请求上使用FUA,省略barrier请求之后的刷新。
Sequence: drain => preflush => barrier
QUEUE_ORDERED_TAG
请求被ordered tag排序,不需要刷新缓存。
Sequence: barrier
QUEUE_ORDERED_TAG_FLUSH
请求被ordered tag排序,在barrier请求之前和之后,需要刷新缓存。
Sequence: preflush -> barrier -> postflush
QUEUE_ORDERED_TAG_FUA
请求被ordered
tag排序,在barrier请求之前需要刷新缓存。在barrier请求上使用FUA,省略barrier请求之后的刷新。
Sequence: preflush -> barrier
----------------
*SCSI层当前不能使用TAG ordering,即使磁盘驱动器,控制器和驱动支持这个特性。其主要原因是SCSI中间层的请求分发函数不是原子的。在分发请求时,它会释放请求队列的锁,转而获取SCSI host的锁。这样,就可能发生请求相对位置改变的情况。一但这个问题解决了,TAG ordering将会启用。
*当前,无论那种顺序模式被使用,某一时刻,只能有一个barrier请求被处理。所有I/O barrier都会被通用块层扣留起来,直到前面的I/O barrier处理完成。这和DRAIN顺序的设备没有什么不同。但是,对于TAG 顺序的设备,由于其命令时延较长,如果I/O barrier分发频
繁,传递多个I/O barrier到底层可能会有帮助。
*完成顺序。顺序的请求被顺序的分发,但不要求顺序完成。Barrier实现可以处理乱序完成的请求。也就是说,请求必须被顺序处理,但是硬件/软件完成路径允许以乱序进行通知。比如,当前SCSI中间层就没在错误处理时预设完成顺序。
*重新入队顺序。底层驱动可以自由重新入队任何请求。由于barrier序列在重新入队时必须保持顺序,通用电梯程序中保证插入的请求符合barrier顺序。参见blk_ordered_req_seq() 和 ELEVATOR_INSERT_REQUEUE handling in __elv_add_request()。
注意,在完成一个顺序序列后面的请求时,块设备不能重新入队之前的请求。当前,没有针对这种错误的检查。
*错误处理。当前,当在一个顺序序列中的请求发生错误时,通用块层会把错误报告给上层。不幸的是,这样做往往还不够。比如有如下的请求,使用QUEUE_ORDERED_TAG_FLUSH顺序模式。
[0] [1] [2] [3] [pre] [barrier] [post] < [4] [5] [6] ... >
still in elevator
假设[2],[3]是用来更新文件系统metadata的写请求(日志等)。[barrier]用来标识这些更新是有效的。考虑以下序列:
i.请求[0]~[post] 离开了请求队列,进入底层驱动。
ii. 过了一会儿,不幸发生了,磁盘驱动器出现故障,[2]失败。注意,此时[0],[1],和[3]都已经完成了。但是[pre]还没有完成,因为磁盘驱动器必须按照顺序来处理它。它在也会失败。
iii.此时,错误处理介入。它确定错误不能恢复,[2]操作失败,并重启操作。
iv.[pre][barrier][post]得到处理
v.掉电
问题在于,barrier请求本来认为文件系统更新请求[2]和[3]会被安全地写入物理介质。如果机器在barrier被写入后crash掉。文件系统的恢复代码可以依靠它。不幸的是,这种情况不再成立。也就是说,I/O barrier的成功,必须依赖前面某些请求的成功,这究竟是那些请求,只有上面的文件系统才知道。
要解决这个问题,可以通过实现一种方式,来告诉通用块层哪些请求影响之后的barrier请求,使底层驱动只有在得到通用块层通知后才进行错误处理。
栅栏与闭锁不同的是,栅栏可以重复使用。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Test {
// 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)
final CyclicBarrier barrier;
// 线程数
int count;
class Worker implements Runnable {
int index;
Worker(int index) {
this.index = index;
}
public void run() {
System.out.println("第" + index + "个线程休眠" + (2 * index) + "秒!");
try {
Thread.sleep(2000 * index);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + index + "个线程结束休眠!");
try {
// 等待其它线程都处理完毕后,再继续以下代码的执行
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(index);
}
}
public Test(int count) {
this.count = count;
// 公共屏障点 等待到5个线程后,执行相应的barrierAction
barrier = new CyclicBarrier(count, new Runnable() {
public void run() {
System.out.println("全部线程已执行完毕!");
}
});
for (int i = 1; i <= this.count; i++) {
new Thread(new Worker(i)).start();
}
}
public static void main(String[] args) {
new Test(5);
}
}