Linux Kernel 中软件 RAID 技术的实现
计算机科学家 David Wheeler 有一句名言:"计算机科学中的任何问题,都可以通过加上一层逻辑层来解决。"这个原则在 Linux Kernel 中的许多场合被广泛的应用。其中最为大家所熟知的应用,恐怕要算是在文件系统的层次上。Linux Kernel 为了能支持多种不同的文件系统,采取的解决办法,就是在 Kernel 和文件系统之间添加一个逻辑层,也即所谓的 VFS(Virtual Filesystem Switch)。VFS 把来自 Kernel 的请求转发给各个不同的文件系统驱动进行处理。这些文件系统驱动,有的实现的是传统的硬盘分区上的文件系统,有的实现的是基于网络的分布式文件系统,甚至还有一些更加富有想象力和实验性质的文件系统驱动。Linux 上丰富多彩的文件系统的实现都得益于 VFS 这一逻辑层的加入。
Linux Kernel 中软件 RAID 技术的实现,同样体现出了计算机科学家 David Wheeler 的这一原则。不过,我们首先要看到的是这一原则在 Linux Kernel 中的 Block 设备的实现上的应用。
Linux Kernel 支持多种 Block 设备,比如基于 IDE 总线或者是 SCSI 总线的硬盘、光盘驱动器和磁带机,又比如在一个普通文件上实现的 Loopback Block 设备,或者是基于 TCP 的网络 Block 设备,甚至是基于内存的 RAM-DISK。这个对多种 Block 设备的支持,其实就是 Block 设备层这一逻辑层在发挥作用。每一个具体的 Block 设备的驱动,都要向 Kernel 中的 Block 设备层注册,并提供一组入口。这以后 Kernel 就可以通过这一组入口,访问这个 Block 设备。Block 设备驱动的这一组入口的确切定义如下。
struct block_device_operations {
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);
int (*check_media_change) (kdev_t);
int (*revalidate) (kdev_t);
struct module *owner;
};
一个 Block 设备驱动向负责的逻辑层注册了这一组入口之后,系统用一个号码,也即所谓的 Major Number,标识这一 Block 设备。用户空间的程序通过访问文件系统上的 /dev 目录获知某一 Block 设备和 Major Number 之间的对应关系。Kernel 则通过这一 Major Number 区分不同的设备,把来自用户空间的请求转发给相应的设备驱动。在这里,Major Number 实际上担任了 Block 设备层这一逻辑层的转换开关这一角色。相应的,我们知道,在文件系统的 VFS 逻辑层上担任转换开关角色的则是文件系统的 Mount 路径。
前面简单地讲述了 Linux Kernel 中的 Block 设备。而 RAID 技术,我们知道,是利用多个 Block 设备来模拟一个单个的 Block 设备,并且,在这个模拟的过程中,提供一定程度的数据冗余,以保护用户数据不会因为某个单一的 Block 设备的故障而完全损毁。根据技术规范的细节的不同,RAID 又被分为几个级别。RAID 0 并不提供任何程度的数据冗余,而只是把单个硬盘 Block 设备的负载平均分担到参加 RAID 0 的所有硬盘 Block 设备上,这在一定程度上增加了对硬盘上的用户数据进行访问的速度。而 RAID 4,5 都提供了相当程度的数据冗余,可以保护用户数据不会因为某个单个硬盘 Block 设备的故障而损毁。关于 Linux Kernel 中软件 RAID 技术的原理的介绍,读者可以参考 IBM developerWorks 上面的 Daniel Robbins 的相关文章,以及 Linux Documentation Project 上面的 The Software-RAID HOWTO。相关链接,请参见文后所列参考文献。
本文所关心的,是这些不同级别的软件 RAID 技术在 Linux Kernel 中的具体实现办法。在这里,我们看到 Linux Kernel 向上面说到的 Block 设备层这一逻辑层,注册了一个特殊的 Block 设备,称之为 Multi-Disk Block Device(md)。可以说,这个 Block 设备形成了又一个逻辑层,来支持不同级别的 RAID 技术,甚至还可以支持其它的多硬盘 Block 设备,比如 LVM,逻辑卷控制等。我们看到,为了支持不同级别的 RAID 技术而引进的这又一个逻辑层,并没有妨碍其它 Block 设备。其它的那些不牵扯到多硬盘技术的 Block 设备,并不需要通过 md 这一个逻辑层,这保证了这些设备的效率不会受到影响。
自然而然,我们就要问道,在 md 这一逻辑层上担任转换开关角色的是谁呢?并且,我们还要问,各种级别的软件 RAID,是如何向这一逻辑层进行注册的呢?
首先回答第一个问题,在 md 这一逻辑层上担任转换开关角色的是 Block 设备的 Minor Number。用户程序从文件系统上的 /dev 目录下找到一个设备名,随之就找到了一个 Major Number 以及一个 Minor Number。而 Linux Kernel 就根据 Major Number 找到 Block 设备的驱动,在我们这里,就是 md 多硬盘设备。然后 md 逻辑层根据 Minor Number 再往下找。找什么呢?找不同的 Personality,这是 Linux Kernel 中的 md 逻辑层所使用的术语,它的具体定义如下。
struct mdk_personality_s {
char *name;
int (*make_request) (mddev_t *mddev, int rw, struct buffer_head * bh);
int (*run) (mddev_t *mddev);
int (*stop) (mddev_t *mddev);
int (*status) (char *page, mddev_t *mddev);
int (*error_handler) (mddev_t *mddev, kdev_t dev);
int (*diskop) (mddev_t *mddev, mdp_disk_t **descriptor, int state);
int (*stop_resync) (mddev_t *mddev);
int (*restart_resync) (mddev_t *mddev);
int (*sync_request) (mddev_t *mddev, unsigned long block_nr);
};
从上面的简略介绍,我们看到,Linux Kernel 引入 Block 设备层这一逻辑层,来支持多种不同的 Block 设备驱动的实现。然后,在 Block 设备层下面又引入 md 设备层这一较小的逻辑层,来实现对多个级别的 RAID 技术的支持。在这两个层次中担任转换开关角色的,分别是 Block 设备的 Major Number 和 Minor Number。
用户数据的走向
接下来,我们再从另一个角度,来看一看 Linux Kernel 中的软件 RAID 技术的实现。在这里,我们要关注的是在 Linux Kernel 中,用户数据的移动路径。
比如说要读一个用户文件。首先,用户程序传递给 Kernel 一个路径名,比如 /etc/passwd。然后,Kernel 中的 VFS 逻辑层根据当时它记录下来的文件系统的 Mount 的情况,找到相应的文件系统驱动,就指挥它去找用户程序所请求的文件。
对于我们这里所要关心的基于硬盘的传统的文件系统来说,硬盘呈现在它面前的样子就是连续的一长串的 Blocks。所以,对于硬盘文件系统来说,它所要做的,就是要根据用户程序所请求的文件的路径名,找到相对应的若干个 Blocks,这样以后,它的任务就算完成了,接下来的事情,就是属于硬盘 Block 设备驱动的了。对于传统的硬盘文件系统来说,它们通过在硬盘分区的开头的固定位置上,维持一些索引信息,也即所谓的 Meta Information,来完成这一任务。对于 Linux 上比较流行的 Ext2/3 文件系统的硬盘布局的详细介绍,请参见作者在 IBM developerWorks 上的另一篇文章。相关链接,请参见文后的参考文献。
在文件系统之后,接下来登场的就是硬盘 Block 设备的驱动了。它所要完成的任务,就是在文件系统请求某一个 Block 中的数据的时候,把这个数据从硬盘上读出来。我们看到,文件系统根据 VFS 中记录的 Mount 信息,找到相关的设备,比如 /dev/hda1,或者是某一个硬盘 RAID 设备,告诉它,我要读你上面的第几个 Block 的数据。对于一个普通的硬盘 Block 设备驱动来说,它把这个 Block 号数翻译成硬盘上的硬件地址,比如 IDE 设备的 C/H/S 地址或者是 LBA 地址,然后,把来自文件系统的请求转发给 IDE 驱动就行了。
而对于我们所关心的 RAID 技术来说,它要在把来自文件系统的请求转发给底层的驱动(比如 IDE 驱动)之前,多做一些翻译工作。这个翻译工作,把呈现在文件系统面前的一长串的 Blocks 这一形象,翻译到多个硬盘 Block 设备上面。这个翻译工作,定义了 Linux Kernel 中的软件 RAID 技术的实现。对于各个不同级别的 RAID 来说,它们各个采用了不同的翻译技术。简单的 RAID 0,就是把这一长串的 Blocks,均匀的分配到各个硬盘 Block 设备上面。而 RAID 4 和 5 采用的翻译手段,就更为复杂一点。
从上面的分析,我们看到,在整个过程中只有少量的索引数据在各层驱动之间传递,只是到了最后,才出现用户数据的转移,这对整个系统的性能是非常有利的。
给硬盘分区加上一层写保护膜
这层保护膜,其实就是一个清空了的硬盘分区。每一个受到保护的硬盘分区,对应有一个保护膜。在每次系统重新启动之后,这层保护膜都会被清空。另外,在内存中,每个保护膜都维持了一个关于被保护的硬盘分区的 Blocks 的 Bitmap,每一个 Block 对应于 Bitmap 中的一个 Bit。每次启动以后,Bitmap 都被全部置为 0。
保护膜的驱动是在 RAID 0 的基础上稍加改动得来的,我们称之为 RAID 0.0999。它向 Linux Kernel 中的 md 逻辑层注册了自己的 Personality,并负责维护自己的一份 Bitmap。当保护膜的驱动从文件系统接收到一个关于某个 Block 的读请求的时候,它查阅自己维护的 Bitmap。如果相应的位为 0,它就从被保护的硬盘分区上读取该 Block。如果相应的位为 1,它就从保护膜上读取相应的 Block。当保护膜的驱动从文件系统接收到一个关于某个 Block 的写请求的时候,它把自己维护的 Bitmap 的相应位置位为 1,并把数据写入保护膜上相应的 Block 中。这样就有效的实现了对相应的硬盘分区的写保护。
RAID 0.0999 有些什么用处呢?一个用处是对于在光盘上"实地"运行的 Linux 操作系统,由于光盘不可避免的是只读的,那么 RAID 0.0999 加上基于内存的 Block 设备 RAM-DISK 的支持,就可以提供一个可读可写的文件系统,使得在光盘上"实地"运行 Linux 成为一件简单的事情。另外一个用处是对于操作系统的安全方面,由于保证了一份完整的用户数据是只读的,而每次系统重新启动之后,保护膜又都被擦除干净,受系统崩溃和黑客入侵的影响,在一定程度上就被减小了。当然,RAID 0.0999 最主要的用处是作为本文作者的一个小玩具而已。:-)