Loading

System | OS | File System

文件描述符

在linux系统中一切都可以看做是文件。一个进程要求内核打开相应的文件,内核返回一个小的非负整数,称为文件描述符。每个进程在开始时都有三个打开的文件,标准输入(0),标准输出(1)和标准错误(2)。

每个进程有独立的描述符表,包括描述符和一个指向文件表中一个表项的指针。

文件表

内核对所有打开的文件维护一张文件表,所有进程共享这张表。每个文件的表项包括

  1. 当前文件偏移量
  2. 引用计数
  3. 一个指向i-node表中表项的指针
  4. 打开文件时所使用的的状态标识
  5. ……

关闭一个描述符会减少相应的引用计数,当引用计数为0时内核删除该文件表表项。

独立的进程各自打开同一个文件,会在文件表中生成两个文件表项,各自拥有独立的偏移量和状态。不同文件描述符也能共享同一个文件表项。例如fork()后产生的子进程就和父进程共享文件表项,因此在独立操作时要注意关闭对应的文件描述符。

i-node表

所有进程共享i-node表,inode保存给定文件的元数据。每个inode都由一个数字(inumber)隐式引用,操作系统用inumber来识别不同文件。inode通过多级文件索引指向数据块。

在每个inode中,实际上是所有关于文件的信息:文件类型(例如,常规文件、目录等)、大小、分配给它的块数、保护信息(如谁拥有该文件以及谁可以访问它)、一些时间信息(包括文件创建、修改或上次访问的时间文件下),以及有关其数据块驻留在磁盘上的位置的信息(如某种类型的指针)。我们将所有关于文件的信息称为元数据(metadata)。

简单文件系统的构成

文件系统中最关键的就是inode,但除了inode之外,一个简单的文件系统还有很多组成部分

超级块

在文件系统的最开始有一个super block(超级块),用于记录整个文件系统的整体信息,包括 inode 和 block 的总量、已经使用量和剩余量,以及文件系统的格式和相关信息等。

目录

目录也是一种文件,同样拥有inode和数据块,但永远不能直接写入目录。因为目录的格式被视为文件系统的元数据,因此只能间接更新目录。通过这种方式,文件系统可以确保目录的内容始终符合预期。目录最主要包括条目名称(也即文件名)和inumber,每个目录有两个额外的条目:.(点)和..(点点),即当前目录和父目录。

位图

位图是管理空闲空间的一种办法,记录inode和数据块中哪些是空闲的,哪些不是。

硬链接

文件名是对文件inumber的一个链接,在删除文件时,只会删除文件名和inumber之间的链接,并减少引用计数,当引用计数为0时才会释放inode和相关数据块,从而真正删除该文件。

软链接(符号链接)

硬链接不能创建目录的硬链接,因为可能会创造一个环使得部分内存永远不被释放。符号链接本身也是一个文件,将指向的文件路径作为数据。软链接不会增加引用计数,因此可能造成悬空引用。

打开文件

读取文件时,需要从根目录沿着路径名因此读取inode和数据块,因此对于大型目录需要读取很多数据块。现代操作系统通过动态划分,从内存中动态地划分虚拟内存和文件系统缓存,减少读取磁盘。

崩溃一致性

在每次更新文件时,需要同时写入三个块:位图,inode和数据块。理想的做法是atomically填写三个数据,但磁盘一次只提交一次写入,在这些更新之间可能会发生崩溃或断电。导致三个部分结果不一致。

fsck

扫描文件系统中的超级块、位图、inode中的数据,对不一致的情况执行相应的处理。如果位图和inode不一致,则信任inode内的信息。如果inode存在无效字段且不易修复,fsck会清除该inode和相应的位图。

缺点: 太慢,需要扫描整个磁盘。只有少数文件需要检查的情况下,扫描整个磁盘过于昂贵。

日志

在更新磁盘前,先将要做的事情写入日志。

  1. 日志写入:将事务(包括事务开始块,所有即将写入的数据和元数据以及事务结束块)写入日志,等待写入完成
  2. 将待处理的元数据和数据更新写入文件系统的最终位置。

但在写日志期间也可能发生崩溃,我们希望五个块(事务开始块,位图,inode,数据块,事务结束块)按顺序写入。然而:

给定如此大的写入,磁盘内部可以执行调度并以任何顺序完成大批写入的小块。因此,磁盘内部可以写入TxB、I[v2]、B[v2]和TxE,然后才写入Db。

  • 因此有一种解决方案是:将事务写入日志时,在开始和结束块中包含日志内容的校验和。这样做可以使文件系统立即写入整个事务,而不会产生等待。如果在恢复期间,文件系统发现计算的校验和与事务中存储的校验和不匹配,则可以断定在写入事务期间发生了崩溃,从而丢弃文件系统更新。

比较简单的解决方案是分两步发出事务写入,先将出TxE块之外的所有块写入日志,当这些写入完成时,再写入TxE。

同时,磁盘保证任何512字节的写入都是原子的。更新后的文件系统协议如下:

  1. 日志写入
  2. 日志提交
  3. 加检查点

日志的大小有限,当日志接近满时,不能向磁盘提交进一步的事务。因此日志文件系统将日志视为循环数据结构,一旦事务被加检查点,文件系统应释放它在日志中占用的空间,允许重用日志空间。有很多方法可以达到这个目的。例如,你只需在日志超级块(journal superblock)中标记日志中最旧和最新的事务。

然而将每个数据块写入磁盘两次,是沉重的成本,而崩溃则是一件罕见的事情,因此可以寻找更好的办法。

元数据日志

不将数据写入日志,仅写入元数据。提前将数据写入磁盘避免发生崩溃后inode指向无效数据。

  1. 数据写入
    将数据写入最终位置,等待完成(等待是可选的,详见下文)。
  2. 日志元数据写入
    将开始块和元数据写入日志,等待写入完成。
  3. 日志提交
    将事务提交块(包括TxE)写入日志,等待写完成,现在认为事务(包括数据)已提交(committed)。
  4. 加检查点元数据
    将元数据更新的内容写入文件系统中的最终位置。
  5. 释放
    稍后,在日志超级块中将事务标记为空闲。

在元数据日志中需要添加撤销记录,以避免删除目录时的inode重用问题(目录也是元数据)。

到目前为止,我们已经描述了保持文件系统元数据一致性的两个可选方法:基于fsck的偷懒方法,以及称为日志的更活跃的方法。但是,并不是只有这两种方法。Ganger和Patt引入了一种称为软更新[GP94]的方法。这种方法仔细地对文件系统的所有写入排序,以确保磁盘上的结构永远不会处于不一致的状态。例如,通过先写入指向的数据块,再写入指向它的inode,可以确保inode永远不会指向垃圾。对文件系统的所有结构可以导出类似的规则。然而,实现软更新可能是一个挑战。上述日志层的实现只需要具体文件系统结构的较少知识,但软更新需要每个文件系统数据结构的复杂知识,因此给系统增加了相当大的复杂性。

另一种方法称为写时复制(Copy-On-Write,COW),并且在许多流行的文件系统中使用,包括Sun的ZFS [B07]。这种技术永远不会覆写文件或目录。相反,它会对磁盘上以前未使用的位置进行新的更新。在完成许多更新后,COW文件系统会翻转文件系统的根结构,以包含指向刚更新结构的指针。这样做可以使文件系统保持一致。在将来的章节中讨论日志结构文件系统(LFS)时,我们将学习更多关于这种技术的知识。LFS是COW的早期范例。

另一种方法是我们刚刚在威斯康星大学开发的方法。这种技术名为基于反向指针的一致性(Backpointer-Based Consistency,BBC),它在写入之间不强制执行排序。为了实现一致性,系统中的每个块都会添加一个额外的反向指针。例如,每个数据块都引用它所属的inode。访问文件时,文件系统可以检查正向指针(inode或直接块中的地址)是否指向引用它的块,从而确定文件是否一致。如果是这样,一切都肯定安全地到达磁盘,因此文件是一致的。如果不是,则文件不一致,并返回错误。通过向文件系统添加反向指针,可以获得一种新形式的惰性崩溃一致性。

最后,我们还探索了减少日志协议等待磁盘写入完成的次数的技术。这种新方法名为乐观崩溃一致性(optimistic crash consistency),尽可能多地向磁盘发出写入,并利用事务校验和(transaction checksum)的一般形式,以及其他一些技术来检测不一致,如果出现不一致的话。对于某些工作负载,这些乐观技术可以将性能提高一个数量级。但是,要真正运行良好,需要稍微不同的磁盘接口。
——摘自《操作系统导论》


第一篇博客就放最近复习的时候总结的内容吧,想着反正也是要总结着记录下来,不如整理一下发出来。
希望可以坚持记录下去~

posted @ 2021-03-09 17:37  珠玑位  阅读(104)  评论(0)    收藏  举报