文件相关知识

文件

文件是进程创建的信息逻辑单元。一个磁盘一般含有几千或几百万个文件,每个文件是独立于其他文件的,唯一不同的是文件是对磁盘的建模,而非对RAM的建模。

进程可以读取已经存在的文件,并在需要时建立新的文件。存储在文件中的信息必须是持久的,也就是说,不会因为进程的创建与终止而受到影响。一个文件只能在其所有者明确删除它的情况下才会消失。

文件是受操作系统管理的。有关文件的构造,命名,访问,使用,保护,实现和管理方法都是操作系统设计的主要内容。从总体上看,操作系统中处理文件的部分称为文件系统

从用户角度来看,文件系统中最重要的是它在用户眼中表现形式。也就是文件是由什么组成的,怎么样给文件命名,怎么样保护文件,以及可以对文件进行哪些操作等。

文件结构

文件有多种构造方式,在图4-2中列除了常用的三种方式。图4-2a中的文件是一种无结构的字节序列,事实上操作系统不知道也不关心文件内容是什么,操作系统所见到的就是字节,其文件内容的任何含义只在用户程序中解释。

在这里插入图片描述
把文件看成字节序列为操作系统提供了最大的灵活性。用户程序可以向文件中加入任何内容,并以任何方便的形式命名。操作系统不提供任何帮助,但也不会构成障碍。对于想做特殊操作的用户来说,后者是非常重要的。

图4-2b表示在文件结构上的第一步改进。在这个模型中,文件是具有固定长度记录的序列,每个记录都有其内部结构。把文件作为记录序列的中心思想是:读操作返回一个记录,而写操作重写或追加一个记录。

第三种文件结构如图4-2c所示。文件在这种结构中由一棵记录树构成,每个记录不必具有相同的长度,记录的固定位置上有一个字段。这棵树按”键“字段进行排序,从而可以对特定”键“进行快速查找。

虽然在这类结构中取”下一个“记录是可以的,但是基本操作并不是取”下一个“记录,而是获得具有特定键的记录。如图4-2c中的文件zoo,用户可以要求系统读取键为pony的记录,而不必关心记录在文件中的确切位置。更进一步地,用户可以在文件中添加新记录,但是,用户不能决定把记录添加在文件的什么位置上。

文件类型

很多操作系统支持多种文件类型。如UNIX和Windows中都有普通文件和目录,UNIX还有字符特殊文件和块特殊文件。普通文件是包含有用户信息的文件。图4-2中的所有文件都是普通文件。目录是管理文件系统结构的系统文件,字符特殊文件和输出输出有关,用于串行I/O类设备,如中断,打印机,网络等。块特殊文件,用于磁盘类设备。

普通文件一般分为ASCII文件二进制文件。ASCII文件由多行正文组成。在某些系统中,每行用回车符结束,其它系统用换行符结束。文件中的各行的长度不一定相同。

ASCII文件的最大优势是可以显示和打印,还可以用任何文本编辑器进行编辑。再者,如果很多程序都以ASCII文件作为输入和输出,就很容易把一个程序的输出作为另一个程序的输入,如shell管道一样。

与ASCII文件不同的是二进制文件。打印出来的二进制文件是无法理解的,充满混乱字符的一张表。通常,二进制文件有一定的内部结构,使用该文件的程序才了解这种结构。

如图4-3a是一个简单的可执行二进制文件。尽管这个文件只是一个字节序列,但只有文件的格式正确时,操作系统才会执行这个文件。文件头以魔数开始,表明该文件是一个可执行文件(防止非这种格式的文件偶然运行)。魔数后面是文件中各段的长度,执行的起始地址和一些标志位。程序本身的正文和数据在文件头吼面。这些被装入内存,并使用重定位重新定位,符号表用于调试。

在这里插入图片描述
二进制文件的第二个例子是UNIX的存档文件,它由已编译但没有链接的库过程(模块)组合而成。每个文件以模块头开始,其中记录了名称,创建日期,所有者,保护码和文件大小。该模块头与可执行文件一样,也都是二进制数字,打印输出它们毫无意义。

文件访问

早期操作系统的文件访问方式:顺序访问。进程在这些系统中可从头按顺序读取文件的全部字节或记录,但不能跳过某一些内容,也不能不按顺序读取。顺序访问文件是可以返回到起点的,需要时可多次读取该文件。在存储介质是磁带而不是磁盘时,顺序访问文件是很方便的。

当用磁盘来存储文件时,可以不安顺序地读取文件中的字节或记录,或者按照关键字而不是位置来访问记录。这种能够以任何次序读取其中字节或记录的文件称为随机访问文件

随机访问文件对许多应用程序而言是必不可少的,如数据库系统等。

有两种方法可以指示从何处开始读取文件。一种是每次read操作都给出开始读文件的位置。另一种使用一个特殊的seek操作设置当前位置,在seek操作后,从这个当前位置顺序地开始读文件。

目录

文件系统通常提供目录文件夹用于记录文件的位置。

一级目录系统

目录系统的最简单形式就是在一个目录中包含所有的文件。这称为根目录,但是由于只有一个目录,所以其名称并不重要。

在早期的个人计算机中,这种系统很普遍,部分原因是因为只有一个用户。

这一设计的优点在于简单,并且能够快速定位文件,事实上只有一个地方要查看。

在这里插入图片描述

层次目录系统

对于简单的应用而言,一般都采用单层目录方式,但是这种组织形式不适合于现代计算机。因为现代计算机含有成千上万个文件和文件夹。如果都放在根目录下,查找起来会非常困难,为了解决这一问题,出现了层次目录系统,也称为目录树。通过这种方式,可以用很多目录把文件进行分组。进而,如果多个用户共享同一个文件服务器,比如公司的网络系统,每个用户可以为自己的目录树拥有自己的私人根目录。

在这里插入图片描述
根目录含有目录A,B和C。分别属于不同的用户,其中两个用户各自创建了子目录。用户可以创建任意数量的子目录。现在文件系统都是按照这种方式组织的。

路径名

用目录树组织文件系统时,需要有某种方法指明文件名。常用的方法有两种。第一种是,每个文件都赋予一个绝对路径名,它由从根目录到文件的路径组成。例如,路径/usr/ast/mailbox表示根目录中有子目录usr,而usr中又有子目录ast,文件mailbox就在子目录ast下。绝对路径名一定从根目录开始,且是唯一的。在UNIX中,路径各部分之间用/分割。在windows中,分隔符是\

不管采用哪种分隔符,如果路径名的第一个字符是分隔符,则这个路径就是绝对路径

另一种指定文件名的方法是使用相对路径名。它常和工作目录一起使用。用户可以指定一个目录作为当前工作目录。这时,所有的不从根目录开始的路径名都是相对于工作目录的。例如,如果当前的工作目录是/usr/ast,则据对路径名为/usr/ast/mailbox的文件可以直接使用mailbox来引用。也就是说,如果工作目录是/usr/ast,则UNIX命令。

cp /usr/ast/mailbox /usr/ast/mailbox.bak

和命令

cp mailbox mailbox.bak

具有相同的含义。

一些程序需要访问某个特定的文件,而不考虑当前目录是什么。这时,应该采用绝对路径名。

每个进程都有自己的工作目录,这样在进程改变工作目录并退出后,其他进程不会受到影响,文件系统中也不会有改变的痕迹。对进程而言,切换工作目录是安全的,所以只要需要,就可以改变当前工作目录。但是,如果改变了库过程的工作目录,并且工作完毕之后没有修改回去,则其他程序有可能无法正常运行,因为它们关于当前目录的假设已经失效。

支持层次目录结构的大多数操作系统在每个目录中有两个特殊的目录项".""..",读作dotdotdotdot指当前目录,dotdot指父目录。
在这里插入图片描述

文件系统的实现

文件系统布局

文件系统存放在磁盘上。多数磁盘划分为一个或多个区,每个分区中有一个独立的文件系统磁盘的0号扇区称为主引导记录(MBR),用来引导计算机。在MBR的结尾是分区表。该表给出了每个分区的起始和结束地址。表中的一个分区被标记为活动分区。在计算机被引导时,BIOS读入并执行MBR。MBR做的第一件事就是确定活动分区,读入它的第一个块,称为引导块,并执行。引导块中的程序将装载该分区中的操作系统。为统一起见,每个分区都从一个引导块开始,即使它不含有一个可启动的操作系统。不过,未来这个分区也许会有一个操作系统的。

除了从引导块开始之外,磁盘分区的布局是随着文件系统的不同而变化的。文件系统经常包含有如图4-9所列的一些项目。第一个是超级块,超级块包含文件系统的所有关键参数,在计算机启动时,或者在该文件系统首次使用时,超级块会被读入内存。超级块中的典型信息包括:确定文件系统类型使用的魔数,文件系统中块的数量以及其他重要的管理信息。

在这里插入图片描述
接着是文件系统中空闲块的信息。例如,可以用位图或者指针列表的形式给出。

然后在后面是一个inode,也称为索引节点。它是一个数据的结构,每个文件有一个inode,每个索引节点都存储对象数据的属性和磁盘块位置。

inode节点主要包含了以下信息。

  • 模式、权限(保护)
  • 所有者ID
  • 组ID
  • 文件大小
  • 文件的硬链接数
  • 上次访问时间
  • 最后修改时间
  • inode上次修改时间

文件分为两个部分,索引节点和块,每种类型的块数都是固定的。你不能增加分区上inode的数量,也不能增加磁盘块的数量。

紧跟在inode后面的是根目录,它存放的是文件系统目录树的根部。

文件的实现

连续分配

最简单的分配方案是把每个文件作为一连串连续数据块存储在磁盘上。所以,在块大小为1KB的磁盘上,50KB的文件要分配50个连续的块。对于块大小为2KB的磁盘,将分配25个连续的块。

在图4-10a中是一个连续分配的例子。这里列出了头40块,从左面从0块开始。初始状态下,磁盘是空的。接着,从磁盘开始处(块0)开始写入长度为4块的文件A。紧接着,在文件A的结尾开始写入一个3块的文件B。

请注意,每个文件都从一个新的块开始,这样如果文件A实际有几分之几块,那么最后一块的结尾会浪费一些空间。在图4-10中,一共列出了7个文件,每一个都从前面文件结尾的后续块开始。

连续磁盘空间分配方案有两大优势。首先,实现简单,记录每个文件用到的磁盘块简化为只需记住两个数字即可:第一块的磁盘地址和文件的块数。给定了第一块的编号,一个简单的加法就可以找到任何其他块的编号。

其次,读操作性能较好,因为在单个操作中就可以从磁盘上读出整个文件。只需要一次寻找(对第一个块)。之后不再需要寻道和旋转延迟,所以,数据以磁盘全带宽的速率输入。可见连续分配实现简单且具有高的性能。

在这里插入图片描述
但是,连续分配方案也同样有相当明显的不足之处:随着时间的推移,磁盘会变得零碎。开始时,碎片并不是问题,因为每个新的文件都在先前文件的结尾部分之后的磁盘空间里写入,但是,磁盘最终会被充满,所以要么压缩磁盘,要么重新使用空洞所在的空闲空间。

链表分配

存储文件的第二种方法是为每个文件构造磁盘块链表,如图4-11所示。每个块的第一个字作为指向下一块的指针,块的其他部分存放数据。

与连续分配方案不同,这一方法可以充分利用每个磁盘块。不会因为磁盘碎片而兰妃存储空间。同样,在目录项中,只需要存放第一块的磁盘地址,文件的其他块就可以从这个首块地址查找到。

另一方面,在链表分配方案中,尽管顺序读文件非常方便,但是随机访问却相当缓慢。要获得块 n n n,操作系统每一次都必须从头开始,并且要先读前面的 n − 1 n-1 n1块。

而且,由于指针占去了一些字节,每个磁盘块存储数据的字节数不再是2的整数次幂。虽然这个问题不是很严重,但是怪异的大小确实降低了系统的运行效率,因为许多程序都是以长度为2的整数次幂来读写磁盘块的。由于每个块的前几个字节被指向下一个块的指针所占据,所以要读出完整的一个块大小的信息,就需要从两个磁盘块中获得和和拼接信息,这就因复制引发了额外的开销。

在这里插入图片描述

采用内存中的表进行链表分配

如果取出每个磁盘块的指针字,把他们放在内存的一个表中,就可以解决上述链表的两个不足。图4-12就表示了图4-11所示例子的内存中表的内容。这两个图中都有两个文件,文件A依次使用了磁盘块4,7,2,10和12,文件B依次使用了磁盘块6,3,11和14。利用图4-12中的表,可以从第4块开始,顺着链走到最后,找到文件A的全部磁盘块。同样,从第6块开始,顺着链走到最后,也能够找到文件B的全部磁盘块。这两个链都以一个不属于有效磁盘编号的特殊标记(如-1)结束。内存中的这样的一个表格称为文件分配表

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

这种方法的主要缺点就是必须把整个表都存放在内存中。

在这里插入图片描述

inode

最后一个记录各个文件分别包含哪些磁盘块的方法是给每个文件赋予一个称为inode的数据结构,其中列出了文件属性和文件块的磁盘地址。图4-13是一个简单例子的描述。给定inode,就能找到文件的所有块。相对于在内存中采用表的方式而言,这种机制具有很大优势,即只有在对应文件打开时,其inode才在内存中。如果每个inode占有 n n n个字节,最多 k k k个文件同时打开,那么为了打开文件而保留inode的数组所占据的全部内存仅仅是 k n kn kn个字节,只需要提前保留这么多空间即可。

这个数组通常比上一节中叙述的文件分配表所占据的空间要小。其原因很简单,保留所有磁盘块的链表的表大小正比于磁盘自身的大小。如果磁盘有 n n n个块,该表需要 n n n个表项。由于磁盘变得很大,该表格也随之线性增加。相反,inode机制需要在内存中有一个数组,其大小正比于可能要同时打开的最大文件个数。

inode的一个问题是,如果每个inode只能存储固定数量的磁盘地址,那么当一个文件所含的磁盘块的数目超过了inode所能容纳的数目怎么办?一个解决方法是最后一个”磁盘地址“不指向数据块,而是指向一个包含额外磁盘块地址的块的地址。如果4-13所示。更高级的解决方案是:可以有两个或更多个包含磁盘地址的块,或者指向其他存放地址的磁盘块的磁盘块。

在这里插入图片描述

目录的实现

在读文件前,必须先打开文件。打开文件时,操作系统利用用户给出的路径名找到相应目录项。目录项中提供了查找文件磁盘块所需要的信息。因系统而异,这些信息有可能是整个文件的磁盘地址(对于连续分配方案),第一块的编号(对于两种链表分配方案)或者是inode。无论怎么样,目录系统的主要功能是把ASCII码文件名映射成定位文件数据所需的信息。

与此密切相关的问题是在何处存放文件属性。每个文件系统维护诸如文件所有者以及创建时间等文件属性,它们必须存储在某个地方。一种显而易见的方法是把文件属性直接放在目录项中。很多系统确实是这样实现的。这个办法用图4-14a说明。在这个简单设计中,目录中有一个固定大小的目录项列表,每个文件对应一项,其中包含一个(固定长度)的文件名,一个文件属性的结构体以及用以说明磁盘块位置的一个或多个磁盘地址(至某个最大值)。

在这里插入图片描述
对于采用inode的系统,还存在另一种方法,即把文件属性存放在inode中而不是目录项中。在这种情况下,目录项会更短:只有文件名和inode。这种方法参见图4-14b。

到目前为止,我们已经假设文件具有较短的,固定长度的名字。在MS-DOS中,文件有18个字符的基本名和13字符的可选扩展名。在UNIX V7中文件名有1~14个字符,包括任何扩展名。但是,几乎所有的现代操作系统都支持可变长度的长文件名。那么它们是如何实现的呢?

最简单的方法是给予文件名一个长度限制,典型值为255个字符,然后使用图4-14中的一种设计,并为每个文件名保留255个字符空间。这种处理很简单,但是浪费了大量的目录空间,因为只有很少的文件会有如此长的名字。

一种替代方案是放弃”所有目录项大小一样“的想法。这种方法中,每个目录项有一个固定部分,这个固定部分通常以目录项的长度开始,后面是固定格式的数据,通常包括所有者,创建时间,保护信息以及其他属性。这个固定长度的头的吼面是一个任意长度的实际文件名,可能是如图4-15a中的正序格式防止。在这个例子中,有三个文件,project-budgetpersonnelfoo。每个文件名以一个特殊字符(通常是0)结束。在图4-15中用带叉的举行表示,为了使每个目录项从字的边界开始,每个文件名被填充成整个字。

在这里插入图片描述
这个方法的缺点是,当移走文件后,就引入了一个长度可变的空隙,而下一个进来的文件不一定正好适合这个空隙。这个与我们已经看到的连续磁盘文件的问题是一样的,由于整个目录在内存中,所以只有对目录进行紧凑操作才可节省空间。另一个问题是,一个目录项可能会分布在多个页面上,在读取文件名时可能发生缺页中断。

处理可变长度文件名字的另一种方法是,使目录项自身都有固定长度,而将文件名放置在目录后面的堆中,如图4-15b所示。这一方法的优点是,当一个文件目录项被移走后,另一个文件的目录项总是可以适合这个空隙。当然,必须要对堆进行管理,而在处理文件名时缺页中断仍旧会发生。另一个小有点就是文件名不再需要从字的边界开始。这样,原先在图4-15a中需要的填充字符,在图4-15b中的文件名之后就不再需要了。

到目前为止,在需要查找文件名时,所有的方案都是线性地从头到尾对目录进行搜索。对于非常长的目录,线性查找就太慢了。加快查找速度的一个方式是在每个目录中使用散列表。设表的大小为 n n n,在输入文件名时,文件名被散列到 1 1 1 n − 1 n-1 n1之间的值。

添加一个文件时,不论哪种方法都要对与散列表相对应的散列表表项进行检查。如果该表项没有被使用,就将一个指向文件目录项的指针放入,文件目录项紧连在散列表后面。如果该表项被使用了,就构造一个链表,该链表的表头指针存放在该表项中,并链接所有具有相同散列值的文件目录项。

查找文件按照相同的过程进行。散列处理文件名,以便选择一个散列表项。检查链表头在该位置上的链表的所有表项,查看要找的文件名是否存在。如果名字不再链表上,该文件就不再这个目录上。

使用散列表的优点是查找非常迅速。其缺点是需要复杂的管理。只有在预计系统中的目录京杭会有成百上千个文件时,才能把散列表真正作为备用方案考虑。

共享文件

当几个用户同在一个项目里工作时,它们常常需用共享文件。其结果是,如果一个共享文件同时出现在属于不同用户的不同目录下,工作起来就很方便。图4-16再次给出了图4-7所示的文件系统,指示C的一个文件现在也出现在B的目录下。B的目录与该共享文件的联系称为一个链接。这样,文件系统本身是一个有向无环图,而不是一棵树。将文件组织成有向无环图使得维护复杂化,但也是必须付出的代价。

共享文件是方便的,但也带来一些问题。如果目录中包含磁盘地址,则当链接文件时,必须把C目录中的磁盘地址复制到B目录中。如果B或C随后又往该文件中添加内容,则新的数据块将只列入进行添加工作的用户的目录中。其他的用户对此改变是不知道的。所以未被了共享的目的。

有两种方法可以解决这一问题。在第一种解决方案中,磁盘块不列入目录,而是列入一与文件本身关联的小型数据结构中。目录将指向这个小型数据结构。这是UNIX系统中所采用的方法。(小型数据结构是inode

在第二种解决方案中,通过让系统建立一个类型为LINK的新文件,并把该文件放在B的目录下,使得B与C的一个文件存在链接。新的文件中知包含了它所链接的文件的路径名。当B读该链接文件时,操作系统查看到要读的文件时,操作系统查看到要读的文件是LINK类型,则找到该文件所链接的文件的名字,并且去读那个文件。与传统(硬)链接相对比起来,这一方法称为符号链接

以上每一种方法都有其缺点。第一种方法中,当B链接到共享文件时,inode记录文件的所有者是C。建立一个链接并不改变所有关系,但它将inode的链接计数加1,所以系统知道目前有多少目录项指向这个文件。

如果以后C试图删除这个文件,系统将面临问题。如果系统删除文件并清除inode,B则有一个目录项指向一个无效的inode。如果该inode以后分配给另一个文件,则B的链接指向一个错误的文件。系统通过inode中的计数可知该文件仍突然被引用,但是没有办法找到该文件的全部目录项以删除它们。指向目录的指针不能存储在inode中,原因是有可能有无数个这样的目录。

唯一能做的就是只删除C的目录项,但是将inode保留下来,并将计数置为1。如图4-17c所示。而现在的状况是,只有B有指向该文件的文件项,而该文件的所有者是C。如果系统进行记账或有配额,那么C将继续为该文件付账直到B决定删除它,如果真是这样,只有到计数变为0的时刻,才会删除该文件。

在这里插入图片描述
对于符号链接,以上问题不会发生。因为只有真正的文件所有者才有一个指向inode的指针。链接到该文件上的用户只有路径名,没有指向inode的指针。当文件所有者删除文件时,该文件被销毁。以后若试图通过符号链接访问该文件将导致失败,因为系统不能找到文件。删除符号链接根本不影响文件。

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

还有另一个由链接带来的问题,在符号链接和其他方式都存在。如果允许链接,文件有两个或多个路径,查找一指定目录及其子目录下的全部文件的程序将多次定位到被链接的文件。例如,一个将某一目录及其子目录下的文件转储到磁带上的程序有可能多次复制一个被链接的文件。进而,如果接着把磁带读进另一台及其,除非转储程序具有智能,否则被链接的文件将被两次复制到磁盘上,而不是只是被链接起来。

日志结构文件系统

设计日志结构文件系统的主要原因是,CPU的余小宁速度越来越快,RAM内存容量变得很大,同时磁盘高速缓存也迅速增加。进而,不需要磁盘访问操作,就有可能满足直接来自文件系统高速缓存的很大一部分读请求。从上面的事实可以推出,未来多数的磁盘访问是写操作,这样,在一些文件系统中使用的提前读操作,并不能获得更好的性能。

更为糟糕的是,在大多数文件系统中,写操作往往都是零碎的。

出于这样的原因,日志结构文件系统的设计者决定重新涉嫌一种UNIX文件系统,该系统即使面对一个大部分由零碎的随机写操作组成的任务,同样能够充分利用磁盘的带宽。其基本思想是将整个磁盘结构化为一个日志。每个一段时间,或是有特殊需要时,被缓冲在内存中的所有未决的写操作都被放到一个单独的段中,作为在日志末尾的一个邻接段写入磁盘。这个单独的段可能会包含inode,目录块,数据块或者都有。每一个段的开始都是该段的摘要,说明该段中都包含哪些内容。

在LFS的设计中,同样存在着inode,且具有与UNIX中一样的结构,但是inode分散在整个日志中,而不是放在磁盘的某一个固定为止。尽管如此,当一个inode被定位后,定位一个块就用通常的方式来完成。当然,由于这种设计,要在磁盘中找到一个inode就变得比较困难了,因为inode的地址不能像在UNIX中那样简单地通过计算得到。为了能够找到inode,必须要维护一个由inode编号索引组成的inode图。在这个图中的表项 i i i指向磁盘中的第 i i iinode。这个图保存在磁盘上,但是也保存在高速缓存中,因为,大多数情况下这个图的最常用部分还是在内存中。

总而言之,所有的写操作最初都被缓冲在内存中,然后周期性的把所有已缓冲的写作为一个单独的段,在日志的末尾写入磁盘。要打开一个文件,则首先需要从inode图重找到文件的inode。一旦inode定位之后就可以找到相应的块的地址。所有的块都放在段中,在日志的某个位置上。

如果磁盘空间无限大。那么有了前面你的讨论就足够了。但是马士基的硬盘空间是有限的,这样最终日志将会占用整个磁盘,到那个时候将不能往日志中写任何新的段。幸运的是,许多已有的段包含了很多不再需要的块,例如,如果一个文件被覆盖了,那么它的inode就会指向新的块,但是旧的磁盘块仍然在先前写入的段中占据着空间。

为了解决这个问题,LFS有一个清理线程,该线程周期的扫描日志进行磁盘压缩。该线程首先读日志中的第一个段的摘要,检查有哪些inode和文件。然后该线程查看当前inode图,判断该inode是否有效以及文件快是否仍在使用中。如果没有使用,则该信息被丢弃。如果仍然使用,那么inode和块就进入内存等待写回到下一个段中。接着,原来的段被标记为空闲,以便日志可以用它来存放新的数据。用这种方法,清理线程遍历日志,从后面移走旧的段,然后将有效的数据放入内存等待写到下一个段中。由此,整个磁盘成为一个大的环形的缓冲区,写线程将新的段写到前面,而清晰线程则将旧的段从后面移走。

日志的管理并不简单,因为当一个文件快被写回到一个新段的时候,该文件的inode必须首先要定位,更新,然后放到内存中准备写回到下一个段。inode图必须更新以指向新的位置。尽管如此,对日志进行管理还是可行的,而且性能分析的结果表明,这种由管理而带来的复杂性是值得的。

日志文件系统

虽然基于日志结构的文件系统是一个很吸引人的想法,但是由于它们和现有的文件系统不相匹配,所以还没被广泛使用。尽管如此,它们内在的一个思想,即面对出错的鲁棒性。却可以被其他文件系统所借鉴。这里的基本思想是保存一个用于记录系统下一步将要做什么的日志。这样当系统在完成它们即将完成的任务前崩溃时,重新启动后,可以通过查看日志,获取崩溃前计划完成的任务,并完成它们。这样的文件系统称为日志文件系统,并已经被实际应用。

为了看清这个问题的实质,考虑一个简单的,普通并经常发生的操作:移除文件。这个操作需要三个步骤完成。

  • 在目录中删除文件
  • 释放inode到空间inode
  • 将所有磁盘块归坏空闲的磁盘块池

在Windows中,也需要类似的步骤。不存在系统崩溃时,这些步骤执行的顺序不会带来问题;但是当存在系统崩溃时,就会带来问题。假如在第一步完成后系统崩溃。inode和文件块将不会被任何文件获得,也不会被再分配;它们只存在废物池中的某个地方,因此减少了可利用的资源。如果崩溃发生在第二步后,那么只有磁盘块会丢失。

如果操作顺序被更改,并且inode最想被释放,这样在系统重启后,inode可以被再分配,但是旧的目录入口将继续指向它,因此指向错误文件。如果磁盘块最先被释放,这样一个再inode被清除前的系统崩溃将意味着一个有效的目录入口指向一个inode,它所列出的磁盘块当前存在于空闲块存储池中并可能很快被再利用。这将导致两个或更多的文件分享同样的磁盘块。这样的结果都是不好的。

日志文件系统先写一个日志项,列除三个将要完成的动作。然后日志项被写入磁盘。只有当日志项已经被写入,不同的操作才可以进行。当所有的操作成功完成后,删除日志项。如果系统这时崩溃,系统恢复后,文件系统可以通过检查日志来查看是不是有未完成的操作。如果有,可以重新运行所有未完成的操作。直到文件被正确的删除。

为了让日志文件系统工作,被写入日志的操作必须是幂等的,它意味着只要有必要,它们就可以重复执行很多次,并不会带来破坏。

为了增加可靠性,一个文件系统可以引入数据库中的原子事务概念。使用这个海联,一组动作可以被界定在开始事务和结束事务操作之间。

虚拟文件系统

即使在同一台计算机上或同一个操作系统下,都会使用很多不同的文件系统。Windows有一个主要的NTFS文件系统,但是也有一个包含老的仍然使用的FAT-32或者FAT-16驱动器或分区,并且不时地需要一个CD-ROM或者DVD。windows通过指定不同地盘符来处理这些不同的文件系统,比如C:D:等。当一个进程打开一个文件,盘符是显式或者隐式存在的,所以Windows直到向哪个文件系统传递请求,不需要尝试将不同类型文件系统整合为统一的模式。

相比之下,所有现代的UNIX系统做了一个很认真的尝试,即将多种文件系统整合到一个统一的结构中。一个Linux系统可以用ext2作为根文件系统,ext3分区装载在/usr下,另一块采用ReiserFS文件系统的硬盘装载在/home下。从用户的角度来看,只有一个文件系统层级。它们事实上是多种(不相容的)文件系统,对于用户和进程是不可见的。

但是,多种文件系统的存在,在实际应用中式明确可见的。绝大多数UNIX操作系统都是用**虚拟文件系统(VFS)**概念尝试将多种文件系统统一成一个有序的结构。关键的思想就是抽象出所有文件系统都共有的部分,并且将这部分代码放在单独的一层,该层调用底层的实际文件系统来具体管理数据。大体上的结构在图4-18中有阐述。以下的介绍不是单独针对Linux和FreeBSD或者其他版本的UNIX,而是给出了一种普遍的关于UNIX下文件系统的描述。

所有和文件相关的系统调用在最初的处理上都指向虚拟文件系统。这些来自用户进程的调用,都是标准的POSIX系统调用,比如openreadwritelseek等。因此,VFS对用户进程有一个”上层“接口,它就是著名的POSIX接口。

VFS也有一个对于实际文件系统的”下层“接口,就是在图4-18中被标记为VFS接口的部分。这个接口包含许多功能调用,这样VFS可以使每一个文件系统完成任务。因此,当创造一个新的文件系统和VFS一起工作时,新文件系统的设计者就必须确定它提供VFS所需要的功能调用。关于这个功能的一个明显的例子就是从磁盘中读某个特定的块,把它放在文件系统的高速缓冲中,并且返回指向它的指针。因此,VFS有两个不同的接口:上层给用户进程的接口和下层给实际文件系统的接口。

当系统启动时,根文件系统在VFS中注册。另外,当装载其他文件系统时,不管在启动时还是在操作过程中,它们也必须在VFS中注册。当一个文件系统注册时,它做的最基本的工作就是提供一个包含VFS所需要的函数地址的列表,可以是一个长的调用矢量(表),或者是许多这样的矢量,每个VFS对象一个。因此,只要一个文件系统在VFS注册,VFS就知道如何从它哪里读一个块——它从文件系统提供的矢量中直接调用第四个功能。同样地,VFS也知道如何执行实际文件系统提供的每一个其他的功能:它只需要调用某个功能,该功能所在的地址在文件系统注册时就提供了。

装载文件系统后就可以使用它了。比如,如果一个文件系统装载在/usr并且一个进程调用它。

open("/usr/include/unistd.h", O_RDONLY)

当解析路径时,VFS看到新的文件系统被装载在/usr,并且通过搜索已经装载文件系统的超块表来确定它的超块。做完这些,它可以找到他所装在的文件的根目录,在那里查找路径include/unistd.h。然后VFS创建一个v节点并调用实际文件系统,以返回所有的在文件inode中的信息。这个信息和其他信息一起复制到v节点中,而这些所谓其他信息中最重要的是指向包含调用v节点操作的函数表的指针,比如readwriteclose等。

vnode被创建后,为了进程调用,VFS在文件描述符表中创建一个表项,并且将它指向新的vnode。最后,VFS向调用者返回文件描述符,所以调用者可以用它去读,写或者关闭文件。

随后,当进程用文件描述符进行一个读操作,VFS通过进程表和文件描述符表确定vnode的位置,并跟随指针指向函数表。这样就调用了处理read函数,运行在实际文件系统中的代码并得到所请求的块。VFS并不知道数据是来源于本地硬盘还是来源于网络中的远程文件系统。所有有关的数据结构在图4-19中展示。从调用者进程号和文件描述符开始,进而是vnode,读函数指针,然后是对实际文件系统的访问函数定位。

在这里插入图片描述

在这里插入图片描述

文件系统管理和优化

磁盘空间管理

文件通常存放在磁盘上,所以对磁盘空间的管理是系统设计者要考虑的一个主要问题。存储一个有 n n n个字节的文件可以有两种策略:分配 n n n个字节的连续磁盘空间,或者把文件分成很多个连续(或并不一定连续)的块。

按连续字节序列存储文件有一个明显问题,当文件扩大时,有可能需要在磁盘上移动文件。内存中分段也有同样的问题。不同的是,相对于把文件从磁盘的一个位置移动到另一个位置,内存中段的移动操作要快得多。因此,几乎所有的文件系统都把文件分割成固定大小的块来存储,各块之间不一定相邻。

块大小

一旦决定把文件按固定大小的块来存储,就会出现一个问题:块的大小应该是多少?

拥有大的块尺寸意味着每个文件,甚至一个1字节的文件,都要占用一整个柱面,也就是小的文件浪费了大量的磁盘空间。另一方面,小的块尺寸意味着大多数文件会跨越多个块,因此需要多次训导与旋转延迟才能读出它们,从而降低了性能。因此,如果分配的单元太大,则浪费了空间;如果太小,则浪费时间。

记录空闲块

一旦选定了块大小,下一个问题就是怎么跟踪空闲块。有两种方法被广泛使用,如图4-22所示。第一种方法是采用磁盘块链表,链表的每个块中包含尽可能多的空闲磁盘块号。对于1KB大小的块和32位的磁盘块号,空闲表中每个块包含有255个空闲块的块号(需要有一个位置存放指向下一个块的指针)。考虑一个1TB的磁盘,拥有10亿个磁盘块。为了存储全部地址块号,如果每块存放255个快号,则需要400万个块。通常情况下,采用空闲块存放空闲表,这样不会影响存储器。

在这里插入图片描述

另一种空闲磁盘空间管理的方法是采用位图 n n n个块的磁盘需要 n n n位位图。在位图中,空闲块用1表示,已分配块用0表示。对于1TB磁盘的例子,需要10亿位标识,即需要大于130000个1KB块存储。位图方法所需的空间较少,因为每块只用一个二进制位标识。而在链表方法中,每一块要用到32位。只有在磁盘快满时链表方案需要的块才比位图少。

如果空闲块倾向于成为一个长的连续分块的话,则空闲列表可以改成记录连续分块而不是单个的块,一个8,16,32位的计数可以与每一个块相关联,来记录连续空闲块的数目。在最好的情况下,一个基本上空的磁盘可以用两个数表达:第一个空闲块的地址,以及空闲块的计数。另一方面,如果磁盘产生了很严重的碎片,记录连续分块会比记录单独的块效率要低,因为不仅要存储地址,还要存储计数。

回到空闲表的方法,只需要在内存中保存一个指针块。当文件创建时,所需要的块从指针块中取出。现有的指针块用完时,从磁盘中读入一个新的指针块。类似的,当删除文件时,其磁盘块被释放,并添加到内存的指针块中。当这个块填满时,就把它写入磁盘。

在某些特定情形下,这个方法产生了不必要的磁盘I/O。考虑图4-23a中的情形,内存中的指针块只有两个表项了。如果释放了有三个磁盘块的文件,该指针块就溢出了,必须将其写入磁盘。这就产生了图4-23b的情形。如果现在写入含有三个块的文件,满的指针块不得不再次读入。这将回到图4-23a的情形。如果有三个块的文件只是作为临时文件被写入,当它被释放时,就需要另一个磁盘写操作,以便把满的指针块写回磁盘。总之,当指针块几乎为空时,一系列短期的临时文件就会引起大量的磁盘I/O。

在这里插入图片描述
一个可以避免过多磁盘I/O的替代策略是,拆分满了的指针块。这样,当释放三个块时,不再是从图4-23a变到图4-23b,而是从图4-23a变化到图4-23c。现在,系统可以处理一系列临时文件,而不需进行任何磁盘I/O。如果内存中指针块满了,就写入磁盘,半满的指针块从磁盘中读入。这里的思想是:保持磁盘上的大多数指针块为满的状态(减少磁盘的使用),但是在内存中保留一个半满的指针块。这样,它可以处理文件的创建又同时处理文件的删除操作,而不会为空闲表进行磁盘I/O。

对于位图,在内存中只保留一个块是可能的,只有在该块满了或空了的情形下,才到磁盘上取另一块。这样处理的好处是,通过在位图的单一块上进行所有的分配操作,磁盘块会较为紧密地据集在一起,从而减少了磁盘比的移动。由于位图是一种固定大小的数据结构,所以如果内核是分页的,就可以把位图放在虚拟内存中,在需要时将位图的页面调入。

磁盘配额

为了防止人们贪心而占有太多的磁盘空间,多用户操作系统常常提供一种强制性磁盘配额机制。其思想是系统管理员分给每个用户拥有文件和块的最大数量,操作系统确保每个用户不超过分给它们的配额。

当用户打开一个文件时,系统找到文件属性和磁盘地址,并把它们送入内存中的打开文件表。其中一个属性高速文件所有者是谁。任何有关该文件大小的增长都记到所有者的配额上,以防止一个用户垄断所有的inode

第二张表包含了每个用户当前打开文件的配额记录,即使是其他人打开该文件也一样。这张表如图4-24所示,该表的内容是从被打开文件的所有者的磁盘配额文件中提取出来的。当所有文件关闭时,该记录被写回配额文件。

当在打开文件表中建立一个新表项时,会产生一个指向所有者配额记录的指针,以便很容易找到不同的限制。每一次往文件中添加一块时,文件所有者所用数据块的总数也增加,引发对配额硬限制和软限制检查。可以超出软限制,但硬限制不可以超出。当已经达到硬限制时,在往文件中添加内容将引发错误,同时,对文件数目也存在着类似的检查。

当用户试图登录时,系统检查配额文件,查看该用户文件数目或磁盘数目是否超过软限制。如果超过了任一限制,则显式一个警告,保存的警告计数减1。如果该计数为0,标识用户多次忽略该警告,因而将不允许该用户登录。要想再得到登录的许可,就必须与系统管理员协商。

这一方法具有这样的性质,即只要用户在退出系统前消除所超过的部分,它们就可以在依次终端会话期间超过其软限制,但无论什么情况都不能超过硬限制

在这里插入图片描述

文件系统性能

高速缓存

最常用的减少磁盘访问次数计数是块高速缓存或者缓冲区高速缓存

管理高速缓存有不同的算法,常用的算法是:检查全部的读请求,查看在高速缓存中是否有所需要的块。如果存在,可执行读操作而无须访问磁盘。如果该块不存在,首先要把它读到高速缓存再复制到所需的地方。之后,对同一个块的请求都通过高速缓存完成。

高速缓存的操作如图4-28所示。由于在高速缓存中许多块(通常有上千块),所以需要有某种方法快速确定所需要的块是否存在。常用方法是将设备和磁盘地址进行散列操作,然后,在散列表中查找结果。具有相同散列值的块在一个链表中连接在一起,这样就可以沿着冲突链查找其他块。

如果高速缓存已满,此时需要调入新的块,则要把原来的某一块调出高速缓存(如果要调出的块在上次调入以后修改过,则要把它写回磁盘)。

块提前读

第二个明显提高文件系统性能的技术是:在需要用到块之前,试图提前将其写入高速缓存,从而高高命中率。特别地,许多文件都是顺序读的。如果请求文件系统在某个文件中生成块 k k k,文件系统执行相关操作且在完成之后,会在用户不察觉的情形下检查高速缓存,以便确定块 k + 1 k+1 k+1是否已经在高速缓存。如果还不在,文件系统会为块 k + 1 k+1 k+1安排一个预读,因为文件系统希望在需要用到该块时,它已经在高速缓存或者至少马上就要在高速缓存中。

当然,块提前读策略只适用于实际顺序读取的文件。对随机访问文件,提前读丝毫不起作用。相反,它还会帮倒忙,因为读取无用的块以及从高速缓存中删除潜在有用的块将会占用固定的磁盘带宽。那么提前读策略是否值得采用呢?文件系统通过跟踪每一个打开文件的访问方式来确定这一点。例如,可以适用与文件相关联的某个位协助跟踪该文件到底是”顺序访问方式“还是”随机访问方式“。在最初不能确定文件属于哪种存取方式时,先将该位设置成顺序访问方式。但是,查找已完成,就将该位清除。如果再次发生顺序读取,就再次设置该位。这样,文件系统可以通过合理的猜测,确定是否应该采取提前读的策略。即便弄错了依次也不会产生严重后果,不过是浪费一小段磁盘的带宽。

减少磁盘臂运动

高速缓存和块提前读并不是提高文件系统性能的唯一方法。另一种重要技术是把有可能顺序访问的块放在一起,当然最好是在同一个柱面上,从而减少磁盘臂的移动次数。当写一个输出文件时,文件系统就必须按照要求一次一次的分配磁盘块。如果用位图来记录空闲块,并且整个位图在内存中,那么选择与前一块最近的空闲块是很容易的。如果用空闲表,并且链表的一部分存在磁盘上,要分配紧邻着的空闲块就困难得多。

不过,即使采用空闲表,也可以采用块簇技术。即不用块而用连续块簇来跟踪磁盘存储区。如果一个扇区有512个字节,有可能系统采用1KB的块(两个扇区),但却按每2块(4个扇区)一个单位来分配磁盘存储区。这和2KB的磁盘块并不相同,因为在高速缓存中它依然适用1KB为单位进行,但在一个空闲的系统上顺序读取文件,寻道的次数可以减少一半,从而使文件系统的性能大大改善。若考虑选中定位则可以得到这类方案的变体。在分配块时,系统尽量把一个文件中的连续块存放在同一个柱面上。

在使用inode或任何类似inode的系统中,另一个性能瓶颈是,读取一个很短的文件也需要两次磁盘访问:一次是访问inode,另一次是访问块。通常情况下,inode的放置如图4-29a所示。其中,全部inode都放在靠近磁盘开始位置,所以inode和它指向的块之间的平均距离是柱面数的一半,这将需要较长的寻道时间。

在这里插入图片描述
一个简单的改进方法是,在磁盘中部而不是开始处存放inode,此时,在inode和第一块之间的平均寻道时间减为原来的一半。另一种做法是:将磁盘分成多个柱面组,每个柱面组有自己的inode,数据块和空闲表,将图4-29b。在文件创建时,可选取任一inode,但选定之后,首先在该inode所在的柱面组上查找块。如果在该柱面组中没有空闲的块,就选用与之相邻的柱面组的一个块。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值