前文提到,RAID0并没有在大卫·帕特森论文中提及。但是RAID0其实也是一个比较重要的RAID等级,因为RAID0实现了条带化技术,实现了通过多个物理硬盘提供更大容量,更高性能的方式。
在介绍具体代码之前我们先解释条带化相关的几个概念,这将对我们理解代码非常有帮助。这几个概念分别是扇区(sector)、实现条带(stripe)和块(chunk)。可以结合下图理解上述概念。

上图是基于3块硬盘构建的一个RAID0,上面横向的为虚拟盘RAID0的逻辑空间,下面纵向的为3块物理盘的物理空间。逻辑空间与物理空间的对应关系如上图中数字所示,可以看出,在虚拟硬盘中连续的3个逻辑块被分配映射到3个物理硬盘上。基于上述设计,保证在连续IO的情况下,可以由3个物理硬盘分别承担负载,从而提升读写性能。
接下来我们看看涉及的几个基本概念,包括扇区、块、条带、条带宽度和条带深度等概念。
扇区(sector):在块子系统中处理IO的基本单元,机械硬盘的扇区大小为512字节。扇区的大小通常与物理设备有关。
条带(stripe):在介绍条带之前首先介绍一下条带化(striping)的概念。条带化是一种将逻辑空间划分为等大小的块,并将这些等大小的块分散在不同物理硬盘上的技术。上述等大小的块称为条带,上图中条带的大小是6KB。
块(chunk):一个条带(可以称为逻辑条带)在一个具体数据盘(成员盘)上的大小。通常这个大小是可以配置的,上图中块的大小是2KB。
在介绍内核实现之前我们再介绍一个非常有用的工具,可以借助这个工具查看内核函数的调用关系,这个工具就是ftrace。如果想使用ftrace,需要通过cd目录切换到一个特定的目录,具体命令如下所示。
cd sys/kernel/debug/tracing
如果没有这个目录,应该是Linux内核不支持,可以重新编译安装内核。在编译内核的时候需要修改配置文件,增加对ftrace的支持。可以通过如下命令实现对md设备相关API的跟踪。
echo nop > current_tracer
echo function_graph > current_tracer
echo “md*” > set_graph_function
echo 1 > tracing_on
如果想跟踪其他函数,可以继续往set_graph_function中追加,比如想跟踪submit_bio,可以通过执行如下命令。需要注意的是上面命令是一个大于号,下面是两个大于号。
echo “submit_bio” >> set_graph_function

创建RAID0设备
有了上面的准备知识,我们就可以继续Linux RAID0的学习了。首先我们看看怎么创建一个RAID0,方法很简单,可以通过下面命令创建一个RAID0。下面命令创建一个包含两个物理硬盘的RAID0,块大小是4KB,RAID0的名称是md0。
mdadm --create -c 4K /dev/md0 -l 0 --raid-devices=2 /dev/sdb /dev/sdc
这里需要注意的是RAID的名称必须是以md作为前缀的,在代码实现中有所限制。这里的md其实就是Linux内核的一种特殊的块设备,称为Multi-Disk,简称MD。如果我们看一下内核的源代码文件,可以看到与MD相关的源代码文件如下图所示。
在下图中的代码文件中,md.c是框架性的内容,而raid0.c则是RAID0的具体实现,raid1.c是RAID1的具体实现,以此类推。可以看出Linux内核的代码结构还是比较清晰的。

如果我们用ftrace跟踪就会发现,创建RAID0时会调用md_alloc函数,这个函数就是用来创建MD设备的,也就是创建虚拟块设备的。该函数的流程也比较简单,主要是创建一个通用块层的块设备,也就是前文中的md0。

上图中比较重要的时处理函数的初始化,其中md_fops是MD框架的函数集合,具体如下图所示。对于MD设备的操作由这些函数完成,比如打开MD设备会调用到md_open,读写数据会调用到md_submit_bio等。

经过上述步骤后,RAID0并没有完全创建好,只是创建了一个空虚拟设备。后面mdadm还会触发到内核的md_add_new_disk函数用于添加成员盘。至此才完成了虚拟块设备到物理设备的映射。

RAID的读写流程完成映射后,我们就可以对RAID设备进行读写了,最简单的方式就是通过dd命令。比如下面我们通过dd命令将data.log文件写入前文创建的RAID0中。这里data.log中的内容有一定的规律,其中第一个4KB的内容全为1,第二个4KB的内容全为2,第三个4KB的内容全为4,以此类推。
dd if=./data.log of=/dev/md0
我们可以通过查看sdb和sdc上了解一下RAID0的数据布局情况,如下是sdb的部分数据,可以看到数据是从2MB偏移的地方开始的。

我们继续分析一下Linux内核的IO处理流程。当调用MD框架的md_submit_bio时,调用流程如下图所示。最后会调用到一个名为make_request的函数指针。对于RAID0来说,这个函数指针raid0_make_request。这些信息也是在创建RAID的时候被初始化到MD的数据结构中的。

我们继续分析一下raid0_make_request函数的实现,如下图是该函数的前半部分的代码。这部分代码的关键是判断IO请求(bio)是否存在跨块(chunk)的情况,换而言之是否存在跨物理硬盘的情况。如果存在上述情况,则对IO进行切割。

在上图的逻辑中,如果存在跨物理硬盘的场景,那么IO会被一切为二,前半部分肯是在某一个物理硬盘上,而后半部分仍然可能存在跨物理硬盘的情况,因此会被重新提交到队列中处理。
该函数后半部分的逻辑如下所示,这部分主要用来处理切割的前一部分的IO,或者不需要切割的IO。在具体的处理逻辑中,首先要查找具体的zone。根据zone中包含的信息可以通过map_sector得到具体物理硬盘。

根据上述找到的信息,在第3步将更新bio的设备信息和偏移信息。这一步相当于将请求的逻辑偏移转换成了物理设备的偏移地址。最后第4步调用submit_bio_noacct将请求提交到具体的设备。
至此,我们完成了Linux RAID0相关概念、操作和关键实现流程的介绍。后续我们将继续介绍RAID1、RAID5和RAID6等不同的RAID等级。
由于公众号文章不能自由编辑,一些历史文章难免有些错误。本号整理了一些系统的文档,有兴趣的同学可以下载阅读。





