文件系统(一)

目录的结构

在复习完我们之前学习过的与文件和文件系统有关的知识以后,我们将从这一节开始正式开始学习文件系统的有关内容

在引言中我们就提到过,为了有效地管理持久性存储设备上存储的数据,我们将数据抽象为了文件。文件的存在使得用户在调用数据时可以方便地通过一个文件名称访问一个文件,而无需输入磁盘的位置。如果我们只需要实现这一个功能,那么我们的文件系统可能就只需要一种基本功能:通过文件名称获得其对应的磁盘位置。我们可以设计的最简单的文件系统就是一个单层目录的文件系统,所有文件都处于同一个目录中。但这样的文件系统很可能不符合我们的要求——在日常生活中,我们经常把一个科目的作业和卷子归拢在一个文件夹里、以便复习时使用,在计算机中,我们也希望将内容相关的文件存储于一个目录里;不仅如此,目录的存在还方便了我们实现权限管理——如果我们需要保护多个文件,我们可以将它放在同一个目录中,然后对目录的权限作出限制,这样我们就省去了给每一个文件分别设置权限的过程。
**一个针对上面的单层目录文件系统的自然优化就是引入多层树状目录——每个目录中都可以包含多个子目录,文件则是这一树状目录中的叶节点。**为了实现何种多层树状目录,我们需要一个新的基本功能——我们必须能够通过目录获取其中子目录和文件所在的位置。

最基本的多层树状目录是双层树状目录,一些操作系统中根目录的子目录分别代表一个用户,每个用户的文件都存在于自己的子目录下,不能被其它用户访问,这样就方便了文件的区分和管理。但它也有一定的坏处——如果用户之间想要共享文件,那么权限的设定就非常困难。我们希望其它用户能访问某一子目录下的内容,但不能访问其它子目录的内容,这就要求我们对每个子目录的权限分别作出限制。我们当然也可以通过将被分享的文件复制到另一个用户的空间里来实现这一功能,但这样维持两个空间里的文件的一致性又成为了一个新的问题。我们希望有一种办法,能够使我们在不复制文件的情况下方便的实现文件共享,这种方法就是 DAG 目录结构。

**DAG 是 directed acyclic graph 的缩写,表示无回路有向图。在树状结构中,我们不需要表明一条边的方向,因为树状结构中不存在封闭环;但当我们的结构中出现了一个封闭环时,我们就需要通过表明每条边的方向来避免回路的存在。试想,如果一个目录中存在回路的话,我们就会在浏览这个目录时陷入死循环,这是我们想要避免的。
在 DAG 目录结构中,每个目录都有指向其每个子目录和文件的有向边,而共享文件的方法就是使得两个用户的子目录都包含指向同一个文件或目录的有向边,如下图所示。
在这里插入图片描述
熟悉 Linux 系统的同学应该知道,Linux 系统中存在一个系统调用叫做link(),它可以被用来在一个目录中加入指向原本不在这个目录中的文件的链接,因而可以用来实现我们上面提到的 DAG 目录结构。
在 Linux 的终端中,你可以输入ln来执行 link 操作。在这个命令的 man page 上,你会看到这句话:“Create hard links by default, symbolic links with --symbolic.”这句话中提到了两种不同的 link,一种是硬链接(hard link),一种是软链接(soft link, or symbolic link),这两种链接的不同之处是
硬链接中每个链接都指向文件在存储设备中的实际位置,而软链接指向的是另一个路径名称。**当我们用软链接指向一个文件时,如果我们指向的文件被用户删除,我们通过软链接建立的链接文件就不能够再被打开;与之相反,当我们用硬链接指向一个文件时,即使被指向的文件被删除,我们仍然可以查看文件的内容,因为我们知道这个文件在磁盘中的实际位置。在 Linux 系统中,只有所有指向一个文件的硬链接都被删除时,文件才会在磁盘上被删除。这个功能的实现显然需要我们再添加一种基础功能:一个文件的数据结构中必须包含一个 reference count,用于记录有多少个目录有指向这个文件的硬链接。

一个简单的文件系统–FAT

上一节中我们看到,为了实现基本的文件功能和目录功能,我们需要在文件的数据结构中包含链接个数、磁盘位置、最近修改时间等数据。我们知道,在磁盘中最小的存储单位是一个扇区,因此当一个文件的长度大于一个扇区时,我们就需要通过某种方式将文件所对应的所有扇区按顺序存储在文件中;不仅如此,在我们需要延长一个文件时,文件系统需要能够寻找空闲的分区、分配给文件,因此文件系统需要通过某种方式记录磁盘中空闲的分区。不同的文件系统在这方面的处理方式都不完全相同,这里我们先来看一种最简单的解决方式:FAT 文件系统。
FAT 的全称是 File Allocation Table,即文件配置表。**顾名思义,这种文件系统的核心是一个文件配置表,表中的每一项都对应着磁盘中一个实际的数据块。**这个数据块可能包含了多个扇区,其大小由操作系统决定,因此它被称为逻辑数据块,以便于与物理扇区做出区分。每个文件至少拥有一个数据块,因此我们可以通过文件在系统中的“编号”寻找表中对应的项,每一项都以链表的形式存储了同一个文件下一个数据块对应项的位置,如果其存储值为 \ -1 −1​ 则代表这是这个文件的最后一个数据块。在寻找一个文件时,我们先进入这个配置表的第一项,也就是代表了根目录的数据块,从中找到根目录中存储的子目录或文件对应的项的编号。

FAT 中所有的空闲分区也以链表的形式存储在配置表中,因此当我们需要延长一个文件时,我们就可以从链表中选取第一项;当一个文件被删除时,我们将它所占有的所有数据块都接入这个空闲分区链表中。

FAT 的基本结构如下图所示。最初的版本中,它使用的地址是12 位的,因此它最多只能包含2^12 = 4096 个数据块;随着存储设备技术的进步,它被逐渐扩充到了 32 位。由于它易于实现,它曾经被应用于包括 MS-DOS 在内的多个系统中;直到今天、它仍然被应用在 USB 设备等需要被多种操作系统识别的设备中。但它也有几个明显的缺点,接下来我们就来看一看这些缺点。
在这里插入图片描述
首先,我们可以看到,在这个文件系统中、如果我们选择较小的逻辑数据块,那么一个大型文件就会包含很多个小分区,由于 FAT 文件系统给文件分配的物理数据块不一定是连续的,这样的结构会导致读写效率很低。然而,如果我们转而选择较大的逻辑数据块,那么一些小文件就会产生很多内部碎片。

由此可见,FAT 文件系统下大文件和小文件的读写效率都不高,且它没有利用磁盘连续读写时速度快的优势,因此连续读写和随机读写的表现都较为糟糕。在设计一个文件系统时,我们不仅需要考虑基本功能的实现,也需要考虑其效率。接下来我们就会给你介绍几种效率更高的文件系统实现。

FFS

上一章中我们已经看到, FAT 是一个效率不高的文件系统,且它不包括控制使用权限等对于系统安全来讲至关重要的功能。为了充分利用磁盘连续读写时速度较快的优势、提高读写文件的效率、并存储更多与文件相关的元数据(Metadata,即表示文件特点、而非文件内容的数据),我们需要一种新的文件系统设计。这种新的文件系统就是 BSD Fast File System ,也就是 Unix 中使用的文件系统。
FFS 就发明了这样一种结构。在 FFS 中,每个文件都被一个 inode 代表。这个 inode 中存储的是文件的元数据和数据扇区的指针,但是与一般指针不同的是,inode 中不只包含直接指向数据扇区的指针,还包含二层指针和三层指针。二层指针指向的扇区中包含的不是数据、而是指向数据扇区的指针;以此类推,三层指针指向的扇区包含的是指向二层指针扇区的指针。这样假如我们的一个扇区原本可以存储 n 个指针,现在我们就可以通过二层指针存储 \n^2 个指针。不过,由于大多数文件都是小文件,FFS 对于 inode 中二层指针和三层指针的数量作出了限制,大多数指针仍然是直接指针。
下面的代码是对于我们上面提到的功能的一种简单的实现:

struct inode {
  /* 这一结构用于在内存中存储 inode,包含的内容除了 inode 实际数据外还有打开这一文件的数量,以及这个 inode 在磁盘中的位置 */
  int open_count;
  int sector_number; // inode 所在的扇区编号
  struct inode_disk_data *data;
}

struct inode_disk_data {
  /* 这个结构表示的是磁盘中实际存储的 inode 数据 */
  int rwx; // 表示读写执行权限
  int start_block;
  int direct_pointers[12];
  int indirect_pointer[2]; // 二层指针
  int doubly_indirect_pointer; // 三层指针
}

在 FFS 系统中,目录与文件的结构是相同的,也包含了元数据和指向数据扇区的指针,但与之不同的是,目录的数据扇区中存储的数据是文件或子目录名称与其对应的 inode 位置。当我们打开一个文件时,文件的 inode 会被存储到内存中,以方便读写文件时获取相关数据的位置。为了避免不同的进程打开同一文件时内存中存储了同一个 inode 的多份拷贝、浪费空间,文件系统中一般含有一个包含了所有被打开文件的表格,其中记录了有多少个进程打开了一个文件;当打开某一文件的进程数量下降至零时、这个文件就会被从表中移除,其 inode 在内存中占有的空间也会被释放。每个进程也有自己的已打开文件表,其中的每一项对应的是一个文件描述符。

当我们打开一个文件时,如果这个文件还没有被任何其它进程打开,那么我们就会在全局文件表中加入代表这个文件的项,并将打开数量初始化为 1,否则我们就在已有的项中将打开数量加 1​,然后我们会在进程自己打开的文件表中加入这一项、选择一个空闲的文件描述符与之对应,将文件指针初始化至文件开头。

多个文件系统的结合

在前面的章节中,我们讲到了多种文件系统。我们平常使用的存储设备,如 U盘、移动硬盘等都带有自己的文件系统,但我们在将这些设备接入到计算机中时、它们仍然可以作为我们计算机中文件系统的一部分被浏览。用于实现这一功能的过程集叫做 挂载(mount) 。
在一台设备上可能存在多个文件系统,每个可以独立存在的文件系统就被称为 “卷”(Volume) 。当一个设备被插入到计算机中时,操作系统会先识别设备上存在的文件系统,然后将这些文件系统挂载到操作系统自带的文件系统下。用过 Windows 的同学应该知道,在你向计算机中插入 USB 或移动硬盘时,“我的电脑”中可能会出现“新加卷 X”的图标,这里的新加卷指的就是这个存储设备上的文件系统。在 Unix 系统中,你可以调用mount命令自己选择挂载设备的目录。

细心的同学可能会意识到一个问题——如果操作系统自带的文件系统与设备上的文件系统版本不同,那么操作系统如何才能兼容这个设备上的系统呢?
一种简单的方法是针对每个不同的文件系统写一段用于兼容的代码,但这种解决方法的效率显然不高,而且很难维护——每当一种新的文件系统出现时,我们都需要更新操作系统的代码。在计算机这一领域中,解决这类问题有一个常见的方法,那就是增加一个新的抽象层,因为一个新的抽象层可以使我们在不考虑更底层的抽象层的细节的前提下实现我们需要的功能。在下一章中讲解计算机网络时,我们就会看到,这一方法在网络设计中也得到了广泛的应用。

在文件系统中,这个新的抽象层就是 虚拟文件系统(Virtual File System,VFS) 。虚拟文件系统基于一个类似于 inode 的概念,vnode,定义了一个使用文件的界面,所有实现这一界面的文件系统无论实现方式多么不同都能够被支持 VFS 的系统方便地读写、使用;不仅如此,vnode 不同于 inode,它的编号在整个网络上都是唯一的,因此它可以被用来支持远程文件系统。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值