《Operating System:Three Easy Pieces》阅读笔记<二十七>—实现文件系统

125 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

实现文件系统

在有了前面那些知识的铺垫后,我们已经能构建一个非常简单的文件系统了,并且文件系统是纯软件,所以不需要学习额外的硬件支持。构建文件系统有很大的灵活性,所以没有明显的设计规范,这一点会在我们的构建过程中表现出来。

文件系统有两大设计结构,数据存储结构和数据访问方法,现代文件系统一般用比较复杂的树结构作为文件的存储结构,访问方法的意思就是当调用open()、rea()、write()时,它们的访问路径是怎样的。更直接的说,这些系统调用是怎么实现的。

在深入上面的结构之前,我们先看一看文件系统的总体结构是怎样的,简单来说如下图所示

上图是一个最简单的文件系统,每一块4KB,一共64个块,这些块里最多的是D块即数据块,存储最多的是用户数据,称为数据区域。还有一个inode结构跟踪每个文件的信息,比如有哪些数据块,文件的大小、访问权限等,存储在I块即inode块。我们也可以把这一部分称为inode表,inode表大小表示文件系统可以存放的最大文件数量,这是可以动态分配的。我们还缺一种用来跟踪I块和D块是否空闲或者是否已分配的结构,我们采用一种简单的方法,也就是维护一个i块即inode bitmap块和d块即data bitmap块,它们结构类似都很简单,简单结构:每个位用来表示对应的对象/块是空闲的(0)还是正在使用的(1)。最后一个块我们用来存放超级块,用来存储这一整个文件系统各方面的信息,如文件系统的inode和数据块数量,inode表从哪里开始读取,以及用来表示文件系统类型的超级数。这些就是文件系统的基本结构,大多数文件系统都会有类似的结构但是可能会有不同的名字。

文件的组织方式,文件系统中最重要的磁盘结构之一是inode,每一个inode在系统中都有一个独有的i-number。每个inode里面实际上是你需要的关于一个文件的所有信息:它的类型(例如,常规文件,目录,等等),它的大小,分配的块的数量,保护信息(如谁拥有这个文件,以及谁可以访问它),一些时间信息,包括文件时创建,修改,或上次访问,以及其数据块的信息驻留在磁盘(例如,指针之类的)。我们把关于一个文件的所有这些信息称为元数据(metadata)

inode中还存储着直接可以索引到文件所在块的指针,它是我们读取文件的直接方式。如果一个指针只指向一个块,那么大文件将需要非常多的指针来索引,这不利于我们的设计,我们需要一种能够灵活处理大小文件的方法

  1. 一种简单的压缩思路是在众多直接指针之外设立一个间接指针,指向更多直接指针,当直接指针不够用的时候就用间接指针,
  2. 一种更加理想的方式是基于区段,一个磁盘指针加上一个长度,问题是很难找到那么多连续的空闲磁盘块

一般我们会认为第一种方法更加常见,这种方法统称为多级索引方法,本质上是不平衡的树,这种结构就是为了适应文件小的很小,大的很大的特点,很多文件系统使用这种索引方式,这大大提升了inode指向块的数量,一个三级指针可以将4KB的块索引扩大到4GB的块索引。

目录的组织方式,目录本身的结构比较简单,就是一个包含(名字、inode号)对的列表,对于给定目录中的每个文件或目录,在该目录的数据块中有一个字符串和一个数字。对于每个字符串,也可能有一个长度(假设名称大小可变)。

其中reclen是record length的意思,貌似是在删除文件时标记用的。

目录在文件系统中被视为一种特殊类型的文件,因此也是用inode进行索引存储,该inode的类型字段被标记为”目录“而不是”常规文件“,简单的线性列表并不是存储目录信息的唯一方式,事实上,更加精细的现代文件系统会用树结构来存储目录信息,如在XFS里面是B+树,这让文件创建更快。

我们用链表来存储文件块索引,有时这种方式十分低效,因此老FAT会在内存里保存链接信息表,要分配文件空间的时候就查表

空闲空间管理,文件系统必须跟踪哪些inode节点和数据库是空闲的,在VSFS中,我们的解决方案是提供两个简单的bitmap,在这种方式下分配一个新的数据块的流程是:分配inode——更新inode bitmap——更新on-disk bitmap——分配数据块,而位图只是管理空闲磁盘的一种方式,现代系统可能用更精细的结构如B-tree,同样其它空间管理方案也会有类似的一系列活动。

在分配数据块的时候,一些更精细的系统如ext2和ext3,会在分配的时候将空间分的更连续一些,保证文件的一部分在硬盘上是连续的,从而提高性能。

访问路径,接下来我们结合上面的内容,看一下实际在一个简单的文件系统中读写文件会发生的一系列活动有哪些,是怎样组织的。

首先是读取文件,我们以读取名为bar的文件为例,读取路径为/foo/bar,文件大小为12KB(3个数据块),具体过程可以看下面的时间轴。任何读取首先都是在root节点开始的。

然后是写入文件,同样是写入名为bar的文件为例,读取路径为/foo/bar,文件大小为12KB(3个数据块),具体过程可以看下面的时间轴。

可以看出来写入操作产生的IO次数比读取要多不少,这样常见的操作产生的IO流量比较大,我们需要减少文件系统IO成本的方法

缓存和缓冲,读写开销大,但是可以发现都是在一些相同的块中反复读写,因此我们考虑使用缓存的方式来减少IO开销,早期系统用固定大小的缓存页来缓存块,也就是在内存中开辟一块固定大小的缓存区,这样可能造成浪费。所以现代系统将虚拟内存页和文件系统页统一到一个页缓存中,可以在内存和文件缓存之间灵活的分配内存。

动态的方法和静态的方法各有千秋,静态分区可以确保每个用户都能获得一定份额的资源,通常提供更可预测的性能,而且通常更容易实现。动态分区可以实现更好的利用(通过让需要资源的用户使用空闲资源),但实现起来可能更复杂,并且可能会导致用户性能下降,因为这些用户的空闲资源被其他人使用,然后需要时需要很长时间才能回收。

缓存对读操作的影响不言而喻,对写操作也有很重要的作用,首先,通过延迟写操作,文件系统可以将一些更新批处理到较小的I/ o集合中;其次,通过在内存中缓冲大量的写操作,系统可以调度随后的I/ o,从而提高性能;最后,有些写操作通过延迟完全避免。这三点是逐渐推进的。

有些数据库不想数据写入操作有延迟,因此调用fsync强制写入,虽然大多数应用程序都需要通过文件系统进行权衡,但如果统一的调度不能令人满意,则也有足够的控制来让系统执行您希望它执行的操作。