23. 文件系统的实现
File Allocation Table 要设计并实现一个文件系统,我们首先需要关注并解决存储媒介带来的两大核心挑战: 读写放大 (Read/Write Amplification):现代存储设备(无论是机械硬盘还是固态硬盘)的物理特性决定了,它们最高效的读写方式是操作连续的大块数据区域,我们称之为一个块 (Block)。如果需要修改一个块中哪怕一个字节的数据,也必须将整个块读入内存、修改、再完整写回。这种“操作少量数据却导致整块数据被读写”的现象就是读写放大,它会严重影响性能。 局部性 (Locality):程序的内存访问行为通常具有局部性原理 (Principle of Locality),即在一段时间内,访问的地址会集中在某个区域。文件系统可以通过合理的数据排布,让物理上相邻的数据块在逻辑上也相关联(例如,属于同一个文件),从而在读取一块数据时,可以利用预读机制将后续可能被访问的数据也加载到内存缓存中,提高效率。 在软盘上实现文件系统 我们的需求是为一个存储容量很小的设备(如软盘)设计一个文件系统。在这种场景下,使用复杂的树形数据结构(如 B+ 树)会因为元数据本身占用过多空间而显得浪费。因此,一个简单的链式结构是更合适的选择。 目录的实现 在简单的文件系统中,目录本身可以被实现为一个普通的文件。这个文件的特殊之处在于,它的内容遵循一种固定格式,即一个目录项数组。 1 2 3 4 5 6 // 一个简单的目录项 (dentry) 结构 struct dentry { char filename[256]; // 文件名 unsigned int start_block; // 文件数据起始块的编号 unsigned int size; // 文件大小 (以字节为单位) }; 当我们需要打开一个目录时,文件系统只需读取这个文件的内容,并将其解析为一个 struct dentry 数组即可。 文件数据的存储 用链表来组织一个文件的所有数据块,主要有两种实现思路。 方法一:在每个数据块后放置指针 这种方法非常直观。每个数据块的末尾都留出一小块空间,用于存放下一个数据块的地址或编号。 优点:实现简单,逻辑清晰。 缺点: 极差的随机访问性能:要访问文件的第 N 个数据块,必须从第一个块开始,依次读入前 N-1 个块来找到第 N 块的指针。这需要 N-1 次磁盘 I/O,对于大文件而言是毁灭性的。 空间浪费:每个数据块都不能被 100% 用来存储文件内容,必须牺牲一部分空间给指针。 方法二:将指针集中存放在文件系统的某个区域 为了解决上述问题,我们可以将所有数据块的“链表指针”抽离出来,集中存放在一个被称为文件分配表 (File Allocation Table, FAT) 的核心数据结构中。 FAT 本质上是一个大数组。数组的下标与磁盘上的数据块编号一一对应。数组中存储的值则是该文件链表中的下一个数据块的编号。 ...