在看文章前可以先看下这个,
吴海波:专栏的序。
先有个大概的认识会对阅读有所帮助。
KenThompson自己为UNIX编写了第一个文件系统。我们称之为“旧UNIX文件系统”,它非常简单。基本上,它的数据结构在磁盘上是这样的:

超级块(S)包含关于整个文件系统的信息:卷有多大,有多少节点等等。磁盘的inode区域包含文件系统的所有节点。最后,大部分磁盘被数据块占用。旧文件系统的好处在于它很简单,并且支持文件系统的基本抽象:文件和目录的层次结构。相对于过去笨拙的的难以使用的系统,新系统是非常易于使用的。
问题是:这个文件系统性能表现很糟糕。正如伯克利的Kirk McKusick和他的同事所测试的那样,性能开始很差,而且随着时间的推移会变得更糟,文件系统只使用了整个磁盘带宽的2%!主要问题是,旧的UNIX文件系统将磁盘视为随机内存进行访问;数据分散到各处进行保存,而不考虑磁盘这个介质的特殊性,因此寻址的时候代价非常大。例如,一个文件的数据块通常离它的inode很远,因此每当有人第一次读取inode,然后读取一个文件的数据块(一个非常常见的操作)时,就会产生一个代价很大的查找操作。更糟糕的是,由于没有仔细管理空闲空间,文件系统最终会变得非常分散。空闲列表将指向散乱分布在磁盘上的块,并且当文件被分配时,只是简单的将空闲列表的下一个块分配出去。最终导致一个逻辑上连续的文件散步在磁盘上存储,导致读取的时候产生非常大的延迟,从而大大地降低性能。例如,假设以下数据块区域,其中包含四个文件(A、B、C和D),每个大小为2块:

如果删除B和D,则产生的布局为:

正如所看到的,自由空间被分割成2个2个块大小的小块,而不是由四个块组成的连续块。假设你现在希望分配一个文件E,大小为四个块:

最终结果是e也被分散到磁盘上,因此,当访问E时,不会从磁盘中获得峰值性能。相反,你首先读E1和E2,然后寻道,接着才能读E3和E4。在旧的UNIX文件系统中,这种碎片问题时有发生,影响了性能。另一个问题是:原始的磁盘的块大小太小(512字节)。因此,从磁盘传输数据本质上是低效的。较小的块是好的,因为它们最小化了内部碎片(块内的浪费),但不利于传输,因为每个块可能需要一个寻道开销才能够访问。因此,我们面临的问题是:
如何组织文件系统数据结构以提高性能?在这些数据结构之上,我们需要哪些类型的分配策略?如何使文件系统“磁盘感知”(也就是能够契合磁盘的特性来进行存储)?
伯克利的一个小组建立了一个更好、更快的文件系统,他们称之为快速文件系统(Fast File System,FFS)。其想法是重新设计文件系统结构和分配策略,以“磁盘感知”(也就是文件系统能够很好的利用磁盘的特性)的方式,从而提高性能。FFS开创了一个文件系统研究的新时代;新实现保持与文件系统相同的接口(相同的API,包括open()、read()、write()、CLOSE()和其他文件系统调用),但改变了内部实现,为新的文件系统构建铺平了道路,并且这一工作现在还在继续。几乎所有现代文件系统都坚持现有接口(从而保持与应用程序的兼容性),同时出于性能、可靠性或其他原因更改其内部结构。
新的数据组织方式
第一步是改变磁盘上的已有的结构划分。FFS将磁盘划分为多个柱面组。单柱面是硬盘不同表面上距离中心有相同的距离的一组磁道。FFS将N个连续的柱面聚合成一个组,因此整个磁盘可以被看作是柱面组的集合。下面是一个简单的例子,显示了一个带有六个盘的驱动器的四个最外层的磁道,以及一个由三个柱面组成的柱面组:

注意,现代硬盘输出的信息不足以使文件系统真正了解是否在使用一个特定的柱面;如前所述,磁盘提供块的逻辑地址空间,并向客户端隐藏其几何细节。因此,现代文件系统(如Linux ext2、ext3和ext4)将硬盘组织成块组,每个块组只是磁盘地址空间的一个连续部分。下面的图片举例说明每8个块被组织成一个不同的块组(注意,真正的组由更多的块组成):

无论称它们为柱面组还是块组,这些组都是FFS用于提高性能的核心机制。至关重要的是,通过将两个文件放置在同一个组中,FFS可以确保一个接一个的顺序文件访问不会导致长时间的磁盘搜索。要使用这些组来存储文件和目录,FFS需要能够将文件和目录放置到一个组中,并跟踪有关它们的所有必要信息。要做到这一点,FFS每个组中都应该包含文件系统应该具有的所有结构,例如,inode、数据块的空间,以及追踪这些区域是否已经被分配的数据结构。这里描述了FFS在单柱面组中保存的内容:

现在让我们更详细地研究这个单柱面组。由于可靠性原因,FFS在每个组中保留了超级块(S)的副本。挂载文件系统需要超级块;通过保留多个副本,如果一个副本损坏,仍然可以使用副本安装和访问文件系统。在每个组中,FFS需要跟踪组的inode和数据块是否被分配。每组都有inode位图(Ib)和数据位图(Db)。位图是管理文件系统中空闲空间的极好方法,因为很容易找到大量空闲空间并将其分配给文件,从而可能避免旧文件系统中空闲列表的一些碎片问题。最后,inode和数据块区域就像以前非常简单的文件系统(Vsfs)中的区域一样。与往常一样,每个柱面组中的大多数都是由数据块组成的。
有了这个组结构之后,FFS现在就必须决定如何将文件和目录以及相关的元数据放在磁盘上以提高性能。思路很简单:把相关的东西放在一起(把不相关的东西放在很远的地方)。因此,要服从这个思路,FFS必须决定什么是“相关的”,并将其放在同一个块组中;相反,不相关的项应该放在不同的块组中。为了达到这个目的,FFS使用了一些简单的布局启发式方法。
第一种是目录的放置。FFS采用了一种简单的方法:查找分配目录数量较少并且有大量空闲inode的柱面组(以平衡组间的目录),将目录数据和inode放在该组中。当然,这里还可以使用其他启发式方法(例如,考虑空闲数据块的数量)。
对于文件,FFS做两件事。首先,它确保(在一般情况下)将文件的数据块分配到与inode相同的组中,从而防止在inode和数据之间进行长时间的搜索。其次,它将位于同一目录中的所有文件放置在相同的柱面组中。因此,如果用户创建四个文件/a/b、/a/c、/a/d和b/f,FFS将尝试将前三个文件放置在一起(同一组),第四个文件放在较远的地方(在另一个组中)。
让我们看一个例子来加深理解。在这个例子中,假设每个组中只有10个inode和10个数据块(都是不切实际的小数字),而这三个目录(根目录/、/a和/b)和四个文件(/a/c、/a/d、/a/e),/b/f)根据FFS策略放置在其中。假设常规文件每个有两个块大小,并且目录只有一个数据块。我们对每个文件或目录使用自明的符号表示(例如,/对于根目录,a表示/a,f表示/b/f,以此类推)。

FFS做了两件积极的事情:每个文件的数据块位于每个文件的inode附近,而同一目录中的文件彼此接近(即/a/c、/a/d和/a/e都在第1组中,目录/b及其文件/b/f在第2组中)。作为对比,让我们看一看inode的一个其他的分配策略,这种策略只是简单地在各个组中平均的分配数据,以确保任何组的inode表都不会很快被填满。因此,最终分配可能如下所示:

如图所示,虽然此策略确实将文件(和目录)数据保存在其各自的inode附近,但是目录中的文件被任意地散布在磁盘上。按照FFS方法,访问文件/a/c、/a/d和/a/e现在跨越三个组,而不是一个组,这肯定会让效率大打折扣。
值得注意的是,FFS策略不是基于对文件系统流量的广泛研究或任何特别细微的研究而得出的,而是基于一些历史常识。即目录中的文件通常是一起访问的:想象一下编译一堆文件,然后将它们链接到单个可执行文件中。由于这种基于名称的空间局部性的存在,FFS通常会提高性能,确保在相关文件之间的搜索是快速的。

为了更好地理解这些启发式方法是否有意义,让我们分析一些对文件系统访问的跟踪记录,看看是否确实存在名称的局部性。我们将使用SEER跟踪,并分析目录树中文件访问一般有多远。例如,如果文件f被打开,然后在跟踪中重新打开(在打开任何其他文件之前),则这两个文件在目录树中打开的距离为零(因为它们是同一个文件)。如果打开目录dir中的文件f(即dir/f),然后在同一目录(即dir/g)中打开文件g,则两个文件访问之间的距离为1,因为它们共享相同的目录,但不是相同的文件。换句话说,我们距离度量的是有相同祖先的2个文件在目录树中的距离;它们在树中越近,度量就越低。图41.1显示了在SEER集群中的所有工作站上的SEER跟踪程序观察到的数据。该图表的x轴表示路径长度,y轴表示文件打开的累积百分比。可以看到大约7%的文件访问是对先前打开的文件进行的,而接近40%的文件访问是指向同一个文件或同一目录中的一个(即,差为0或1)。因此,FFS局部性假设似乎是有意义的(至少对于这些跟踪而言是如此)。
有趣的是,25%左右的文件访问是距离为2的访问。当用户以多层次的方式构造了一组相关目录并始终在它们之间跳转时,这种类型的局部性就会发生。例如,如果用户有一个src目录,并将对象文件(.o文件)构建到obj目录中,并且这两个目录都是主proj目录的子目录,则常见的访问模式是proj/src/foo.c,后面是proj/obj/foo.o。这两个访问之间的距离是两个,因为proj是共同的祖先。FFS不会在其策略中考虑这种类型的局部性,在这些访问之间会发生更多的查找。为了进行比较,该图表还显示了“随机”跟踪的局部性。随机跟踪是通过随机顺序从现有的SEER跟踪中选择文件并计算这些随机顺序访问之间的距离来生成的。如你所见,随机跟踪中的名称空间局部性比预期的要小。但是,因为每个文件最终共享一个共同的祖先(例如根),所以存在一定的局部性,因此随机结果作为比较的基准是有用的。
大文件处理
在FFS中,文件放置的策略有特殊的情况需要处理:大型文件。如果没有不同的规则,一个大型文件将完全填充满首先放置它的组(也可能是其他的)。以这种方式填充块组是不可取的,因为它阻止了后续的“相关”文件被放置在这个块组中,因此可能会损害文件访问的局部性。因此,对于大型文件,FFS执行以下操作。在将一定数量的块分配到第一块组(例如,12个块,或inode内可用的直接指针的数目)之后,FFS将文件的下一个“大”块(例如,第一个间接块所指向的块)放置在另一个块组中,依此类推。让我们看一些图表来更好地理解这个策略。如果没有大文件异常,一个大文件就会将它的所有块放置到磁盘的一个部分中。我们研究一个文件(/a)的一个小例子,该文件包含FFS中的30个块,每个组配置了10个inode和40个数据块。这里在没有大文件特殊处理情况下的ffs:

正如在图片中看到的那样,/a填充了Group 0中的大多数数据块,而其他组仍然是空的。如果现在根目录(/)中创建了其他一些文件,那么组中的数据空间就不多了。对于大文件,FFS将文件分散到多个组中,因此在任何一个组中的利用率都不会太高:

精明的读者(也就是你)会注意到,在磁盘上传播文件块会损害性能,特别是在顺序访问文件的时候(例如,当用户或应用程序按顺序读取块0到29时)。你是对的。但是可以通过仔细选择块大小来解决这个问题。具体来说,如果块大小足够大,文件系统将花费大部分时间从磁盘传输数据,而只需要(相对地)很少的时间在块的块之间寻找数据。这种通过增加每次操作的工作量来减少开销的过程叫做摊销,是计算机系统中的一种常见技术。让我们看一个例子:假设磁盘的平均定位时间(即寻道和旋转)为10 ms。进一步假设磁盘以40 MB/s的速度传输数据。如果你的目标是将一半的时间花在块之间的寻道,另一半的时间用于传输数据(从而达到峰值磁盘性能的50%),那么计算如下:

上面这个等式是说:如果以40 MB/s的速度传输数据,那么每次10ms只需要传输409.6KB,这样就可以将传输效率达到50%。类似地,可以计算实现90%峰值带宽所需的块大小(原来大约为3.69MB),甚至是峰值带宽的99%(40.6MB!)。因此越接近峰值,这些块就越大(关于这些值的图见图41.2)。然而,FFS并不是这样设计。相反,它采用了一种简单的方法,基于inode本身的结构。前12个直接块与inode放在同一组中;随后的每个间接块和它所指的所有块都放在不同的组中。在块大小为4KB和32位磁盘地址的情况下,此策略意味着每1024个文件块(4MB)被放置在不同的组中,唯一的例外是直接指针指向的文件的第一个48 KB。请注意,磁盘驱动器的趋势是传输速率提高得相当快,因为磁盘制造商擅长将更多的比特塞到同一个盘面,但是驱动器的机械方面(磁盘臂速度和转速)改善得相当缓慢[。这意味着,随着时间的推移,机械成本变得相对昂贵,因此,要摊销上述成本,你必须在2次寻道之间传输更多的数据。

FFS也引入了一些其他创新。特别是对于小文件,因为许多文件的大小为2KB或更小,但是使用4KB块,虽然传输数据不会受影响,但对于空间效率来说并不好。因为这种内部碎片可能导致大约一半的磁盘被浪费了。FFS设计人员的解决方案是通过简单的合并来解决问题。他们决定引入大小为512字节的子块文件系统。因此,如果创建了一个小文件(如大小为1KB),它将占用两个子块,不会浪费整个4KB数据块。随着文件的增长,文件系统将继续将512字节块分配给它,直到它获得完整的4KB数据为止。在那时,FFS将找到4KB块,将子块复制到其中,并释放子块。你可能会注意到此过程效率低下,需要大量额外的文件系统的工作(尤其是许多额外的I/O性能)。通过修改libc库的行为,FFS通常避免了这种现象。

FFS引入的第二个聪明的机制是一个针对性能进行了优化的磁盘布局。当文件被放置在连续扇区上时,会出现问题。如图41.3所示。假设FFS首先读取第0块,读取完成时,请求读取第一块,但是这个时候磁头已经越过了块1,如果要读块1则必须再绕一圈回来。FFS用不同的数据布局解决了这个问题,如图41.3中的右侧。通过将数据分散分布,FFS有足够的时间在磁头越过数据块之前来请求下一个块。事实上,FFS可以针对不同的磁盘计算出应该跳多少块来放置,以避免额外的旋转;这种技术被称为参数化。事实上,这样的布局仅获得50%的峰值带宽,因你必须绕过每个磁道两次才能读取每个块一次。幸运的是,现代磁盘更智能:磁盘内部会读取整个磁道作为缓存,然后,对磁道的后续读取,只需从缓存中返回所需的数据即可。因此文件系统就不必担心这些低级别的细节了。
FFS是第一个允许长文件名的文件系统。此外,引入了符号链接的新概念。另外FFS还引入了用于重命名文件的原子rename()操作。