加入收藏 | 设为首页 | 会员中心 | RSS
您当前的位置:首页 > Linux技术 > 编程开发

Linux Barrier I/O实现分析笔记(一)

时间:2010-11-05 10:25:08  来源:  作者:随便
一直以来,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);

  }

  }

来顶一下
近回首页
返回首页
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表
推荐资讯
疑似Google Chrome OS谍照曝光
疑似Google Chrome OS
相关文章
    无相关信息
栏目更新
栏目热门