分类: LINUX
2009-09-02 17:27:07
1. 如果该请求是限制在一个条带内的读请求的话,则调用chunk_aligned_read处理完以后返回,该bio的结束回调设置为raid5_align_endio。
2. 根据请求的数据大小,对其中的每个stripe大小段执行下面工作(先暂不考虑expand):
a) 使用raid5_compute_sector计算出数据盘,效验盘,以及起始扇区。
b) 查找或者获取对应的stripe结构。
c) 调用add_stripe_bio将bio添加到stripe中。
i. 先判断bio是否和stripe中已有的bio存在重叠情况,否则设置本bio访问的磁盘对应的缓存R5_Overlap标志,返回。
ii. 将bio挂入到strip的对应队列中(如果是读请求,挂到toread;如果是写请求,挂到towrite),增加bio的硬件段计数。(第一个strip其实应该不加1,这个多余加上的1在函数make_request的最后去掉了)
iii. 如果是strip的第一个写bio,则调用bitmap_startwrite设置内存bitmap。并设置条带的STRIPE_BIT_DELAY位,这个位的解释见后面说明。
iv. 对于写请求,还要判断这个条带下写请求是否覆盖了整个stripe的页面,如果覆盖,则设置条带的R5_OVERWRITE标志,这个标志表示,可以直接对磁盘写,而不需要将该数据块从磁盘读出,改写一部分再写回。
d) handle_stripe处理这个stripe
e) release_stripe释放这个stripe
3. 查看stripe的物理段,如果物理段为0,表示bio没有提交给任何的stripe,可以结束处理。(不太能解释什么情况下会这样)
如果一个bio的读操作限制在一个条带内,则读操作完成后调用该函数。
将在chunk_aligned_read中申请的bio结构释放,如果读成功完成,则调用bio_endio将用户的bio结束。否则调用add_bio_to_retry将bio添加到retry_read_aligned_list队列中。
在RAID5向磁盘提交的写请求完成后,调用该函数。
如果写错误,则设置目标盘失败;清除缓存区的R5_LOCKED标志,设置条带的STRIPE_HANDLE标志,在release_stripe中用到这个标志对条带进行队列之间的转移。
向磁盘提交的读请求完成后,调用该函数。
如果读成功,则设置对应缓存区的R5_UPTODATE标志,清除R5_ReadError和R5_ReWrite标志(在重读或重写时会设置这些标志)。
如果读失败,在磁阵降级,重写(用于纠正磁盘错误),或者读错误次数超过门限时,不进行读的重新尝试,设置磁盘错误;否则设置缓存区的R5_ReadError标志,进行重新尝试。
清除缓存区的R5_LOCKED标志,设置条带的STRIPE_HANDLE标志,在release_stripe中用到这个标志对条带进行队列之间的转移。
RAID5的数据处理主流程是针对条带这个对象来开展的。条带中为每一个磁盘留了一个缓存。条带和这些缓存都有一系列的标志,这些标志决定了条带和缓存的状态,我们在后面进行说明。
需要注意的是:代码中定义的条带stripe和实际RAID设备的条带不是一回事!!strip的大小是一个PAGE,而RAID设备的条带的大小是chunk_size。Stripe的大小不是可以定制的。
条带的空间刚刚从内存中被分配时,调用该函数进行初始化。
主要的工作是将每个磁盘的缓存数据中,req是该缓存中要向磁盘提交的请求(读或者写),这个req中的bi_io_vec关联到vec(这个请求只有一个段),而段的页框和缓存中的page进行关联。
static struct stripe_head *get_active_stripe(raid5_conf_t *conf, sector_t sector, int disks,
int pd_idx, int noblock)
根据条带在磁盘上的起始物理扇区,条带磁盘数,效验和磁盘下标,查找或者获取一个条带数据结构。Noblock标志表示是否调用者可以睡眠。
函数流程如下:
4. 获取设备锁
5. 磁阵可能被人工沉默(quiesce),即禁止其上进行业务操作,等待这个禁止放开。
6. 在hash桶中查找(hash桶的数组大小为一页,所以桶大小为HASH_MASK= (PAGE_SIZE / sizeof(struct hlist_head))-1,hash算法更简单,stripe% HASH_MASK)。
7. 如果hash桶中找到条带,判断条带的引用计数是否为0,不为0的话,要保证其不在inactive或者handle队列中;如果为0,说明其必然在其中一个队列中,将其从该队列中取出,同时如果其没有在handle队列中,需要增加设备的active_stripes计数;转到6结束。
8. 如果hash桶中没有找到条带,则尝试从空闲链表inactive_list中获取一个条带结构,如果获取成功,则使用入参对条带进行初始化(hash中找到的是已经和入参对应的条带,而从空闲链表中获取的则不是),转到6结束;如果空闲链表是空的,调用者又不允许睡眠,则返回NULL,否则睡眠等待该链表中有可用的条带(这个睡眠唤起的条件比较复杂,大致是链表不为空,并且激活的条带不超过总条带的3/4;或者前面在此睡眠的进程已经被唤醒了),睡醒以后转到3。
9. 如果找到条带,则增加该条带的引用计数;释放设备锁,返回找到的sh
每次handle_stripe5调用返回后,都会调用这个release_stripe函数,这个函数不仅仅是将条带释放这么简单,它还进行了一些重要标志的设置。
1. 对条带的引用技术递减,如果引用计数尚不为0,则返回
2. 如果条带的STRIPE_HANDLE标志被设置,说明条带已经准备好被处理了。此时:
a) 如果STRIPE_DELAYED标志被设置,说明条带虽然可以有被处理的需要,但还是要等待一段时间
b) 如果STRIPE_BIT_DELAY标志被设置,条带还要等待一段时间
c) 否则,将条带移入handle_list链表
d) 唤醒守护进程。
3. 如果STRIPE_HANDLE标志未设置,说明条带并未被处理的需要,此时:
a) 如果STRIPE_PREREAD_ACTIVE被设置,则清除,并且将设备的preread_active_stripes计数器减1,表示该条带已经没有预读的需要了。
b) 将设备的激活条带数active_stripes减1。
c) 如果条带未处于STRIPE_EXPANDING状态,则将条带添加到inactive_list链表中。该链表中的条带可以被重新利用。
在守护进程中,使用这个函数来对失败的单条带读访问请求进行重试。
重试时,不再像chunk_aligned_read中使用用户bio的克隆进行提交,而是和make_request中的常规的bio一样,通过strip的方式来提交。如果获取strip失败或者调用add_stripe_bio发现和其他请求有重叠时,将该strip放入设备的retry_read_aligned指针中。
该函数是处理条带数据,代码写得很长(550行),好在逻辑相对清晰,主要是两部分功能:将条带已完成的读写处理结束;如果条带需要新发起读写请求,则向磁盘发出请求。
代码前一部分是做判断,判断是否有读写完成,是否有新的读写需要发出,这部分是在条带的自旋锁保护下进行的;代码后面一部分是具体的结束读写和发出读写请求。
如果有读写完成,前半部分会把完成的读写操作放在以return_bi为头的链表中;如果有读写需要提交,则设置条带的相应磁盘的缓存标志R5_Wantwrite,R5_Wantread。
在开始代码分析前,还需要说明的是:在stripe的缓存中toread,towrite中挂的是原始的bio,这些原始bio可能跨多个stripe。这些bio不会直接提交给磁盘,stripe提交的读写请求是另外自行构建的(条带缓存中的req域,在init_stripe 中,对req进行了初始化),仅仅是本stripe范围内的读写,前面说过,一个stripe上缓存大小只有一个页。
前面是大致的描述,下面看看具体的操作:
1. 获取条带的锁;清除条带的STRIPE_HANDLE,STRIPE_DELAYED标志。
2. 判断是否有读完成——针对条带中的每个disk,如果disk上toread不为空,并且已经UPTODATE,说明读已经完成:
a) 将toread链表取出另外保存,将条带的toread清空。
b) 如果缓存上R5_Overlap标志被设置,说明有其他的请求由于和条带的请求重叠,而只好睡眠等待,此时对其唤醒。
c) 将req的数据拷贝给toread中的bio。这段程序需要注意的是,由于toread中可能有多个bio,而且bio可能跨多个strip,只有在bio的所有物理段都完成读取后,这个bio才能算最后完成。
3. 遍历条带中所有的磁盘缓存,对toread,towrite,writen,fail(读错误,磁盘未工作,磁盘未同步等)的各种情况进行统计,后面会根据这些信息进行处理。
4. 如果条带fail的数目大于1,表示该条带上的读写请求将无法继续,此时要终止这些读写请求,等待这些读写请求的睡眠队列也被唤醒。对于写请求,还要设置bitmap为需要同步。如果条带的fail数目大于1,此条带上的同步也将取消。
5. 到这里,最多只可能有1个fail了,由于RAID5的属性,其上的数据读写操作总是可以继续的。
6. 这一步检查条带是否有写操作已经完成。写操作完成的标志是(有可能有多个数据盘还在写,但效验盘写成功,则认为可以返回已完成写操作的内容):
a) 有磁盘的written队列不为空,该队列是在上一次写操作中从towrite队列移到written队列中的(compute_parity5)。
b) 同时满足:效验盘数据工作正常(Insync)并且效验盘写已经结束(!Locked,Uptodate),或者条带中恰好是效验盘损坏了
对于满足写操作完成的磁盘,如果其上的bio已经完成全部物理段操作,则结束该bio的处理。这里还有一个处理,如果磁盘上的towrite队列为空,则调用bitmap_endwrite清除内存中位图(只是减写操作计数而已,具体的操作见bitmap相关的描述)。
7. 已完成的读写处理完之后,我们来看看是否有需要提交的读写请求。首先看读,在这些情况下需要向磁盘提交读请求:
a) 有用户提交的读请求
b) 用户写请求并未覆盖条带的整个页,也就是说要先读出,改写后再写回
c) 同步未完成,此时条带中并非所有的磁盘都处于Uptodate状态
d) Expanding进行中,后面专题讨论
如果条带满足上述条件之一,则条带需要继续处理,设置条带的HANDLE标志。
针对需要提交读请求的盘,如果磁盘工作正常,则设置磁盘的wantread标志位,设置Locked位。
有一种情况,就是如果有一个坏盘,且这个坏盘恰好需要读取,而且条带中其他磁盘的数据已经更新,此时就可以将这个坏盘上的数据计算出来。
8. 针对写操作继续判断是否需要提交读请求,前面仅考虑了非覆盖写的情况。对于写操作,需要重新计算效验和,效验和的计算有两种形式:rmw和rcw。Rmw是先将要写的数据盘数据D和效验盘数据P读出,根据新写入的数据D‘计算出新的效验数据P‘=P^D^D’。rcw是取出不需要写的数据盘的数据,加上要写入的数据盘数据,重新计算出效验和。
Rmw和rcw这两种情况都需要读出数据,Rmw需要读的是效验盘和写数据盘;rcw需要读的是不需要写的数据盘。rmw和rcw的选择,以需要的读操作为最少为原则。
选出需要读的磁盘后,和7一样,如果磁盘工作正常,则设置磁盘的wantread标志位,设置Locked位。和7稍有不同的是,如果此时条带还不在handle队列中,设置其DELAYED标志和HANDLE标志,继续等待。
而如果条带中有写,而且写操作要求的相关读操作此时已经完成的话,则此时可以计算出效验和,进行写操作的提交了,在compute_parity5中,将写目标磁盘和效验和磁盘的R5_LOCKED位都进行了设置。
9. 下面是对同步的[c1] ,如果磁阵完好,条带并未处于已同步态,此时其他磁盘的数据都已经读入了,将效验和计算后写效验盘;如果磁阵有故障盘,此时计算出故障盘的数据提交给故障盘,尝试恢复。
10. 如果条带发生一个读错误,则尝试修复这个读错误,具体的修复方法和RAID1是一样的,就是先写后读,要完成这个纠正需要对这个函数进行三次调用。第一次是发现读错误,在7中对其他好的磁盘提交读请求;第二次这些读已经完成,在7中计算出错误盘的数据,在10这里提交写请求;第三次是提交读请求。而这个读请求是在哪一步返回给用户的呢??,在7中调用compute_block时,在这个函数中会设置R5_UPTODATE标志,所以第三次调用时,会向用户返回读完成。
11. 这一步的expand处理我们后面再看。
12. 到这一步已经完成了是否有读写请求结束,以及是否有新的读写请求提交的判断。下面的工作就是根据这个判断结果进行实际的处理,将结束的读写请求提交给调用者;将新的读写请求提交给磁盘,前面说过,此时提交的并非用户的bio,而是条带中对应磁盘缓存的req。注意在提交过程中,条带的引用计数加1。