现代文件系统(例如Ext4和XFS等)具有多种附加特性,不仅扩展了文件系统的应用场景,而且使得文件系统的容错性(例如日志特性)和性能得到很大的提高。而下一代文件系统(例如Btrfs和ZFS)则提供了更加高级的功能特性,比如存储池、RAID支持和快照等特性,使得文件系统超出了严格文件系统的界限,甚至具备的卷管理的能力。
文件系统已经发展的如此完善,我们是否有必要在去了解那些老古董。本号以为是有这个比较要的。一方面是通过这个我们可以了解文件系统最初的样子,理解文件系统的理念;另外一方面是现代的问题系统都比较庞大(最少的也几万行代码),不容易理解,而老的文件系统才几千行代码,理解起来比较容易。
废话说了半天,还没进入正题。首先我们可能想知道Ext4的老祖宗到底是谁,我们知道Linux操作系统是参考MINIX操作系统写的。而Linux操作系统的第一代文件系统Ext也正式参考该系统的实现。目前在最新的内核代码树仍然保留这Minix文件系统。该文件系统功能特性非常简单,能力有限,但麻雀虽小五脏俱全,代码总量才2千行左右。
图1 Minix文件系统代码统计
Linux系统中文件系统的基本原理是比较简单的,简单的理解就是2次映射的过程。一次是根据文件名找到inode节点,第二次是根据inode节点找到文件存储数据的位置。
在学习任何一个本地文件系统之前,我们最好先对其数据的布局有一个整体的认识。这样,我们在理解数据读写逻辑和元数据等代码时才能更容易一些。这个是符合人类认识事物的规律的,因为人类认识事物总是从具体到抽象,从简单到复杂的。对于文件系统磁盘布局是比较具体的内容,代码实现则是比较抽象的内容,因此从磁盘的数据布局开始比较容易一些。
minix文件系统的布局比较简单,如图2是该文件系统的磁盘布局。包含的主要内容为:启动块、超级块、inode位图、zone位图、inode列表和存储数据的zone。需要注意的是minix存储的数据是以zone为单位的(默认为1KB),而不是以磁盘的扇区。
我们在Linux系统下进行格式化的时候,可以看到输出该文件系统的基本信息,其内容如下:
# mkfs.minix /dev/loop0 160 inodes 400 blocks Firstdatazone=9 (9) Zonesize=1024 Maxsize=268966912
超级块
超级块是整个文件系统的入口,里面包含inode数量、数据块数量、zone大小和第一个zone的位置等。如下是代码中对超级块的定义(本图是V1版本,minix有多个版本,本文以V1版本为例):
inode位图和zone位图
这两个位图分别占用一个块的数据,通过其中的一个位(bit)来表示对应的inode或者zone是否已经被使用了。如果已经被使用了则为1,否则为0。如图为位图在磁盘上的数据示例。
inode列表
在文件系统中通过inode记录文件的元数据(管理数据)信息。在minix文件系统中inode记录着文件的模式信息、时间信息和文件数据位置信息等内容。如图是minix V1版本inode的结构,该结构占用32字节。inode表可以理解为一个inode的数组,inode在表中依次排列,通过偏移就可以找到指定的inode信息。
在V2版本中对inode进行了调整,inode具备3级间接块和更加丰富的时间信息。如图是minix V2版本的inode节点结构。
在Linux操作系统中,任何文件系统都有一个根目录,minix也不例外。在Linux中,目录是一种特殊的文件,其中的数据存储的是一个名为目录项的数据。根据目录项,我们可以实现从文件名到文件元数据的查找。在minix中目录项的结构体如下所示。
struct minix_dir_entry { __u16 inode; char name[]; };
目录项的内容很简单,其中两个域分别是inode的id和文件名。因此,在目录中检索是,通过文件名可以找到inode的id,而该id其实就是inode表中inode的索引。
理解了上面关于磁盘布局和目录内容的相关内容,对后续的内容也就容易理解了。从上面的介绍,我们可以看出文件的创建主要涉及如下几个方面:
上面是从用户角度来说的,其实对于第3步从内核角度应该属于写数据的流程了,而非创建文件的流程。
在代码层面,创建文件的函数调用流程为minix_create->minix_mknod->minix_new_inode,这个流程是从inode表中分配inode的核心流程。另外一个流程是minix_create->minix_mknod->add_nondir,这个流程是在父目录中创建目录项的流程。具体代码实现都比较简单,我们这里就不贴代码了。
minix文件系统并没有定义自己的数据读写接口,如图所示,这些接口都是VFS框架提供的通用读写接口。调用这里面的写接口,数据将被写入页缓存中,而后续在调用底层的接口实现向磁盘写数据的流程。
图3 文件访问接口
最终调用底层的函数集合来完成实际的数据读写。具体的流程可以参考本号之前关于Ext2和Ext4等文件系统中关于读写的介绍,本文不再赘述。
对于写数据来说,核心的内容就是分配磁盘块,分配磁盘块其实就是上文中所说的第二个映射关系。也就是,文件的inode与文件数据之间的映射关系。
我们先了解一下minix文件系统是如何组织文件数据的。前文我们看到了在inode里面有一个i_zone的数组,其大小为9。这个数组就是用来存储文件数据的位置信息的。在这个数组中,其前7个元素直接存储文件数据的位置信息。而第8个元素存储的不是文件的数据位置信息,但也是一个磁盘块,而在该磁盘块中存储的文件的位置信息。以此类推,第9个元素通过2级块来记录文件的买手机游戏账号地图数据位置信息。这种中间有1级或者2级磁盘块存储文件位置信息的方式称为间接块的方式。如下是minix文件系统文件存储数据的示意图。
为了更加容易理解,我们举几个例子(minix文件系统的块大小为1KB)。假设文件比较小,只有几十个字节,此时通过一个块就可以存储这些数据,因此通过i_zone中的第一个元素就可以表示该文件的数据。如上图中,第一个元素为50,表示数据存储在磁盘偏移为50个块的位置。
如果文件的数据大于7KB,比如最简单的位于8KB的位置。此时前7个元素只能存储7KB的数据,因此需要用到第8个元素。而第8个元素存储的是一个间接块的位置信息,比如上图中52是间接块的位置。而在该间接块中会依次存储文件逻辑偏移对应的数据的位置。比如该间接块中第一个数据为57,表示文件8KB偏移的数据存储在磁盘57KB的位置。
由于minix文件系统是通过间接块的方式存储文件中的数据的。因此这里核心的逻辑是根据用户写数据的位置和大小计算出来需要的磁盘块的数量,同时界定存储的位置信息应该在inode中i_zone数组的什么位置。这个逻辑是通过get_block函数实现的,大家可以自行看一下,本文不再赘述。
结合我们前面介绍的超级块、磁盘布局和文件数据组织的内容。我们可以实际分析一下磁盘上的数据。比如我们以前文格式化的磁盘为例,可以通过dd工具将数据导出到一个文件中。并通过vim工具以二进制的方式打开。
超级块
前文说了,超级块是入口,我们先看一下超级块。在格式化完成后,工具给出了如下信息:
160 inodes 400 blocks Firstdatazone=9 (9) Zonesize=1024 Maxsize=268966912
这些信息其实就是超级块的主要信息。根据磁盘的布局,我们知道超级块位于第2个块(块大小为1KB)位置,由于磁盘以0位开始,因此其起始位置为1KB,终止位置为2KB(0x400正是这个位置)。
结合前面超级块的定义,s_ninodes表示的是inode的数量,16位小端对齐。因此下图中的a000表示的是该字段的内容,转换为16进制为0x00a0,也就是10进制的160,这个数据正好是上面格式化的输出信息中的inode数量信息。其它数据也可以对应,请自行分析。
inode位图
接下来是inode位图,这部分数据从第3个块开始,也就是2KB的位置(0x800)。下图的数据是位图的数据,可以看出标1的为已经使用的inode,0位没有使用的inode。
数据位图
以此类推,数据块位图位于第4个块的位置,不过多解释了。
inode表
inode表位于第5个块的位置,每个inode占用32字节。其中第15个字节为i_zones的起始位置,可以对照上文结构体看一下。本文创建了3个非常小的文件,名称分别是a、b和c。对比如下导出的数据,其中0x1000-0x101F的数据是跟目录的inode。其中0x09表示其数据的位置在磁盘第9个块(9KB)的位置。
目录数据内容
我们导出这里的数据(0x2400),在结合目录项的定义我们可以对其中的内容进行分析。
struct minix_dir_entry { __u16 inode; char name[]; };
可以看出每一项占用32字节,其中前2项分别是当前目录和上一级目录(也就是. 和 ..),后面是具体文件的信息。可以看出后面3项分别是0x0002,61(字符a)、0x0003,62(字符b)和0x0004,63(字符c)。
文件数据内容
以文件a为例,根据前面的信息可以知道其inode id为2,因此在从inode表中找到其i_zones的内容为0x0a,也就是数据位于10KB的位置(0x2800)。如图所示,可以看出这里的内容为616161...,也就是我们写入的aaa...字符。
到这里我们对Ext4文件系统的老祖宗minix文件系统有了一个基本的了解,也对如何分析文件系统的数据有了概要的认识。今天先到这,后续我们再对文件系统相关的内容进行更加深入的介绍。