那么MD要如何看起?这个问题在我脑中也没有特别清晰的脉络,因为MD中的代码很杂,并没有很清晰的层次或类别关系,所以我打算想到哪里就写到哪里。但不管怎么说先建立一个对MD的总体印象是很有帮助的。

首先,创建了一个MD设备,我们可以通过mdadm –E来查看MD中各设备的情况(E -examine),或者通过mdadm –D来直接查看MD的状态(D –detail)。那么这些信息是从哪里来的呢?这就牵涉到MD如何存放自身信息的问题。事实上,MD是通过自己的superblock来存放这些信息的,而MD中每一个设备都有自己的一套superblock,但我们必须保证各子设备的superblock是一致的。通过这些信息,即使MD被Stop以后再Assemble又能够起来。因此MD有一套自己的管理superblock的机制。

其次,既然DVD机要能让人控制,当然要提供一些按钮给人们。MD自然也要提供一些与外界交互的接口。我们知道linux驱动程序中,一般是通过提供ioctl来进行交互的。关于MD如何实现没什么新鲜,但是这里面提供了那些交互,有何作用,如何实现那可能才是我想讨论的。

再次,resync和recovey是如何工作的。在讨论RAID-5中已经提及,但那只是RAID-5内部的处理,事实上在MD中如何调度resync和recovery其实也是很tricky的,第一次看的时候很容易就云里雾里了,这是经验之谈…

次中又次,其实这一点并不次,不过突然想起来应该将它

最后就是bitmap,不过这我始终还是不敢碰,毕竟还没有空研究透。

万变不离其宗,说代码总是应该从数据结构说起,下一篇我就简单说说MD的数据结构。:)

要看MD的数据结构,那就得看md_k.h了。这里有最主要的三个结构定义:mddev_s这是MD本身的结构;mdk_rdev_s这是对应于MD中子设备的结构;mdk_personality这是个有函数指针组成的结构,各函数交由各RAID级自己去实现。

 

首先来说说mdk_rdev_s,我觉得其中最重要的有这么几个字段:
 struct list_head same_set; /* RAID devices within the same set */
 sector_t size; /* Device size (in blocks) */
 mddev_t *mddev; /* RAID array if running */
 unsigned long flags;
 int desc_nr; /* descriptor index in the superblock */
 int raid_disk; /* role of device in array */
same_set相当于一个链表节点,通过它一个rdev可以被组织到一个md中。size就是这个rdev的容量大小。mddev则是其所在的MD的指针。在RAID-5中我们提到过flags的一些内容,注释里对这些状态的解释其实都很清楚了。desc_nr其实就是cat /proc/mdstat中看到的[]中索引值,大多数情况下其值是与raid_disk是一致的,但是当它没有正式加入到RAID中时,raid_disk的值是-1,而此时desc_nr就是一个不同的值。所以raid_disk的值就是rdev在RAID中的索引。

 

关于mddev_s,由于里面的东西太多了,一个个摘取出来分门别类介绍也是个精细活儿,偷个懒,还是放到后面去,碰到需要解释的字段再花力气去解释更好些。mdk_personality在RAID-5中也有介绍,这里也不多说,又碰到要讲的再说。

上一篇已经提到了一些MD中我能想到的比较重要的一些内容,由于是从RAID-5讲起的,有点从下到上的意思,所以下一篇打算从resync的调度为切入点,进入到MD内部。

到底什么是resync和recovery?虽然我们在RAID-5中讨论了它们的作用,但是它们并不只存在于RAID-5中,所以很多通用的操作被提到了MD中来。

关于resync,简而言之,就是在MD设备运行之前对子设备上的数据进行初始化,使各子设备上的数据同步(IN_SYNC),比如说在RAID-5中,resync的就是计算Parity并写入,这样就是同步了,而在RAID-1则需要从一个子设备中读出数据而后写入其他数据来达到同步。忘了在哪里看到这样的叙述了,有一定的道理:除了RAID-5以外,resync其实作用并不大。为什么这样说呢?我是这么想的,关于RAID-5中的resync的作用,以前已经讨论过,就是为了rmw的正确。而RAID-1中由于写入数据时总是保证各镜像是一致的,而读取尚未写入的数据就是读取无用数据,所以这些数据各镜像上是否一致并不重要。而RAID-6,由于都是做rcw所以也不存在rmw可能导致的问题。

所谓recovery,其实就是恢复数据。子设备是有可能发生错误的,这时MD会将Faulty的子设备从RAID中移出(虽然还留在MD之中),那么对于那些有数据恢复能力的RAID阵列,如RAID-1456,我们就能够通过加入spare恢复Faulty设备上的数据,recovery就是从头到尾恢复数据的过程。recovery完成之后spare就会成为正式的成员取代原来Faulty设备的位置。各级RAID有不同的恢复方式,RAID-1就是从某个镜像中读出数据并写入到spare上,而RAID-56则是通过其它子设备上的数据通过计算出要恢复的数据写入到spare上。

当然,很多RAID级是不需要resync和recovery,最典型,最熟悉的就是RAID-0,它没有冗余数据,所以自然不用resync,也没有recovery的能力。

下一篇将对resync和recovery调度展开详细的讨论。:)

说到调度,一般都是说进程或者线程调度,这里也不例外,因为resync和recovery都是由一个后台的MD工作线程在执行的,我们且称之为syncd。不再象2.4那样在RAID启动时为其注册一个syncd,如果没有工作就一直休眠,有resync的需要时就起来工作,2.6内核是在有resync需要时才注册syncd,在resync完毕就将其注销;2.6也不象2.4那样将resync线程与recovery线程分开,而是在需要时注册一个线程需要做resync就做resync,需要recovery就做recovery。我们暂且将如何注册MD工作线程放到一边,集中精力看看何时启动syncd,它又是何时结束的。 

这一切的玄机都在一个函数里——md_check_recovery。这个函数是挺唬人的,至少我在读它的时候懵住了好一会儿,因为在这个函数中处理syncd结束的代码在启动的代码之前,而且里面有些条件并不好理解。在代码中搜一下这个函数,我们就会看到,它是在RAID-1456的守护线程中被调用,所以我们可以得出结论:要想判断是否需要启动或结束,我们要做的就是唤醒守护线程。事实也的确如此,我们可以再搜一下MD_RECOVERY_NEEDED这个宏,就不难发现在set_bit(MD_RECOVERY_NEEDED, &mddev->recovery)的后面总能找到md_wakeup_thread(mddev->thread)的,mddev->thread就是守护线程的指针。说道这里,不得不讲一讲mddev的recovery字段,它是表示resync/recovery的状态的。其中的0~5位这里先拿来说道说道,顾名思义,它们的作用都很直观。RUNNING表示syncd在运行;DONE表示syncd完成;NEEDED表示可能需要resync或recovery;SYNC如果设置表示在做resync,否则是recovery;ERR表示resync/recovery过程中发生错误;INTR表示resync/recovery过程中收到中断信号。 

还是先来总结何时需要启动syncd,还是老办法,在md.c中搜一下设置MD_RECOVERY_NEEDED的地方: 

1.在action_store函数中,这是为用户提供的发起resync的操作。 

2.在MD开始运行时,自然需要检查 

3.restart_array中自然也要检查 

4.add_new_disk或者hot_add_disk的时候很有可能就是要发起recovery 

5.md_error被调用的时候,我们很可能需要recovery 

另外还有几处设置了MD_RECOVERY_NEEDED位,但是它们的作用并不是很直观,有些我也还没有特别明白,所以略去不提。 

那么下面,我们就集中火力来讨论syncd的生与死,生死都在md_check_recovery中进行。当然,我们看md_check_recovery的代码,会发现他并不仅仅是处理syncd相关的事,还要处理super-block更新的事情,前者自然是我们目前关注的重点。 

首先先要检查是否有syncd在运行而且没有结束工作,如果是,那我们就不要多此一举再去启动syncd了。如果没有syncd在跑,那又有两件事要先做:移除一些设备或加入一些设备。这两件事实际上就是在处理md_error或add_disk引发的recovery需求。这里移除设备就是移除MD中Faulty的设备或者没有同步的设备。所谓没有同步(In_sync)就是还没有能正常工作的设备,这种状态好像在前面RAID-5中有所提及。这里我们要注意的是,不仅仅是Faulty的设备要被移除,还有在做recovery的spare设备,我虽然不是很明白为什么要将spare设备先移除然后又立即加回,但是这样做代码的确是比较简洁而直观的。至于这里的移除实际上只是从RAID中将设备隔离出来,实际上并没有从MD中移除,只是通过调用personality中的hot_remove_disk函数完成的,这类设备在/proc/mdstat中可以通过看它后面有没有(F)或者(S)来区别。那么加入设备就是一处设备的逆过程。当然,只有在MD是degraded的时候才要加入spare设备。也许我还需要提一提什么是spare设备,实际上我们可以为RAID准备一些hot spare设备随时在RAID中有设备故障时挺身而出替代故障的设备。所以说这里的spare设备实际上已经在MD中,只是没有参与RAID工作而已。同样,通过调用personality中的hot_add_disk函数完成添加。 

接下来就是要判断事要做resync还是做recovery,其差别就是是否在前面加入了spare设备进来。如果有spare加入,那就是recovery,否则就是resync。当然,在启动resync之前我们还要看recovery_cp是否已经到MaxSector了。这里就要先说说什么是recovery_cp,全称是recovery check point,其作用就是在上次resync被打断以后(MD被stop而不是由于md_error打断)记下被打断的位置,下次启动时就可以从这个打断的位置继续做resync而无需再从头来过。而当resync完成时,recovery_cp就会被置为MaxSector,也就是如果recovery_cp的值为MaxSector,表明resync已完成。所以检查recovery_cp的目的就是检查resync是否完成。从这里其实我们也应该看出来了,recovery是不记断点的,所以它总是从头开始。如果既不是recovery,又不用做resync,那就是说没有syncd的必要了这个函数就直接退出了。如果要recovery或者resync,就进入启动syncd的过程。 

启动syncd的过程很简单,就是调用md_register_thread,然后将其wake up就行。

说完启动再来看它的结束。同样是在md_check_recovery中,但我们会发现处理结束的代码处于处理启动的代码之前。其中的逻辑其实也很好理解,无非就是先处理已经启动的线程,但是由于我在阅读这部分代码时花了点时间,所以多罗嗦几句。实际上,光是md_check_recovery中的代码实际上并没有完全结束resync和recovery,只是结束了syncd,从这段代码注释中的“double check”就可以看出来。我们还是先看看syncd是如何结束的,再看resync和recovery如何停止。结束syncd很简单,既然启动它用的是md_register_thread,那结束它用md_unregister_thread就行。每次结束syncd都要做一次检查,就是检查是否需要将spare激活。这是针对recovery的动作,所谓激活spare,就是调用各RAID中的spare_active函数来将完成recovery的spare转换为正式与其他设备同步的设备,它内部是怎么工作可以参考RAID-5。当然,如何判断recovery正常结束呢?很简单,就看在recovery是否发生了error或者syncd是否收到打断的信号,也就是recovery一直从头到尾没有出现I/O错误,也没有外界干扰。在syncd结束时还有一个重要的事情就是更新md的super_block,此时更新应该说是比较好的时机,因为此时md很可能发生了某些状态变化并需要记录到super_block中。当然什么时候需要更新super_block,那是由super_block的作用及其内容来决定,这个应该放到以后讨论。代码再接下去就是将md的resync/recovery状态设为NEEDED,这就是要做“double check”。 

就我目前的理解而言,“double check”在resync/recovery遇到error而停止时有用,它要做一件重要的事,那就是决定是要在剩下的设备中继续做resync/recovery还是结束resync/recovery。因为将resync结束时并不只是将syncd停止就行,还有好些状态还需要修改,而在md_check_recovery中并没有改动这些状态。我现在以RAID-5的resync中遇到error的过程为例来展示这些代码的功能,recovery也有类似的过程,区别就在于recovery只需是否有spare就可以判别是否该结束了。 

一切都从md_error开始。在md_error中,结束后我们会发现mddev->recovery有三个位被设置:ERR,INTR和NEEDED。其中NEEDED不是重点,syncd会看到ERR和INTR位被设置,于是md_do_sync结束,mddev->recovery的DONE置位。请注意,此时recovery_cp值并没有改变,也就是说他并不是MaxSector。然后RAID的守护线程被唤醒,md_check_recovery被调用,syncd被注销以后该函数也就结束了,但resync还没有结束,因为NEEDED位置1了,也就是说下一步还得由“double check”决定。

于是md_check_recovery会被再次执行,“double check”就此开始。这次md_check_recovery并不会去注销syncd了,因为它已经被注销了。那么,md_check_recovery首先从RAID-5中移除Faulty的设备。这里我们假设没有spare设备在MD中,这样也就没有spare被加入到RAID-5中去,因为如果有spare的话,recovery会被启动,这并不是我要讨论的。到这里,我们知道syncd会再次被注册起来工作,因为没有spare加入而且mddev->recovery_cp确实小于 MaxSector。即使我们知道RAID-5现在已经少了一个设备,根本没必要resync了,但syncd的确是要起来的。syncd最终会调用RAID-5的sync_request,这个函数一看到是要做resync,但是又已经degraded了,他就“谎报军情”对MD说:“后面的sync都做完了,不用做了。”于是syncd将mddev->recovery_cp设为MaxSector,syncd工作完成。接着故事又回到md_check_recovery:第一次md_check_recovery,syncd被注销;第二次md_check_recovery,没有spare,mddev->recovery_cp已经是MaxSector,这一次resync才算真正结束了。呜呼!似乎拐了好几个弯,但愿您没都绕进去。

看md_check_recovery中注册syncd的代码,就能知道syncd执行的动作就是函数md_do_sync。这个函数也不是省油的灯! 

 

首先来看这个函数中最前面的一个循环。这个循环的作用避免resync/recovery冲突。这里所谓“冲突”,我的理解就是同一个设备处于不同的MD中,这些MD同时做resync就会发生冲突。用一般的IDE硬盘举个例子来说,假如我们有三个硬盘hda,hdb和hdc,将他们分别创建两个分区,用hd[abc]1三个设备组建一个RAID-1,同时用hd[abc]2组建一个RAID-5,于是他们都需要做resync,他们各自的syncd都注册启动,但是只有一个能进行resync,另一个只能被delay。要避免这种冲突应该是出于resync效率的考虑,因为同一个hd上两个分离的区域同时做I/O并不是高效的做法。关于这个循环是如何完成这项避免冲突的任务的,我也不想多讨论,因为对于curr_resync的重用我也还没有完全理解,而且对于那两个指针来比较大小的做法我总觉得很奇怪,如果有谁花时间理解了这些内容,希望能跟我讲解讲解。在这个循环中最重要的无非就是match_mddev_units这个函数的调用,它能发现冲突的MD。 

 

在正式进行resync/recovery之前,先要确定有多少扇区要做resync/recovery。Resync与recovery需要的扇区数可能会不同,但通常来讲都是设备的物理大小。接着,就是对resync/recovery做初始化。首先,如果是resync,我们会将recovery_cp作为resync的起始扇区。接着,初始化一些时间戳变量,用作resync/recovery计算速度的数据。在resync/recovery中会用到一个变量window,它的值被设为32*(PAGE_SIZE/512),也就是256个扇区。Window的作用就是将sync的请求分组,每次处理都是一组一组进行,每组之间的间隔就来查看是否需要释放CPU一段时间给正常的请求让让路,毕竟正常I/O能保证才是我们的目标。 

 

md_do_sync函数里最重要的就是那个while循环。它的结束条件很简单,那就是将需要sync的扇区都sync完了,所以在这个循环之前就已经把要sync的总扇区数都算好了。于是这个循环就按从小到大的扇区顺序进行同步。同步的动作各级RAID各不相同,所以需要同步的RAID都会提供sync_request函数来给md_do_sync调用。如果是RAID-5,我们已经知道它是怎么做resync/recovery的了。调用sync_request需要将要sync的起始扇区号传给它,这个函数会返回它做了多少扇区的sync。如果返回0,那只能是发生了错误。Sync_request也从skipped这个参数返回信息,如果skipped为1,说明这次同步被跳过,那么关于这次同步的统计信息就不用进行了。通过观察RAID-5的代码,我们很容易会发现,skipped被置1通常就是缺盘的时候,这个时候当然不用sync。

 

现在应该是来看看标签repeat这里的代码了。这段代码在while循环中形成了一个子循环,它的作用就是前面提到的检查及给正常I/O让路。它是怎么做的?首先,需要统计当前sync的速度,sync的速度就是我们要监测和控制的对象。Sync速度的算法是这样,我们使用一个循环数组,每一个数组元素记录一次当前sync的位置和时间,那么 

     Sync速度=(数组尾位置-数组头位置)/(数组尾的时间-数组头的时间) 

 

有了速度我们就可以判断是否需要给正常I/O让路了。在支持resync/recovery的RAID级中,都可以设置resync的最大和最小速度,当然如果没有设置这些值,那就会使用MD的默认值作为最大和最小速度。于是我们就有了速度要满足的一个条件:要保证在最大和最小速度之间。相关代码如下: 

if (currspeed > speed_min(mddev)) { 

     if ((currspeed > speed_max(mddev)) || 

             !is_mddev_idle(mddev)) { 

         msleep(500); 

         goto repeat; 

     } 


可以看到如果当前速度小于等于最小速度,则不用sleep,直接去做下一轮的resync。如果当前速度超过了最大速度,则需要睡上500毫秒,这样速度就降下来了。这里我们还能看到一个有趣的函数is_mddev_idle(),它的作用就是判断是否MD处于空闲状态,如果不是就需要给正常I/O让路。 

 

打开is_mddev_idle()的函数体,可以看到这个函数很简单,但是能说一说的东西还真不少,毕竟我在这个地方也走过弯路。用一句话来概括的话,这个函数就是用子设备上正常读写的数量来判断MD是否空闲。这个函数会对每一个子设备做这么几件事:首先,用子设备上已经记录的io数量减去sync io数量(就是resync/recovery的io),得到curr_events。然后用curr_events与last_events(上一次的curr_events)做个比较,如果差值超过了一个阈值,就认为该MD不是空闲的。看函数中的这个条件语句: 

if ((curr_events - rdev->last_events + 4096) > 8192) (1) 

我想大家应该都会象我一样有个疑问,为什么不写成: 

if ((curr_events - rdev->last_events) > 4096) (2) 

其实很简单,因为两个events都是unsigned long,做减法的话如果被减数比减数小,差值将仍会是一个正数,而且通常是个很大的正数。这里加上一个4096,就是为了将这个异常的正数调整回一个正常正数。随之而来的又有两个问题:为什么两个events的差值会出现负数?4096又是怎么得出来的?代码中那段长长的注释就是为了说明这两个问题。第一个问题的答案很简单,就是因为sync io的数量并不是立即被计入到子设备的io数中。像这样的sync io作者称之为in-flight sync io。至于4096的来历就是通过对比RAID5和RAID1的最大可能的in-flight sync io的数量,因为RAID5的默认stripe cache数是256,而stripe size是4k,因此它的in-flight sync io最大将只能是2048个扇区;而RAID1的sync io它的深度(DEEPTH宏)是32,最大的bio是64k,算一算就得到了4096个扇区。 

 

虽然上面花了不少的语言去解释这个函数,但是这里面却是隐含着几个Bug。最大的一个Bug就是,4096太不灵活了。我们看RAID5的代码就知道,RAID5的stripe cache的数量是可调的,而且如果内存充裕可以调到很大的值,那么in-flight sync io的数量就有可能超过4096,那么用4096作为调整值就不够用了。非常高兴的看到最近kernel中RAID的patch已经fix了这个bug,更高兴的是他的改法跟我的不谋而合,那就是将两个events作为signed long而不是unsigned long。这样语句(1)就能写成(2)。但是这样改我还是觉得在某些非常极端的条件下会出异常,但是因为非常极端,其实也许很难碰到。 

 

回到md_do_sync(),while循环里的东西好像都说完了,那就该说说md_do_sync结束之前,也就是resync停止之前的一些动作。首先是unplug MD的queue,让sync io尽快返回,然后就等待所有的sync io结束。然后调用最后一次RAID的sync_request来做收尾。如果在resync/recovery过程中没有出现错误那就分两种情况:如果resync/recovery是被中途打断,则将其中断的位置计入recovery_cp中,否则如果resync/recovery正常完成则将recovery_cp设为MaxSector。最后recovery的状态被设为DONE,唤醒RAID守护线程去处理。

实际上MD中线程的调度都是植根于linux中的调度,我这里为什么要单独拿出来讨论讨论?因为我觉得MD的线程写法挺有意思,让我受益匪浅。

 

在MD中使用的守护线程都是系统线程,它的优先级是非常高的。先来看MD线程的数据结构mdk_thread_t。内容非常简单,最关键的就是run函数指针,才是线程中真正的工作函数;线程通过mddev指针来访问MD的数据;线程中会有个等待队列,以便于线程的睡眠与唤醒;flags字段在MD线程中有点控制标志的味道;毫无疑问tsk就是指向该线程在linux中的结构;timeout字段用于定时。

 

MD线程是如何创建的呢?代码在md_register_thread中可以找到。这个函数分配并初始化了了一个mdk_thread_t结构。然后调用kthread_run函数创建了一个内核线程,这个内核线程执行的函数是md_thread,这个函数我们接下来会重点讨论。与此相对,MD线程的结束是通过md_unregister_thread函数,停止线程并释放分配的mdk_thread_t结构。

 

虽然md_register_thread启动的线程并没有直接用mdk_thread_t的run函数,但是这个run函数才真正决定了这个线程的工作内容。那就跳转到md_thread里面去看看。这个函数太简单了,核心就是一个while循环。而且循环体中的内容也非常简单,无非就是如果这个MD线程检测到THREAD_WAKEUP标志,就起来执行run函数,执行结束后就会被加入到等待队列中等待下一次的唤醒。所以说md_thread函数实际上完成一种线程调度的封装。

 

需要唤醒MD线程的时候就要调用md_wakeup_thread函数,它的工作就是打上THREAD_WAKEUP标志,然后调用内核wake_up函数唤醒等待对列中的线程。

 

好了,似乎短短几段就把MD线程的相关内容说完了,如果结合我们在RAID-5中介绍的raid5d,就更清楚他们是怎么工作的了。虽然MD线程的内容并不深,但我觉得这种方式非常漂亮,值得我学习。

Superblock通常总是用来保存一些配置信息,MD中的superblock也不例外,通过看md_p.h中的mdp_superblock_s的定义大致就能了解其内容。MD的Superblock保存的信息主要包括:
(1)基本信息(Constant generic information) :主从设备号,UUID,RAID级,size,设备数量等;
(2)一般状态信息(Generic state information):包括一般MD运行的统计信息;
(3)Personality information:看上去似乎是能够手动配置的信息;
(4)磁盘信息(Disks information):记录子设备的信息;
(5)保留(Reserved):留做他用。
有了superblock,MD即使停止以后,通过收集这些子设备也能再重建起来使用。这一点在重启计算机时尤为重要,我想着也不用多说。因此保持各子设备上superblock的一致和更新是一个重要的动作。

关于superblock,更多的还是跟MD控制信息相关,大多数是细节问题,能抽出来讨论并不多,而且有点凌乱,所以我打算从总体上说一说就行。在2.6内核中支持两种superblock,按版本分为0.90版和1.0版。显然1.0版的更强、更灵活,但是因为我更多的是使用0.90版,感觉0.90的superblock更明白些。
Superblock是保存在每一个子设备的特定区域中。我曾经有过疑问,superblock可不可以写在MD设备上而不是子设备上?既然是写在各子设备上,那么mdadm –D显示的信息是从那个设备读出来的?下面是我的一些结论,当然先声明,这些结论我还没有机会证实。写在子设备上的原因很简单,那就是superblock是用来重组MD信息,但是如果MD不能重组的话,就不能读出superblock,这就是个死循环了。Mdadm –D显示的信息应该不是从设备上读出来的,而是从内存中将MD的各种变量读出来显示的。
MD中将对superblock的操作分为3个:load,validate和sync。Load就是将superblock从子设备中读到内存;validate我觉得就是用内存中superblock来构建md的信息;sync就是将更新了的MD信息写回到各子设备上。Load和validate两个操作主要是在重组MD的时候,在md_import_device或者do_md_run等函数中能看到他们的身影。关于对superblock的读和分析,似乎都跟具体的应用有关,没什么可说的,我想还是花点儿精力在更新superblock上。
更新superblock的函数就是md_update_sb()。在代码中搜索这个函数,就能找到MD何时需要更新superblock。通常需要更新superblock时,会将sb_dirty标志设为1。现在来看看md_update_sb函数。这个函数一上来就先记下了in_sync这个子段的值,我打算在下一篇中专门讲解这个字段的用法。紧接着,记下更新superblock的时间,更新events计数。当然,event计数器可能会溢出,这就需要处理一下溢出问题。接下来sync_sbs()函数被调用来更新各子设备的sb结构,于是各子设备内存中的superblock信息就是同步的了。如果我们不在子设备上存储superblock(称为nonpersistent),那这个函数就可以结束任务,直接返回就行。但通常superblock都是persistent的,所以接下来我们还需要将superblock写入到子设备中。可想而知,这就需要一个循环,对每一个子设备调用md_super_write()函数,它的作用就是构造一个bio将内存中的superblock送到子设备中。md_update_sb必须等所有bio都返回了才能进行下一步的工作,这是通过调用md_super_wait()函数来完成的。
在向各子设备写superblock的过程中,也可能出IO错误,md_update_sb也对这种情况做了处理。它首先在函数的开始部分将sb_dirty标志设为2,如果在函数结束之前,sb_dirty又被设为1了,很可能就是某些子设备写失败,那么md_update_sb会再重新写一次superblock。更新完毕以后就将sb_dirty清零。

 

关于mddev->in_sync的作用我一直只是一知半解,前段时间终于有机会将它的用法重新理解了。原来它是出于数据安全考虑的。

考虑一下这样一种情况:一个RAID-5设备在进行读写。这个时候如果计算机突然掉电,也就是说很有可能一些写请求已经被子设备处理,而有些可能还没有被处理。要知道RAID-5的写请求必须要在parity也写完以后才算写完成。假设在同一个stripe中,数据已经被写入子设备,而parity还没有被写入,又或者parity已经被写入而某些数据还没被写入,无论哪种情况,可以想象的是,当这个RAID-5再次被重组以后这个stripe中parity已经与数据不再一致。根据我以前对RAID-5的分析(RAID-5(十二)),parity与数据必须是同步的,否则会有数据错误隐患。

那我们如何去解决这个问题?最简单的办法就是在RAID-5被重组后再次发起resync。但这种简单的办法开销是很大的,因为现在的磁盘动则几百G,要做完一次resync没几个小时是搞不定的,如果重组以后RAID-5就立即投入处理IO请求,那么resync估计做几天都有可能,那在这期间这个RAID-5是很脆弱的。然而,就目前这种情形,在无法确定哪个stripe有隐患的情况下,发起一次全面的resync是最可靠的办法了。

In_sync字段就是为了确定是否需要再发起resync。在2.4的内核中,也有类似的问题,但是并没有使用in_sync这种更为灵活的形式,其做法很死板:就是在MD运行起来以后,就将MD_SB_CLEAN标志从superblock中清掉,在MD停止的时候再重新设置回去,这样如果这时候发生非法关机,MD_SB_CLEAN标志就是被清除的,MD重组之后就知道应该要发起resync。我之所以说他死板,就是因为不管三七二十一,即使没有读写,或者读写都已经完成了,也还会发起resync。

再回过头来说说in_sync的灵活用法。因为只有在处理写请求的时候有可能存在这种问题,因此,在发起写请求的时候将in_sync设为0,表示:注意,我们可能需要resync。与此同时sb_dirty被设为1,更新superblock的操作开始执行,将superblock中的MD_SB_CLEAN标志清掉。那什么时候它会被设为1呢?两个地方,一是,如果所有的写都完成了,那么就可以将其设为1,另一个是MD正常停止。两种情况都回发起更新superblock,将MD_SB_CLEAN标志恢复。这里要罗嗦一句:以上讨论都是在初始resync已经完成以后的。大家看出来了吧,发起resync的条件更为严格了,避免了不必要的resync。

MD正常停止,就直接调用md_update_sb来更新superblock,没什么可多说的了。看看写完成时的处理,注意md_write_end函数,并没有我说的将in_sync置1的代码,也就是说并没有立即更新,只是根据safemode分两种处理。Safemode 2就是唤醒守护线程,守护线程会处理superblock更新;safemode 1则是延迟一段时间后再唤醒守护线程处理。这两种模式的具体用途我还不是特别清楚,有兴趣的话大家可以继续深入。

到此为止,我想对MD的讨论告一段落,MD有点凌乱,而且我还有不少地方没有仔细读过,不确定的地方还有不少,毕竟不如对RAID-5那般熟悉,没法做到条块分割,如果有什么纰漏或者谬误,请见谅,欢迎指正。

Logo

更多推荐