unix支持哪些原始文件系统操作_持久化(5):文件系统的实现

在看文章前可以先看下这个,

吴海波:专栏的序。

先有个大概的认识会对阅读有所帮助。

在本章中,我们将介绍一个简单的文件系统实现,称为vsfs(the Very Simple File System)。此文件系统是典型UNIX文件系统的简化版本,因此可用于介绍当今许多文件系统中的一些基本磁盘结构,访问方法和各种策略。文件系统是纯软件;与我们开发的CPU和内存虚拟化不同,我们不会添加硬件功能来使文件系统的某些方面更好(尽管我们必须注意设备特性以确保文件系统运行良好)。由于我们在构建文件系统方面具有很大的灵活性,因此建立了许多不同的文件系统,从AFS(the Andrew File System)到ZFS(Sun’s Zettabyte File System)。所有这些文件系统都有不同的数据结构,并且比其他类型的文件系统更好地做了某些事情。因此,我们学习文件系统的方式是通过案例研究:首先,本章中的简单文件系统(vsfs)介绍大多数概念,然后对真实文件系统进行一系列研究,以了解它们在实践中的区别。

我们的入手点是考虑文件系统的两个不同方面;如果你理解这两个方面,你可能会理解文件系统基本上是如何工作的。第一个是文件系统的数据结构。换句话说,文件系统使用什么类型的数据结构来组织磁盘上的数据和元数据?我们将看到的第一个文件系统(包括下面的vsfs)使用简单的数据结构,如块数组或其他对象,而更复杂的文件系统,如SGI’s XFS,,使用更复杂的基于树的结构。文件系统的第二个方面是它的访问方法。它如何将系统调用(例如open(),read(),write()等)映射到其结构中?在执行特定系统调用期间读取哪些结构?所有这些步骤的执行效率如何?如果你了解文件系统的数据结构和访问方法,那么你已经实现了一个很好的模型。在我们深入研究第一个实现时,尝试自己对这2个方面进行思考。

我们现在开始介绍vsfs文件系统的数据结构。我们需要做的第一件事就是将磁盘分成块;一般选择常用的4 KB大小。因此,对构建文件系统的磁盘分区的看法很简单:由一系列块组成,每块大小为4 KB。我们先从一个简单的例子说起,假设有一个非常小的磁盘,只有64个块:

559fb7f8a0338413e07abc259265b8a8.png

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

5176dfb05a30cd2739fbac1e274f4dbf.png

上一章中我们了解到,文件系统必须跟踪每个文件的信息。该信息是元数据的关键部分,元数据会记录文件由哪些块构成,文件的大小,文件的所有者或者权限,修改时间等等。为了存储这些信息,文件系统通常有一个名为inode的结构(我们将在下面详细介绍inode)。为了容纳inode,我们还需要在磁盘上为它们预留一些空间。让我们将磁盘的这一部分称为inode table,它是保存在磁盘上的inode数组。因此,我们的磁盘映像现在看起来像这张图片,假设我们使用64个块中的5个用于inode(在图中用I表示):

970c04fe655aa5de41120262b755a11f.png

我们应该注意这里inode通常不那么大,例如128或256字节。假设每个inode 是256个字节,一个4 KB的块可以容纳16个inode,我们上面的文件系统包含80个inode。在我们的简单文件系统中,它建立在一个很小的64块分区上,这个数字代表了我们文件系统中可以拥有的最大文件数。但请注意,构建在较大磁盘上的相同文件系统可以简单地分配更大的inode表,从而容纳更多文件。到目前为止,我们的文件系统有数据块(D)和inode(I),但仍然缺少一些东西。正如你可能已经猜到的那样,我们需要记录inode区域和data区域哪些是空闲哪些是已经分配了的。因此,这种allocation structures是任何文件系统中的必需元素。当然,许多种跟踪分配方法都是可能的。我们选择一种简单而流行的结构,称为位图,一种用于数据区域(数据位图),另一种用于inode表(inode位图)。位图是一种简单的结构:每个位用于指示相应的对象/块是空闲(0)还是正在使用(1)。因此我们新的磁盘布局,带有inode位图(i)和数据位图(d):

cf8bdd8263a59480b9bdb837a8dd8e3f.png

你可能会注意到,对这些位图使用整个4 KB块有点过大;因为这样的位图可以跟踪是否分配了32K对象,但是我们只有80个inode和56个数据块。但是,为简单起见,我们为每个位图使用一个完整的4 KB块。细心的读者(即仍然清醒的读者)可能已经注意到在我们非常简单的文件系统的磁盘结构的设计中还剩下一个块。这是为超级块保留的,在下图中用S表示。超级块包含有关此特定文件系统的信息,例如,文件系统中有多少inode和数据块(在本例中分别为80和56),inode表开始块(块3),依此类推。它可能还包括一些神奇的数字来识别文件系统类型(在本例中为vsfs)。因此,在安装文件系统时,操作系统将首先读取超级块,初始化各种参数,然后将卷附加到文件系统树。当访问卷中的文件时,系统将可以确切地知道在哪里寻找所需的磁盘上的结构。

a5155ec19102b4c72d8153859d137e75.png

inode是文件系统最重要的磁盘结构之一;几乎所有文件系统都具有与此类似的结构。名称inode是index node的缩写。每个inode都由一个数字(称为i-number)隐式引用,我们之前称之为文件的低级名称。在vsfs(和其他简单文件系统)中,给定一个i-number,应该能够直接计算磁盘上相应inode所在的位置。例如,如上所述获取vsfs的inode表大小为20-KB(5个4-KB块),包含80个inode(假设每个inode为256个字节);inode区域从12KB开始(超级块从0KB开始,inode位图在地址4KB,数据位图在8KB,因此inode表就在之后。要读取inode编号32,文件系统将首先计算到inode区域的偏移量(32 · sizeof(inode) or 8192),将它加到磁盘上的inode表的起始地址(inodeStartAddr = 12KB),从而得到所需的inode块的正确地址:20KB。回想一下,磁盘不是字节可寻址的,而是由大量可寻址扇区组成,通常是512字节。因此,为了获取包含inode 32的inode块,文件系统将向扇区40发出读取数据的命令,以获取所需的inode块。一般地,inode块的扇区地址iaddr可以如下计算:

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

在每个inode内部实际上是关于文件所需的所有信息:其类型(例如,常规文件,目录等),它的大小,分配给它的块数,保护信息(例如谁拥有该文件,以及谁可以访问它),一些时间信息,包括文件创建,修改或上次访问的时间,以及有关其数据块驻留在磁盘上的位置的信息(例如,某种指针)。我们将关于文件的所有此类信息称为元数据;实际上,文件系统中不是纯用户数据的任何信息通常都是这样的。ext2 的示例inode如图40.1所示。设计inode时最重要的决定之一是它如何引用数据块的位置。一种简单的方法是在inode中有一个或多个直接指针(直接指向磁盘地址);每个指针指的是属于该文件的一个磁盘块。这种方法是有限的:例如,如果你想要一个非常大的文件(例如,大于块大小乘以inode中的直接指针数),那你就没办法表示了啊。

52146974b0ab1b3621b39e2cc565a56f.png

为了支持更大的文件,文件系统设计者不得不在inode中引入不同的结构。一个常见的想法是使用一个称为间接指针的特殊指针。它不是指向包含用户数据的块,而是指向包含更多指针的块,对应的每个指针指向用户数据。因此,inode可以具有一些固定数量的直接指针(例如,12)和单个间接指针。如果文件变得足够大,则分配间接块(从磁盘的数据块区域),并将间接指针设置为指向它。4byte磁盘地址,一个4-KB块会增加另外1024个指针;该文件可以增长到(12 + 1024)*4K=4144KB。在这种方法中,你可能希望支持更大的文件。为此,我们可以使用:双重间接指针。该指针指的是一个包含间接指针的块,其中的每个间接指针指向的块中又全是指针,而这些指针指向了真正的数据块(有点绕,其实和多级页表是一回事)。因此,双重间接块增加了额外的1024*1024或1百万个4KB块,换句话说,支持超过4GB大小的文件。但是你可能想要更多,我们打赌你知道它的发展方向:三重间接指针。总的来说,这种不平衡树被称为指向文件块的多级索引(multi-level index)方法。让我们来检查一个带有十二个直接指针的例子,以及一个单个间接指针和一个双重间接指针。假设块大小为4KB,并且指针大小4byte,该结构可以容纳大小超过4GB的文件(即,(12 + 1024 + 1024*1024)×4KB)。你能想出通过添加三重间接块后能处理文件的大小吗? (提示:非常大)许多文件系统使用多级索引,包括常用的文件系统,如Linux ext2 [P09]和ext3,NetApp’sWAFL,以及原始的UNIX文件系统。其他文件系统,包括SGI XFS和Linux ext4,使用extents而不是简单的指针;有关基于extens的方案如何工作的详细信息,请参阅前面的内容(它们类似于讨论虚拟内存中的段)。你可能想知道:为什么要使用这样的不平衡树?为什么不采用不同的方法?许多研究人员已经研究过文件系统及其使用方法,而且有些“真理”几十年来都适用。其中一个发现是大多数文件都很小。这种不平衡的设计由此来说是有意义的,因为如果大多数文件确实很小,那么针对这种情况进行优化就是有意义的。因此,使用少量直接指针(12是典型数字),inode可以直接指向48 KB的数据,需要一个(或多个)间接块用于较大的文件。图40.2总结了这些结果。当然,在inode设计的空间中,存在许多其他可能性;毕竟,inode只是一个数据结构,任何可以存储相关信息并且可以有效查询的数据结构都可以。由于文件系统软件很容易改变,根据不同工作场景或技术变化,你应该探索不同的设计。

9c1e9ab4c2fc3cd8a4d4c1ddf70d3623.png

目录结构

在vsfs中(如在许多文件系统中),目录具有简单的组织; 目录基本上只包含(条目名称,inode编号)的列表。对于字符串,也可能有一个长度(假设可变大小的名称)。例如,假设目录为 dir (inode编号5)有三个文件(foo, bar, 和 foobar is a pretty longname), inode编号分别为12, 13, 和 24。dir磁盘上的内容应该如下:

inum | reclen | strlen | name
5 12 2 .
2 12 3 ..
12 12 4 foo
13 12 4 bar
24 36 28 foobar_is_a_pretty_longname

在本例中,每个条目都有一个inode号、记录长度(名称的总字节加上任何剩余的空格)、字符串长度(名称的实际长度),最后还有条目的名称。注意,每个目录都有两个额外的条目。“.”和.。“..”;"."是当前目录(在本例中是dir),而".."是父目录(在本例中是根目录)。删除一个文件(例如,调用unlink()可以在目录中间留下一个空间,因此也应该有一些方法来标记被删除文件(例如,使用保留的inode编号(例如0)。这样的删除是使用记录长度的原因之一:一个新条目可以重用一个旧的、更大的条目,这样就可能有额外的空间剩余。你可能想知道目录究竟存储在哪里。通常,文件系统将目录视为一种特殊类型的文件。因此,一个目录存储在inode表的某个inode中(inode的type字段标记为“目录”而不是“常规文件”)。目录的inode指向的数据块也是位于我们简单文件系统的数据块区域中。因此,我们的磁盘数据结构仍然保持不变。再次指出,这个简单的目录条目线性列表并不是存储此类信息的唯一方法。和以前一样,任何数据结构都是可能的。例如,XFS以B树形式存储目录,使得文件创建操作(必须确保文件名在创建之前没有被使用)比必须全部扫描的简单列表的方式更快。

空闲空间管理

文件系统必须跟踪哪些节点和数据块是空闲的,哪些不是空闲的,以便在分配新的文件或目录时,它可以为其找到空间。因此,自由空间管理对所有文件系统都很重要。在VSF中,我们有两个用于此任务的简单位图。

例如,当我们创建一个文件时,我们必须为该文件分配一个inode。因此,文件系统将在位图中搜索空闲的inode,并将其分配给文件;文件系统必须将inode标记为已经使用的inode(使用1),并最终使用正确的信息更新磁盘上的位图。当分配数据块时,也会发生类似的一组活动。在为新文件分配数据块时,还可能会考虑其他一些问题。例如,一些Linux文件系统,如ext2和ext3,在创建新文件时会寻找一段连续的块(例如8块);通过找到这样的空闲块序列,将它们分配给新创建的文件,可以使文件系统保证文件的一部分在磁盘上是连续的,从而提高性能。因此,在为数据块分配空间时,这种预分配策略是一种常用的启发式方法。

读写文件

现在我们已经了解了文件和目录是如何存储在磁盘上的,进一步的我们应该能够知道读取和写入是怎么进行的。因此,了解通过路径来读取和写入是理解文件系统如何工作的第二个关键;请注意!对于下面的示例,我们假设文件系统已经挂载,因此超级块已经在内存中。其他所有内容(即inode、目录)仍在磁盘上。

09c321a9099a00b9ee4f01ecfee45638.png

在这个简单的例子中,让我们首先假设你只想打开一个文件(例如/foo/bar),读取它,然后关闭它。对于这个简单的例子,让我们假设文件只有12 KB大小(即3个块)。当发出OPEN(“/foo/bar”,O_RDONLY)调用时,文件系统首先需要找到文件bar的inode,以获得有关文件的一些基本信息(权限信息、文件大小等)。要做到这一点,文件系统必须能够找到bar文件对应的inode,但是它现在只有完整的路径名。所以文件系统必须遍历路径名,从而找到所需的inode。所有遍历都从文件系统的根目录开始,根目录简单地称为/。因此,FS从磁盘读取的第一件事是从根目录的inode开始。但这个inode在哪里?要找到一个inode,我们必须知道它的i-number。通常,我们在父目录中找到文件或目录的i-number,但是根目录没有父目录(根据定义),因此,根inode号必须“众所周知”;FS必须知道挂载文件系统的根目录i-number是什么。在大多数UNIX文件系统中,根inode编号为2。因此,为了开始这个过程,FS读取包含inode编号2(第一个inode块)的块。一旦inode被读取,FS就可以在其中查找指向数据块的指针,在本例中就是查找foo的条目。通过读取一个或多个目录数据块,将找到foo的条目;一旦找到,FS还将找到foo的inode号(假设它是44)。在这个例子中,FS读取包含foo的inode的块,然后读取其目录数据,最后找到bar文件的inode号。open()的最后一步是将BAR的inode读入内存;FS然后执行最终权限检查,分配文件描述符,并将其返回给用户。文件一旦打开,程序就可以发出read()系统调用从文件中读取数据。第一次(在偏移量0处,除非调用了lseek()) 将在文件的第一个块中读取,地址是根据inode号来计算得出;还可以更新该inode的最近访问时间。读取会更新该文件描述符对应的内存中打开的文件表中的文件偏移量,以便下一次读取第二个文件块。

关闭文件要做的工作要少得多;显然,文件描述符应该被取消分配,实际上,FS真正需要做的只有这一个操作,这一步是不会发生磁盘I/O的。对整个过程的描述见图40.3(时间向下增加)。在图中,打开将导致大量读取,最终定位到了bar文件的inode。之后,根据inode块中的信息去读取数据块,然后更新inode的最后访问时间。还请注意,OPEN生成的I/O量与路径名的长度成正比。对于路径中的每一个附加目录,我们都必须读取它的inode和它的数据。更糟糕的是大目录的存在;在这里,我们只需读取一个块就可以获得目录的内容,而对于一个大目录,我们可能需要读取许多数据块才能找到所需的条目。是的,当你阅读一个文件时,情况会变得非常糟糕;正如你将要发现的那样,写一个文件(尤其是创建一个新的文件)就更糟糕了。

写入文件是一个类似的过程。首先,必须打开文件(如上面所示)。然后,应用程序可以发出write()调用,用新内容更新文件。最后,文件被关闭。与读取不同,写入文件可能分配新块。当写入新文件时,每个写入不仅必须将数据写入磁盘,而且必须首先决定分配给文件哪个块,然后相应地也需要更新磁盘的其他结构(例如,数据位图和inode)。因此,对一个文件的每一次写入在逻辑上生成五个I/O:一个读取数据位图(然后决定分配的块),一个写入数据位图(将其新状态反映到磁盘),另两个读取和写入inode(根据新块的位置进行更新),最后,编写实际的块本身。如果考虑一个简单和常见的操作(如文件创建),那么产生的I/O次数会更多。要创建文件,文件系统不仅必须分配一个inode,还必须在包含新文件的目录中分配空间。一次读取inode位图(查找空闲inode),一次写入inode位图(标记其分配),一次写入新inode本身(去初始化inode),一个链接到目录的数据(将文件的高级名称链接到其inode编号),另一个对inode所在目录的读写。如果目录需要增长以容纳新条目,则还需要额外的I/O。所有这些仅仅只是为了创建一个文件!让我们看一个具体的示例:创建/foo/bar文件,并将三个块写入其中。图40.4显示了在open()(创建文件)期间以及三个4KB写入过程中发生的情况。

96681ddeb0f9470bdb9d199eb9246c8e.png

在图中,对磁盘的读和写被分组。可以看到创建文件的工作量:在本例中为10个I/O,遍历路径名,然后最后创建文件。那么一个文件系统如何以合理的效率完成上述任何一项任务呢?

缓存和缓冲

在上面的例子中,读取和写入文件对磁盘造成许多I/O请求。为了补救这个巨大的性能问题,大多数文件系统都是尽量多的使用系统内存(DRAM)来缓存重要的块。像上面的打开文件的例子:如果不缓存,对于打开文件这个操作,至少需要进行2次目录层面的读取(一个是目录的inode,另外一个是inode对应的数据块)。长路径名(例如,/1/2/3/.../100/file.txt),文件系统需要执行数百个读取来打开文件!早期文件系统因此引入了一个固定大小的缓存来缓存经常使用的块。就像我们对虚拟内存的讨论,诸如LRU的策略将决定哪些块保存在高速缓存中。固定大小的缓存通常会在机器启动时分配,大小大致为总内存的10%。然而,这种静态分配可能会造成浪费的;如果文件系统在给定时间点不需要10%的内存?使用上述固定大小的方法,未使用的缓存不能用于某些其他用途,因此会浪费。

相比之下,现代os采用动态划分方法。具体来说,许多现代操作系统将虚拟存储器页面和文件系统页面集成为统一的页缓存。以这种方式,系统根据当前时间谁需要更多的存储器,在虚拟内存和文件系统之间更灵活地分配内存。现在,先考虑在使用缓存的情况下打开一个文件。没有缓存的情况下一个open操作会产生很多的I/O请求去读取目录的inode和对应的数据块,但是如果目录信息已经缓存了,open操作就不会产生I/O了。让我们也考虑缓存对写入的影响。相较于open产生的I/O可以使用足够大的高速缓存完全避免,write是必须将数据写入磁盘的。因此,open和write的对缓存的使用在某些时候是不一样的。首先,通过延迟写入,文件系统可以批处理一些较小的I/O操作。其次,通过缓冲内存中的多个写入,系统可以通过合理的策略来调度i/o请求,从而提高性能。最后,可以通过延迟,完全避免了一些写入;例如,如果应用程序创建文件,然后删除该文件,则延迟将完全避免i/o请求。在这些案例中,懒惰是一种美德。出于上述原因,大多数现代操作系统都会等待5到30秒再将数据真正写入到磁盘,当然如果系统在更新之前崩溃,对于磁盘来说数据会丢失;一些应用程序(如数据库)不喜欢这种因素效率而带来的损失。因此,为了避免由于写入缓冲而导致的意外数据丢失,它们简单地通过调用FSYNC()写入磁盘,或使用原始磁盘接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值