>

英文原文:
http://pages.cs.wisc.edu/~remzi/OSTEP/file-implementation.pdf

标题:简单文件系统的实现

这篇文章介绍一个简单的文件系统实现,称为 vsfs(非常简单的文件系统)。该文件系统是典型UNIX文件系统的简化版本,因此可用于介绍当今许多文件系统中的一些基本磁盘结构,访问方法和各种策略。

文件系统是纯软件;与我们开发的CPU和内存虚拟化不同,我们不会添加硬件功能来使文件系统的某些方面更好地工作(尽管我们将要注意设备特征以确保文件系统正常运行)。由于我们在构建文件系统方面具有很大的灵活性,因此从AFS(Andrew文件系统)[H + 88]到ZFS(Sun的Zettabyte文件系统)[B07]的字面上构建了许多不同的文件系统。所有这些文件系统都有不同的数据结构,并且做得比其他同行更好或更差。因此,我们将通过案例研究来了解我们将如何学习文件系统:首先,本章介绍一个简单的文件系统(vsfs),介绍大多数概念,然后对真实文件系统进行一系列研究,以了解它们在实践。

文件系统的功能

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

首先是文件系统的数据结构。换句话说,文件系统使用什么类型的磁盘结构来组织其数据和元数据?我们将看到的第一个文件系统(包括下面的vsfs)使用简单的结构,如块或其他对象的数组,而更复杂的文件系统(如SGI的XFS)使用更复杂的基于树的结构[S + 96]。

文件系统的第二个方面是访问方法。它如何将进程所调用的映射映射到它的结构上?比如open() read() write()等等。在执行特定系统调用期间读取哪些结构?哪些被写?所有这些步骤的执行效率如何?

如果您了解文件系统的数据结构和访问方法,那么您已经开发了一个关于它如何工作的良好mental model,这是系统思维的一个关键部分。在深入研究我们的第一个实施时,努力开发您的mental model

文件系统的组织

可以参考鸟哥的Linux私房菜 p199-200

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

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

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

正如我们在最后一章中了解到的,文件系统必须跟踪每个文件的信息。 这些信息是元数据的关键部分,可以跟踪数据块(在数据区域中)包含文件,文件大小,所有者和访问权限,访问和修改时间以及其他类似的 信息。 为了存储这个信息,文件系统通常有一个叫inode的结构(我们将在下面阅读更多关于inode的内容)。

为了存放inode,我们还需要为磁盘保留一些空间。 让我们将这部分磁盘称为inode表,它只保存一个磁盘inode数组。 因此,假设我们将64个块中的5块用于inode(由图中的I表示),我们的磁盘映像现在看起来就像这张图片:

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

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

当然,许多分配跟踪方法都是可能的。例如,我们可以使用一个空闲列表指向第一个空闲块,然后指向下一个空闲块,等等。我们选择一个简单流行的结构,称为位图 bitmap,一个用于数据区域(数据位图),另一个用于 inode表(inode位图)。位图是一个简单的结构:每个位用于指示相应的对象/块是空闲的(0)还是正在使用的(1)。因此,我们新的磁盘布局,包含一个 inode位图(i)和一个数据位图(d):

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

仔细的读者(即仍然醒着的读者)可能已经注意到,在我们非常简单的文件系统的磁盘结构设计中还剩一块。 我们保留这个超级块,在下图中用S表示。 超级块包含关于该特定文件系统的信息,包括例如文件系统中有多少 inode 和数据块(在这个例子中分别为80和56),inode 表开始(块3)等等。 它可能还会包含某种类型的幻数以识别文件系统类型(在本例中为vsfs)。

实验

在我的服务器上进行相关的测试:

1
2
3
4
5
6
7
8
9
10
~$ df
Filesystem 1K-blocks Used Available Use% Mounted on
udev 1005964 0 1005964 0% /dev
tmpfs 204832 3280 201552 2% /run
/dev/vda1 41151808 4352784 34685596 12% /
tmpfs 1024144 0 1024144 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 1024144 0 1024144 0% /sys/fs/cgroup
tmpfs 204832 0 204832 0% /run/user/0
tmpfs 204832 0 204832 0% /run/user/1001

用 df 显示挂载的设备,可以看到 /dev/vda1 表示主要的 fs

查看这个块,可以获得一个很长的信息:

1
2
3
4
~$ dumpe2fs /dev/vda1
dumpe2fs 1.42.13 (17-May-2015)
dumpe2fs: Permission denied while trying to open /dev/vda1
Couldn't find valid filesystem superblock.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
~$ sudo dumpe2fs /dev/vda1
dumpe2fs 1.42.13 (17-May-2015)
Filesystem volume name: /
Last mounted on: /
Filesystem UUID: e2048966-750b-4795-a9a2-7b477d6681bf
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags: signed_directory_hash
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 2621440
Block count: 10485248
Reserved block count: 524261
Free blocks: 9890156
Free inodes: 2545206
First block: 0
Block size: 4096
Fragment size: 4096
Reserved GDT blocks: 1021
Blocks per group: 32768
Fragments per group: 32768
Inodes per group: 8192
Inode blocks per group: 512
RAID stride: 32639
Flex block group size: 16
Filesystem created: Thu Aug 17 15:42:55 2017
Last mount time: Thu Oct 26 20:34:20 2017
Last write time: Fri Oct 27 04:34:18 2017
Mount count: 1
Maximum mount count: -1
Last checked: Fri Oct 27 04:34:18 2017
Check interval: 0 (<none>)
Lifetime writes: 3921 MB
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 256
Required extra isize: 28
Desired extra isize: 28
Journal inode: 8
First orphan inode: 931259
Default directory hash: half_md4
Directory Hash Seed: 24cbf2f0-10b9-4f68-9cdd-33073e41c59a
Journal backup: inode blocks
Journal features: journal_incompat_revoke
Journal size: 128M
Journal length: 32768
Journal sequence: 0x00139661
Journal start: 7881
Group 0: (Blocks 0-32767) [ITABLE_ZEROED]
Checksum 0xeaa0, unused inodes 3739
Primary superblock at 0, Group descriptors at 1-3
Reserved GDT blocks at 4-1024
Block bitmap at 1025 (+1025), Inode bitmap at 1041 (+1041)
Inode table at 1057-1568 (+1057)
20947 free blocks, 3886 free inodes, 474 directories, 3739 unused inodes
Free blocks: 11821-32767
Free inodes: 664, 3956, 4070, 4082, 4104, 4112, 4119, 4122, 4126-4127, 4140, 4147, 4166, 4173, 4176, 4180, 4183, 4187, 4189, 4193, 4207, 4217, 4244, 4247, 4270, 4274, 4277, 4286-4287, 4292-4296, 4298, 4301-4302, 4305-4306, 4309, 4311, 4313-4315, 4318-4319, 4321-4323, 4326-4327, 4329-4332, 4334-4339, 4341-4343, 4345-4347, 4349-4351, 4353-4355, 4357-4359, 4361-4363, 4365-4367, 4369-4371, 4373-4378, 4384-4387, 4389-4390, 4393-4395, 4397-4399, 4401, 4403, 4405-4407, 4409, 4411, 4413-4416, 4418-4422, 4425-4427, 4429-8192

可以看到 Ubuntu 对应的文件信息。

文件组织与 inode

ASIDE:数据结构 - INODE
inode是在许多文件系统中使用的通用名称,用于描述保存给定文件的元数据的结构,例如其长度,权限和组成块的位置。 这个名称至少可以追溯到UNIX(如果不是早期的系统,可能还会回到Multics); 它是索引节点的简称,因为索引节点号用于索引磁盘inode数组,以便查找该索引节点的索引节点。 我们将看到,inode的设计是文件系统设计的一个关键部分。 大多数现代系统对于它们跟踪的每个文件都有这样的结构,但也许称它们为不同的东西(如dnodes,fnodes等)。

文件系统最重要的磁盘结构之一是inode;几乎所有的文件系统都有类似的结构。名称inode是索引节点的缩写,它是由UNIX开发人员Ken Thompson [RT74]给出的历史名称,因为这些节点最初排列在一个数组中,并且在访问特定索引节点时索引了数组。

每个inode都被一个数字(称为inumber)隐式引用,我们之前称之为文件的低级名称。在vsfs(和其他简单的文件系统)中,给定一个i-number,你应该能够直接计算磁盘上相应节点的位置。例如,如上所述,获取vsfs的inode表:大小为20 KB(5个4 KB块),因此由80个inode(假设每个inode为256个字节)组成;进一步假设inode区域从12KB开始(即,超级块从0KB开始,inode位图在4KB地址,数据位图在8KB,因此inode表紧随其后)。在vsfs中,我们因此在文件系统分区的开始部分(在特写视图中)具有以下布局:

inode summary

要读取inode编号32,文件系统将首先计算inode区域的偏移量(32·sizeof(inode)或8192),将其添加到磁盘inode表的起始地址(inodeStartAddr = 12KB),然后到达所需的inode块的正确字节地址:20KB。重新调用磁盘不是字节可寻址的,而是由大量的可寻址扇区组成,通常是512字节,因此,要获取包含inode的块inode 32,文件系统将向扇区发出读取
20×1024或40,以获取所需的inode块。

可以如下计算inode块的地址

$iaddr:blk =(inumber sizeof(inode_t))/ blockSize$;
$sector =((blk blockSize)+ inodeStartAddr)/ sectorSize;$

在每个inode中,实际上是所有关于文件的信息:其类型(例如,常规文件,目录等),大小,分配给它的块数,保护信息(如谁拥有文件,以及谁可以访问它),一些时间信息,包括文件的创建,修改或上次访问时间,以及有关其数据块在磁盘上的位置(例如某种指针)的信息。我们将所有关于文件的信息称为元数据;实际上,文件系统内部不是纯粹的用户数据的任何信息通常都是这样的。 ext2 [P09]中的一个inode示例如下表40.1所示。

inode table

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

多级索引
为了支持更大的文件,文件系统设计者必须在inode中引入不同的结构。一个常见的想法是有一个称为间接指针的特殊指针。它不是指向包含用户数据的块,而是指向包含更多指针的块,每个指针指向用户数据。因此,inode可以具有一些固定数量的直接指针(例如12)和单个间接指针。如果文件变得足够大,则会分配一个间接块(来自磁盘的数据块区域),并将inode的间接指针槽设置为指向它。假设一个块是4KB和4个字节的磁盘地址,则会增加1024个指针;该文件可以增长为(12 + 1024)·4K或4144KB。

直接指针:inode条目–>block(存的是文件数据)

​ 间接指针:inode条目–>block(存的是指针)–>block(存的是文件数据)

​ 二重指针:inode条目–>block(存的是指针)–>block(存的是指针)–>block(存的是文件数据)

​ 三重指针:inode条目–>block(存的是指针)–>block(存的是指针)–>block(存的是指针)–>block(存的是文件数据)

毫不奇怪,在这种方法中,您可能想要支持更大的文件。为此,只需向inode添加另一个指针:双重间接指针。该指针指向包含指向间接块的指针的块,每个块都包含指向数据块的指针。因此,双重间接块增加了使用额外的1024×1024或1百万个4KB块扩展文件的可能性,换句话说,支持超过4GB大小的文件。不过,你可能想要更多,我们打赌你知道这是在哪里:三重间接指针。

总的来说,这种不平衡的树被称为指向文件块的多级索引方法。我们来看一个带有十二个直接指针的例子,以及一个单独的和一个双重的间接块。假设块大小为4 KB,并且指针为4字节,则该结构可以容纳一个仅超过4 GB的文件(即(12 + 1024 + 10242)×4 KB)。你能否通过增加一个三重间接块来判断文件的大小? (提示:很大)

许多文件系统使用多级索引,包括常用文件系统,如Linux ext2 [P09]和ext3,NetApp的WAFL以及原始UNIX文件系统。其他文件系统,包括SGI XFS和Linux ext4,使用扩展而不是简单的指针;有关基于范围的方案如何工作的细节(它们与虚拟内存讨论中的细分类似)。

您可能想知道:为什么使用这样的不平衡树?为什么不采用不同的方法?那么,事实证明,许多研究人员已经研究过文件系统以及它们的使用方式,几乎每次他们发现几十年来都有的“真相”。一个这样的发现是大多数文件很小。这种不平衡的设计反映了这样的现实;如果大多数文件确实很小,那么为这种情况优化是有意义的。因此,使用少量直接指针(12是典型数字),inode可以直接指向48 KB的数据,需要一个(或多个)间接块来存放较大的文件。见Agrawal等人。 a [A + 07]为最近的研究;表40.2总结了这些结果。

设计inode的另一个更简单的方法是使用链表。因此,在一个inode中,而不是有多个指针,你只需要一个,指向文件的第一个块。要处理较大的文件,请在该数据块的末尾添加另一个指针等,这样就可以支持大文件。
正如您可能已经猜到的那样,链接文件分配对于某些工作负载表现不佳;例如,考虑阅读文件的最后一个块,或者只是进行随机访问。因此,为了使链接分配更好地工作,一些系统将保留链接信息的内存表,而不是将下一个指针与数据块本身一起存储。该表由数据块D的地址索引;一个条目的内容只是D的下一个指针,即D后面的文件中的下一个块的地址。空值也可以在那里(指示文件结束)或其他标记来指示一个特定的块是免费的。拥有这样的下一个指针表使得链接分配机制可以有效地进行随机文件访问,只需首先扫描(在内存中)表来查找所需的块,然后直接访问(在磁盘上)。

这样的table听起来很熟吗?我们所描述的是所谓的文件分配表或FAT文件系统的基本结构。是的,在NTFS [C94]之前,这款经典的旧Windows文件系统基于简单的基于链接的分配方案。与标准的UNIX文件系统还有其他不同之处;例如,本身没有inode,而是存储关于文件的元数据的目录条目,并且直接指向所述文件的第一个块,这使得不可能创建硬链接。参见Brouwer [B02]了解更多不雅的细节。

目录组织
在vsfs中(如在许多文件系统中),目录有一个简单的组织;一个目录基本上只包含一个(入口名,inode号)对的列表。对于给定目录中的每个文件或目录,目录的数据块中都有一个字符串和一个数字。对于每个字符串,可能还有一个长度(假定可变大小的名称)。

例如,假设目录dir(inode number 5)中有三个文件(foo,bar和foobar),它们的inode编号分别为12,13和24。 dir的磁盘数据可能如下所示:

inum | reclen | strlen | name
5 4 2 .
2 4 3 ..
12 4 4 foo
13 4 4 bar
24 8 7 foobar
在本例中,每个条目都有一个inode编号,记录长度(名称的总字节数加上空格剩余空间),字符串长度(名称的实际长度)以及条目的名称。请注意,每个目录有两个额外的条目,。 “点”和“点 - 点”; dot目录就是当前目录(在本例中为dir),而dot-dot是父目录(在本例中为根目录)。

删除一个文件(例如调用unlink())会在目录中间留下一个空白空间,因此应该有一些方法来标记它(例如,保留的inode数字,例如零)。这种删除是使用记录长度的一个原因:新条目可能会重复使用旧的,更大的条目,从而在其中留有额外的空间。

您可能想知道确切的目录存储在哪里。通常,文件系统将目录视为特殊类型的文件。因此,目录有一个inode,inode表中的某处(inode表中的inode标记为“目录”的类型字段而不是“常规文件”)。该目录具有由inode指向的数据块(也可能是间接块);这些数据块位于我们简单文件系统的数据块区域中。我们的磁盘结构因此保持不变。
我们还应该再次注意到,这个简单的线性目录列表并不是存储这些信息的唯一方法。如前所述,任何数据结构都是可能的。例如,XFS [S + 96]以B-树形式存储目录,使文件创建操作(必须确保文件名在创建之前未被使用)比具有必须在其中扫描的简单列表的系统更快整体。

空闲空间管理
创建文件 –> 为你的文件申请inode–> 从块中的inode bitmap中寻找到对应的inode –> 把inode标注为used.

空闲空间的管理方式不唯一,你可以使用freelist, 也可以使用B-Tree(LIKE XFS).

读写文件:实际发生的故事
现在我们已经知道文件和目录如何存储在磁盘上,我们应该能够在读取或写入文件的过程中遵循操作流程。理解这个访问路径上发生的事情是开发人员理解文件系统如何工作的第二个关键;请注意!
对于下面的例子,让我们假设文件系统已经被挂载,因此超级块已经在内存中。其他所有内容(即inode,目录)仍在磁盘上。

从磁盘读取文件
在这个简单的例子中,让我们先假设你想简单地打开一个文件(例如/ foo / bar,读取它,然后关闭它)。对于这个简单的例子,假设文件的大小只有4KB(即, 1块)。

当您发出打开(“/ foo / bar”,ORDONLY)调用时,文件首先需要找到文件栏的inode,以获取有关文件的一些基本信息(权限信息,文件大小等)。要做到这一点,他的文件系统必须能够找到inode,但它现在的所有文件都是完整的路径名。文件系统必须遍历路径名,从而找到所需的inode。

所有遍历都从文件系统的根目录开始,在根目录中简单地称为/。因此,FS从磁盘读取的第一件事是根目录的inode。但是这个inode在哪里?要找到一个inode,我们必须知道它的i号码。通常,我们在其父目录中找到文件或目录的i编号;根没有父(根据定义)。因此,根节点号必须是“众所周知的”;当安装文件系统时,FS必须知道它是什么。在大多数UNIX文件系统中,根索引节点编号为2.因此,要开始该进程,FS会读入包含索引节点编号2(第一个索引节点块)的块。

一旦inode被读入,FS可以在里面查找指向数据块的指针,数据块包含根目录的内容。因此,FS将使用这些磁盘上的指针来读取目录,在这种情况下寻找foo的条目。通过读入一个或多个目录数据块,它将找到foo的条目;一旦找到,FS也会找到下一个需要的foo的inode数量(比如44)。

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

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

在某个时候,文件将被关闭。这里要做的工作要少得多;很明显,文件描述符应该被释放,但现在,这是所有FS真正需要做的。没有磁盘I / O发生。

整个过程的描述见图40.3(时间向下)。在该图中,打开导致无数次读取以便最终找到文件的索引节点。之后,读取每个块需要文件系统首先查询inode,然后读取该块,然后使用写入更新inode的最后访问时间字段。花一些时间,试着理解发生了什么。

还要注意open所产生的I / O的数量与路径名的长度成正比。对于路径中的每个附加目录,我们都必须读取它的inode及其数据。更糟糕的是会出现大型目录;在这里,我们只需要读取一个块来获取目录的内容,而对于大目录,我们可能需要读取很多数据块才能找到所需的条目。是的,阅读文件时生活会变得很糟糕;正如你将要发现的那样,写一个文件(尤其是创建一个新文件)更糟糕。

read in fs

写入
write&create in fs

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

与读取不同,写入文件也可能会分配一个块(例如,除非块被覆盖)。当写出新文件时,每次写入不仅必须将数据写入磁盘,而且必须首先决定将哪个块分配给文件,并相应地更新磁盘的其他结构(例如,数据位图)。因此,对文件的每次写入在逻辑上会生成三个I / O:一个读取数据位图,然后更新它以标记所使用的新分配的块,一个写入位图(以反映其新的状态到磁盘),和一个写实际的块本身。

如果考虑简单和常见的操作(例如文件创建),写入流量会更差。要创建一个文件,文件系统不仅要分配一个inode,还要在包含新文件的目录中分配空间。这样做的I / O流量总量非常高:一个读取inode位图(查找空闲inode),一个写入inode位图(将其标记为已分配),一个写入新的inode本身(初始化它),一个目录的数据(将文件的高级名称链接到它的inode编号),以及一个读写目录inode以更新它。如果目录需要增长以适应新条目,则还需要额外的I / O(即数据位图和新的目录块)。所有这些只是为了创建一个文件!

我们来看一个具体的例子,其中创建了file / foo / bar,并且向它写入了三个块。图40.4显示了在open()(创建文件)期间以及在3个4KB写入期间发生的情况。

在该图中,对磁盘的读取和写入被分组在导致它们发生的系统调用下,并且它们可能发生的粗略排序从图的顶部到底部进行。您可以看到创建该文件需要多少工作:在这种情况下,有10个I / O,用于遍历路径名,然后创建文件。您还可以看到每个分配写入需要5个I / O:一对读取和更新inode,另一对读取和更新数据位图,最后写入数据本身。文件系统如何以合理的效率完成这些任务?

Caching and Buffering
Caching
As the examples above show, reading and writing files can be expen- sive, incurring many I/Os to the (slow) disk. To remedy what would clearly be a huge performance problem, most file systems aggressively use system memory (DRAM) to cache important blocks.
Imagine the open example above: without caching, every file open would require at least two reads for every level in the directory hierarchy (one to read the inode of the directory in question, and at least one to read its data). With a long pathname (e.g., /1/2/3/ … /100/file.txt), the file system would literally perform hundreds of reads just to open the file! Early file systems thus introduced a fix-sized cache to hold popular blocks. As in our discussion of virtual memory, strategies such as LRU and different variants would decide which blocks to keep in cache. This fix-sized cache would usually be allocated at boot time to be roughly 10% of total memory. Modern systems integrate virtual memory pages and file system pages into a unified page cache [S00]. In this way, memory can be allocated more flexibly across virtual memory and file system, depending on which needs more memory at a given time.
Now imagine the file open example with caching. The first open may generate a lot of I/O traffic to read in directory inode and data, but sub- sequent file opens of that same file (or files in the same directory) will mostly hit in the cache and thus no I/O is needed.

Buffer
Let us also consider the effect of caching on writes. Whereas read I/O can be avoided altogether with a sufficiently large cache, write traffic has to go to disk in order to become persistent. Thus, a cache does not serve as the same kind of filter on write traffic that it does for reads. That said, write buffering (as it is sometimes called) certainly has a number of performance benefits. First, by delaying writes, the file system can batch some updates into a smaller set of I/Os; for example, if an inode bitmap is updated when one file is created and then updated moments later as another file is created, the file system saves an I/O by delaying the write after the first update. Second, by buffering a number of writes in memory, the system can then schedule the subsequent I/Os and thus increase per- formance. Finally, some writes are avoided altogether by delaying them; for example, if an application creates a file and then deletes it, delaying the writes to reflect the file creation to disk avoids them entirely. In this case, laziness (in writing blocks to disk) is a virtue.
For the reasons above, most modern file systems buffer writes in mem- ory for anywhere between five and thirty seconds, representing yet an- other trade-off: if the system crashes before the updates have been prop- agated to disk, the updates are lost; however, by keeping writes in mem- ory longer, performance can be improved by batching, scheduling, and even avoiding writes.
Some applications (such as databases) don’t enjoy this trade-off. Thus, to avoid unexpected data loss due to write buffering, they simply force writes to disk, by calling fsync(), by using direct I/O interfaces that work around the cache, or by using the raw disk interface and avoiding the file system altogether1. While most applications live with the trade- offs made by the file system, there are enough controls in place to get the system to do what you want it to, should the default not be satisfying.

参考:
https://www.maplewish.cn/2018/06/05/%E7%AE%80%E5%8D%95%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E7%9A%84%E5%AE%9E%E7%8E%B0/