4. 文件系统 (FILE SYSTEM)

所有的应用都需要存储和检索信息。当进程运行时,它可以存储有限数量的信息到自己的地址空间中。然而,其存储能力受限于虚拟地址空间的大小。对一些应用来说,虚拟地址空间并不能满足其存储需求。

将信息保存在进程的地址空间的第二个问题是,当进程结束时信息会丢失。对很多应用来说 (e.g.,数据库),信息必须维持几周,几个月,甚至是永远。当使用它的进程终止时,信息就丢失了是不能接受的。进一步而言,当计算机崩溃导致进程被杀死时,信息也不能丢失。

第三个问题是多个进程需要同时访问同一信息是十分常见的,将信息存储在某进程的地址空间中,其他进程则不能对其访问。解决这一问题的方法是使信息本身独立于进程。

因此,对长期信息的存储有 3 个本质的需求:

  1. 必须能存储大量信息
  2. 信息必须在使用它的进程终止时仍然存在
  3. 多个进程必须能同时访问同一信息

磁盘被长期存储已经有多年了。最近几年,固态驱动器越来越受欢迎,因为它们没有任何容易损坏的活动部件。同时,它们也提供更快的随机访问。磁带和光盘也被广泛使用,但是它们的性能更低且常用于备份。我们将在第 5 章进一步学习硬盘,目前,将硬盘认为是固定大小的块组成的线性序列并支持两种操作:1. 读块 k,2. 写块 k 就足够了。实际上有更多的操作,但是理论上使用这两种操作就足够解决长期存储的问题了。

然而,这些操作都非常便利,特别是在多应用多用户的大型系统上 (e.g.,服务器)。我们考虑下述几个问题:

  1. 如何查找信息?
  2. 如何避免用户读取其他用户的数据?
  3. 如何知道块还未被使用?

还有很多其他的问题。

正如操作系统将处理器的概念抽象出来创造出进程的抽象,将物理内存的概念抽象出来创造出进程(虚拟)地址空间的抽象,我们可以使用新的抽象来解决这写问题:文件。

文件是进程创建的信息的逻辑单元。硬盘通常包含成千上万的文件,它们间彼此独立。事实上,如果你把每个文件认为是一种地址空间,与实际已相差不远,除了文件是用来模型化硬盘而非 RAM。

进程能读取已存在的文件或创建新的文件。存储在文件中的信息必须是持久化的,即不受进程创建和终止的影响。文件仅当其所有者显式地移除该文件时才会消失。虽然读写文件的操作十分常见,还存在一些其他的操作,接下来我们会讨论其中的一些。

文件由操作系统管理。它们如何被结构化命名访问使用保护实现,以及管理是操作系统设计的主要课题之一。总之,操作系统处理文件的部分被称为文件系统,它是本章的主题。

从用户的角度看,文件系统最重要的方面是它如何呈现,换句话说,什么组成了文件,文件如何被命名和保护,在文件上允许执行什么操作,等等。细节诸如是使用链表还是位图来追踪未使用的存储,以及逻辑硬盘块中有多少个扇区,用户并不关心。因此,我们将本章划为几个小节。前两节讨论文件和目录的用户接口,然后详细讨论文件系统的实现和管理。最后,给出几个现实的文件系统的示例。

4.1 文件

接下来我们从用户的视角出发观察文件,即如何使用文件以及文件具有什么属性。

4.1.1 文件命名

文件是一种抽象机制。它提供一种方法将信息存储到硬盘上,并能在之后读取它。用户不需要知道信息如何被存储,在哪里存储,以及硬盘是如何工作的。

可能任何抽象机制中最重要的特征就是对象如何被管理和命名的,所有我们先开始了解文件系统中文件的命名。当进程创建一个文件,它给与文件一个名字。当进程结束时,文件继续存在并能被其他进程使用该名字进行访问

准确的文件命名的规则因系统而异,但是所有当前的操作系统允许一个 1 到 8 个字母的字符串作为一个合法的文件名。因此 andreabruce,和 cathy 都可能是文件名。通常数字和特殊字符也是被允许的,所以像名字 2,urgent!,和 Fig.2-14 通常也是合法的。很多文件系统支持的文件名长度长达 255 个字符。

一些文件系统区分大写和小写字母,其他的系统则不区分。UNIX 属于第一种类型;旧式的 MS-DOS 属于第二种类型。因此,UNIX 系统将下述三个文件视为不同的文件:mariaMaria,和 MARIA。在 MS-DOS 中,所有的名字都引用相同的文件。

Windows 95 和 Windows 98 都使用 MS-DOS 文件系统,称为 FAT-16,因此继承了很多 MS-DOS 文件系统的属性,例如文件名如何被构建。Windows 98 对 FAT-16 作了一些拓展,产生 FAT-32,但是这两者十分相似。此外,Windows NT,Windows 2000,Windows XP,Windows Vista,Windows 7 和 Windows 8 依然支持 FAT 文件系统,现在来说该系统已经过时了。然而,更新的系统有更高级的原生文件系统 (NTFS),该文件系统有不同的属性(例如文件名使用 Unicode 编码)。实际上,Windows 8 有第二种文件系统,称为 ReFS (或 Resilient File System),但是该文件系统的目标是 Windows 8 的服务器版本。本章中,当我们引用 MS-DOS 或 FAT 文件系统时,指的是在 Windows 上使用的 FAT-16 和 FAT-32,除非另有说明。我们将在本章的稍后讨论 FAT 文件系统,在 12 章讨论 NTFS。顺便一提,还有新的 FAT-like 的文件系统,称为 exFAT 文件系统,一个对 FAT-32 的 Mircosoft 拓展,优化了闪存驱动和大的文件系统。Exfat 是唯一的现代 Microsoft 文件系统,OS X 可以对其进行读写。

很多操作系统支持两部分的文件名,这两部分由小数点分割,例如 prog.c。小数点后的部分被称为 file extension,通常指明关于文件的一些信息。例如在 MS-DOS 中,文件名有 1 到 8 个字符,加上 1 到 3 个字符的额外拓展。在 UNIX 中,如果有拓展名,拓展名的大小取决于用户,文件可能有两个或多个拓展名,比如 homepage.html.zip.html 表示 HTML 格式的 Web 页,.zip 表示文件(homepage.html) 使用 zip 程序进行了压缩。一些常见的文件拓展名和它们的含义如图 4-1 所示:
在这里插入图片描述
在一些系统中 (e.g.,所有 UNIX 的发行版),文件拓展名仅仅是为了便利起见,操作系统并不对其强制要求。命名为 file.txt 的文件可能是某种文本文件,但是该名字更多的是提醒所有者而非向计算机传递实际的信息。换句话说,C 编译器实际上可能坚持要被编译的文件以 .c 结尾,如果不是这样它可能拒绝编译该文件。然而,操作系统对此并不关心。

像这样的便利在相同的程序能处理多种文件时十分有用。例如,C 编译器可能会给出一个要编译和链接到一起的文件列表,其中一些是 C 文件,另一些是汇编语言文件。此时拓展名就十分必要了,被编译器用于区分哪些是 C 文件,哪些是汇编文件,哪些是其他文件。

相反,Windows 关心拓展名,并为它们分配含义。用户(或进程)能像操作系统注册拓展名,并指定哪个程序 “拥有” 这个拓展名。当用户双击文件时,被分配给该拓展名的程序使用该文件名作为参数启动。例如,双击 file.docx 启动 Microsoft Word 开始编辑 file.docx

4.1.2 文件结构

文件能以几种方式被结构化。图 4-2 展示了三种可能的方法。图 4-2(a) 中的文件未结构化的字节序列。实际上,操作系统不知道也不关心文件内容,它只能看到字节。用户程序负责解释文件内容的含义。UNIX 和 Windows 都使用这种方式。
在这里插入图片描述
操作系统仅认为文件是字节序列提供了最大的灵活性。用户程序能存放任意内容到文件中,并以它们认为便利的方式命名文件。操作系统不会提供帮助,但也不会妨碍。所有版本的 UNIX (包括 Linux 和 OS X) 和 Windows 都使用这一文件模型。

结构化的第一步如图 4-2(b) 所示。在此模型中,文件是固定长度的记录的序列,每个记录都有一些内部的结构。文件是记录的序列这一理念的核心是读操作返回一个记录,写操作重写或追加一个记录。如今没有通用目的的系统使用这一模型作为主要的文件系统了。

第三种文件结构如图 4-2© 所示。在这一组织中,文件组成一颗记录的树,记录不必具有相同的长度,每条记录都在某个固定的位置包含一个 key 字段。树根据 key 字段进行排序,以允许快速搜索特定的键。

此时,基础操作不是获取下一个记录,而是获取具有特定键的记录。例如,对于图 4-2© 中的 zoo 文件,可以请求系统获取其键为 pnoy 的记录,不需要关心它在文件中的实际位置。进一步来说,添加到文件的新的记录由操作系统而非用户决定放在哪里。这种类型的文件与 UNIX 和 Windows 系统使用的未结构化的字节流十分不同,它被用于一些大型主机计算机以进行商业数据的处理。

4.1.3 文件类型

大部分操作系统支持好几种文件类型。例如,UNIX (包括 OS X) 和 Windows 有常规文件目录。UNIX 还有字符(character)特殊文件块(block)特殊文件Regular files 是包含用户信息的文件。图 4-2 中的所有文件都是常规文件。Directories 是系统文件,用于维护文件系统的结构。Character special files 与输入/输出相关,被用于模型化串行 I/O 设备,例如终端,打印机,和网络。Block special files 被用于模型化磁盘。本章我们主要讨论常规文件。

常规文件通常是 ASCII 文件二进制文件。ASCII 文件由文本行组成。在一些系统中每一行都由回车符结束。其他系统中,使用 line feed 字符。一些系统 (e.g.,Windows) 二者都使用。行不必具有相同的长度。

ASCII 文件最大的优点是它能被显式和打印,也能被文本编辑器编辑。进一步来说,如果大量的程序使用 ASCII 文件进行输入和输出,连接一个程序的输出与另一个程序的输入将会很容易,就像 shell 管道那样。(进程间管道的实现不会变简单,但是如果使用一种标准约定,例如 ASCII,进行表达,解释信息将会变得更容易。)

其他文件是二进制的,二进制文件仅仅意味着它们不是 ASCII 文件。在打印机上列出它们会给出一个不能理解的列表,上面全是随机的垃圾。通常,它们有一些内部结构,使用它们的程序知道这些结构。

例如,图 4-3(a) 中我们可以看到一个简单的可执行二进制文件,来自 UNIX 的早期版本。虽然技术上文件仅是字节的序列,操作系统仅当文件具有合适的格式才会执行它。它有 5 个 sections:header,text,data,relocation bits,和 symbol table。header 以一个 magic number 作为起始,标识文件是一个可执行对象文件 (为了避免不是该格式的文件被意外执行)。然后是文件各部分的大小,可执行部分的起始地址,和一些 flag 位。header 之后是程序本身的 text 和 data。这些被加载到内存中并使用 relocation bits 进行重定位。符号表被用于调试。

第二个二进制文件的例子是 archive (静态库),同样来自 UNIX。它由已编译未链接的库 procedures(modules) 的集合组成。每个 procedure(module) 都有一个 header 作为开头,包含它的名字,创建日期,所有者,保护码,和大小。
在这里插入图片描述
每个操作系统必须至少识别一种文件类型:它自己的可执行文件;一些系统能识别的更多。旧的 TOPS-20 系统检查被执行文件的创建时间,然后它定位源文件并查看自二进制文件创建出来后其源文件是否被更新,如果有更新,它自动重编译源文件。在 UNIX 系统中,make 程序被内置到 shell 中。文件拓展名是强制性的,所以 make 能够告知二进制文件继承自哪个源文件。

强类型的文件在用户做出系统设计者不期望的事情时会导致问题。假设某个系统中,程序输出的文件具有 .dat(数据文件) 的拓展名。如果用户编写了一个程序格式化工具,它读取 .c 文件(C 程序),并改变它(e.g.,将它转换为标准的首行缩进布局),然后将转换后的文件作为输出进行写操作,输出文件的类型将为 .dat。如果用户尝试向 C 编译器提供该文件进行编译,系统将拒绝该行为,因为文件具有错误的拓展名。尝试将 file.dat 拷贝到 file.c 将被系统拒绝,因为该行为也非法(为了避免用户错误)。

这种 “用户不友好性” 可能对初学者有所帮助,但是它使富有经验的用户抓狂,因为他们必须付出巨大的努力来绕过操作系统关于什么是合理的什么不是的理念。

4.1.4 文件访问

早期的操作系统仅提供一种文件访问方式:sequential access。在这些操作系统中,进程能顺序读取文件中所有的字节或记录,从起始点开始,但是不能跳跃或无序读取。然而,顺序文件能倒带,这样它们就能按需经常读取了。顺序文件在存储媒体是磁带而非硬盘时十分便利。

当硬盘用于存储文件时,无序地读取文件中的字节或记录,或通过键而非位置访问记录成为可能。文件其字节或记录能以任意顺序进行读取被称为 random-access files。它们被许多应用所需要。

随机访问文件对很多应用来说是必不可少的,例如数据库系统。

有两种方式指定从哪里开始读取。第一种方式,每一个 read 操作都给出一个文件中的位置,从该位置开始读。第二种方式,有一个特殊的操作,seek,用于设置当前位置。在 seek 后,文件能从当前位置顺序读取。后一种方式被用于 UNIX 和 Windows。

4.1.5 文件属性

每个文件都有名字数据。此外,操作系统还为每个文件关联一些其他的信息,例如,文件最后修改的时间和日期,以及文件的大小。我们称这些额外的项为文件的 attributes。一些人称它们为 metadata。属性的列表因系统而异。图 4-4 中的表展示了一些可能的属性,但是也存在其他的属性。没有任何现存的系统包含所有这些属性,但是每个属性都存在于某个系统中。
在这里插入图片描述
前 4 个属性与文件的保护相关,告知谁能访问该文件谁不能访问。在一些系统中用户必须提供密码以访问该文件,这种情况下密码必须是其中一个属性。

flag 是 bits 或短字段,用于控制或启用某些特定的属性。例如,隐藏的文件不会出现在所有文件的列表中。存档 flag 是一个位,它追踪文件最近是否被备份了。备份程序清除该标志,当文件被改变时操作系统设置该标志。使用这种方式,备份程序能告知哪个文件需要被备份。临时 flag 允许文件被标记位在创建它的进程终止时,自动删除该文件。

记录长度,键位置和键长度字段仅出现在其记录使用键进行查找的文件。它们提供找到一个键所需的信息。

多个时间用于追踪文件何时被创建,最近何时被访问,最近何时被修改。它们用于不同的目的。例如,在创建对应的对象文件之后,如果源文件被修改,则对应的对象文件需要被重写编译。这些字段为此提供必要的信息。

当前的大小告知文件当前有多大。一些旧的主机操作系统在创建文件时需要最大的大小被指定,以让操作系统提前预留出最大大小的存储空间。如今的工作站和个人电脑足够聪明,不需要这一特性。

4.1.6 文件操作

不同的系统提供不同的操作来允许存储文件信息及检索文件信息。下述为最常见的与文件相关的系统调用。

  1. Create。创建一个空的文件,没有数据。该操作的目的是宣称一个文件的到来并设置一些属性。
  2. Delete。当文件不再被需要时,它必须被删除以释放磁盘空间。
  3. Open。在使用文件前,进程必须先打开它。open 调用的目的是允许系统获取属性磁盘地址列表到主存中,以便在后续的调用实现快速访问。
  4. Close。当所有的访问都被完成时,属性和硬盘地址不再被需要,所以文件应该被关闭以释放内部表空间。很多系统通过限制进程能打开文件的最大数量来鼓励这一操作。硬盘以块为单位写入,关闭文件强制写入文件的最后一个块,即使该块还没有被完全填满。
  5. Read。从文件中读取数据。通常,字节来自于当前位置。调用方必须指定需要读取多少数据,以及提供一个缓冲来存放这些数据。
  6. Write。数据被再次写入文件,通常在当前的位置。如果当前的位置是文件末尾,则文件的大小增加。如果当前的位置在文件中间,现存的数据将被重写,原来的数据将永久丢失。
  7. Appendwrite 调用更严格的版本。仅能在文件末尾添加数据。
  8. Seek。对于随机访问文件,需要一个方法来指定获取数据的位置。最常见的方法是系统调用,seek,它将文件指针重新定位到文件中指定的位置上。此调用完成后,数据能从该位置进行读取和写入。
  9. Get attributes。进程经常需要读取文件属性来完成它们的工作。例如,UNIX make 程序通常被用于管理由很多源文件组成的软件开发项目。当调用 make 时,它检查所有源文件和对象文件的修改时间,以确定仅需要编译哪些文件即可完成编译。为了完成该工作,它必须检查文件属性,具体来说,是修改时间。
  10. Set attributes。一些属性是用户可以设置的,在文件被创建后能被修改。此系统调用使之成为可能。保护模式的信息是一个明显的例子。 大部分 flags 也属于这一类。
  11. Rename。用户需要修改现存的文件的名字是很常见的。此系统调用使之成为可能。此方法并不总是必要的,因为文件通常能被拷贝到一个新文件中,新文件具有新的名字,旧文件随后被删除。

4.2 目录(DIRECTORIES)

为了追踪文件,文件系统通常包含 directoriesfolders,它们本身也是文件。本节我们讨论目录,它们的组织,它们的属性,在它们上能执行的操作。

4.2.1 单层目录系统

目录系统最简单的形式是只有一个文件夹,它包含所有文件。有时它被称为 root directory,但是因为只有一个目录,所以叫什么名字已经无所谓了。在早期的个人计算机中,该系统十分常见,有一部分原因是只有一个用户。有趣的是,世界上第一台超级计算机 CDC6600,仅有一个目录保存所有文件,即使它能被多个用户同时使用。该决定无疑是为了保证软件设计的简单。

图 4-6 是只有一个目录的系统的一个示例。该目录包含 4 个文件。该方案的优点是简单,能快速定位文件,毕竟只有一个地方需要搜索。它有时仍被用于简单的嵌入式设备例如数码相机和一些可移植的音乐播放器。
在这里插入图片描述

4.2.2 分层的目录系统

对于非常简单的专用应用,单层就足够了,但是对于拥有上千文件的现代用户,如果所有文件都在单个目录中,就不可能找到任何事。

总之,需要一个方法把相关的文件分组到一起。此时,则需要一个分层结构 (i.e.,目录树)。使用该方法,就能以原生的方式将文件分组到各自的目录中。进一步来说,多个用户共享一个通用的文件服务器,每个用户都有一个私有的根目录,用于他自己的分层结构。该方法如图 4-7 所示。这里,目录 A,B,和 C 被包含在根目录中,它们属于不同用户,其中还有两个用户为他们工作的项目创建了子目录。
在这里插入图片描述
用户能创建任意数量的子目录,这一能力为用户组织他们的工作提供了强大的结构化工具。因此,几乎所有的现代文件系统都是以这种方式组织的。

4.2.3 路径名

当文件系统被组织成目录树时,需要一种方法来指定文件名。存在两种通常使用的方式。第一种方式,每个文件都被给予一个 absolute path name,由从根目录到文件的路径组成。例如,路径 /usr/ast/mailbox。绝对路径名总是从根目录开始,且它是唯一的。

另一种名字是 relative path name。它在使用时结合了 working directory 概念 (也被称为 current directory)。用户可以指定一个目录为当前的工作目录,所有不以根目录起始的路径都是相对于工作目录而言的。例如,如果当前的工作路径是 /usr/ast,文件的绝对路径是 /usr/ast/mailbox,则可以使用 mailbox 对其进行引用。

每个进程都有自己的工作目录,当它改变自己的工作目录然后退出时,其他进程不受影响,也不会在文件系统中留下工作目录改变的痕迹。另一方面,library procedure 改变了工作目录且在返回时没有改回来,其他的程序可能不能正常工作,因为它对自己的工作目录的假设突然变得非法了。因此,libraries procedure 几乎不改变工作目录,当必须改变时,它们也会在返回前将工作目录改回来。

大部分支持层级目录结构的操作系统在每个目录中都包含两个特殊的条目,“.” 和 “…”。“.” 引用当前目录,“…” 引用父目录 (根目录除外,它引用自己)。

4.2.4 目录操作

相较对文件的系统调用,管理目录的系统调用在不同的系统之间表现出更多的差异。其操作如下:

  1. Create。创建目录,仅包含 “.” 和 “…”,由系统自动添加。
  2. Delete。删除目录,仅有空目录能被删除,仅包含 “.” 和 “…” 的目录被认为是一个空目录,“.” 和 “…” 不能被删除。
  3. Opendir。读取目录。例如,列出目录中的文件,列出程序打开目录读取目录中包含的所有文件的文件名。在读取目录前,目录必须是打开的,与打开和读取文件类似。
  4. Closedir。关闭目录以释放内部的表空间。
  5. Readdir。返回打开的目录的下一个条目。从前,使用一般的 read 系统调用能读取目录,但是该方法强制要求程序员知道并处理目录的内部结构。相反,readdir 总是以标准格式返回一个条目,无论使用的目录结构是什么。
  6. Rename。在很多方面,目录与文件类似,以与文件相同的方式进行重命名。
  7. Link链接允许一个文件出现在多个目录中。该系统调用指定一个已存在的文件和一个路径名,创建一个从已存在的文件到指定的路径名的链接。使用该方法,相同的文件可能出现在多个目录中。这种链接,增加了文件的 i-node 的计数器 (追踪包含该文件的目录条目的数量),有时被称为 hard link
  8. Unlink。移除目录条目。如果被解除链接的文件仅存在于一个目录中(通常情况),从文件系统中移除它。如果它存在于多个目录中,只用指定的路径名被移除。其他的被保留下来。在 UNIX 中,删除文件的系统调用实际上是 unlink

上述列表给出最重要的调用,也有几个其他的调用,例如,管理关于目录的保护信息。

链接文件理念的一个变体是 symbolic link。并非让两个名字指向相同的内部数据结构 (代表文件),而是创建一个名字指向一个小文件,这个小文件命名另一个文件。当第一个文件被使用时,比如,被打开,文件系统跟随路径找到位于尾部的名字。然后它使用这个新名字开始整个查找过程。符号链接的优点是它能跨越磁盘边界,甚至对远程计算机上的文件进行命名。虽然其实现在某种程度上较硬链接效率更低。

4.3 文件系统实现(FILE-SYSTEM IMPLEMENTATION)

用户关心文件如何被命名,能对文件执行什么操作,目录树是什么样的,等等类似的接口问题。实现者则会考虑文件和目录如何被存储,如何管理磁盘空间,如何让一切高效可靠的工作。下面我们将讨论这其中的一些领域,看看存在什么问题以及做出了哪些权衡。

4.3.1 文件系统布局

文件系统被存储在硬盘上。大部分硬盘能被分为一个或多个部分,每个部分都有独立的文件系统。硬盘的扇区 0 被称为 MBR(Master Boot Record),被用于引导计算机。MBR 的尾部包含一个分区表。该表给出每个分区的起始地址和终止地址。表中的一个分区被标记为活动。当计算器被引导时,BIOS 读取并执行 MBR。MBR 程序所做的第一件事就是定位活动的分区,读取它的第一个块,被称为 boot block,并执行它。引导块中的程序加载该分区内的操作系统。不幸的是,每个分区都是以引导块起始的,即使它不包含可引导的操作系统。此外,它可能在将来有一个操作系统。

除了从启动块开始,磁盘分区的布局因系统而异。文件系统通常包含的某些项如图 4-9 所示。第一项是 superblock。它包含所有关于文件系统的关键参数,且在计算机被引导或文件系统第一次被使用时被读入到内存。通常超级块中的信息包括标识文件系统类型的魔术字,文件系统中块的数量,以及其他关键的管理信息。
在这里插入图片描述
接下来的信息可能是关于文件系统中未使用的块,例如以位图或指针列表的形式。紧跟着的可能是 i-nodes,一个包含文件信息的数据结构的数组,每个文件一个。之后则是根目录,包含文件系统树的顶层。最后,硬盘的剩余部分包含所有其他的目录,以及所有文件

4.3.2 实现文件

或许实现文件存储最重要的点就是追踪某个硬盘块被分配给了哪个文件。不同的操作系统使用不同的方法。本节我们将讨论其中几种。

连续分配

最简单的分配方案是使用连续运行的硬盘块存储某个文件。因此在 1-KB 块的硬盘上,50 KB 的文件将被分配 50 个连续的块。2-KB 的块上,将被分配 25 个连续的块。

图 4-10(a) 是一个连续存储分配的示例。展现了前 40 个硬盘块,以最左边的块 0 为起始。最初时,硬盘块是空的。然后 4 个块长的文件 A 被写入到硬盘的起始点处 (块 0)。在那之后是一个 6 个块的文件 B,在文件 A 的末尾被立即写入。

注意每个文件都从新的块开始写入,所以如果文件 A 的大小只有 3.5 块,则会浪费最后一个块的部分空间。图中总共有 7 个文件,每个都从前一个文件的结尾开始。
在这里插入图片描述
连续分配硬盘空间有两个重大好处:一,易于实现,因为追踪文件的块仅需记住两个数字:第一个块的硬盘地址,以及文件所在的块的数量。给出第一个块的编号,就能通过简单的加法得到其他块的编号。二,读取性能优秀,因为单次操作就能从硬盘中读取整个文件。仅需要一次查找 (查找第一个块)。此后,不需要更多的查找或旋转延迟,所以数据能利用硬盘的全部带宽读入。连续分配易于实现且具有高性能。

不幸的是,连续分配也有非常严重的缺点:随着时间的推移,硬盘变得碎片化。如图 4-10(b) 所示,有两个文件 D 和 F 已经被移除了。当文件被移除时,它占据的块自然被释放,重新成为未被使用的块。硬盘并没有填满因文件移除而出现的块空洞,因为这需要拷贝这些空洞之后的所有块并向前移动它们,这实在是太耗费资源了。最终,硬盘由文件和空洞组成,如图所示。

最开始,碎片化不是一个问题,因为每个新文件都可以写在硬盘的末端,紧随前一个文件。然而,当硬盘最终被填满时,要么对硬盘进行压缩,这是一个昂贵的操作,要么重用空洞中未使用的空间。重用空间需要维护一个空洞的列表,然而,当新的文件被创建时,为了选择合适大小的空洞存放该文件,必须提前知道整个文件的最终大小,这无疑是不能做到时时刻刻得以保证的。

然而,有一种情况下连续分配时可行的,且实际上依然在被使用:在 CD-ROMS。所有的文件大小都是提起知道的,在 CD-ROM 文件系统随后的使用中不会改变。

连续分配实际上在多年前被用于磁盘文件系统,因为它的简单性和高性能 (用户友好在那时并不是考虑的重点)。然后这个方案便被弃置,因为在创建文件时必须指定文件的最终大小十分麻烦。但是随着 CD-ROMs,DVDs,Blu-rays 等一次写入的光学媒体的出现,连续的文件突然再次变得适用起来。

链表式分配

存储文件的第二种方式是让文件成为硬盘块的链表,如图 4-11 所示。每个块的第一个字被用做指向下一个块的指针,剩下的部分存储文件数据。
在这里插入图片描述
与连续分配不同,此方法中每个硬盘块都能被使用。没有因为硬盘碎片化造成的空间损失(除了最后一个块内部的碎片化)。通用,目录条目仅存储第一个块的硬盘地址就足够了。剩下的块能依据块存储的下一个块的信息查找。

换句话说,虽然按序读取文件很直观,但随机访问则十分缓慢。为了得到块 n,操作系统必须从头开始,读取 n-1 个先于它的块,一次一个。很明显,执行这么多次读将会很缓慢。

同样,存储于块中的数据数量不再是 2 的幂指数,因为指针占据了几个字节。虽然并不严重,但是使用特殊的大小会损失效率,因为很多程序都是在 2 的幂的大小的块中进行读取和写入的。块中的前几个字节被指向下一个块的指针占据,读取一个完整的块大小的数据需要获取和联结来自两个块的信息,这会因为拷贝而造成额外的开销。

使用内存中的表的链表分配

两个链表分配的缺点都能通过将指针字从硬盘块中取出存放到内存中的表中得以缓解。图 4-12 展现了图 4-11 的示例中的表实现应该是什么样子。表的索引为块的编号,表中槽的值为文件中下一个块的编号,-1 则表示文件结束。主存中这样的表被称为 FAT(File Allocation Table)
在这里插入图片描述
使用这样的组织,整个块都能用于数据。随机访问也更简单,虽然还是要沿着链搜索,但是表存在于内存中,不需要引用硬盘。跟之前的方式一样,目录条目仅需要保存文件的第一个块的编号即可,延文件的块链可以定位文件的任意块。

该方法的主要问题是整个表必须一直存在于内存中。1TB 的硬盘,1 KB 的块大小需要 100 万个条目,每个硬盘块一个。每个条目至少需要 3 个字节。为了加速查找,可能需要 4 个字节。因此表将一直占据 3 GB - 2.4 GB 大小的主存,取决于系统是否在空间和时间上进行优化。显然,FAT 的方式不能很好的拓展到大的硬盘上。它是最初的 MS-DOS 文件系统,且依然被所有版本的 Windows 系统所支持。

I-nodes

最后一个追踪块属于哪个文件的方法是为每个文件关联一个数据结构,称为 i-node(index-node),它包含文件块的地址文件的属性。图 4-13 是一个简单的例子。有了 i-node,就能找到文件中所有的块。对比使用内存中的表链接文件,该方案最大的优点是仅当对应的文件被打开时,i-node 才需要存在于内存中。
在这里插入图片描述
i-nodes 的一个问题是如果每个节点只为固定数量的硬盘地址提供存储空间,当文件增长超过这一限制应该怎么办?方案之一是保留最后一个硬盘地址,不用于数据块,而是保存一个块地址,该块包含更多的硬盘块地址,如图 4-13 所示。

4.3.3 实现目录

在读取文件前,它必须被打开。当文件被打开时,操作系统使用用户提供的路径名在硬盘上定位目录条目目录条目提供查找硬盘块所需的信息。取决于系统,信息可能是整个文件的硬盘地址(连续分配),第一个块的编号 (所有的链表方案),或 i-node 的编号。所有情况下,目录系统的主函数都将文件的 ASCII 名字映射为定位数据所需的信息。

一个密切相关的问题是属性应该存储在哪。每个文件系统维护不同的文件属性,例如文件的所有者和创建时间,它们必须存储在某处。将它们直接存储在目录条目中显然是可行的。一些系统之前就是如此。图 4-14(a) 展现了这种方式。在此设计中,目录由固定大小条目的列表组成,每个文件一个条目,包含固定长度的文件名文件属性的结构体一个或多个硬盘地址(直到数量上限)告知硬盘块的位置。
在这里插入图片描述
对于使用 i-node 的系统,属性被存储在 i-nodes 中,而非目录条目。目录条目仅存储文件名和一个 inode 编号,如图 4-14(b) 所示。

目前为止我们假设文件的文件名很短,且长度固定。MS-DOS 文件的基本名字有 1-8 字符,拓展名有 1-3 字符。UNIX 版本 7 中,文件名为 1-14 个字符,包含任何拓展名。然而,几乎所有的现代操作系统都支持更长,变长的文件名。这是如何实现的呢?

最简单的方法是限制文件名的长度,通常为 255 个字符,然后使用图 4-14 中的设计之一,为每个文件名保留 255 个字符。该方法很简单,但是浪费了大量的目录空间,因为几乎没有文件有这么长的名字。因为效率问题,需要不同结构。

一可可选的方案是放弃所有的目录条目都有相同的大小这一想法。每个目录条目包含固定的部分,通常以条目的长度起始,紧跟着是固定格式的数据,通常包含所有者创建时间保护信息,和其他属性。定长的 header 后跟实际的变长文件名,如图 4-15(a) 所示 (大端格式)。此示例中,我们有 3 个文件,project-budgetpersonnel,和 foo。每个文件名都以一个特殊的字符终止(通常是 0)。为了保证每个目录条目都以字边界起始,文件名被填充为字的整数数量,见图中的阴影部分。
在这里插入图片描述
该方法的缺点是当文件被移除时,目录中会出现变长的 gap,下一个进入的目录不一定能填满该 gap。另一个问题是单个目录条目可能跨越多个页,在读取文件名时可能发生页错误。

另一个处理变长名字的方法是使目录条目本身定长,使文件名聚集在文件的末尾的堆中,如图 4-15(b) 所示。该方法的优点是当条目被移除时,下一个进入的文件总是合适的。当然,堆必须被管理,且在处理文件名时,页错误仍然可能发生。只是对于文件名来说现在不再需要进行填充以满足目录条目位于字边界上了。

目前为止在所有的设计中,当查找文件名时,目录被从头到尾线性搜索。对于特别长的目录,线性搜索可能会很慢。加速搜索的方法之一是在目录中使用哈希表。对应哈希码的表条目被检查,如果未被使用,指向文件条目的指针就放在那里,文件条目紧随着哈希表。如果槽已经被使用了,则构造一个链表,以表条目为头,遍历相同哈希值的所有条目。

文件的查找也有相同的过程。文件名被哈希以选择一个哈希表条目。所有该槽上链起来的条目都会被检查,以确认该文件名是否存在。如果名字不再链上,则文件不存在于目录中。

使用哈希表能更快的查找,但是管理起来更加复杂。它仅是一个系统的候选人,该系统的目录中可能有成百上千个文件。

加速搜索大型目录的另一个方法是缓存搜索结果。在开始搜索前,首先检查文件名是否在缓存中。如果在,就能够快速定位。当然,仅当相对少量的文件包含搜索的主体时,缓存才有效。

4.3.4 共享文件

当几个用户合作完成一个项目时,他们通常需要共享文件。结果,共享文件出现在属于不同用户的目录中有助于共享文件。图 4-16 再次展现了图 4-7 中的文件系统。C 中的一个文件出现也出现在 B 中。共享文件和 B 的目录之间的连接被称为 link。文件系统现在是一个 Directed Acyclic Graph (有向无环图),或 DAG,而不是一个树。
在这里插入图片描述
当文件被链接时,该文件的硬盘地址会被复制到链接的目录项中。随后对该文件进行追加操作,只会影响执行追加操作的目录项,新的块地址会被追加到该目录项中。追加的结果对其他用户来说不可见,因此不满足共享的目的。

解决这个问题有两种方法。UNIX 使用第一种方法,创建一个小的数据结构,inode, 用于保存所有的块地址,对块地址的追加也在 inode 中进行。目录项中包含指向 inode 的指针,这样多个目录中的目录项就可以指向同一个 inode,从而影响同一个 inode。

第二种方法被称为 symbolic linking,以区分传统(上述)的(硬)链接。在链接文件时,操作系统创建一个新的文件,其类型是 LINK。LINK 类型的文件仅包含它所链接的文件的路径名。当操作该文件时,操作系统发现文件的类型是 LINK,则搜索其链接文件的路径名,对其链接的文件进行对应的操作。

两种方法都有其缺点。第一种方法中,链接共享文件并不会改变该文件所属的目录,在图 4-16 中,共享文件仍属于目录 C。但是链接共享文件会增加 inode 中的引用计数器,引用计数器记录当前有多少个目录项在引用该文件,如图 4-17 所示。
在这里插入图片描述
在进行链接后,如果尝试删除该文件,则会导致文件的引用计数减一,如图 4-17© 所示,只有当文件的引用计数降为 0 时,该文件才会从硬盘中被真正删除。

使用符号链接则没有文件所有者这样的问题,因为符号链接是一个新的文件,链接文件的 inode 仅对所属目录中的目录项可见,对符号链接不可见,符号链接仅指定链接文件的路径名。当删除文件时,inode 和文件数据都会被删除,因为 inode 的计数降为了零。此时,使用符号链接访问时,因为操作系统无法定位该文件 (该文件已被删除),所以会返回一个错误。删除符号链接对链接文件没有任何影响。

符号链接的问题是需要额外的开销。时间上,对文件进行操作,需要先读取符号链接文件,获取链接文件的路径名,然后对路径名进行定位 (依次定位其组件),直到找到对应的 inode。所有这些操作都需要大量额外的硬盘访问。空间上,每个符号链接文件都有一个额外的 inode,甚至有一个硬盘块用于存储链接文件的路径名,虽然对于较短的文件名我们可以直接放到 inode 中存储,作为一种优化。符号链接有一个优势,就是它可以链接任何一个文件,甚至是网络上其他主机上的文件, 只需要将路径名指定为机器的网络地址加上在该机器中文件的路径名即可。

链接还会导致另一个问题,无论是符号链接还是其他链接。列出目录包括子目录中的所有文件时,链接文件可能被程序定位多次,因为一个文件具有多个路径。比如在拷贝一个目录到磁带中时,链接文件可能被拷贝多次,而非识别出文件链接关系只拷贝一次。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值