文件系统实现

1 思维模型

要考虑文件系统,我们通常建议考虑它们的两个不同方面;如果您了解这两个方面,您可能就会了解文件系统的基本工作原理。

  • 首先是文件系统的数据结构。换句话说,文件系统使用什么类型的磁盘结构来组织其数据和元数据?我们将看到的第一个文件系统(包括下面的 vsfs)采用简单的结构,如块数组或其他对象,而更复杂的文件系统,如 SGI 的 XFS,使用更复杂的基于树的结构。
  • 文件系统的第二个方面是它的访问方法。它如何将进程发出的调用(例如 open()read()write() 等)映射到其结构上?在执行特定系统调用期间会读取哪些结构?写了哪些?所有这些步骤的执行效率如何?

如果您了解文件系统的数据结构和访问方法,您就已经开发了一个关于它如何真正工作的良好思维模型,这是系统思维的关键部分。

文件系统思维模型

思维模型是你在学习系统时真正想要开发的东西。对于文件系统,您的思维模型最终应该包括以下问题的答案:

  1. 哪些磁盘结构存储文件系统的数据和元数据?
  2. 当进程打开文件时会发生什么?
  3. 在读取或写入期间访问哪些磁盘结构?

通过研究和改进您的思维模型,您可以对正在发生的事情形成抽象的理解,而不仅仅是试图理解某些文件系统代码的细节。

2 VSFS的整体组织

我们现在开发 vsfs 文件系统数据结构的整体磁盘组织。我们需要做的第一件事是将磁盘分为块;简单的文件系统仅使用一种块大小,这正是我们在这里要做的。我们选择常用的大小 4 KB。

因此,我们对构建文件系统的磁盘分区的看法很简单:一系列块,每个块大小为 4 KB。在大小为 $N$ 个 4 KB 块的分区中,块的寻址范围为 $0$ 到 $N − 1$。假设我们有一个非常小的磁盘,只有 64 个块:

image-20240417111330145

现在让我们考虑一下需要在这些块中存储什么来构建文件系统。当然,首先想到的是用户数据。事实上,任何文件系统中的大部分空间都是(并且应该是)用户数据。我们将用于用户数据的磁盘区域称为数据区域,并且为了简单起见,为这些块保留磁盘的固定部分,例如磁盘上 64 个块中的最后 56 个块:

image-20240417111520646

文件系统必须跟踪每个文件的信息。该信息是元数据的关键部分,跟踪诸如哪些数据块(在数据区域中)组成文件、文件的大小、其所有者和访问权限、访问和修改时间以及其他类似信息等。为了存储这些信息,文件系统通常有一个称为inode的结构。

为了容纳inodes,我们还需要在磁盘上为它们保留一些空间。我们将磁盘的这一部分称为inode表,它仅保存磁盘上inodes的数组。因此,我们的磁盘映像现在看起来像下图,假设我们使用 64 个块中的 5 个作为inodes(在图中用 I 表示):

image-20240417112112851

这里我们应该注意,inode 通常不会那么大,例如 128 或 256 字节。假设每个 inode 256 字节,一个 4 KB 的块可以容纳 16 个 inodes,而我们上面的文件系统总共包含 80 个 inodes。在我们的简单文件系统中,构建在一个微小的 64 块分区上,这个数字代表我们的文件系统中可以拥有的最大文件数;但是,请注意,构建在更大磁盘上的相同文件系统可以简单地分配更大的inode表,从而容纳更多文件。

到目前为止,我们的文件系统已经有了数据块(D)和 inodes(I),但仍然缺少一些东西。正如您可能已经猜到的,仍然需要的一个主要组件是某种跟踪inodes或数据块是否空闲或已分配的方法。因此,这种分配结构是任何文件系统中必需的元素。

当然,有许多可行的分配跟踪方法。例如,我们可以使用一个指向第一个空闲块的空闲列表,该块再指向下一个空闲块,依此类推。相反,我们选择了一种简单且流行的结构,称为位图,其中包括数据区域(数据位图)和inode表(inode位图)。位图是一个简单的结构:每个位用于指示相应的对象/块是空闲(0)还是正在使用(1)。因此我们新的磁盘布局,带有inode位图 (i) 和数据位图 (d):

image-20240417112825881

您可能会注意到,为这些位图使用整个 4 KB 块有点过大;这样的位图可以跟踪是否分配了32K个对象,但我们只有80个inode和56个数据块。然而,为了简单起见,我们还是为每个位图使用整个 4 KB 块。

我们非常简单的文件系统的磁盘结构的设计中还剩下一个块。我们将其保留给超级块,在下图中用 S 表示。超级块包含有关此特定文件系统的信息,例如,包括文件系统中有多少个 inodes 和数据块(在本例中分别为 80 和 56)、inode 表开始的位置(块 3)等等。它还可能包含某种幻数来标识文件系统类型(在本例中为 vsfs)。

image-20240417113149921

因此,在挂载文件系统时,操作系统会首先读取超级块,初始化各种参数,然后将卷附加到文件系统树上。这样,当访问卷内文件时,系统就能准确知道在哪里可以找到所需的磁盘结构。

3 文件组织:Inode

3.1 基本介绍

文件系统最重要的磁盘结构之一是 inode;几乎所有文件系统都有与此类似的结构。名称 inode 是index node的缩写,这是 UNIX和可能更早的系统中赋予它的历史名称,使用它是因为这些节点最初排列在数组中,并且在访问特定 inode 时索引到该数组。

数据结构 — INODE

inode 是许多文件系统中使用的通用名称,用于描述保存给定文件元数据的结构,例如其长度、权限及其组成块的位置。这个名字至少可以追溯到 UNIX(如果不是更早的系统的话,可能更早可以追溯到 Multics);它是index node(索引节点)的缩写,因为inode number用于索引磁盘上inodes数组,以便找到该编号的inode。正如我们将看到的,inode的设计是文件系统设计的关键部分之一。大多数现代系统对于它们跟踪的每个文件都有某种类似的结构,但可能将它们称为不同的东西(例如 dnodes、fnodes 等)。

每个inode都隐式地通过一个数字(称为i-number)引用,我们之前称之为文件的底层名称。在vsfs(以及其他简单的文件系统中),给定一个i-number,您应该能够直接计算出对应inode位于磁盘上的位置。例如,以上述的vsfs inode表为例::大小为20KB(5个4KB块),因此包含80个inodes(假设每个inode为256字节);进一步假设inode区域从12KB开始(即超级块从0KB开始,inode位图在地址4KB处,数据位图在8KB处,因此inode表紧随其后)。在vsfs中,我们因此有以下布局来表示文件系统分区开头部分的情况(特写视图):

image-20240417125143762

要读取number为 32 的 inode,文件系统首先要计算 inode 区域的偏移量($32 \cdot sizeof (inode)$ 或 $8192$),将其与磁盘上 inode 表的起始地址(inodeStartAddr = 12KB)相加,从而得出所需 inode 块的正确字节地址:20KB。回想一下,磁盘不是字节寻址的,而是由大量可寻址扇区(通常为 512 字节)组成。因此,要获取包含 inode 32 的 inode 块,文件系统将向 $\frac{20×1024}{512}$ 扇区或 40 扇区发出读取命令,以获取所需的 inode 块。更一般地说,inode 块的扇区地址sector可按如下方式计算:

1
2
blk = (inumber * sizeof(inode_t)) / blockSize;
sector = ((blk * blockSize) + inodeStartAddr) / sectorSize;

每个 inode 内部实际上包含了文件所需的所有信息:文件类型(例如常规文件、目录等)、大小、分配给它的块数、保护信息(例如谁拥有该文件、以及谁可以访问它)、一些时间信息,包括文件创建、修改或上次访问的时间,以及有关其数据块驻留在磁盘上的位置的信息(例如某种指针)。我们将有关文件的所有此类信息称为元数据;事实上,文件系统中除了纯用户数据之外的任何信息通常被称为元数据。 ext2中的一个inode示例如下图所示。

image-20240417130232939

inode 设计中最重要的决策之一是它如何引用数据块的位置。一种简单的方法是在 inode 内有一个或多个直接指针(磁盘地址);每个指针指向属于该文件的一个磁盘块。这种方法是有限的:例如,如果您想要一个非常大的文件(例如,大于块大小乘以 inode 中的直接指针数量),那么您就不走运了。

3.2 多级索引

为了支持更大的文件,文件系统设计者不得不在 inodes 中引入不同的结构。一种常见的想法是使用一种被称为间接指针的特殊指针。它不指向包含用户数据的块,而是指向包含更多指针的块,每个指针都指向用户数据。因此,一个 inode 可能有一定数量的直接指针(如 12 个)和一个间接指针。如果文件长得足够大,就会分配一个间接块(来自磁盘的数据块区域),并将间接指针的 inode 插槽设置为指向它。假设有 4KB 的数据块和 4 字节的磁盘地址,则又增加了 1024 个指针;文件可以增长到 $(12 + 1024) \cdot 4K$ 或 4144KB。

毫不奇怪,在这种方法中,您可能希望支持更大的文件。要做到这一点,只需向inode添加另一个指针:双间接指针。该指针指向一个包含指向间接块的指针的块,每个间接块都包含对数据块的指针。因此,双间接块增加了通过额外 $1024 × 1024$ 或 100 万个 4KB 块来扩展文件的可能性,换句话说支持超过 4GB 大小的文件。然而您可能需要更多,并且我们打赌您知道这将导致什么:三重间接指针

总体而言,这种不平衡树被称为多级索引方法来定位文件块。让我们以十二个直接指针为例进行研究,并且还有单间接块和双间接块。假设每个块大小为 4 KB,并且每个指针占用 4 字节,则该结构可以容纳略大于 4 GB 大小的文件(即 $(12 + 1024 + 1024^2) × 4 KB)$。您能计算出通过添加三重间接块可以处理多大尺寸的文件吗?($1024^3$)

许多文件系统使用多级索引,其中包括常用文件系统如 Linux ext2和ext3、NetApp 的WAFL ,以及原始 UNIX 文件系统等等 。其他一些文件系统如 SGI XFS 和 Linux ext4 使用范围而不是简单指针(它们类似于虚拟内存讨论中段)。

考虑基于范围的方法

另一种方法是使用范围而不是指针。范围只是一个磁盘指针加上一个长度(以块为单位);因此,不需要为文件的每个块提供一个指针,而只需要一个指针和一个长度来指定文件在磁盘上的位置。只有单个范围是有限的,因为在分配文件时可能很难找到磁盘上连续的可用空间块。因此,基于盘区的文件系统通常允许多个盘区,从而在文件分配期间为文件系统提供了更多的自由度。

比较这两种方法,基于指针的方法最灵活,但每个文件使用大量元数据(特别是对于大文件)。基于范围的方法不太灵活,但更紧凑;特别是,当磁盘上有足够的可用空间并且文件可以连续布置时(这实际上是任何文件分配策略的目标),它们可以很好地工作。

您可能想知道:为什么要使用这样的不平衡树?为什么不采用其他方法呢?事实证明,许多研究人员研究了文件系统及其使用方式,几乎每次他们都会发现几十年来一直存在的某些“真理”。其中一项发现是大多数文件都很小。这种不平衡的设计反映了这样的现实;如果大多数文件确实很小,那么针对这种情况进行优化是有意义的。因此,使用少量的直接指针(典型数字为 12),一个 inode 可以直接指向 48 KB 的数据,而对于较大的文件则需要一个(或多个)间接块。Agrawal等人 最近的一项研究总结了这些结果。

image-20240417143438784

当然,在inode设计的空间中,还存在许多其他的可能性;毕竟,inode 只是一种数据结构,任何存储相关信息并能够有效查询的数据结构就足够了。由于文件系统软件很容易更改,因此如果工作负载或技术发生变化,您应该愿意探索不同的设计。

4 目录组织

在 vsfs 中(与许多文件系统一样),目录的组织很简单;目录基本上只包含(条目名称,inode number)对的列表。对于给定目录中的每个文件或目录,目录的数据块中有一个字符串和一个数字。对于每个字符串,也可能有一个长度(假设名称可变)。

例如,假设目录 dir(inode number为 5)中包含三个文件(foobarfoobar_is_a_pretty_longname ),inode number分别为 12、13 和 24。 dir 的磁盘数据可能如下所示:

image-20240417150136421

在此示例中,每个条目都有一个 inode number、记录长度(名称的总字节数加上任何剩余空间)、字符串长度(名称的实际长度),最后是条目的名称。请注意,每个目录都有两个额外的条目:.点 “和... “点-点”;点目录只是当前目录(本例中为 dir),而点-点是父目录(本例中为根目录)。

删除文件(例如调用 unlink())可能会在目录中间留下一个空位,因此也应该有某种方法来标记这个空位(例如使用保留的 inode number,如 0)。这种删除是使用记录长度的原因之一:新的条目可能会重复使用旧的、更大的条目,因此会有额外的空间。

基于链接的方法

设计 inode 的另一种更简单的方法是使用链表。因此,在inode内,您不需要多个指针,而只需要一个指针来指向文件的第一个块。要处理更大的文件,请在该数据块的末尾添加另一个指针,依此类推,这样就可以支持大文件。

正如您可能已经猜到的,链接文件分配对于某些工作负载来说表现不佳;例如,考虑读取文件的最后一个块,或者只是进行随机访问。因此,为了使链接分配更好地工作,一些系统将在内存中保留链接信息表,而不是将下一个指针与数据块本身一起存储。该表由数据块D的地址索引;条目的内容只是 D 的下一个指针,即文件中 D 后面的下一个块的地址。空值也可能存在(指示文件结束),或者其他一些标记来指示特定块是空闲的。拥有这样的下一个指针表使得链接分配方案可以有效地进行随机文件访问,只需首先扫描(在内存中)表以找到所需的块,然后直接访问(在磁盘上)它。

这样的表是不是听起来很熟悉?我们所描述的是文件分配表FAT 文件系统的基本结构。是的,这个经典的旧 Windows 文件系统(在 NTFS之前)基于简单的基于链接的分配方案。与标准 UNIX 文件系统还存在其他差异;例如,本身没有inodes,而是存储有关文件的元数据并直接引用该文件的第一个块的目录条目,这使得创建硬链接变得不可能。

您可能想知道目录到底存储在哪里。通常,文件系统将目录视为一种特殊类型的文件。因此,目录在 inode 表中的某个位置有一个 inode(inode 的 type 字段标记为“目录”而不是“常规文件”)。该目录具有inode指向的数据块(也许还有间接块);这些数据块位于我们简单文件系统的数据块区域中。因此,我们的磁盘结构保持不变。

我们还应该再次注意,这个简单的线性目录条目列表并不是存储此类信息的唯一方法。和以前一样,任何数据结构都是可能的。例如,XFS以 B 树形式存储目录,使文件创建操作(必须确保文件名在创建之前未使用过)比具有必须在其目录中完整扫描的简单列表的系统更快。

5 空闲空间管理

文件系统必须跟踪哪些 inodes 和数据块是空闲的,哪些不是,以便在分配新文件或目录时能为其找到空间。因此,空闲空间管理对所有文件系统都很重要。在 vsfs 中,我们有两个简单的位图可以完成这项任务。

例如,当我们创建一个文件时,必须为该文件分配一个 inode。因此,文件系统将在位图中搜索空闲的 inode,并将其分配给文件;文件系统必须将 inode 标记为已用(用 1 表示),并最终用正确的信息更新磁盘位图。在分配数据块时,也会进行类似的操作。

在为新文件分配数据块时,还可能需要考虑一些其他因素。例如,一些 Linux 文件系统(如 ext2ext3)在创建新文件并需要数据块时,会寻找一连串空闲的块(如 8 个);通过找到这样一连串空闲的块,然后将它们分配给新创建的文件,文件系统可以保证文件的一部分在磁盘上是连续的,从而提高性能。因此,这种预分配策略是为数据块分配空间时常用的启发式方法。

管理可用空间的方法有很多种;位图只是一种方式。一些早期的文件系统使用空闲列表,其中超级块中的单个指针被保留指向第一个空闲块;在该块内,保留下一个空闲指针,从而形成系统空闲块的列表。当需要一个块时,使用头块并相应地更新列表。

现代文件系统使用更复杂的数据结构。例如,SGI 的 XFS使用某种形式的 B 树来紧凑地表示磁盘的哪些块是空闲的。与任何数据结构一样,不同的时空权衡都是可能的。

6 访问路径:读写

既然我们对文件和目录在磁盘上的存储方式有了一定的了解,我们就应该能够在读取或写入文件的过程中跟踪操作流程。因此,了解访问路径上发生的事情是理解文件系统如何工作的第二个关键;请注意!

在下面的示例中,我们假设文件系统已经加载,因此超级块已经在内存中。其他一切(即 inodes、目录)仍在磁盘上。

6.1 从磁盘读取文件

在这个简单的示例中,我们首先假设您只想简单地打开一个文件(例如 /foo/bar),读取它,然后关闭它。对于这个简单的示例,我们假设文件大小仅为 12KB(即 3 个块)。

当发出 open("/foo/bar", O RDONLY) 调用时,文件系统首先需要找到文件 barinode,以获取有关文件的一些基本信息(权限信息、文件大小等) 。为此,文件系统必须能够找到inode,但它现在拥有的只是完整路径名。文件系统必须遍历路径名,从而找到所需的inode。

所有遍历都从文件系统的根目录(简称为 /)开始。因此,FS首先从磁盘读取的是根目录的inode。但是这个索引节点在哪里呢?要找到一个 inode,我们必须知道它的 i-number。通常,我们在其父目录中查找文件或目录的 i-number;根没有父父目录(根据定义)。因此,根 inode number必须是“众所周知的”;当文件系统被挂载时,FS必须知道它是什么。在大多数 UNIX 文件系统中,根 inode number为 2。因此,为了开始该过程,FS 读取包含 inode number为2 的块(第一个 inode 块)。

一旦读入 inode,FS 就可以在其中查找指向数据块的指针,其中包含根目录的内容。因此,FS 将使用这些磁盘上的指针来读取目录,在本例中查找 foo 的条目。通过读入一个或多个目录数据块,它将找到 foo 的条目;一旦找到,FS 也将找到接下来需要的 foo 的 inode number(假设是 44)。

下一步是递归遍历路径名,直到找到所需的 inode。在这个例子中,FS读取包含foo的inode的块,然后读取其目录数据,最后找到bar的inode number。 open() 的最后一步是将 bar 的 inode 读入内存;然后,FS 进行最终的权限检查,在每个进程的打开文件表中为此进程分配一个文件描述符,并将其返回给用户。

打开后,程序可以发出 read() 系统调用来读取文件。因此,第一次读取(在偏移量 0 处,除非已调用 lseek())将读取文件的第一个块,并查询 inode 以查找该块的位置;它还可能用新的上次访问时间更新inode。读取将进一步更新该文件描述符的内存中打开文件表,更新文件偏移量,以便下一次读取将读取第二个文件块等。

在某个时刻,文件将被关闭。这里要做的工作要少得多;显然,文件描述符应该被释放,但现在,这就是 FS 真正需要做的。不发生任何磁盘 I/O。

下图描述了整个过程;时间在图中向下增加。在图中,打开文件会导致发生大量读取,以便最终找到文件的 inode。之后,读取每个块需要文件系统首先查看 inode,然后读取该块,然后通过写入更新 inode 的上次访问时间字段。

image-20240417154339356

另请注意,打开操作生成的 I/O 量与路径名的长度成正比。对于路径中的每个附加目录,我们必须读取其inode及其数据。大型目录的存在会使情况变得更糟;在这里,我们只需要读取一个数据块来获取目录的内容,而对于一个大目录,我们可能需要读取许多数据块才能找到所需的条目。

6.2 从磁盘写入文件

写入文件的过程与此类似。首先,必须打开文件(如上所述)。然后,应用程序可以发出 write() 调用,用新内容更新文件。最后,关闭文件。

与读取不同的是,向文件写入也可能分配一个数据块(除非该数据块被覆盖等)。在写入一个新文件时,每次写入不仅要向磁盘写入数据,还要首先决定向文件分配哪个块,并相应地更新磁盘的其他结构(如数据位图和 inode)。因此,对文件的每次写入在逻辑上会产生 5 次 I/O:

  • 一次读取数据位图(然后更新数据位图,将新分配的块标记为已使用);
  • 一次写入数据位图(将其新状态反映到磁盘上);
  • 两次读取并写入 inode(根据新块的位置更新 inode);
  • 最后一次写入实际块本身。

如果考虑到创建文件这种简单而常见的操作,写入流量甚至会更大。要创建一个文件,文件系统不仅要分配一个 inode,还要在包含新文件的目录中分配空间。这样做的 I/O 总流量相当大:

  • 一次读取 inode 位图(查找空闲的 inode);
  • 一次写入 inode 位图(标记已分配);
  • 一次写入新 inode 本身(初始化);
  • 一次写入目录数据(将文件的高级名称与其 inode number联系起来);
  • 以及一次读取和写入目录 inode 以更新它。

如果目录需要增长以容纳新的条目,还需要额外的 I/O(即数据位图和新目录块)。所有这些都只是为了创建一个文件!

让我们看一个具体的示例,其中创建了文件 /foo/bar,并向其中写入了三个块。下图显示了 open()(创建文件)期间以及三个 4KB 写入的每一个期间发生的情况。

image-20240417160026092

在图中,对磁盘的读取和写入按照引起它们发生的系统调用进行分组,并且它们可能发生的粗略顺序从图的顶部到底部。您可以看到创建文件的工作量:在本例中需要 10 个 I/O,遍历路径名,然后最终创建文件。您还可以看到,每次分配写入都会花费 5 个 I/O:一对读取和更新 inode,另一对读取和更新数据位图,最后写入数据本身。

文件系统如何以合理的效率完成这些任务?即使是最简单的操作,如打开、读取或写入文件,也会产生大量分散在磁盘上的 I/O 操作。文件系统如何才能降低这么多 I/O 操作带来的高昂成本呢?

7 缓存和缓冲

正如上面的例子所示,读写文件的成本很高,需要对(慢速)磁盘进行多次 I/O。大多数文件系统都会积极使用系统内存(DRAM)来缓存重要的数据块,以解决明显存在的巨大性能问题。

想象一下上面的打开示例:如果没有缓存,每次打开文件都需要对目录层次结构中的每一级进行至少两次读取(一次读取相关目录的 inode,至少一次读取其数据)。对于长路径名(例如,/1/2/3/.../100/file.txt),文件系统光是打开文件就需要执行数百次读取!

因此,早期的文件系统引入了固定大小的缓存来保存常用数据块。就像我们在讨论虚拟内存时一样,LRU 等策略和不同的变体将决定在缓存中保留哪些区块。这种固定大小的缓存通常在启动时分配,大约占总内存的 10%。

然而,这种静态的内存分区可能会造成浪费;如果文件系统在某个时间点不需要 10%的内存怎么办?如果采用上述固定大小的方法,文件缓存中未使用的页面就无法重新用于其他用途,从而造成浪费。

相比之下,现代系统采用的是动态分区方法。具体来说,许多现代操作系统将虚拟内存页和文件系统页整合到统一的页面缓存中。这样,内存就可以更灵活地分配给虚拟内存和文件系统,具体取决于哪个系统在特定时间需要更多内存。

了解静态与静态动态分区

在不同的客户端/用户之间划分资源时,可以使用静态分区或动态分区。静态方法只是将资源一次划分为固定比例;例如,如果有两个可能的内存用户,您可以将一些固定部分的内存分配给一个用户,并将其余部分分配给另一个用户。动态方法更加灵活,随着时间的推移提供不同数量的资源;例如,一个用户可能在一段时间内获得较高百分比的磁盘带宽,但随后,系统可能会切换并决定为不同的用户提供更大比例的可用磁盘带宽。

每种方法都有其优点。静态分区可确保每个用户获得一定的资源份额,通常可以提供更可预测的性能,并且通常更容易实现。动态分区可以实现更好的利用率(通过让资源匮乏的用户消耗原本空闲的资源),但实现起来可能更复杂,并且如果用户的闲置资源被其他用户占用,在需要时需要很长时间才能收回,则会导致性能下降。通常情况下,没有最好的方法;相反,你应该思考手头的问题,然后决定哪种方法最合适。

现在想象一下带缓存的文件打开示例。第一次打开可能会产生大量的 I/O 流量来读取目录 inode 和数据,但同一文件(或同一目录中的文件)的后续文件打开大部分会在缓存中进行,因此不需要 I/O。

我们还要考虑缓存对写入的影响。如果缓存足够大,就可以完全避免读取 I/O,而写入流量必须进入磁盘才能持久化。因此,缓存对写入流量的过滤器与对读取流量的过滤器不同。也就是说,写缓冲确实具有许多性能优势。

  • 首先,通过延迟写入,文件系统可以将一些更新批处理到较小的一组 I/O 中;例如,如果在创建一个文件时更新inode位图,然后在创建另一个文件时更新inode位图,则文件系统会通过在第一次更新后延迟写入来节省 I/O。
  • 其次,通过在内存中缓冲大量写入,系统可以调度后续 I/O,从而提高性能。
  • 最后,有些写入可以通过延迟来完全避免。例如,如果应用程序创建了一个文件然后将其删除,则延迟写入以将文件创建反映到磁盘可以完全避免它们。在这种情况下,懒惰(将块写入磁盘)是一种美德。

基于上述原因,大多数现代文件系统都会在内存中缓冲写入 5 到 30 秒,这也是另一种权衡:如果系统在更新传播到磁盘之前崩溃,更新就会丢失;但是,如果在内存中保留更长时间,就可以通过批处理、调度甚至避免写入来提高性能。

了解持久/性能的权衡

存储系统通常会向用户提供持久/性能的权衡。如果用户希望写入的数据立即持久,系统必须全力将新写入的数据提交到磁盘,因此写入速度很慢(但安全)。但是,如果用户可以容忍少量数据的丢失,系统可以在内存中缓冲写入一段时间,然后将其写入磁盘(在后台)。这样做会使写入看起来很快完成,从而提高感知性能;但是,如果发生崩溃,尚未提交到磁盘的写入将会丢失,因此需要进行权衡。要了解如何正确进行这种权衡,最好了解使用存储系统的应用程序需要什么;例如,虽然丢失网络浏览器下载的最后几张图像可能是可以容忍的,但丢失向您的银行帐户添加资金的数据库事务的一部分可能会更难以容忍。

有些应用程序(如数据库)并不喜欢这种权衡。因此,为了避免因写入缓冲而造成意外数据丢失,它们只需通过调用fsync()、使用绕过缓存的直接 I/O 接口或使用原始磁盘接口来强制写入磁盘,从而完全避开文件系统。虽然大多数应用程序都能接受文件系统的取舍,但如果默认情况不能令人满意,也有足够的控制措施让系统按照你的意愿行事。


相关内容

Buy me a coffee~
HeZephyr 支付宝支付宝
HeZephyr 微信微信
0%