1、前言
RAID是IO路径中的关键模块,甚至在整个存储系统中,RAID是最为核心和复杂的模块。在Linux操作系统中,提供了一个开源的RAID,那就是MD RAID。该RAID可以实现RAID0、RAID1、RAID5和RAID6的功能,在产业界得到了广泛的应用。MD RAID对外采用块设备的形式导出,因此,通过块设备层的调度,BIO请求会被发送到RAID模块进行进一步的IO处理。在MD RAID一层,请求会被按照条带的方式进行切分,然后再按照RAID中磁盘的个数进行磁盘请求的映射。在请求的处理过程中,会存在条带小写问题,因此会引入“读-修改-写”的过程。在一个写请求发送往多个磁盘之前,校验信息(Parity)需要被计算处理,并且按照left-asymmetric或者right-asymmetric的数据布局方式将校验信息放置到多个磁盘中。
多年前,本人对MD-RAID进行了深入的代码分析,从中可以一窥RAID的实现机制。本代码分析基于Linux-2.6.18版本。
2、标记定义
MD-RAID中定义了很多标记,这些标记其实是一个Stripe在处理过程中所处的状态信息。具体的标记定义如下所示:
#define R5_UPTODATE 0 /* 缓存页中有当前数据 */ #define R5_LOCKED 1 /* IO请求已经被分发下去 */ #define R5_OVERWRITE 2 /* 整页写操作 */ /* and some that are internal to handle_stripe */ #define R5_Insync 3 /* rdev && rdev->in_sync at start */ #define R5_Wantread 4 /*调度一个读IO的过程 */ #define R5_Wantwrite 5 /*调度一个写IO的过程 */ #define R5_Syncio 6 /* this io need to be accounted as resync io */ #define R5_Overlap 7 /* There is a pending overlapping request on this block */
标记说明:
R5_UPTODATE和R5_LOCKED组合代表的操作状态
Empty:
!R5_UPTODATE,!R5_LOCKED:没有任何数据,也没有活动的请求
Want:
!R5_UPTODATE, R5_LOCKED:一个read请求已经被分发到相应的逻辑块
Dirty:
R5_UPTODATE, R5_LOCKED:缓存区中有新的数据,并且需要往磁盘上写
Clean:
R5_UPTODATE,!R5_LOCKED:缓存区中有数据,用户可以读取,和磁盘上的数据一致
写方法定义:
更新写:
#define RECONSTRUCT_WRITE 1
读-修改-写
#define READ_MODIFY_WRITE 2
条带状态定义:
#define STRIPE_ERROR 1 :条带出现错误 #define STRIPE_HANDLE 2 :条带正在处理 #define STRIPE_SYNCING 3 :条带数据正在同步之中 #define STRIPE_INSYNC 4 :条带已经同步 #define STRIPE_PREREAD_ACTIVE 5 :条带准备活动/有效 #define STRIPE_DELAYED 6 :条带删除
3、主要数据结构
3.1 Stripe_head数据结构
条带头部信息定义如下:
struct stripe_head { /* hash表的指针,连接所有stripe结构 */ struct stripe_head *hash_next, **hash_pprev; struct list_head lru; /* RAID5的私有配置信息 */ struct raid5_private_data *raid_conf; sector_t sector; /* sector of this row */ /* 校验盘的索引号 */ int pd_idx; /* 操作状态标记信息 */ unsigned long state; atomic_t count; /* nr of active thread/requests */ spinlock_t lock; /* 自旋锁 */ struct r5dev { struct bioreq; struct bio_vecvec; struct page *page; /* page缓存 */ /* toread: MD设备的读请求bio * towrite: MD设备的写请求bio,第一阶段写的bio指针 * written: MD设备的血请求bio,已经开始被调度写,第二阶段写的bio指针 */ struct bio*toread, *towrite, *written;/* three kinds of queue */ /* 这一页中的块 */ sector_t sector; unsigned long flags; /* 操作标记 */ } dev[1]; /* allocated with extra space depending of RAID geometry */ };
RAID5的写过程分成两个阶段:首先从stripe上读取数据,然后通过校验和计算之后,再往相应的stripe上写入数据。towrite这个bio对应于前一阶段的写,即从stripe上读取数据,written对应于后一阶段的写,即将计算完毕的数据写到相应的stripe(磁盘)上去。towrite和written结构体的交换发生在compute_parity()函数中,以便调度函数处理。
这个数据结构很重要,RAID5的操作采用条带化的方式,即每个磁盘上面分出一个chunk,多个磁盘的chunk组成一个stripe,在n个chunk中,有一个chunk用作校验盘。从RAID5开始,stripe的思想被应用到磁盘阵列中。
在stripe_head结构体中,定义了*toread,*towrite,*written三个bio结构体,用于数据读写缓存。将读写缓存分开简化了读写操作,
条带是RAID5操作的最基本单元,其采用hash表的方式将多个条带信息组织在一起。
3.2 Private_data数据结构
在MD驱动中,每类RAID都定义了一个private_data的数据结构体,在该结构体中包含了操作磁盘阵列的链表、属性参数等内容。RAID5的结构体定义如下:
struct raid5_private_data { /* 条带操作的hash链表 */ struct stripe_head **stripe_hashtbl; /* MD设备结构体指针 */ mddev_t *mddev; /* MD的扩展设备 */ struct disk_info *spare; /* chunk_size:RAID5设备的chunk长度,可能包括一个或者多个扇区, * chunk_size >> 9将返回每个chunk包含的扇区数(sectors per chunk)。RAID5 * 设备对磁盘的操作是以扇区为单位的,但是,单位条带的长度为chunk,因此, * 必须完成扇区和chunk之间的转换 */ /* algorithm:该域描述了RAID5所采用的校验分布算法,通过该算法RAID5可以 * 得到parity disk的分布。RAID5支持四种不同的校验分布算法,即: * 1、向左非对称算法 ALGORITHM_LEFT_ASYMMETRIC * 2、向右非对称算法 ALGORITHM_RIGHT_ASYMMETRIC * 3、向左对称算法 ALGORITHM_LEFT_SYMMETRIC * 4、向右对称算法 ALGORITHM_RIGHT_SYMMETRIC */ int chunk_size, level, algorithm; /* raid_disks:磁盘总数 * working_disks:工作盘的数量 * failed_disks:失效盘的数量 */ int raid_disks, working_disks, failed_disks; int max_nr_stripes; /*下面这些list是条带列表*/ struct list_head handle_list; /* 需要信息处理的条带列表 */ struct list_head delayed_list; /* 已经发送请求,延迟处理的条带列表 */ atomic_t preread_active_stripes; /* */ char cache_name[20]; kmem_cache_t *slab_cache; /* for allocating stripes */ /* * Free stripes pool */ atomic_t active_stripes; /* inactive的stripe列表,当stripe hash中没有需要的stripe时, * 可以在inactive_list中申请一个stripe。 */ struct list_head inactive_list; wait_queue_head_t wait_for_stripe; wait_queue_head_t wait_for_overlap; /* release of inactive stripes blocked, * waiting for 25% to be free */ int inactive_blocked; spinlock_t device_lock; struct disk_info disks[0]; };
4、主要函数说明
4.1 RAID5中主要函数总结
1、 error( ),该函数用来出错处理,出错处理的过程是设置标志位,然后守护进程调用recovery的时候检测到标志位,就对出错信息进行处理。
2、 raid5_computer_sector( ),compute_blocknr( )函数用来将上层的逻辑块映射成物理设备的逻辑块号,另外还用来计算逻辑块号对应的stripe索引以及校验盘的索引号,这个函数应该实现了parity-algorithm。
3、 handle_stripe( ),该函数是处理stripe中IO请求的关键函数
4、 make_request( ),进行设备的I/O操作
5、 sync_request( ),进行设备的数据同步操作
6、 raid5d( ),内核的RAID管理守护进程
7、 run( ),RAID运行函数,这个函数在personality中
从上面的总结可以看出,系统中最重要的函数是handle_stripe( )。与RAID磁盘阵列打交道的函数都要通过该函数。
4.2 raid5_compute_sector
该函数实现了RAID5条带(stripe)的管理,通过这个函数可以实现如下三个方面的功能:
1、 计算逻辑块号在实际物理设备中的块号
2、 设备块号在raid磁盘阵列中的索引号
3、 计算得到校验盘在磁盘中的索引号
对RAID5设备的块I/O请求最终映射到一个或多个成员磁盘上,这个映射是在函数raid5_compute_sector()中完成的,函数原型如下:
static unsigned long raid5_compute_sector(unsigned long r_sector, unsigned int raid_disks, unsigned int data_disks, unsigned int * dd_idx, unsigned int * pd_idx, raid5_conf_t *conf);
参数说明如下:
r_sector:请求I/O的逻辑扇区号
raid_disks:RAID5设备的磁盘总数
data_disks:数据磁盘数,data_disks=raid_disks-1
conf:RAID5设备的私有数据结构体
raid5_compute_sector()函数的目的是计算出响应该IO请求的数据磁盘索引和校验磁盘索引,以及在这些磁盘上的物理扇区索引。前两者的计算结果通过指针dd_idx和pd_idx返回,物理扇区索引值直接返回。
函数执行过程:
1、 根据逻辑扇区编号r_sector计算出扇区所在的chunk编号和在chunk内的扇区偏移
chunk_number = r_sector / sectors_per_chunk chunk_offset = r_sector % sectors_per_chunk
2、 如果不考虑校验数据的因素,编号为chunk_number的数据块所在的磁盘编号为:
dd_idx = chunk_number % data_disks
chunk在对应磁盘上的物理stripe号为:stripe = chunk_number / data_disks。由此,可以计算出逻辑扇区号在目标磁盘上的编号:
new_sector = (sector_t)stripe * sectors_per_chunk + chunk_offset
在数据操作的时候,可以直接往dd_idx盘的new_sector位置处写数据。
3、 以raid_disk为一组,则对应chunk在组中的编号为:stripe % raid_disks。
根据不同的算法得到不同的parity disk和 data disk的索引。
向左算法的parity disk的编号为:data_disks – stripe % raid_disks
向右算法的parity disk的编号为:stripe % raid_disks
4、 数据盘编号计算如下:
不对称算法:chunk_number % data_disks,如果该值大于/等于校验磁盘编号,则数据盘需要加1
对称算法:( 校验磁盘号 + 1 + chunk_number % data_disks ) % raid_disks
4.3 compute_block
函数原型:static void compute_block(struct stripe_head *sh, int dd_idx);
输入参数:
*sh:需要操作的条带
dd_idx:数据盘索引号,需要恢复数据的盘
无论是从故障盘上恢复数据,还是在同步过程中将故障盘的信息恢复到备用盘,都需要从磁盘条带上将故障信息恢复出来,这一过程可以在compute_block函数中完成。
其中sh指向要计算的条带单元所在的条带头结构体,dd_idx为故障磁盘的编号。需要注意的是,dd_idx所处的条带单元在条带头结构体中可能代表一个数据条带,或者一个校验条带。
计算方法采用异或算法,所有操作都是在成员磁盘缓冲头结构体(page)上进行。为保证计算正确,在调用前,已经将所有其它磁盘上条带单元的数据读取到对应这个磁盘的缓冲头结构体所关联的缓冲区中。在计算过程中,进行检查,如果缺少任何一个磁盘的数据,则打印内核错误消息。在计算完成后,修改故障磁盘对应缓冲头结构体的状态标志位,以表明其中的条带数据是有效的。
4.4 compute_parity
compute_parity()函数实现如下两个方面的功能:
1、 RAID5校验和的计算
2、 将towrite结构体挂接到written中,使得写操作进入第二个阶段
在RAID5程序中,完成数据校验有两种方法:
1、 read-modefy-write:读-修改-写
采用这种方法的基本运算过程如下:[(旧校验数据)XOR(旧数据)]XOR(新数据)=新校验值。
2、 reconstruct-write:更新写
采用这种方法的基本运算过程如下:(好的旧数据)XOR(新数据)=新的校验值
这两种方法实现的示意图如下:
(图中假设第2块数据需要更新)
RAID5校验算法的实现:
RAID5在写的时候有两种方法,一种是读-修改-写操作,另一种是更新写操作。
在读-修改-写操作过程中,需要将旧的校验值、旧的更新数据读出来,然后需要做两次XOR操作,其算法的基本过程如下:[(旧校验数据)XOR(旧数据)]XOR(新数据)=新校验值。
更新写过程需要将无须更新的旧数据读出来,做一次XOR操作(这里所说的一次XOR操作是指调用check_xor()这个宏一次),其算法的基本过程如下:(好的旧数据)XOR(新数据)=新的校验值。
4.5 handle_stripe
handle_stripe函数是RAID5驱动中最重要的函数。在RAID5驱动中有三个函数会调用handle_stripe(),如下图所示:
访问该函数的话可能返回的结果:
1、 如果有数据,那么返回读请求的数据
2、 如果数据被成功的写入磁盘,那么返回写请求
3、 调度读进程
4、 调度写进程
5、 返回奇偶校验的确认信息
handle_stripe函数实现了IO读写等功能,主要功能说明如下:
1、 当Page页(缓存)中存在数据的时候,可以将缓存中的数据拷贝到bio中,实现IO的正常读写,并且返回
2、 当RAID出现故障的时候,不需要进行任何操作了,但是需要将系列IO读写请求取消掉。
3、 实现IO写操作,RAID5的IO写操作比较复杂,其分为满块写和非满块写,当为非满块写的时候需要首先读取stripe中的数据,然后再计算校验和写入磁盘(置相应的标志位)。调度一个写过程。
4、 实现IO读操作,完成一个读过程的调度,其操作过程是置相应的标志位want_read。
5、 实现IO request的请求分发,构造bio,调用generic_make_request ( )函数。
下面对handle_stripe函数中实现的功能单元进行分析。
1、IO读完成操作
当handle_stripe()调用generic_make_request()函数向底层驱动程序分发bio请求以后,接下来的读数据操作就由底层的驱动和硬件设备完成。当驱动程序完成读操作之后回调读完成函数:raid5_end_read_request()。在这个回调函数中,清除IO读标记R5_LOCKED,并且有可能置读数据有效标记R5_UPTODATE(如果此次操作没有错误的话)。关键代码如下:
set_bit(R5_UPTODATE, &sh->dev[i].flags); clear_bit(R5_LOCKED, &sh->dev[i].flags);
并且设定list挂接标记STRIPE_HANDLE,代码如下:
set_bit(STRIPE_HANDLE, &sh->state);
最后调用list分配函数release_stripe()。
在release_stripe()函数中,将sh挂接到handle_list中,并且唤醒守护线程Raid5d()。通过Raid5d()函数调用handle_stripe()。
此时,程序满足条件 toread R5_UPTODATE,因此,进入IO读完成代码区,该代码的实现比较简单,通过copy_data()函数调用将page缓存中的数据拷贝到bio中,并且返回给上层。
具体的操作过程可以描述如下:
2、故障发生时清除IO请求
RAID5只能处理最多坏一个盘的情况,如果坏盘数目大于1的话,那么IO请求等操作将变得没有任何意义。
在handle_stripe()中,会统计所有的标记位数量和坏盘的数目,但出现failed > 1的情况时,停止一切的IO读写操作,调用结束函数直接结束IO读写操作。
当驱动程序正在进行syncing的时候,如果发现坏盘数据大于1,那么立即停止sync操作,调用md_done_sync(conf->mddev, STRIPE_SECTORS,0)函数来停止同步操作。
3、写操作完成功能
写操作是一个比较复杂的过程,守护进程raid5d()第二次调用handle_stripe()函数,即第三次进入handle_stripe()函数时(第一次由make_request()调用,第二次由raid5d()调用),会执行写操作完成功能。
写操作结束时的条件如下:
1、 written有效
2、 RAID盘阵同步,R5_Insync
3、 没有IO请求操作,!R5_LOCKED
4、 数据是有效的,R5_UPTODATE
在(1)满足的条件下,或者满足如下两个条件:
存在一个故障盘,failed == 1
故障盘的索引号正确,failed_num == sh->pd_idx
4、读调度功能
在以下四种情况下,内核需要调度读操作:
u 正常数据读。标记的状态为:!R5_LOCKED !R5_UPTODATE toread
u 非整页写的时候,需要读取数据,标记状态为:towrite !R5_OVERWRITE
u 同步的时候需要读取数据,标记的状态为:syncing
u 出错的时候需要读取数据,标记的状态为:failed toread || ( towrite && !R5_OVERWRITE )
如果在降级模式下,即存在一块坏盘的情况下,可以通过compute_block()来计算得到所需要的数据。如果在同步的情况下,首先设定相应的标记,来调度一个读数据过程,关键代码如下:
set_bit(R5_LOCKED, &dev->flags); set_bit(R5_Wantread, &dev->flags);
在handle_stripe()函数的最后,通过R5_Wantread标记构造一个读bio,并且调用generic_make_request()函数将这个读bio请求分发到底层驱动程序,那么具体的读操作就由低层驱动程序和硬件来实现了。实现完毕之后再调用回调函数r5_end_read_request()。接下来的过程就回到IO读完成操作这个地方了。
5、写调度功能
RAID5写操作是驱动程序中比较复杂的一块。他通过多次调用handle_stripe()函数来实现一个完整的写过程。
对于RAID5的写操作有多种方式,包括满块写和非满块写,又有read_modefy-write和reconstruct_write两种写方法,这两种写方法和盘阵的具体数据校验方法相关。
整个RAID5的写操作可以分为两个阶段,第一个阶段实际上是数据读阶段,标记为towrite,等到将磁盘里的旧数据读上来之后,调用compute_parity()函数进行相应的数据校验,得到新的校验值。然后进入第二阶段,将towrite挂接到written上,开始将实际缓存中有效的数据写到磁盘上去,完成整个IO写过程。
这两个IO写过程可以用图表示如下:
写过程的具体描述:
上层发起IO请求,通过make_request_fn()->make_request()来调用handle_stripe()函数,这是写过程第一次进入这个函数。由于没有此时的状态处于Empty(!R5_LOCKED,!R5_UPTODATE),因此,程序需要判断其需要进行的读写方法,即读-修改-写还是更新写,用两个变量来表示,即rmw和rcw。
选择好读写方法之后,程序会调度一个写进程,因为他需要读取磁盘上的旧数据,关键代码为:
set_bit(R5_LOCKED, &dev->flags); set_bit(R5_Wantread, &dev->flags);
在handle_stripe()函数的最后会根据这些设定的标志来构造一个读bio,最后会将这个读请求bio通过gengeric_make_request()函数发送到底层驱动程序。
接下来的事情由低层驱动程序和硬件完成…
读操作完成之后,底层驱动程序回调raid5_end_read_request()函数,该函数是读请求结束处理函数。在该函数中将R5_LOCKED置位无效,并且如果没有读错误的情况下,设定R5_UPTODATE标记有效(实际上,该标记有没有效,并不是说还要去将page缓存中的数据读到bio中,后面所涉及到的数据校验操作都是在page缓存中完成的)。另外,需要设置STRIPE_HANDLE标记,如下:
set_bit(STRIPE_HANDLE, &sh->state);
该标记的作用是将sh挂接到handle_list中,然后由raid5d()守护进程对sh进行操作。
因此接下来的操作为:
raid5_end_read_request()-> release_stripe()-> 唤醒 raid5d()
raid5d()守护进程调用handle_stripe(),这是第二次进入handle_stripe()。
此次进入handle_stripe()函数,满足如下条件:
towirte rmw == 0 rcw == 0
此时可以开始一个正常的写过程调度…
首先需要进行缓存区的数据校验,调用compute_parity(),通过该函数一方面得到了正确的校验数据,另一方面使得写过程进入第二个阶段,towrite已近挂接到written。
标记R5_Wantwrite调度一个写过程,即:
set_bit(R5_Wantwrite, &sh->dev[i].flags);
在handle_stripe()函数的最后通过上面的标记构造一个写IO的bio数据块,并且调用generic_make_request()函数将该请求发送到底层驱动程序。
接下来的工作就让底层驱动程序和硬件来完成吧…
请求完成之后,底层驱动程序会调用raid5_end_write_request()回调函数,该回调函数清除请求标记R5_LOCKED,设置STRIPE_HANDLE标记,然后调用release_stripe()函数,该函数又将sh挂接到handle_list上,然后唤醒守护进程raid5d(),raid5d()会第二次调用handle_stripe()。
第三次进入handle_stripe()。
此次进入handle_stripe()完成写操作结束处理。
写过程的详细描述见下图:
6、IO请求分发功能
在handle_stripe()函数中实现IO请求分发功能。构造读写IO的bio。
调用generic_make_request()函数将请求分发给底层驱动程序。
7、同步操作的支持
Raid5d() -> md_check_recovery() -> md_do_sync() -> sync_request() -> handle_stripe()
上述过程是一个同步操作过程的函数调用关系链。从这个关系链中可以知道,同步操作最后是由handle_stripe()函数来实现的。
程序会判断如下三个标记来决定是否执行同步数据处理程序段:
syncing:该标记说明驱动程序此刻需要完成同步的操作
STRIPE_INSYNC:该标记说明磁盘阵列是否处于同步的状态,如果!(test_bit(STRIPE_INSYNC)),那么说明磁盘处于非同步状态。
failed:标记了故障磁盘实效的数目
满足数据同步的条件之后,驱动进入数据同步的代码段。
该代码段有三种功能:
1、 校验和的确认,if(failed == 0)。当md_check_recovery()程序结束同步操作的时候,会仍然置RECOVERY_NEEDED标记,这看起来很奇怪,实际上做一次校验确认罢了。这时候,需要用到if(failed == 0){}下面的代码。
在这段代码中,调用compute_parity(sh, CHECK_PARITY)来计算校验和,然后通过memcmp()函数进行比较确认。
2、 调用compute_block()函数计算恢复数据,然后设置R5_LOCKED和R5_Wantwrite标记来调度一个写过程。
3、 结束stripe同步操作。当处于同步操作时,数据已经处于同步状态后调用md_done_sync()函数结束整个同步过程,并且清除STRIPE_SYNCING标记。
<待续>