2011年2月11日星期五

linux内核raid5一次整条带写操作的流程(基于2.6.33内核)

Table of Contents
=================
1 raid5写入数据的顺序
2 make_request函数
        2.1 rand5_compute_sector
        2.2 get_active_stripe
            2.2.1 get_free_stripe
            2.2.2 init_stripe
                2.2.2.1 raid5_build_block
            2.2.3 get_active_stripe的其他部分
            2.2.4 __find_stripe
        2.3 add_stripe_bio
        2.4 在add_stripe_bio和release_stripe之间的操作
        2.5 release_stripe
            2.5.1 __release_stripe
3 raid5d
    3.1 __get_priority_stripe
    3.2 handle_stripe
        3.2.1 handle_stripe5
            3.2.1.1 handle_stripe_dirtying5
                3.2.1.1.1 schedule_reconstruction
            3.2.1.2 raid_run_ops
            3.2.1.3 handle_stripe5结束
    3.3 release_stripe
4 ops_complete_reconstruct
5 再次进入raid5d
    5.1 ops_run_io
6 raid5_end_write_request
7 第三次进入raid5d
    7.1 handle_stripe_clean_event
    7.2 release_stripe


1 raid5写入数据的顺序
######################
  假设,stripe size是4k大小,chunk size是32k,共有4块盘。首先,第一块
  盘的4k数据会写入第一块盘的第一个chunk的第一个stripe,如图1所示:



图1






图中桔黄色的方块表示写入的数据,蓝色的方块表示该处将用于存储校验数
  据。图中只画出了第一个chunk,所以校验数据都存于最后一个磁盘中。
  接下来的4k数据会写入第一块盘的第一个chunk的第二个stripe,如图2所示:




图2

直到写入8次4k的数据后,第一块盘的第一个chunk全部写完,如图3所示:


图3
写完这32k数据后,接下来的4k数据会写入第二块盘的第一个chunk的第一个
  stripe,如图4所示:



图4

依此类推,直到第三块盘的第一个4k数据写完,此时,第一个stripe即
  stripe0就被填满了,如图5所示:



图5
在raid5的make_request函数中,如果能够找到一个stripe来描述当前这次写
  操作,则将该stripe提交到一个全局链表中,然后唤醒守护进程raid5d来进
  行处理,make_request并不等到raid5d将数据真正写入磁盘,就直接返回了。
  所以,有大量的数据连续写入时,make_request函数会被连续调用,每次使
  用一个stripe来描述这次任务,就返回了。默认一共有256个stripe可用。当
  这256个stripe都使用完了之后,再有写操作的时候,make_request函数就会
  进入休眠,直到至少有四分之一的stripe被释放之后才会被唤醒,使用新释
  放出来的stripe来描述新的写操作。
  我们来看一下第一个stripe(即stripe0)中的数据的处理流程。

2 make_request函数
###################
  对一次正常的写操作,make_request函数中需要调用4个比较重要的函数。下
  面依次介绍。

2.1 rand5_compute_sector
~~~~~~~~~~~~~~~~~~~~~~~~~
    首先会调用raid5_compute_sector函数通过整个md设备的扇区号
    logical_sector换算出在实际磁盘上对应的扇区号new_sector,以及需要使
    用的是第几块磁盘dd_idx。我们只看和stripe0相关的部分。
    在本例中,第一次写入磁盘的4k数据会落入到stripe0中,即
    logical_sector=0,此时计算出的new_sector=0,dd_idx=0,即写入第一块
    磁盘的第一个sector。
    然后,在写入第九个4k的数据的时候,又落入到stripe0中,此
    时,new_sector = 0, dd_idx = 1, logical_sector = 64。
    在写入第17个4k的数据时,会再次落入到stripe0中,此时,new_sector =
    0, dd_idx = 2, logical_sector = 128。
    至此,第一个stripe被填满。
    每次调用raid5_compute_sector函数后,会返回new_sector和dd_idx两个变
    量。同一个stripe中的每块盘的扇区号是一样的,所以通过new_sector的值
    来判断本次写操作发生在哪个stripe中,并使用一个stripe_head类型的结
    构体来描述这次写操作。

2.2 get_active_stripe
~~~~~~~~~~~~~~~~~~~~~~
    分配stripe_head的操作是在get_active_stripe函数中完成的。首先,这个
    函数会尝试调用__find_stripe函数查看本次操作所在的stripe是不是已经
    在使用中了,如果是的话,就不用新分配stripe了。
    在第一次写入4k数据时,stripe0肯定不在使用中,所以__find_stripe函数
    的返回值是NULL,然后,会调用get_free_stripe函数去获得一个空闲的
    stripe。

2.2.1 get_free_stripe
======================
     进入get_free_stripe函数看一下,该函数很简单,从inactive_list链表
     中取下一个sh,然后给active_stripes的值加一,表示又有一个stripe被
     使用了。此外,还会将sh从它所在的hash表中拿下来(如果它确实挂在一
     个hash表上的话)。这个hash表是在__find_stripe中使用的,介绍后续的
     写操作时会进行说明。

2.2.2 init_stripe
==================
     如果成功的分配到了一个sh,就会调用init_stripe函数对该sh进行初始化。
     包括这个stripe的起始扇区号,使用第几块盘存放校验数据,以及将它的
     状态sh->state设成0。后面我们会看到这个状态在不停的变化。
     对于属于这个sripe的每一个块设备,都有一个r5dev类型的结构体来描述。
     这些个r5dev型的结构体也需要初始化。首先将表示该块设备状态的标志
     dev->flags清零。然后调用raid5_build_block函数初始化与bio相关的变
     量。

2.2.2.1 raid5_build_block
--------------------------
      raid5_build_block函数首先设置提交一次bio所使用的bi_io_vec以及存
      放数据的page之类的变量。然后会调用这样一个函数:
      dev->sector = compute_blocknr(sh, i, previous);
      这里计算出的sector是该磁盘在该stripe中的起始扇区,在整个md设备中
      的扇区号。

2.2.3 get_active_stripe的其他部分
==================================
     在get_active_stripe函数的末尾部分,有这样一句话:
     atomic_inc(&sh->count);
     将sh的记数器加1,后面会介绍到,每当进入release_stripe函数时,会将
     count的值减1,如果减到0了就表示当前对这个sh的处理都完成了,唤醒守
     护进程,来决定对sh的下一步处理。

2.2.4 __find_stripe
====================
     对于写入raid设备的第一个4k的数据,__find_stripe函数会返回空。但当
     写入第九个4k的数据时,也就是像图2中所示的情况发生时,__find_stripe函
     数通过sector为key值在一个全局hash表中查找,由于这时的sector值和第
     一个4k数据的sector值是一样的,而写入第一个4k数据时,在init_stripe
     函数中已经把那时分配到的sh以sector为key值挂到hash表中了,所以这
     时__find_stripe函数会找到写入第一个4k数据所用的sh,也就是用来描述
     stripe0的sh。

2.3 add_stripe_bio
~~~~~~~~~~~~~~~~~~~
    add_stripe_bio函数写在一个条件表达式中的不起眼的位置上,但功能很重
    要。
    首先判断是要执行读操作还是写操作。
    然后用一个临时变量bip指向写磁盘时需要使用的bio(即towrite)的地址,
    后面操作bip就等于改变了towrite的值。然后判断是否发生了overlap的情
    况,也就是前面已经有一个读写请求发生在这个stripe的这块盘上,而本次
    操作又发生在同一个stripe的同一块盘上,并且两次读写数据的位置还有重
    叠。对于我们的顺序写操作,肯定是不会发生这种情况的。
    接下来,对于写操作,要判断是否发生了overwrite的情况。所谓
    overwrite,就是这次写操作是不是覆盖了stripe在这块磁盘上的整个区间。
    如果是的话,在计算xor校验值的时候,对于这块磁盘,就直接使用上层传
    下来的数据,如果不是的话,就需要读回stripe在这块磁盘上的数据到内
    存,然后在把上层传下来的数据覆盖到内存,再计算xor,最后把内存中的
    数据写入磁盘。
    判断写入数据是否覆盖整个stripe的方法也很简单,如果写入数据的起始扇
    区号小于等于stripe的扇区号(bi->bi_sector <= sector),并且写入数据的
    结束扇区号大于等于stripe的结束扇区号(sector >=
    sh->dev[dd_idx].sector + STRIPE_SECTORS),那么这次写操作就是
    overwrite的。
    在我们的例子中,每次写入4k数据,刚好等于stripe的大小,所以是
    overwrite的。因此,下面语句会被调用:
    set_bit(R5_OVERWRITE, &sh->dev[dd_idx].flags);

2.4 在add_stripe_bio和release_stripe之间的操作
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    此时make_request函数中会执行两步比较有意义的操作:
    set_bit(STRIPE_HANDLE, &sh->state);
    clear_bit(STRIPE_DELAYED, &sh->state);
    设置STRIPE_HANDLE位很重要,它表示这个stripe需要进一步的处理,在
    release_stripe函数中会通过该位来判断是否需要唤醒守护进程。

2.5 release_stripe
~~~~~~~~~~~~~~~~~~~
    接下来进入make_request中会调用到的最后一个比较重要的函数。
    release_stripe函数只有几行:获取锁,调用__release_stripe函数,释放
    锁。所以真正重要的操作都在__release_stripe函数中。

2.5.1 __release_stripe
=======================
     首先会判断sh的引用计数sh->count,如果它减小到0则说明所有对sh的操
     作都完成了,需要唤醒守护进程进行下一步的处理。
     我们记得,在前面调用过的init_stripe函数中有这样一行语句:
     BUG_ON(atomic_read(&sh->count) != 0);
     就是说一开始获取到的sh,引用计数肯定是0,然后,在
     get_active_stripe函数的的末尾处,会执行这样一行语句:
     atomic_inc(&sh->count);
     将sh的引用计数加1。
     对于stripe0共进行了3次4k数据的读写,每次进入get_active_stripe函数
     后,会让sh->count加1,进入__release_stripe函数后,又将其减到0。
     然后检测sh->state,目前sh->state的值应该是0x00000004,只有
     STRIPE_HANDLE被置位(在make_request函数调用add_stripe_bio之后,调
     用release_stripe函数之前)。因此在条件判断中会进入这一行:
     list_add_tail(&sh->lru, &conf->handle_list);
     将stripe挂在handle_list链表上。
     然后执行:
     md_wakeup_thread(conf->mddev->thread);
     唤醒raid5d守护线程。
     虽然守护线程会在这里被唤醒,但并不会马上执行。实际上,除非
     make_request函数由于sh耗尽而进入休眠,否则在它返回之前raid5d线程
     是一直都得不到机会执行的。所以,当写入stripe0的3块4k数据都执行
     过__release_stripe之后,再等到make_request函数返回或进入休
     眠,raid5d线程会开始执行,提交到strpe0的数据会得到进一步处理。

3 raid5d
#########
  raid5d线程开始执行后,会进入一个大循环,这个循环中,首先调
  用__get_priority_stripe函数获取一个需要处理的sh,然后调用
  handle_stripe函数对这个sh进行处理,最后调用release_stripe函数来决定
  是否要再次唤醒raid5d线程,或是把sh释放掉。一直等到所有的sh都处理完
  了,才退出循环。在raid5d的最后会调用async_tx_issue_pending_all函数,
  如果在处理过程中有进行dma操作的话,这个函数会确保dma开始运行。

3.1 __get_priority_stripe
^^^^^^^^^^^^^^^^^^^^^^^^^^
   这个函数会尝试从handle_list和hold_list两个链表上获取sh,对于写操作
   来说,如果一个sh已经是整条带写,那么它会被挂在handle_list上,否则就
   会挂在hold_list上。所以总是优先搜索handle_list,如果有整条带写,就
   先处理。等到整条带写的都处理完了,可能已经又有新的写操作提交进来,
   让那些之前不是整条带写的sh也变成整条带写了。这样可以提高性能。
   在这里,将stripe0的sh从handle_list上取下来,然后给sh->count加1。通
   常,把sh放到链表上的操作都是在release_stripe函数里完成的。而
   release_stripe函数只有将sh->count减到0才会把它挂到某个链表上。所以,这
   里给sh->count加1后,结果总是1。

3.2 handle_stripe
^^^^^^^^^^^^^^^^^^
   通过__get_priority_stripe函数获取到sh之后,接下来就是调用
   handle_stripe来处理这个sh。handle_stripe判断这个sh的raid等级来调相
   应的处理函数。

3.2.1 handle_stripe5
~~~~~~~~~~~~~~~~~~~~~
    首先循环查询一遍每个dev的flag,根据flag的状态设置相应的变量。对于
    stripe0的sh,它的dev0 dev1 dev2对应的flag都是0x04,dev3对应的flag
    是0x00。即,0,1,2三块盘的R5_OVERWRITE被置位。
    循环结束后还要进行许多的状态判断,用来确定这个sh究竟要执行哪些操作。
    最终,实际会被调用到的是handle_stripe_dirtying5函数。

3.2.1.1 handle_stripe_dirtying5
================================
     对于一次写操作,该函数用来判断是要使用rcw还是rmw。比如在一个
     stripe中我只写1块盘。那么我可以通过把要写的这块盘的数据与校验盘的
     数据读回,与新的数据做异或,得出校验数据,这就是rmw。我也可以把所
     有其他盘的数据读回,与新写入的数据做异或,算出校验数据,这就是rcw。
     该函数统计出使用rcw与rmw两种操作时所需的读盘次数,哪种操作需要读
     盘的次数少,就采用哪种操作。
     对于整条带写,肯定是使用rcw,因为一次读盘操作都不需要。

3.2.1.1.1 schedule_reconstruction
----------------------------------
      对于一次回读都不需要的情况,还需调用schedule_reconstruction函数
      来设置一些状态标志。
      对于我们的整条带写操作,会进行如下的设置:
      sh->reconstruct_state = reconstruct_state_drain_run;
      set_bit(STRIPE_OP_BIODRAIN, &s->ops_request);
      set_bit(STRIPE_OP_RECONSTRUCT, &s->ops_request);

3.2.1.2 raid_run_ops
=====================
     接下来,handle_stripe5函数会调用raid_run_ops函数来进行xor运算。在
     raid5.c中有如下宏定义:
     #define raid_run_ops __raid_run_ops
     所以,实际调用的函数是__raid_run_ops。
     该函数首先用memcpy把bio中的数据拷贝到sh的buffer中,然后对sh的
     buffer中的数据进行xor计算。如果硬件支持memcpy和xor操作的话,这些
     操作将会异步进行。在完成后调用callback函数,callback函数中会调用
     release_stripe,以便在适当的时候唤醒raid5d线程进行后续的处理。

3.2.1.3 handle_stripe5结束
===========================
     我们假设使用异步的硬件dma进行memcpy和xor运算,那么,对于整条带的写
     操作,handle_stripe5接下来不会再进行什么实质性的操作了。直到硬件
     操作完成,再次唤醒raid5d后才会处理。

3.3 release_stripe
^^^^^^^^^^^^^^^^^^^
   在raid5d中调用完handle_stripe后,回再次调用release_stripe。由于在
   ops_run_reconstruct5函数中执行了:
   atomic_inc(&sh->count);
   此时sh->count的值是2,减1后得1,不是0,所以不会进行任何操作,直接退
   出。

4 ops_complete_reconstruct
###########################
  memcpy与xor操作都完成后,会调用ops_complete_reconstruct函数。该函数
  会再次调用release_stripe函数,进入release_stripe函数时,sh->count的
  值是1,减1后成为0。因此会再次唤醒raid5d线程。

5 再次进入raid5d
#################
  与第一次执行raid5d一样,依然是先获取sh,调用handle_stripe处理sh,最
  后调用release_stripe。只是这次进入handle_stripe5函数后,会调用
  ops_run_io函数将计算完的校验数据与上层通过make_request传递下来的数据
  一起写入磁盘。

5.1 ops_run_io
^^^^^^^^^^^^^^^
   第一次调用handle_stripe函数的时候也会进入ops_run_io,只是当时
   R5_Wantwrite标志和R5_Wantread都没有被设置,所以不会进行任何操作。然
   而在ops_complete_reconstruct函数中,sh->reconstruct_state的值会被设
   置成reconstruct_state_drain_result。这样,在handle_stripe5函数中,
   就会将所有需要执行写入操作的dev的flag置上R5_Wantwrite标志。
   ops_run_io通过这个标志判断那块盘需要执行写操作。没执行一次写操作前,都
   会把sh->count的值加1。每执行完一块盘的写操作,就会调用一次回调函数
   raid5_end_write_request。该函数会调用release_stripe函数,而
   release_stripe函数会将sh->count的值减1,并检测sh->count是不是已经减
   到0了。。这样,当最后一次写操作完成后,release_stripe函数中会发现
   sh->count的值减到0了,于是第三次唤醒raid5d线程。

6 raid5_end_write_request
##########################
  每一次写磁盘完成后调用,设置一些标志位,并调用release_stripe函数,当
  最后一个写操作完成后,release_stripe函数会唤醒raid5d守护线程。

7 第三次进入raid5d
###################
  与前两次一样,依然是先获取sh,然后处理sh,最后release sh,只是这次在
  handle_stripe5函数中会调用handle_stripe_clean_event函数。

7.1 handle_stripe_clean_event
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   当一个sh处理完成后,调用该函数设置一些相应的状态。

7.2 release_stripe
^^^^^^^^^^^^^^^^^^^
   sh处理完成,将其挂到inactive_list链表上,并将active_stripes减一。