正确写流程的总体步骤是,raid1接收上层的写bio,申请一个r1_bio结构,将其中的所有bios[]指向该bio。假设盘阵中有N块盘。然后克隆N份上层的bio结构,并分别将每个bios[]指向克隆出来一个bio结构,然后进行相应设置。
对于没有Write Behind模式而言,之后将所有这些bios[](共用页结构)放入队列pending_list中,对内存bitmap置位。接着由守护进程摘取pending_list链中的bio,然后将内存bitmap同步下刷到磁盘,紧接着立即一次性下发bio,写成功返回,同时更新bitmap状态,然后异步刷磁盘。如图4所示。
对于设置了Write Behind模式而言,还需要将接收到的上层bio的页结构拷贝到WriteMostly盘对应的bios[]中(每个WriteMostly盘对应一份拷贝),之后将所有这些bios[]放入队列pending_list中,对内存bitmap置位。接着由守护进程摘取pending_list链中的bio,然后将内存bitmap同步下刷到磁盘,紧接着立即一次性下发bio。当只剩下WriteMostly盘未完全写成功后(即非WriteMostly盘都写成功了),则认为已经写成功,返回。等到所有WriteMostly盘真正全部写完之后才释放拷贝的页结构和r1_bio。同时更新bitmap状态,然后异步刷磁盘。如图1、2所示。
整体的函数调用关系、进程切换关系和大体流程,如图3所示。
图1 无Write Behind模式的写流程
图2 有Write Behind模式的写流程
图3 raid1读流程整体框架图
写流程主要涉及以下函数:
请求函数make_request
写请求下raid1d
回调函数raid1_end_write_request
写出错处理raid1d
下面具体分析写流程。
1)请求函数make_request
写请求封装成bio后,由md设备的md_make_request下发请求,md又发给具体的设备raid1,对应raid1的make_request函数,下面将从raid1的make_request开始理解该部分的流程。总体流程如图4所示。
图4 make request函数写流程整体框架图
代码的具体分析如下:
1. 调用md_write_start,等待盘阵的超级快更新完成之后继续下面的步骤。
1.1 如果不为写则直接返回。
1.2 如果阵列为临时只读状态,则设置为读写状态,设置阵列mddev的MD_RECOVERY_NEEDED位,并唤醒守护进程和同步守护线程。
注:
set_bit(MD_RECOVERY_NEEDED, &mddev->recovery);表示可能需要resync或recovery;
resync使各子设备上的数据同步,recovery就是恢复数据的过程。
1.3 如果阵列为安全模式,则设置为不安全模式。
1.4 如果阵列mddev的in_sync=1,则设置in_sync=0,表示阵列要开始进行写操作了。唤醒守护进程。
set_bit(MD_CHANGE_CLEAN, &mddev->flags);也就是将superblock中的MD_SB_CLEAN标志清掉。
1.5 同步in_sync标志到磁盘中阵列超级块上。
2. 如果访问要求设置barrier,而MD设备(这里是指raid1)不支持设置barrier,则结束bio,立即返回,将-EOPNOTSUPP信息反馈给上层。
注:这里的barrier指的是bio带有的barrier属性。
3. 等待设备上的barrier消除。
注:这里是指raid1自己为同步做的一套barrier。
4. 申请一个r1_bio结构(该结构主要用于管理raid1的bio),该结构中有一个数组bios数组指向对应各磁盘的bio。
5. 遍历盘阵中所有盘。
5.1 如果盘存在,但是阻塞了(Blocked),那么跳出循环等待阻塞消除,重新进入循环开头。(通常由用户发ioctl设置和清除)
5.2 如果盘存在,并且盘没有坏(!Faulty[fp6] ),增加该盘的下发IO计数。
5.2.1 如果该盘坏了(Faulty),减少该盘的下发IO计数,r1_bio的bio[]数组中的该盘的bio置NULL。
5.2.2 将r1_bio的数组中的该盘指向用户bio。targets用来表示可用的盘。
5.3 如果是其他情况(一定是出错情况),r1_bio的数组中的该盘的bio置NULL。
6. 如果盘阵中的可用的盘数量targets小于conf->raid_disks,则说明有的盘坏掉了。那么就将盘阵设置为降级(R1BIO_Degraded)状态。
7. 如果设置了延迟写,需要将用户bio的数据通过调用alloc_behind_pages函数拷贝一份保存在behind_pages中。并将盘阵设置为R1BIO_BehindIO状态。
8. 设置r1_bio的未完成请求数和延迟写的未完成请求数都置为0。
9. 根据用户bio中的BIO_RW_BARRIER标志,确定是否设置r1_bio中的barrier标志。也就是判断是否要set_bit(R1BIO_Barrier[fp7] , &r1_bio->state)。
注:根据用户bio中的标志,确定是否设置raid-bio中的barrie标志;
如果下挂的磁盘不支持barrier操作,则在raid1_end_write_request中加以处理,具体的处理就是在守护进程中重试。
10. 初始化一个bio_list链b1。
11. 遍历盘阵中所有盘。
11.1 对于每个磁盘,克隆一份用户bio到r1_bio数组对应元素bios中,并设置相关字段以及回调函数raid1_end_write_request。
11.2 如果设置了延迟写,则r1_bio中的数组bios每个元素的bio_vec指向保存的延迟写拷贝behind_pages。如果设置了WriteMostly模式,则对盘阵增加一个延迟写的未完成请求数。
11.3 r1_bio->remaining记录还未提交的请求数,这里每到一个盘都会+1。
11.4 将克隆的这份bio挂到bio_list 链b1中。
12. 调用bitmap_startwrite,通知bitmap进行写数据块对应的设置。
13. 将该克隆的得到的b1(多份相同的bio)加到raid1的pending_bio_list链中。
14. 如果用户IO为sync io,则唤醒守护进程raid1d,进程切换到raid1d,由守护进程通过操作pending_bio_list链,继续处理r1_bio请求。
2)写请求下发raid1d
pending_bio_list所有bio项是一起提交的,retry_list中的r1_bio则是逐个处理。
如果pending_bio_list队列不为空(有等待的访问请求),则将这些请求逐一提交。在提交写请求之前,需要将内存bitmap刷磁盘(为了避免掉电等情况下,内存中的数据丢失,出现错误),保证在数据写入前完成bitmap的写入。直到pending_bio_list链表的所有请求全部提交。
正常流程走下来,在这里就把写请求下发了。如图5所示。
图5 守护进程下发写请求
3)回调函数raid1_end_write_request
总体流程如图6所示。
首先我们不考虑出错流程。假设有5块盘,其中3块为WriteMostly盘。当设置了Write Behind时,behind remaining = 3,remaining = 5。
如果已经返回了1个WriteMostly盘,1个非WriteMostly盘。那么还剩下2个WriteMostly盘,1个非WriteMostly盘,此时behind remaining = 2,remaining = 3。如果接下来非WriteMostly盘返回,不需要减behind remaining即到了判断语句behind remaining >= remaining – 1,所以这时该条件成立。那么设置R1BIO_Returned,endio,通知上层写请求已经结束。此时只剩下WriteMostly盘,进而达到延迟写的效果。但是此时r1_bio等相关结构体和behind pages还未释放。等WriteMostly盘返回之后,save_put_page(), bitmap_endwrite(),释放behind pages和r1_bio结构。
如果所有WriteMostly盘都返回了,仍然有非WriteMostly盘未返回,那么一直有behind remaining
没有设置Write Behind的情况比较简单,参照流程图和下面的代码走读分析即可理解。
图6 raid1_end_write_request函数流程
下面对具体代码流程进行分析:
1. 选出要回调结束bio的盘号mirror。
2. 如果请求要求设置barrier,但是下挂的设备不支持barrier,则设置该盘阵为R1BIO_BarrierRetry状态。跳到步骤8。
注:这种情况是RAID1设备支持barrier bio,但是下层设备不支持;这里的barrier和make request中刚开始的时候的barrier的不同,这里的-EOPNOTSUPP值,是下发之后,下层回调传上来的值。而make_request中bio_endio传入的-EOPNOTSUPP,是将-EOPNOTSUPP回调给raid1的上层。一个是给接收到的下层设备的返回信息,一个是反馈给上层的返回信息。
3. r1_bio->bios[mirror]指针置为NULL。(所指原区域还未释放,用to_put指针来找)
4. 如果状态不是”有效”的(不是uptodate),就将该盘置为出错。并将盘阵降级处理。
5. 如果状态是”有效”的,将盘阵设置为R1BIO_Uptodate。
6. 记录这次操作结束的在磁盘上的位置。
7. 如果有延迟写。
7.1 如果该盘是WriteMostly,延迟写的未完成请求数-1。
7.2 如果只剩下WriteMostly盘的请求,并且r1_bio的状态是R1BIO_Uptodate,那么就认为写操作成功,endio返回。
7.3 减少该盘的io下发计数。
8. 减少一个remaining,并且检查是否全部请求都完成了(remaining为0)。如果r1_bio中所有请求都完成了,那么进入下面流程。表示该请求真的完全完成,可以释放了相关的结构了。
8.1 如果R1BIO_BarrierRetry状态(前面设置过),那么将这个r1_bio加入retry队列。跳到retry流程。
8.2 释放延迟写的页。
8.3 设置bitmap attr属性为CLEAN。
8.4 关于安全模式。
8.5 end io。
9. 如果计数为0,把to_put这个bio释放掉。
当下发磁盘的写请求完成后,需要将bitmap内存页中相应的bit清零,然后把bitmap文件下刷。这些通过守护进程来做,而这个过程不需要等待写bitmap磁盘文件完成,因此是异步的。(由bitmap_daemon_work完成)这里bitmap不需要同步来做,因为可以保证数据��正确性。即使写失败,最多带来额外的同步,不带来数据的危害。
4)写出错处理raid1d
如果接收到的上层bio是因为设置了barrier属性,而子设备又不支持barrier而失败的(这个情况只发生在写操作),则清除r1_bio的barrier属性,重新提交这个r1_bio。
守护进程处理这种写出错的具体流程如图7所示。
图7 守护进程处理barrier bio造成的写出错流程
具体代码流程如下:
1. 清除r1_bio的R1BIO_BarrierRetry和R1BIO_Barrier状态位。
2. 增加盘阵中r1_bio->remaining请求数,增加个数为盘阵中盘的个数。
3. 对于盘阵中的每一个磁盘,克隆master_bio给它,并进行初始化。(其中原failed bio的每个page要逐一复制给新的bio,因为可能存在write behind设备)。
4. 下发这个新的bio。
更多详情见请继续阅读下一页的精彩内容: http://www.linuxidc.com/Linux/2015-07/120593p2.htm
同步的大流程是先读,后写。所以是分两个阶段,sync_request完成第一个阶段,sync_request_write完成第二个阶段。第一个阶段由MD发起(md_do_sync),第二个阶段由守护进程发起。
如果是用户发起的同步请求。该请求下发到raid1层,首先进入同步读函数sync_request。在正常的成员盘中,将所有active可用的盘(rdev->flags中有In_sync标记)设置为read盘,而所有不可用的盘不做设置。对每一个可用盘对应的bios[]都单独申请页结构,对所有的read盘下发读请求,读成功之后,每块read盘对应的bios[]的页结构中都存放有该read盘中的内容。当所有读请求都完成之时,将r1_bio添加到retry_list队列,交由守护进程处理。接着,切换到守护进程,守护进程获取到retry_list中的内容,判断r1_bio->state为R1BIO_IsSync。然后调用同步写函数sync_request_write来进行同步写操作。接着顺序遍历成员盘的bios[],找到第一个读成功的bio。然后将其与其他所有读成功的bio做对比,如果有不一致的页,那么将不一致的页拷贝到这些其他的读成功bio中,并将这些bio也标记为写请求,并将所有的写请求下发,写成功返回;所有页都是一致的盘,则不用下发写bio。见图1。
图1 用户发起的同步流程
如果是非用户发起的同步请求。该请求下发到raid1层,首先进入同步读函数sync_request。在正常的成员盘中,将有In_sync标记的盘(标记在rdev->flags上)设置为read盘,将没有In_sync标记的盘设置为write盘,对所有找不到的或Faulty盘不设置读写。所有成员盘对应的bios[]都共享一份页结构,选取一个read盘下发读请求,读成功之后,该块read盘中的内容读取到bios[]的页结构中。当读请求完成之时,将r1_bio添加到retry_list队列,交由守护进程处理。接着,切换到守护进程,守护进程获取到retry_list中的内容,判断r1_bio->state为R1BIO_IsSync。然后调用同步写函数sync_request_write来进行同步写操作。接着该函数将除了执行读操作的盘之外的所有盘都标记为WRITE盘,并将写操作下发。由于所有成员盘的bio都共享一份页结构,所以写操作下发的均为读操作读取到这份页结构的所有内容。见图2。
图2 非用户发起的同步流程
1)同步读函数sync_request
函数在MD进行同步操作时调用,作用是对设备上指定的chunk进行数据同步。入参go_faster指示是否应该更快速进行同步。
具体代码流程如下:
1. 如果同步缓存的mempool没有设置,则此时创建。
2. 如果同步位置不在盘阵内,可能同步出错了,或者已经完成同步。更新bitmap的相关信息,并结束同步过程。
3. 如果没有指定bitmap(只有置位的bit对应的chunk才需要进行同步),并且盘阵没有必要进行同步(磁盘正常工作时设置了mddev->recovery_cp = MaxSector),并且没有设置同步请求位(!test_bit( MD_RECOVERY_REQUESTED, & mddev-> recovery)),并且conf设置为不需要全同步(conf->fullsync == 0,fullsync表示阵列要强制完全同步),则设置该段数据跳过,结束返回。
4. 如果bitmap(bitmap_start_sync)认为无需同步,并且没有设置同步请求位(!test_bit( MD_RECOVERY_REQUESTED, & mddev-> recovery)),并且conf设置为不需要全同步(conf->fullsync == 0),则设置该段数据跳过,结束返回。
5. 如果当前设备请求队列忙于处理其他非同步类的访问,并且没有设置快速同步(go_fast为0),该同步请求就延时等待一会。如果设置了快速同步则没有延时等待这一步。
6. 在盘阵上设置barrier,保证盘阵的同步操作不受其他操作打扰。同样,这里也有bitmap的相关设置。
注:raise_barrier(conf),该障碍在同步结束后end_sync_write中调用put_buf清除。
7. 申请r1_bio,具体见r1buf_pool_alloc的流程,将其状态设置为同步状态。
8. 遍历盘阵中所有磁盘,进入循环。
8.1 如果磁盘不存在,或者为Faulty状态,则设置still_degraded 为1,遍历下一个磁盘。
8.2 如果磁盘不为In_sync状态,则就是同步过程写入的对象(非active盘)。
8.3 否则就是同步过程中读的目标。尽量避免通过WriteMostly的盘来读,这是针对系统发起的同步而言的。
注:通过变量wonly和disk来实现。
9. 保证读目标只有一个,其余都是写目标。
10. 如果都为读目标盘,或者都为写目标盘,那么则结束返回。
11. 将max_sector调整为mddev->resync_max,同步不能超过这个值。
12. 构建r1_bio中每个bio的bi_io_vec,将缓存页申请并添加到数组中。直到数组满或者达到一个同步操作要求的数据长度。
13. 如果是用户发起的同步(通过sysfs写入“repair”),则对所有读对象都发起读请求;如果不是,则只发起一个可读的磁盘的读就可以了。
2)同步读结束函数end_sync_read
同步读操作结束后,调用end_sync_read。
如果读成功,该函数设置r1_bio的状态为update态(set_bit(R1BIO_Uptodate, &r1_bio->state))。
如果r1_bio中所有读都完成了,则调用reschedule_retry将r1_bio插入到盘阵的retry队列中(队列头为conf的retry_list,队列元素为r1_bio的retry_list),唤醒守护进程。
3)同步写函数sync_request_write
守护进程调用sync_request_write,进入同步的写流程。
1. 如果同步是用户发起的。
test_bit(MD_RECOVERY_REQUESTED, &mddev-> recovery)
1.1 前面对所有可以读的对象都发出了读请求,而且也已经读完成了。只要其中一个读成功,则r1_bio的状态会被设置为R1BIO_Uptodate,如果此时r1_bio仍然不为R1BIO_Uptodate,说明所有的读都失败了,此时设置所有读的盘为故障,并且结束同步过程,结束返回。
1.2 找到读成功的第一个盘,递减rdev的下发IO计数。并且将它与从其他盘读的数据比较,如果发现有的盘和第一个读成功的盘数据不同,则将这些盘也列为写入对象,将读成功的第一个盘的数据拷贝到这些盘的对应bio中,跳到步骤4。
2. 如果同步不是用户发起的。由于是在mempool中分配了RESYNC_PAGES个页,所有的bio的bvec都指向它们。这就是说,对于非用户发起的同步操作,读和写的缓存区是同一个,所以不需要单独的拷贝操作。直接进行后面的工作。
3. 如果所有读盘的读操作全部失败了,!test_bit(R1BIO_Uptodate, &r1_bio->state),则进行恢复尝试。
3.1 尝试通过同步访问接口sync_page_io从所有可能的磁盘读出数据块。
3.2 如果读成功,则对这个同步操作的读失败的磁盘,尝试进行一次写,进行一次读(都使用同步访问接口)。
3.3 如果都成功,则说明错误已经被纠正,否则调用md_error将该盘作废。
4. 最后,将所有需要写的盘上的同步bio进行提交。设置bio结束操作为end_sync_write,返回。
4)同步写结束函数end_sync_write
最后,同步写操作完成,调用end_sync_write函数,清除盘阵障碍,释放r1_bio缓存区,唤醒障碍上等待的任务。如果写出错,设置磁盘错误,并且调用bitmap_end_sync将本次同步操作的所有chunk对应的bitmap设置为需要进行同步(needed_mask)操作,下次同步时会尝试恢复这个写错误。
5)一些补充
5.1 读写缓冲区分析
读的数据要写入,应该由读的缓存区拷贝到写的缓存区中,在用户发起的同步中,是在sync_request_write的1.2中执行的。
对于非用户发起的同步而言,这个操作是在r1buf_pool_alloc函数中完成的。在函数sync_request的第一步,r1_bio = mempool_alloc(conf->r1buf_pool, GFP_NOIO); [fp3] 这个语句中,mempool会调用r1buf_pool_alloc进行实际的内存分配。
这个函数在分配r1_bio的缓存区时,除分配了其中的bio数组外,还分配了bio中的bvec页,对于用户发起的同步操作,是每个bio都分配了RESYNC_PAGES个页;对于非用户发起的同步操作,则是分配了RESYNC_PAGES个页,所有的bio的bvec都指向它们。这就是说,对于非用户发起的同步操作,读和写的缓存区是同一个,所以不需要单独的拷贝操作。
r1buf_pool_alloc()函数在sync_request刚开始时调用。具体代码流程如下:
r1bio_pool_alloc申请一个r1_bio的空间。
如果r1_bio不存在,则返回NULL。
分配r1_bio中所有盘的bio。
如果同步请求是用户发出的,则针对盘阵中的每一个bio分配RESYNC_PAGES个页。
如果同步请求不是用户发出的,那么先对一个bio分配RESYNC_PAGES个页,再将盘阵中的其他所有bio指针都指向第一个bio的page页面,也就说所有bio的bvec都指向它们。
关于内存池机制(mempool)多说两句:
首先利用mempool_create()来预先分配一定内存空间,作为一个“备用池”,在raid1中这个机制是用来分配r1_bio结构(包含bio的页结构)。入参min_nr是预分配对象的数目;alloc_fn和free_fn是指向分配和回收函数的指针,在raid1中分别对应的是r1buf_pool_alloc和r1buf_pool_free;pool_data指的是缓冲池对象的一些信息,raid1中pool_data是*mddev和raid_disk。
接着在缓冲池机制中,需要分配r1_bio空间的时候,调用mempool_alloc()。该函数首先在“备用池”外寻求分配空间,如果外面的空间不足,那么就在“备用池”中分配。这样就保证了重要的结构在分配的时候减少因为内存不够导致空间分配失败。
其实就是先在内存中圈一片自留地,表明这是我的家底,不到资源紧缺的危急时刻不会动用它;当有结构分配的内存需求时,就先在自留地外面去寻找,先占用公用的地方;如果公用的地方都被占完了,那就只能占用自留地来满足需求了。
5.2 用户发起的同步的流程中,无WRITE盘的相关操作
最初我怀疑是自己漏掉了某些代码流程,后来分析发现用户发起的同步中没有WRITE盘相���操作的代码是正确的。
sync_request()函数在判断每个盘的bio是否为READ盘或者为WRITE盘,并指定对应的bio->bi_end_io是end_sync_read还是end_sync_write,是根据rdev->flages的In_sync是否置位来判断的。但是,在这一步判断之前,先判断的是rdev->flages的Faulty标志是否置位,如果Faulty置位,那么就不设置READ或者WRITE,也不设置回调函数,此时的回调函数指针指向的是NULL。
而rdev->flages的In_sync标志表示的是这个盘是否active,置位则为active。设置In_sync为0的时候,除去sys接口和do_md_run中的特殊情况,只有下面两种情况:
1、盘阵中的盘出错,调用error()函数的时候;
2、Add disk的时候。
而Add disk时候会由系统发起同步,可能还会在后面将rdev->flages的Insync标志置为1,反正不会是用户来进行这种同步操作,没有具体进到函数中去看。
在盘阵出错的时候,会调用到error()函数,而在清除掉In_sync的时候,会降级,并设置Faulty标志。
在用户发起的同步的时候,rdev->flags的In_sync位为1的盘,一定是READ盘;In_sync位为0的盘,Faulty位一定被置1了,所以不会标记为READ或者WRITE盘,并且bio->bi_end_io还是为NULL。这样一来,用户发起同步的时候,就不存在标记为WRITE的盘了,所以只确定READ盘的一致即可,不标记的盘只可能是error盘,用户发起的同步对其不做任何处理。
而在由于用户发起的同步请求mddev->recovery也带有MD_RECOVERY_SYNC标志位,所以流程会走到write_targets += read_targets-1,于是就不会走到if (write_targets == 0 || read_targets == 0)的判断条件中,从而避免了流程的终止。
所以在用户发起的同步时,流程中找不到WRITE操作的相关代码是正确的。
本文永久更新链接地址:http://www.linuxidc.com/Linux/2015-07/120593.htm