设计一个文件系统,须要考虑哪些因素?

文件系统的实现

在对文件有了基本认识以后,如今是时候把目光转移到文件系统的实现上了。以前用户关心的一直都是文件是怎样命名的、能够进行哪些操做、目录树是什么,如何找到正确的文件路径等问题。而设计人员关心的是文件和目录是怎样存储的、磁盘空间是如何管理的、如何使文件系统得以流畅运行的问题,下面咱们就来一块儿讨论一下这些问题。html

文件系统布局

文件系统存储在磁盘中。大部分的磁盘可以划分出一到多个分区,叫作磁盘分区(disk partitioning) 或者是磁盘分片(disk slicing)。每一个分区都有独立的文件系统,每块分区的文件系统能够不一样。磁盘的 0 号分区称为 主引导记录(Master Boot Record, MBR),用来引导(boot) 计算机。在 MBR 的结尾是分区表(partition table)。每一个分区表给出每一个分区由开始到结束的地址。系统管理员使用一个称为分区编辑器的程序来建立,调整大小,删除和操做分区。这种方式的一个缺点是很难适当调整分区的大小,致使一个分区具备不少可用空间,而另外一个分区几乎彻底被分配。node

MBR 能够用在 DOS 、Microsoft Windows 和 Linux 操做系统中。从 2010 年代中期开始,大多数新计算机都改用 GUID 分区表(GPT)分区方案。linux

下面是一个用 GParted 进行分区的磁盘,表中的分区都被认为是 活动的(active)数组

当计算机开始引 boot 时,BIOS 读入并执行 MBR。缓存

引导块

MBR 作的第一件事就是肯定活动分区,读入它的第一个块,称为引导块(boot block) 并执行。引导块中的程序将加载分区中的操做系统。为了一致性,每一个分区都会从引导块开始,即便引导块不包含操做系统。引导块占据文件系统的前 4096 个字节,从磁盘上的字节偏移量 0 开始。引导块可用于启动操做系统。安全

在计算机中,引导就是启动计算机的过程,它能够经过硬件(例如按下电源按钮)或者软件命令的方式来启动。开机后,电脑的 CPU 还不能执行指令,由于此时没有软件在主存中,因此一些软件必须先被加载到内存中,而后才能让 CPU 开始执行。也就是计算机开机后,首先会进行软件的装载过程。网络

重启电脑的过程称为从新引导(rebooting),从休眠或睡眠状态返回计算机的过程不涉及启动。数据结构

除了从引导块开始以外,磁盘分区的布局是随着文件系统的不一样而变化的。一般文件系统会包含一些属性,以下编辑器

超级块

紧跟在引导块后面的是 超级块(Superblock),超级块 的大小为 4096 字节,从磁盘上的字节偏移 4096 开始。超级块包含文件系统的全部关键参数布局

  • 文件系统的大小
  • 文件系统中的数据块数
  • 指示文件系统状态的标志
  • 分配组大小

在计算机启动或者文件系统首次使用时,超级块会被读入内存。

空闲空间块

接着是文件系统中空闲块的信息,例如,能够用位图或者指针列表的形式给出。

BitMap 位图或者 Bit vector 位向量

位图或位向量是一系列位或位的集合,其中每一个位对应一个磁盘块,该位能够采用两个值:0和1,0表示已分配该块,而1表示一个空闲块。下图中的磁盘上给定的磁盘块实例(分配了绿色块)能够用16位的位图表示为:0000111000000110。

使用链表进行管理

在这种方法中,空闲磁盘块连接在一块儿,即一个空闲块包含指向下一个空闲块的指针。第一个磁盘块的块号存储在磁盘上的单独位置,也缓存在内存中。

碎片

这里不得不提一个叫作碎片(fragment)的概念,也称为片断。通常零散的单个数据一般称为片断。 磁盘块能够进一步分为固定大小的分配单元,片断只是在驱动器上彼此不相邻的文件片断。若是你不理解这个概念就给你举个例子。好比你用 Windows 电脑建立了一个文件,你会发现这个文件能够存储在任何地方,好比存在桌面上,存在磁盘中的文件夹中或者其余地方。你能够打开文件,编辑文件,删除文件等等。你可能觉得这些都在一个地方发生,可是实际上并非,你的硬盘驱动器可能会将文件中的一部分存储在一个区域内,另外一部分存储在另一个区域,在你打开文件时,硬盘驱动器会迅速的将文件的全部部分汇总在一块儿,以便其余计算机系统可使用它。

inode

而后在后面是一个 inode(index node),也称做索引节点。它是一个数组的结构,每一个文件有一个 inode,inode 很是重要,它说明了文件的方方面面。每一个索引节点都存储对象数据的属性和磁盘块位置

有一种简单的方法能够找到它们 ls -lai 命令。让咱们看一下根文件系统:

inode 节点主要包括了如下信息

  • 模式/权限(保护)
  • 全部者 ID
  • 组 ID
  • 文件大小
  • 文件的硬连接数
  • 上次访问时间
  • 最后修改时间
  • inode 上次修改时间

文件分为两部分,索引节点和块。一旦建立后,每种类型的块数是固定的。你不能增长分区上 inode 的数量,也不能增长磁盘块的数量。

紧跟在 inode 后面的是根目录,它存放的是文件系统目录树的根部。最后,磁盘的其余部分存放了其余全部的目录和文件。

文件的实现

最重要的问题是记录各个文件分别用到了哪些磁盘块。不一样的系统采用了不一样的方法。下面咱们会探讨一下这些方式。分配背后的主要思想是有效利用文件空间快速访问文件 ,主要有三种分配方案

  • 连续分配
  • 链表分配
  • 索引分配

连续分配

最简单的分配方案是把每一个文件做为一连串连续数据块存储在磁盘上。所以,在具备 1KB 块的磁盘上,将为 50 KB 文件分配 50 个连续块。

上面展现了 40 个连续的内存块。从最左侧的 0 块开始。初始状态下,尚未装载文件,所以磁盘是空的。接着,从磁盘开始处(块 0 )处开始写入占用 4 块长度的内存 A 。而后是一个占用 6 块长度的内存 B,会直接在 A 的末尾开始写。

注意每一个文件都会在新的文件块开始写,因此若是文件 A 只占用了 3 又 1/2 个块,那么最后一个块的部份内存会被浪费。在上面这幅图中,总共展现了 7 个文件,每一个文件都会从上个文件的末尾块开始写新的文件块。

连续的磁盘空间分配有两个优势。

  • 第一,连续文件存储实现起来比较简单,只须要记住两个数字就能够:一个是第一个块的文件地址和文件的块数量。给定第一个块的编号,能够经过简单的加法找到任何其余块的编号。

  • 第二点是读取性能比较强,能够经过一次操做从文件中读取整个文件。只须要一次寻找第一个块。后面就再也不须要寻道时间和旋转延迟,因此数据会以全带宽进入磁盘。

所以,连续的空间分配具备实现简单高性能的特色。

不幸的是,连续空间分配也有很明显的不足。随着时间的推移,磁盘会变得很零碎。下图解释了这种现象

这里有两个文件 D 和 F 被删除了。当删除一个文件时,此文件所占用的块也随之释放,就会在磁盘空间中留下一些空闲块。磁盘并不会在这个位置挤压掉空闲块,由于这会复制空闲块以后的全部文件,可能会有上百万的块,这个量级就太大了。

刚开始的时候,这个碎片不是问题,由于每一个新文件都会在以前文件的结尾处进行写入。然而,磁盘最终会被填满,所以要么压缩磁盘、要么从新使用空闲块的空间。压缩磁盘的开销太大,所以不可行;后者会维护一个空闲列表,这个是可行的。可是这种状况又存在一个问题,为空闲块匹配合适大小的文件,须要知道该文件的最终大小

想象一下这种设计的结果会是怎样的。用户启动 word 进程建立文档。应用程序首先会询问最终建立的文档会有多大。这个问题必须回答,不然应用程序就不会继续执行。若是空闲块的大小要比文件的大小小,程序就会终止。由于所使用的磁盘空间已经满了。那么现实生活中,有没有使用连续分配内存的介质出现呢?

CD-ROM 就普遍的使用了连续分配方式。

CD-ROM(Compact Disc Read-Only Memory)即只读光盘,也称做只读存储器。是一种在电脑上使用的光碟。这种光碟只能写入数据一次,信息将永久保存在光碟上,使用时经过光碟驱动器读出信息。

然而 DVD 的状况会更加复杂一些。原则上,一个 90分钟 的电影可以被编码成一个独立的、大约 4.5 GB 的文件。可是文件系统所使用的 UDF(Universal Disk Format) 格式,使用一个 30 位的数来表明文件长度,从而把文件大小限制在 1 GB。因此,DVD 电影通常存储在 三、4个连续的 1 GB 空间内。这些构成单个电影中的文件块称为扩展区(extends)

就像咱们反复提到的,历史老是惊人的类似,许多年前,连续分配因为其简单高性能被实际使用在磁盘文件系统中。后来因为用户不但愿在建立文件时指定文件的大小,因而放弃了这种想法。可是随着 CD-ROM 、DVD、蓝光光盘等光学介质的出现,连续分配又流行起来。从而得出结论,技术永远没有过期性,如今看似很老的技术,在将来某个阶段可能又会流行起来。

链表分配

第二种存储文件的方式是为每一个文件构造磁盘块链表,每一个文件都是磁盘块的连接列表,就像下面所示

每一个块的第一个字做为指向下一块的指针,块的其余部分存放数据。若是上面这张图你看的不是很清楚的话,能够看看整个的链表分配方案

与连续分配方案不一样,这一方法能够充分利用每一个磁盘块。除了最后一个磁盘块外,不会由于磁盘碎片而浪费存储空间。一样,在目录项中,只要存储了第一个文件块,那么其余文件块也可以被找到。

另外一方面,在链表的分配方案中,尽管顺序读取很是方便,可是随机访问却很困难(这也是数组和链表数据结构的一大区别)。

还有一个问题是,因为指针会占用一些字节,每一个磁盘块实际存储数据的字节数并再也不是 2 的整数次幂。虽然这个问题并不会很严重,可是这种方式下降了程序运行效率。许多程序都是以长度为 2 的整数次幂来读写磁盘,因为每一个块的前几个字节被指针所使用,因此要读出一个完成的块大小信息,就须要当前块的信息和下一块的信息拼凑而成,所以就引起了查找和拼接的开销。

使用内存表进行链表分配

因为连续分配和链表分配都有其不可忽视的缺点。因此提出了使用内存中的表来解决分配问题。取出每一个磁盘块的指针字,把它们放在内存的一个表中,就能够解决上述链表的两个不足之处。下面是一个例子

上图表示了链表造成的磁盘块的内容。这两个图中都有两个文件,文件 A 依次使用了磁盘块地址 四、七、 二、 十、 12,文件 B 使用了六、三、11 和 14。也就是说,文件 A 从地址 4 处开始,顺着链表走就能找到文件 A 的所有磁盘块。一样,从第 6 块开始,顺着链走到最后,也可以找到文件 B 的所有磁盘块。你会发现,这两个链表都以不属于有效磁盘编号的特殊标记(-1)结束。内存中的这种表格称为 文件分配表(File Application Table,FAT)

使用这种组织方式,整个块均可以存放数据。进而,随机访问也容易不少。虽然仍要顺着链在内存中查找给定的偏移量,可是整个链都存放在内存中,因此不须要任何磁盘引用。与前面的方法相同,无论文件有多大,在目录项中只需记录一个整数(起始块号),按照它就能够找到文件的所有块。

这种方式存在缺点,那就是必需要把整个链表放在内存中。对于 1TB 的磁盘和 1KB 的大小的块,那么这张表须要有 10 亿项。。。每一项对应于这 10 亿个磁盘块中的一块。每项至少 3 个字节,为了提升查找速度,有时须要 4 个字节。根据系统对空间或时间的优化方案,这张表要占用 3GB 或 2.4GB 的内存。FAT 的管理方式不能较好地扩展并应用于大型磁盘中。而这正是最初 MS-DOS 文件比较实用,并仍被各个 Windows 版本所安全支持。

inode

最后一个记录各个文件分别包含哪些磁盘块的方法是给每一个文件赋予一个称为 inode(索引节点) 的数据结构,每一个文件都与一个 inode 进行关联,inode 由整数进行标识。

下面是一个简单例子的描述。

给出 inode 的长度,就可以找到文件中的全部块。

相对于在内存中使用表的方式而言,这种机制具备很大的优点。即只有在文件打开时,其 inode 才会在内存中。若是每一个 inode 须要 n 个字节,最多 k 个文件同时打开,那么 inode 占有总共打开的文件是 kn 字节。仅需预留这么多空间。

这个数组要比咱们上面描述的 FAT(文件分配表) 占用的空间小的多。缘由是用于保存全部磁盘块的连接列表的表的大小与磁盘自己成正比。若是磁盘有 n 个块,那么这个表也须要 n 项。随着磁盘空间的变大,那么该表也随之线性增加。相反,inode 须要节点中的数组,其大小和可能须要打开的最大文件个数成正比。它与磁盘是 100GB、4000GB 仍是 10000GB 无关。

inode 的一个问题是若是每一个节点都会有固定大小的磁盘地址,那么文件增加到所能容许的最大容量外会发生什么?一个解决方案是最后一个磁盘地址不指向数据块,而是指向一个包含额外磁盘块地址的地址,如上图所示。一个更高级的解决方案是:有两个或者更多包含磁盘地址的块,或者指向其余存放地址的磁盘块的磁盘块。Windows 的 NTFS 文件系统采用了类似的方法,所不一样的仅仅是大的 inode 也能够表示小的文件。

NTFS 的全称是 New Technology File System,是微软公司开发的专用系统文件,NTFS 取代 FAT(文件分配表) 和 HPFS(高性能文件系统) ,并在此基础上进一步改进。例如加强对元数据的支持,使用更高级的数据结构以提高性能、可靠性和磁盘空间利用率等。

目录的实现

文件只有打开后才可以被读取。在文件打开后,操做系统会使用用户提供的路径名来定位磁盘中的目录。目录项提供了查找文件磁盘块所须要的信息。根据系统的不一样,提供的信息也不一样,可能提供的信息是整个文件的磁盘地址,或者是第一个块的数量(两个链表方案)或 inode的数量。不过无论用那种状况,目录系统的主要功能就是 将文件的 ASCII 码的名称映射到定位数据所需的信息上

与此关系密切的问题是属性应该存放在哪里。每一个文件系统包含不一样的文件属性,例如文件的全部者和建立时间,须要存储的位置。一种显而易见的方法是直接把文件属性存放在目录中。有一些系统刚好是这么作的,以下。

在这种简单的设计中,目录有一个固定大小的目录项列表,每一个文件对应一项,其中包含一个固定长度的文件名,文件属性的结构体以及用以说明磁盘块位置的一个或多个磁盘地址。

对于采用 inode 的系统,会把 inode 存储在属性中而不是目录项中。在这种状况下,目录项会更短:仅仅只有文件名称和 inode 数量。这种方式以下所示

到目前为止,咱们已经假设文件具备较短的、固定长度的名字。在 MS-DOS 中,具备 1 - 8 个字符的基本名称和 1 - 3 个字符的可拓展名称。在 UNIX 版本 7 中,文件有 1 - 14 个字符,包括任何拓展。然而,几乎全部的现代操做系统都支持可变长度的扩展名。这是如何实现的呢?

最简单的方式是给予文件名一个长度限制,好比 255 个字符,而后使用上图中的设计,并为每一个文件名保留 255 个字符空间。这种处理很简单,可是浪费了大量的目录空间,由于只有不多的文件会有那么长的文件名称。因此,须要一种其余的结构来处理。

一种可选择的方式是放弃全部目录项大小相同的想法。在这种方法中,每一个目录项都包含一个固定部分,这个固定部分一般以目录项的长度开始,后面是固定格式的数据,一般包括全部者、建立时间、保护信息和其余属性。这个固定长度的头的后面是一个任意长度的实际文件名,以下图所示

上图是 SPARC 机器使用正序放置。

处理机中的一串字符存放的顺序有正序(big-endian)逆序(little-endian) 之分。正序存放的就是高字节在前低字节在后,而逆序存放的就是低字节在前高字节在后。

这个例子中,有三个文件,分别是 project-budgetpersonnelfoo。每一个文件名以一个特殊字符(一般是 0 )结束,用矩形中的叉进行表示。为了使每一个目录项从字的边界开始,每一个文件名被填充成整数个字,以下图所示

这个方法的缺点是当文件被移除后,就会留下一块固定长度的空间,而新添加进来的文件大小不必定和空闲空间大小一致。

这个问题与咱们上面探讨的连续磁盘文件的问题是同样的,因为整个目录在内存中,因此只有对目录进行紧凑拼接操做才可节省空间。另外一个问题是,一个目录项可能会分布在多个页上,在读取文件名时可能发生缺页中断

处理可变长度文件名字的另一种方法是,使目录项自身具备固定长度,而将文件名放在目录末尾的堆栈中。如上图所示的这种方式。这种方法的优势是当目录项被移除后,下一个文件将可以正常匹配移除文件的空间。固然,必需要对进行管理,由于在处理文件名的时候也会发生缺页异常。

到目前为止的全部设计中,在须要查找文件名时,全部的方案都是线性的从头至尾对目录进行搜索。对于特别长的目录,线性搜索的效率很低。提升文件检索效率的一种方式是在每一个目录上使用哈希表(hash table),也叫作散列表。咱们假设表的大小为 n,在输入文件名时,文件名被散列在 0 和 n - 1 之间,例如,它被 n 除,并取余数。或者对构成文件名字的字求和或相似某种方法。

不管采用哪一种方式,在添加一个文件时都要对与散列值相对应的散列表进行检查。若是没有使用过,就会将一个指向目录项的指针指向这里。文件目录项紧跟着哈希表后面。若是已经使用过,就会构造一个链表(这种构造方式是否是和 HashMap 使用的数据结构同样?),链表的表头指针存放在表项中,并经过哈希值将全部的表项相连。

查找文件的过程和添加相似,首先对文件名进行哈希处理,在哈希表中查找是否有这个哈希值,若是有的话,就检查这条链上全部的哈希项,查看文件名是否存在。若是哈希不在链上,那么文件就不在目录中。

使用哈希表的优点是查找很是迅速,缺点是管理起来很是复杂。只有在系统中会有成千上万个目录项存在时,才会考虑使用散列表做为解决方案。

另一种在大量目录中加快查找指令目录的方法是使用缓存,缓存查找的结果。在开始查找以前,会首先检查文件名是否在缓存中。若是在缓存中,那么文件就能马上定位。固然,只有在较少的文件下进行屡次查找,缓存才会发挥最大功效。

共享文件

当多个用户在同一个项目中工做时,他们一般须要共享文件。若是这个共享文件同时出如今多个用户目录下,那么他们协同工做起来就很方便。下面的这张图咱们在上面提到过,可是有一个更改的地方,就是 C 的一个文件也出如今了 B 的目录下

若是按照如上图的这种组织方式而言,那么 B 的目录与该共享文件的联系称为 连接(link)。那么文件系统如今就是一个 有向无环图(Directed Acyclic Graph, 简称 DAG),而不是一棵树了。

在图论中,若是一个有向图从任意顶点出发没法通过若干条边回到该点,则这个图是一个有向无环图,咱们不会在此着重探讨关于图论的东西,你们能够自行 google。

将文件系统组织成为有向无环图会使得维护复杂化,但也是必需要付出的代价。

共享文件很方便,但这也会带来一些问题。若是目录中包含磁盘地址,则当连接文件时,必须把 C 目录中的磁盘地址复制到 B 目录中。若是 B 或者 C 随后又向文件中添加内容,则仅在执行追加的用户的目录中显示新写入的数据块。这种变动将会对其余用户不可见,从而破坏了共享的目的。

有两种方案能够解决这种问题。

  • 第一种解决方案,磁盘块不列入目录中,而是会把磁盘块放在与文件自己相关联的小型数据结构中。目录将指向这个小型数据结构。这是 UNIX 中使用的方式(小型数据结构就是 inode)。

  • 在第二种解决方案中,经过让系统创建一个类型为 LINK 的新文件,并把该文件放在 B 的目录下,使得 B 与 C 创建连接。新的文件中只包含了它所连接的文件的路径名。当 B 想要读取文件时,操做系统会检查 B 的目录下存在一个类型为 LINK 的文件,进而找到该连接的文件和路径名,而后再去读文件,这种方式称为 符号连接(symbolic linking)

上面的每一种方法都有各自的缺点,在第一种方式中,B 连接到共享文件时,inode 记录文件的全部者为 C。创建一个连接并不改变全部关系,以下图所示。

第一开始的状况如图 a 所示,此时 C 的目录的全部者是 C ,当目录 B 连接到共享文件时,并不会改变 C 的全部者关系,只是把计数 + 1,因此此时 系统知道目前有多少个目录指向这个文件。而后 C 尝试删除这个文件,这个时候有个问题,若是 C 把文件移除并清除了 inode 的话,那么 B 会有一个目录项指向无效的节点。若是 inode 之后分配给另外一个文件,则 B 的连接指向一个错误的文件。系统经过 inode 可知文件仍在被引用,可是没有办法找到该文件的所有目录项以删除它们。指向目录的指针不能存储在 inode 中,缘由是有可能有无数个这样的目录。

因此咱们能作的就是删除 C 的目录项,可是将 inode 保留下来,并将计数设置为 1 ,如上图 c 所示。c 表示的是只有 B 有指向该文件的目录项,而该文件的前者是 C 。若是系统进行记帐操做的话,那么 C 将继续为该文件付帐直到 B 决定删除它,若是是这样的话,只有到计数变为 0 的时刻,才会删除该文件。

对于符号连接,以上问题不会发生,只有真正的文件全部者才有一个指向 inode 的指针。连接到该文件上的用户只有路径名,没有指向 inode 的指针。当文件全部者删除文件时,该文件被销毁。之后若试图经过符号连接访问该文件将会失败,由于系统不能找到该文件。删除符号连接不会影响该文件。

符号连接的问题是须要额外的开销。必须读取包含路径的文件,而后要一个部分接一个部分地扫描路径,直到找到 inode 。这些操做也许须要不少次额外的磁盘访问。此外,每一个符号连接都须要额外的 inode ,以及额外的一个磁盘块用于存储路径,虽然若是路径名很短,做为一种优化,系统能够将它存储在 inode 中。符号连接有一个优点,即只要简单地提供一个机器的网络地址以及文件在该机器上驻留的路径,就能够链接全球任何地方机器上的文件。

还有另外一个由连接带来的问题,在符号连接和其余方式中都存在。若是容许连接,文件有两个或多个路径。查找一指定目录及其子目录下的所有文件的程序将屡次定位到被连接的文件。例如,一个将某一目录及其子目录下的文件转存到磁带上的程序有可能屡次复制一个被连接的文件。进而,若是接着把磁带读入另外一台机器,除非转出程序具备智能,不然被连接的文件将被两次复制到磁盘上,而不是只是被连接起来。

相关参考:

https://zhuanlan.zhihu.com/p/41358013

https://www.linuxtoday.com/blog/what-is-an-inode.html

https://www.lifewire.com/what-is-fragmentation-defragmentation-2625884

https://www.geeksforgeeks.org/free-space-management-in-operating-system/

https://sites.ualberta.ca/dept/chemeng/AIX-43/share/man/info/C/a_doc_lib/aixprggd/genprogc/fsyslayout.htm

https://en.wikipedia.org/wiki/Disk_partitioning

https://en.wikipedia.org/wiki/Master_boot_record

https://en.wikipedia.org/wiki/Booting

https://www.computerhope.com/jargon/f/fileprot.htm

https://en.wikipedia.org/wiki/File_attribute

https://en.wikipedia.org/wiki/Make_(software)

https://unix.stackexchange.com/questions/60034/what-are-character-special-and-block-special-files-in-a-unix-system

https://www.computerhope.com/jargon/d/director.htm

https://www.computerhope.com/jargon/r/regular-file.htm

https://baike.baidu.com/item/固态硬盘/453510?fr=aladdin

《现代操做系统》第四版

《Modern Operation System》fourth