首发于术道经纬
文件系统的原理

文件系统的原理

持久性的数据是存储在外部磁盘上的【注1】,如果没有文件系统,访问这些数据就需要直接读写磁盘的sector,实在太不方便了。而文件系统存在的意义,就是能更有效的组织、管理和使用磁盘上的这些raw data。

文件系统的组成

要管理,就得先划分,那按照什么粒度划分呢?因为磁盘上的数据要和内存交互,而内存通常是以4KB为单位的,所以从逻辑上,把磁盘按照4KB划分比较方便(称为一个block)。现在假设由一个文件系统管理64个blocks的一个磁盘区域:

empty disk

那在这些blocks中应该存储些什么?文件系统的基础要素自然是文件,而文件作为一个数据容器的逻辑概念,本质上是一些字节构成的集合,这些字节就是文件的user data(对应下图的"D")。

user data

除了文件本身包含的数据,还有文件的访问权限、大小和创建时间等控制信息,这些信息被称为meta data。meta data可理解为是关于文件user data的data,这些meta data存储的数据结构就是inode(对应下图的"I",有些文件系统称之为dnode或fnode)。

inode是"index node"的简称,在早期的Unix系统中,这些nodes是通过数组组织起来的,因此需要依靠index来索引数组中的node。假设一个inode占据256字节,那么一个4KB的block可以存放16个inodes,使用5个blocks可以存放80个inodes,也就是最多支持80个文件。

meta data + user data

内存分配一样,当有了新的数据产生时,我们需要选择一个空闲的block来存放数据,此外还需要一个空闲的inode。所以,需要追踪这些inodes和data blocks的分配和释放情况,以判断哪些是已用的,哪些是空闲的。

最简单的办法就是使用bitmap,包括记录inode使用情况的bitmap(对应下图的"i"),和记录data block使用情况的bitmap(对应下图的"d")。空闲就标记为0,正在使用就标记为1。

bitmap + meta data + user date

因为block是最小划分单位,所以这里使用了两个blocks来分别存储inode bitmap和data block bitmap,每个block可以容纳的bits数量是4096*8。而这里我们只有80个inodes和56个data blocks,所以是绰绰有余的。

还剩下开头的一个block,这个block是留给superblock的(对应下图的"S")。

superblock + bitmap + meta data + user date

这个superblock包含了一个文件系统所有的控制信息,比如文件系统中有多少个inodes和data blocks,inode的信息起始于哪个block(这里是第3个),可能还有一个区别不同文件系统类型的magic number。因此,superblock可理解为是文件系统的meta data。

文件寻址

这5个blocks中的80个inodes构成了一个inode table。假设一个inode的大小是256字节,现在我们要访问第32个文件,那就要先找到这个文件的控制信息,也就是第32个inode所在的磁盘位置。它应该在相对inode table起始地址的8KB处(32*256=8192),而inode table的起始地址是12KB,所以实际位置是20KB。

inode table

磁盘同内存不同,它在物理上不是按字节寻址的,而是按sector。一个sector的大小通常是512字节,因此换算一下就是第40个sector(20*1024/512)。

对于ext2/3/4文件系统,以上介绍的这些inode bitmap, data block bitmap和inode table,都可以通过一个名为"dumpe2fs"的工具来查看其在磁盘上的具体位置:

如果只需要查看inode的使用情况,那么直接使用"df -i"命令即可:

inode usage

寻址方式

那通过inode如何找到对应的文件呢?根据大小的不同,一个文件需要占据若干个blocks,这些blocks可能是磁盘上连续分布的,也可能不是。所以,比较好的办法是使用指针,指针存储在inode中,一个指针指向一个block。

不过,在这个简化的示例里,并不需要C语言里那种指针,只需要一个block的编号就可以了。如果文件比较小,占有的blocks数目就比较少,那么一个256字节的inode就能存储这些指针。假设一个inode最多能包含12个指针,那么文件的大小不能超过48KB。

那如果超过了怎么办?可由inode先指向一个block,这个block再指向分散的data block,这种方法称为multi-level index。inode在指向中间block的同时,也可以直接指向data block。假设一个指针占据4个字节,那么一个中间block可存储1024个指针,二级index的寻址范围就可超过4MB,三级index则可超过4GB。

有没有觉得很像内存管理中的多级页表?事实上,它们的原理可以说是一样的,文件在磁盘上的实际block分布对等于内存的物理地址,而各级index就对等于内存的虚拟地址。

这种只使用block指针的方式(可被称为"pointer-based")被ext2和ext3文件系统所采用,但它存在一个问题,对于占据多个data block的文件,需要较多的meta data。

另一种实现是使用一个block指针加上一个length来表示一组物理上连续的blocks(称为一个extent,其中length以block为单位计),一个文件则由若干个extents构成。这种"extent-based"的方式被后来的ext4文件系统所采用。

struct ext4_extent {
    __le32  ee_block;   /* first logical block extent covers */
    __le16  ee_len;     /* number of blocks covered by extent */
    ...
};

相比"pointer-based"而言,"extent-based"由于需要磁盘上连续的free space,灵活性稍差,适用于磁盘空闲空间比较充足的场景。

目录和路径

各级目录构成了访问文件的路径,不同于windows操作系统的drive分区,类Unix系统中的"mount"操作让所有的文件系统的挂载点都是一个路径,形成了树形结构。从抽象的角度,目录也可视作一种文件,只是这种文件比较特殊,它的user data存储的是该路径下的普通文件的inode编号。

所以,如下图所示的这样一个路径结构,假设要在"/foo"目录下创建一个文件"bar.txt",那么需要从inode bitmap中分配一个空闲的inode,并在"/foo"这个目录中分配一个entry,以关联这个inode号。

接下来,我们要读取刚才创建的这个"/foo/bar.txt"文件,那么先得找到"/"这个目录文件的inode号(这必须是事先知道的,假设是2)。然后访问这个inode指向的data block,从中找到一个名为"foo"的entry,得到目录文件"foo"的inode号(假设是44)。重复此过程,按图索骥,直到找到文本文件"bar.txt"的inode号。

小结

现代文件系统的实现是很庞杂的,但文件系统的原理本身并不深奥,基本的构成要素就是file data,管理各个文件的inode和管理整个文件系统的superblock。基于这些要素,下文将开始介绍读写一个文件的具体方式和接口。


注1:所谓持久性(persistance),就是指即便面对困难、挑战,依然可以持续,对于设备来说,就是面对掉电、操作系统crash,依然可以保持数据的持久存在。


参考:


原创文章,转载请注明出处。

编辑于 2021-12-12 19:45