2011年10月18日星期二

linux的deadline io 调度算法分析

1 linux的deadline io 调度算法分析
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  以rhel5.6系统(kernel version 2.6.18)为基础分析linux内核的块设备层
  的deadline io 调度算法。

2 把bio添加到请求中以及把请求添加到请求队列中
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

2.1 最简单的情况,请求队列中一个请求都没有的时候,添加第一个bio
================================================================
   bio通过generic_make_request函数传递进来。generic_make_request函数会
   调用请求队列的make_request_fn函数指针完成实际的io操作。如果使用电梯
   算法,请求队列的make_request_fn函数指针将指向__make_request函数。
   我们只考虑与电梯算法相关的部分。

   首先,调用elv_merge函数检查是否可以将当前的bio合并到以前的request
   中。如果是第一次执行到这里,当然没有可以合并的request,于是调用
   get_request_wait函数获取一个新的request,然后调用
   init_request_from_bio函数将bio添加到request中。接下来调用
   add_request函数将request添加到request_queue中。

   add_request中的函数调用流程:
   add_request->__elv_add_request->elv_insert
   elv_insert函数会将新创建的request挂到request_queue的queue_head链表
   上,并让request_queue的last_merge指针指向当前的request。最
   后,elv_insert函数会调用到函数指针:
   q->elevator->ops->elevator_add_req_fn
   如果deadline是该块设备的io调度算法的话,这个指针就指向了
   deadline_add_request函数。

   deadline算法中有两个红黑树(读写请求各有一个),两个双向链表(读写
   请求各有一个),一个哈希表(所有读写请求都在同一个哈希表上)。

   deadline_add_request函数会将传递进来的request添加到与请求的读写方
   向相对应的红黑树与双向链表中,以及哈希表上。同时,也记录下这个请求
   的到期时间。

   deadline中的红黑树以起始扇区号作为键值进行排序。双向链表按照request提
   交进来的先后顺序进行排序,最先加进来的request放在链表的头部。因此,每
   新添加一个请求的时候,都放在链表的尾部。哈希表使用request的结束扇
   区号(即rq->sector + rq->nr_sectors)作为键值进行索引。读请求的期
   满值默认为0.5秒,写请求的期满值默认为5秒。在合并与分发请求时,我们
   会看到deadline算法如何使用这些数据。

   调用完add_request函数后,__make_request的任务基本就结束了。每一个
   请求队列,在真正处理请求的时候都会有一个延迟,一般默认设为3毫秒。
   如果这3毫秒内又有request到来,则将该请求提交给io调度算法,并重新设
   置延迟的期满时间为3毫秒。

2.2 将bio merge到request中
===========================
   每当一个bio提交到__make_request函数中的时候,该函数总是调用
   elv_merge函数检查是否能将当前的bio合并到以前的request中。elv_merge
   函数分两步进行判断。

   第一步,判断判断bio是否可以合并到request_queue的last_merge指针指向
   的request。last_merge总是指向上一次提交进来的请求。如果进行连续读
   写的话,这是可能性最大的情况。判断的方法很简单,首先看request的结
   尾扇区号是否等于bio的起始扇区号,如果是的话就能够进行back merge,
   然后判断request的起始扇区号是否等于bio的结尾扇区号,如果是的话就能
   够进行front merge。

   第二步,如果bio不能和last_merge指向的request进行合并的话,则调用io
   调度算法的elevator_merge_fn成员函数,判断bio是否能和某个request进
   行合并。

   对于deadline调度算法,elevator_merge_fn指向deadline_merge函数。该
   函数首先判断是否能进行back merge。以bio的起始扇区号为键值在哈希表
   中寻找。由于deadline中的哈希表是以request的结束扇区号为键值的,所
   以如果找到这样的request的话,说明这个request的结语扇区号与bio的起
   始扇区号相等,因此bio可以back merge到这个request中。如果没有找到这
   样的request,说明无法进行back merge,则继续尝试能否进行front merge。
   方法是以bio的结束扇区号为键值在红黑树中进行搜索。红黑树是以request
   的起始扇区号为键值进行排序的,因此,如果找到符合条件的request,说
   明该request的起始扇区号等于bio的结束扇区号,可以进行front merge。
   无论是back merge还是front merge,如果找到符合条件的request的话,都
   要把request从哈希表上拿下来,再按照新的键值重新放上去,至于红黑树,会
   在后续的操作中进行处理。

   若是可以进行合并,elv_merge会返回用来合并的request指针,并且,通过
   返回值表示是何种合并,ELEVATOR_BACK_MERGE表示back merge,
   ELEVATOR_FRONT_MERGE表示front merge,ELEVATOR_NO_MERGE表示无法进行
   合并。

2.2.1 back merge
-----------------
    若是进行back merge,则调用函数指针q->back_merge_fn对request_queue
    和request进行一些必要的处理,这个指针通常指向ll_back_merge_fn函数。
    然后修改request的biotail,biotail->next,以及nr_sector等位,将bio
    添加到request中。接下来调用attempt_back_merge函数,该函数的作用是
    检查是否可以进一步的进行合并,例如,原先请求队列中有这样两个请
    求,request1写块设备的secotr0到sector4,request2写块设备的
    sector 8到sector 12。现在新加入的bio写sector5到sector7,bio被合并
    到了request1中,request1变成写sector0到sector7,此时,request1和
    request2可以进行合并了。若发生这种情况,attempt_back_merge函数会
    进行处理,若没有,则调用elv_merged_request函数进行最后的收尾处理。
    该函数会调用调度器的elevator_merged_fn方法,对于deadline调度器,
    该方法指向deadline_merged_request函数。该函数的作用很简单,由于进
    行了合并,request的起始扇区号和结束扇区号可能改变了,用新的扇区号
    做键值更新request在哈希表和红黑树中的位置。

2.2.2 front merge
------------------
    与back merge的处理基本对称,不再赘述。

3 从请求队列中取出一个请求
~~~~~~~~~~~~~~~~~~~~~~~~~~~
  正常情况下,每当request_queue等待3毫秒还没有新的bio提交后,就会出发
  unplug,最终会调到request_queue的request_fn方法。以scsi磁盘驱动为
  例,该方法指向scsi_request_fn函数。scsi_request_fn会调用
  elv_next_request函数来从请求队列中取出一个请求进行处理。而该函数会
  调用__elv_next_request找出一个合适的请求。

  __elv_next_request函数首先从request_queue的queue_head链表上寻找
  request,如果找到的话就返回这个request,没找到的话就调用调度器的
  elevator_dispatch_fn方法,通常,这个方法会将一些request放到
  queue_head链表上。然后再检查request_queue的queue_head链表。一直循
  环,直到从queue_head上找到一个request,将其返回,或者
  elevator_dispatch_fn方法返回0,则__elv_next_request函数返回一个空指
  针。

  对于deadline调度器来说,elevator_dispatch_fn函数指针指向
  deadline_dispatch_requests函数。

  deadline_dispatch_requests函数负责从已经提交给调度器的request中选择
  一个放到request_queue的queue_head链表中。

  为了优化读写磁盘的效率,deadline调度器会对请求进行批处理,每当选定
  一个请求方向时(读或写),总是连续处理该方向上的16个请求。然后才重
  新选择另一个方向上的请求。当然,如果某个方向上已经没有请求了,则马
  上选择另一个方向上的请求。

  每当处理了一个方向上的请求,就将该方向上的下一个请求放到
  next_drq[WRITE]中或next_drq[READ]中。因此,当进入
  deadline_dispatch_requests函数的时候,首先判断next_drq[WRITE]或
  next_drq[READ]是否为空,这两个指针至多有一个不为空。如果其中有一个
  不为空,我们就获得了一个request。接下来判断这个request和上一次发送
  的请求是否是连续的,若不是连续的,则打破批处理,然后判断当前的批处
  理进行了多少次,如果超过了最大的允许次数,也要打破批处理,重新选择
  request。这是为了防止在某一个方向上有大量连续的request,导致饿死了
  另一个方向上的request。

  如果确定要继续批处理,则直接跳到函数的末尾,即标签
  “dispatch_find_request”处,递增批处理的统计次数,将同方向上的下一个
  请求放到next_drq[WRITE]或next_drq[READ]中,记录当前request的结束扇区
  号,以便再进入本函数时判断下一个request是否和当前这个是连续的。最后
  把这个请求从deadline算法内部维护的哈希表,红黑数和链表上面移除,放到
  request_queue的queue_head链表上。

  如果前面判断不进行批处理,则先看是否有读请求需要处理。若读写请求都
  有的话,就把starved变量加一,表示忽略了一次写请求。如果忽略的写请求
  次数超过了变量writes_starved的值(默认为2),那么就不再忽略写请求,
  跳转到标签“dispatch_writes”的位置,将变量data_dir设为WRITE,表示提
  交一个写请求。否则将data_dir设为READ,表示提交一个读请求。

  现在已经选择了request的方向(READ或WRITE),接下来判断在该方向上最
  先进来的request是否已经超时了,或者该方向上还有没有request的起始扇
  区号比上一个请求更靠后的request了(这样的request即使不连续也可以让
  磁盘的磁头大体上朝一个方向移动)。如果两个条件中有一个条件为真,就
  选择该方向上时间最早的request提交。如果这两个条件都为假的话,即没有
  任何请求超时,而且有一个起始扇区号比上一个request大的请求,就提交这
  个请求。

  完。