Raid1源代码分析--读流程(重新整理)

时间:2022-05-25 06:22:03

五、Raid1读流程分析

  两个月前,刚刚接触raid1,就阅读了raid1读流程的代码,那个时候写了一篇博客。现在回过头看看,那篇的错误很多,并且很多地方没有表述清楚。所以还是决定重新写一篇以更正之前理解的错误和不足之处,与大家分享。博客上不好排版,希望不会对表述产生影响。还有理解上的不足之处,希望批评指正。我阅读的代码的linux内核版本是2.6.32.61。

  正确读流程的总体步骤是,raid1接收上层的读bio,申请一个r1_bio结构。然后根据read balance算法选出盘阵中的一块盘作为读目标盘号(假设盘号为n,有WriteMostly盘的时候,优先读非WriteMostly盘)。克隆上层的bio结构(读流程中的bio都是共用页结构),并将bios[n]指向该bio结构,进行相应设置之后,将该bio直接下发到读目标盘,读成功返回。如图1和图2所示。

  读流程主要涉及以下函数:

      请求函数make_request

      读均衡read_balance

      回调函数raid1_end_read_request

      读出错处理raid1d

      尝试修复读出错fix_read_error

  下面具体分析raid1的读流程。

Raid1源代码分析--读流程(重新整理)

图1 无WriteMostly盘的读流程

Raid1源代码分析--读流程(重新整理)

图2 有WriteMostly盘的读流程

 

1)请求函数make_request

  读请求封装成bio后,由md设备的md_make_request下发请求,md又发给具体的设备raid1,对应raid1的make_request函数,下面将从raid1的make_request开始理解该部分的流程。

  1.  如果访问要求设置barrier,而设备不支持设置barrier,则结束bio,立即返回。

    注:这儿的barrier是接收到的bio自带的barrier,是由上层设置的。

  2.  等待设备上的barrier消除。

    注:这里的barrier是raid1层自己做的一套barrier机制,如果有barrier设置就需要等待barrier清除之后才往后执行。同步一定会设置barrier,代码中有raise_barrier(conf);该barrier在同步结束后end_sync_write中调用put_buf操作conf->barrier来清除。

  3.  申请一个r1_bio结构(该结构主要用于管理raid1的bio),该结构中有一个数组bios数组指向对应各磁盘的bio。

  4.  调用read_balance算法,获取读目标的盘号rdisk。

  5.  如果获取的盘号为错误盘号(即-1),表示目前不能获取读目标,那么结束这个bio(调用raid_end_bio_io(r1_bio)),返回。

  6.  通过盘号rdisk和指针数组首地址conf->mirrors得到读目标的mirror_info结构指针mirror。

  7.  通过mirror->rdev->flags可以知道该磁盘是否设置了WriteMostly属性。如果设置了WriteMostly,则通过bitmap可以知道是否仍然还有延迟写。如果延迟写还没结束,那么等待延迟写结束。其他的情况都不处理,直接往下走。

  8.  将读目标盘号赋值给r1_bio->read_disk。

  9.  将接受到的上层bio(读请求)复制一份,指针read_bio和r1_bio->bios[rdisk]都指向该bio。

  10. 设置该read_bio的相关属性,使得符合r1_bio结构的要求,比如读写属性,盘阵上哪个盘,盘的数据起始扇区。从而让它能正确下发到下层,最终找到相应的读目标盘和进行相关操作;也设置了结束请求函数raid1_end_read_request。

  11. 下发read_bio。

2)读均衡read_balance

这个算法是用来做读均衡的,在raid1中涉及到的读操作均会使用到该算法。入口参数为raid1的私有数据结构*conf和raid1管理bio的结构*r1_bio,整体流程如图3所示。

Raid1源代码分析--读流程(重新整理)

图3 read balance总体流程

具体流程分析如下:

  1.  如果盘阵正在进行同步操作,并且访问落在同步窗口中,按照下面的子流程选出盘号。

    注:

    • conf->mddev->recovery_cp < MaxSector,表示盘阵正在同步。代码中MaxSector的值为扇区数量的上限,而conf->mddev->recovery_cp表示正在进行同步的扇区号,所以自然就约定没有进行同步的时候conf->mddev->recovery_cp = MaxSector。
    • 扇区号加扇区数等于最后一个要读的扇区+1,如果大于等于下一个要同步的扇区,则说明落在同步窗口之中。
    • read_balance函数有可能在守护进程中被调用到,所以是有可能在盘阵同步的时间段内,出现read_balance函数被调用的情况。得到的目标盘号和同步读盘号是一致的。

   1.1  从0号磁盘开始,循环遍历盘阵中每个盘,选出第一个可读的盘号new_disk。如果盘阵中只有WriteMostly盘是可读的盘,才选择WriteMostly盘的盘号。如果盘阵中所有盘都不可读,那么就直接将new_disk盘号赋值为-1。

    注:盘不可读有以下几种情况:

    • 如果盘的r1_bio的bio指向的是IO_BLOCKED;

      注:如果之前读失败的盘为只读状态,在守护进程处理读失败的时候,先调用read_balance函数,如果重新指定读盘成功,则将r1_bio的该盘bio标记为IO_BLOCKED,接着就根据r1_bio来重发read请求;如果重发的这次read请求还是失败了,同样还是会唤醒守护进程来处理读出错流程,这时又会调用到read_balance函数,而将之前的IO_BLOCKED传入进来,参与到这些流程中。也就是在只读盘上读失败之后,重发读请求,再次读失败的场景会使用到。更进一步,只要是连续读失败,不管多少轮,都会使用到。

    • 盘找不到;
    • 盘不可用(rdev->flag的In_sync位为0,表示该盘不是active disk,不能工作,不可对其操作)。

    1.2   如果选出的是一个错误盘号,那么直接出错返回-1。

    1.3   如果选出的盘号是正确的,那么到步骤5。

        注:选出的盘号记录在new_disk中。

  2.  从last_used的磁盘开始,循环遍历盘阵中每个盘,选出第一个可读的盘号new_disk。如果盘阵中只有WriteMostly盘是可读的盘,才选择WriteMostly盘的盘号。如果盘阵中所有盘都不可读,那么就直接将new_disk盘号赋值为-1,出错返回-1。

  3.  如果是顺序读,选出盘new_disk的上次操作结束位置,正好位于本次操作的起点,那么就直接确定为盘号new_disk。分别有如下两种情况:

    • 一种是上次读均衡算法结束之后就更新了conf->next_seq_sect,这个变量由整个raid1盘阵的数据结构记录。
    • 一种是上次成功读写(包括同步读写)完成后,通过update_head_pos函数来更新new_disk的head_position,这个变量是由每个盘的数据结构记录。

    注:这两个量的值也有不同的时候,比如在某次读均衡操作结束之后,又有另外一个读请求进来做读均衡操作,那么此时两个量的值不同。

  4.  如果选出盘new_disk的上次操作结束位置和本次操作的起点不一样,则进入下面的循环。disk作为循环控制的磁盘号,new_disk作为返回的磁盘号。

    4.1  首先将disk--,变成其他盘号,与上次读的盘号不同。(我觉得这里体现均衡)

    4.2  如果盘不能作为正确的读盘,或者是WriteMostly盘,则继续循环。

    4.3  如果磁盘上无IO正在处理,那么确定为该磁盘的盘号,跳出循环。

    4.4  如果所有磁盘都有IO正在处理,那么找到当前记录的上次操作的位置head_position和本次操作位置最为接近的磁盘盘号。

  5.  如果选出盘rdev不存在,那么重新执行read_balance算法。

  6.  递增选出盘rdev的下发IO计数。

  7.  如果到了这一步发现此时该盘不可用(即rdev->flags没有设置In_sync),递减选出盘rdev的下发IO计数,那么重新执行read_balance算法。

  8.  设置新的conf->next_seq_sect(下一个顺序读扇区),新的conf->last_used(最近一次使用的磁盘盘号)

  9.  返回选出的磁盘,即为读均衡磁盘。

3)回调函数raid1_end_read_request

  1.  如果bio的状态为有效(或者称之为最新,即标志位BIO_UPTODATE),那么设置uptodate为1。

  2.  调用update_head_pos()更新当前盘的最后访问的磁盘位置。

  3.  如果bio的状态为有效,设置r1_bio的状态也为有效(R1BIO_Uptodate,用于bio_endio),并调用raid_end_bio_io()结束这个bio,bio_endio反馈信息为成功,释放r1_bio结构。

  4.  如果bio的状态不是有效,并且所有盘都处于降级状态,或者仅有已经尝试了read的一块盘是正常的,那么设uptodate为1(这里不是表示状态为有效,而是表示不用retry了),然后调用raid_end_bio_io结束这个bio,bio_endio反馈信息为失败。

    注:bio_endio之前要进行test_and_set_bit(R1BIO_Returned, &r1_bio->state)来判断是否已经endio了,如果已经endio了,就不再进行bio_endio。R1BIO_Returned标志位的作用就在于此,当设置了延迟写的时候,存在还没有真正将所有盘的写操作完成的时候endio的,所以在真正将所有盘都返回的时候调用raid_end_bio_io(),会出现已经设置R1BIO_Returned的情景,则不需要再endio了。

  5.  其他情况则表示读出错了。并将r1_bio放入retry队列,转由守护进程处理。即通过reschedule_retry()调用md_wakeup_thread(mddev->thread)。

  6.  递减选出盘rdev的下发IO计数。

4)读出错处理raid1d

  raid1d是守护进程,很多情况都会唤醒该进程,而该进程描述了这些不同种情况下的处理流程。先按需启动阵列同步线程,然后处理pending_bio_list的所有请求,再逐一处理retry_list的r1_bio。判断r1_bio->state,如果是R1BIO_IsSync,则转入同步写流程;如果是R1BIO_BarrierRetry,这个情况只发生在写操作,则转入处理barrier的流程;如果是其他情况,则是读出错,进入读失败处理流程。下面分析读出错的处理流程。

  1.  如果md是读写状态。

    1.1  冻结盘阵。调用freeze_array(conf)实现。

    1.1.1 通过barrier计数挡住所有新的I/O。

    1.1.2 把触发freeze_array的I/O(即失败的read I/O)加到wait计数。

    1.1.3 等待所有已经进入raid1层的I/O完成。

       注:这里等待的事件是“所有已进入raid1的readI/O挂入retry表,所有已进入raid1的write I/O完成写动作。”

    1.2  尝试修复读出错。调用fix_read_error()实现。

    1.3  解冻盘阵。调用unfreeze_array(conf)实现。

  2.  如果md是只读状态,那么失效(md_error)读目标盘。

  3.  重新调用读均衡算法,得到一个盘号disk。

  4.  如果返回的盘号仍然为负值,那么表示无法恢复读出错,调用raid_end_bio_io结束这个bio。

  5.  如果返回盘号为正常值,那么根据是否只读,设置原读出错的盘指针为IO_BLOCKED(只读时设置,连续出现读失败时会用到)或者NULL(读写时设置)。

  6.  设置r1_bio->read_disk为新的读目标盘号disk。

  7.  Put掉原bio,bio_put(bio)。

  8.  将master_bio克隆一份给bio,并用r1_bio->bios[r1_bio->read_disk指向该bio。

  9.  设置该bio的一些字段,并将回调函数设置为raid1_end_read_request,然后下发该bio。

  10. cond_resched()如果有进程要抢占,则切换进程;如果不是,则继续循环。

  11. 调用unplug_slaves()唤醒守护进程,通知raid1设备赶紧去处理请求。

    注:raid1设备没有用到queue。

5)尝试修复读出错fix_read_error

  1.  大循环,判断条件为sectors不为0,进入循环。

  2.  将需要处理的扇区数sectors复制给变量s,将读目标盘号read_disk赋值给d。

  3.  控制s的大小在 PAGE_SIZE>>9 以内。

  4.  从read_disk开始遍历盘阵中的每个盘。找到一个可以读这次page的盘,记录盘号为d,并读出来置于conf->tmppage中,跳出循环;或者没有可以读的盘,跳出循环。

  5.  如果没有可以读的盘,那么失效(md_error)原读目标盘,退出大循环,返回。

  6.  从步骤4中选出d号盘,往回遍历,直到read_disk。将conf->tmppage依次写入这些盘中。如果某次磁盘写入错误,那么失效(md_error)该次写入的磁盘。

  7.  从步骤4中选出d号盘,往回遍历,直到read_disk。将conf->tmppage依次从这些盘中读出。

    7.1  如果某次磁盘读出错误,那么失效(md_error)该次写入的磁盘。

    7.2  如果某次磁盘读出正确,那么该次磁磁盘对该大循环对应的page修复成功。

  8.  判断条件sectors = sectors – s,每次循环的sync_page_io起点标记sect = sect+1。进入下一个循环。

本文来自fangpei的博客,转载请标明出处:http://www.cnblogs.com/fangpei/