文件系统实现
文件系统实现
文件系统结构
磁盘提供大量的外部空间来维持文件系统。磁盘的下述两个特点,使其成为存储多个文件的方便介质:
-
可以原地重写,可以从磁盘上读一块,修改该快,并将它写回到原来的位置。
-
可以直接访问磁盘上的任意一块信息。因此,可以简单地顺序或随机地访问文件,从一个文件切换到另一个文件只需要简单地移动读写磁头并等待磁盘转动即可以完成。
为了提供对磁盘的高效且便捷的访问,操作系统通过文件系统来轻松地存储,定位,提取数据。文件系统有两个不同的设计问题。第一个问题是如何定义文件系统对用户的接口。这个任务涉及定义文件及其属性,文件所允许的操作,组织文件的目录结构。第二个问题是创建数据结构和算法来将逻辑文件系统映射到物理外存设备上。
文件系统本身通常由许多不同的层组成。
I/O控制为最底层,由设备驱动程序和中断处理程序组成,实现内存与磁盘之间的信息传输。设备驱动程序可以作为翻译器。其输入由高层命令组成,如"retrieve block 123"。其输出由底层的、硬件特定的命令组成,这些命令用于控制硬件控制器,通过硬件控制器可以使I/O设备与系统其它部分相连。
基本文件系统只需要向合适的设备驱动程序发送一般命令就可对磁盘上的物理块进行读写。每个块由其数值磁盘地址来标识(例如,驱动器I,柱面(cylinder)73,磁道(track)3,扇区(sector) 10)。
文件组织模块(file-organization module):知道文件及其逻辑块和物理块。由于知道所使用的文件分配类型和文件位置,文件组织模块可以将逻辑地址转换成基本文件系统所用的物理块地址。每个文件的逻辑块按从0或1到N来编号,而包含数据的物理块并不与逻辑号匹配,因此需要通过翻译来定位块。文件组织模块也包括空闲空间管理器,用来跟踪未分配的块并根据需要提供给文件组织模块。
最后,逻辑文件系统管理元数据。元数据包括文件系统的所有结构数据,而不包括实际数据(或文件内容)。逻辑文件系统根据给定符号文件名来管理目录结构,并提供给文件组织模块所需要的信息。逻辑文件系统通过文件控制块来维护文件结构。文件控制块(file control block, FCB)包含文件的信息,如拥有者,权限,文件内容位置。逻辑文件系统也负责保护和安全。
采用分层的结构实现文件系统,能够最大限度地减少重复的代码。相同的I/O控制代码(有时候是基于文件系统的代码)可以被多个文件系统采用。然后每个文件系统有自己的逻辑文件系统和组织模块。
UNIX使用UNIX文件系统(UFS)。Windows NT、Windows2000、WindowsXP支持磁盘文件系统FAT,FAT32和NTFS(或Windows NT file System),虽然Linux支持超过40种不同的文件系统,标准的Linux文件系统是可扩展文件系统(extended file system),它最常见的版本是ext2和ext3。同样还存在一些分布式文件系统,即服务器上的文件系统能够被一个或多个客户端加载。
文件系统实现
概述
实现文件系统要使用多个磁盘和内存结构。虽然这些结构因操作系统和文件而异,但是还是有一些通用规律的。
在磁盘,文件系统可能包括如下信息:如何启动所存储的操作系统,总的块数、空闲块的数目和位置,目录结构以及各个具体文件等。
(每个卷的)引导控制块(boot control block)包括系统从该卷引导操作系统所需要的信息。如果磁盘没有操作系统,那么这块的内容为空。它通常为卷的第一块。UFS称之引导块(boot block),NFTS称之为分区引导扇区(partition boot sector)。
(每个卷的)卷控制块(volume control block)包括卷(或分区)的详细信息,如分区的块数、块的大小、空闲块的数量和指针、空闲FCB的数量和指针等。UFS称之为超级块(superblock),而在NTFS中它存储在主控文件表(Master File Table)中。
每个文件系统的目录结构用来组织文件。UFS中它包括文件名和相关的索引节点(inode)号。NTFS中它存储在主控文件表(Master File Table)中。
每个文件的FCB包括很多该文件的详细信息,如文件权限,拥有者、大小和数据块的位置。UFS称之为索引节点(inode)。NTFS将这些信息存在主控文件表中,主控文件表采用关系数据库结构,每个文件占一行。
内存内信息用于文件管理系统并通过缓存来提高性能。这些数据在文件系统安装的时候被加载,卸载的时候被丢弃。这些结构可能包括:
一个内存中的安装表,包括所有安装卷的信息。
一个内存中的目录结构缓存,用来保存近来访问过的目录信息(对于卷所加载的目录,可以包括一个指向卷表的指针)。
系统范围内的打开的文件表包括每个打开文件的FCB副本和其他信息。
单个进程的打开文件表包括一个指向系统范围内已打开文件表中合适条目指针和其他信息。
为了创建一个新文件,应用程序调用逻辑文件系统。逻辑文件系统知道目录结构形式,为创建一个新文件,它将分配一个新的FCB(如果文件系统实现在文件系统被创建的时候就已经创建了所有的FCB,那么只是从空闲的FCB集合中分配一个)。然后系统把相应目录信息读入内存。用新的文件名更新该目录和FCB,并将结果写回到磁盘。
有些操作系统,包括UNIX,将目录按文件来处理,用一个类型域来标识是否为目录。其他操作系统如Windows NT为文件和目录提供分开的系统调用。对文件和目录采用了不同的处理。不管结构如何,逻辑文件系统能够调用文件组织模块来将目录I/O映射成磁盘块的号,再进而传递给基本文件系统和I/O控制系统。一旦文件被创建,它就能用于I/O。不过,首先应该打开文件。调用open()将文件名传给文件系统。系统调用open()会首先搜溹系统范围内的打开文件表以确定某文件是否已被其他进程所使用。如果是,就在单个进程的打开文件表中创建一项,并指向现有的系统范围内的打开文件表。当打开文件时,根据给定文件名来搜溹目录结构。部分目录结构通常缓存在内存中以加快目录操作。一旦找到文件,其FCB就复制系统范围内的打开文件表。该表不但存储FCB,而且还跟踪打开该文件的进程数量。
接着,在单个进程的打开文件表中会增加一个条目,并通过指针将系统范围内的打开文件表的条目和其他域相连。这些其他域可以包括文件当前位置的指针(用于下一次的读写操作)和文件打开模式等。调用open()返回一个指向单个进程的打开文件表中合适条目的指针。所有之后的操作都是通过该指针进行的。文件名不必是打开文件表的一部分,因为一旦完成对FCB在磁盘上的定位,系统就不再使用文件名了。然而它可以被缓存起来以节省后续打开相同文件的时间。对于访问打开文件表的索引有多种名称。UNIX称之为文件描述符(file descriptor),Window称之为文件句柄(file handle)。因此,只要文件没有被关闭,所有文件操作都是通过打开文件表来进行的。
当一个进程关闭文件,就删除一个相应的单个进程打开文件表的条目,系统范围内打开文件表相应文件条目的打开数也会递减。当打开文件的所有用户都关闭一个文件时,更新的文件元数据会复制到磁盘的目录结构中,系统范围内的打开文件表的相应条目也将删除。
有的系统更加复杂,它们将文件系统作为对其他系统方面的访问接口,如网络。例如UFS的系统范围的打开文件表有关于文件和目录的索引节点(inode)和其他信息。它也有关于网络连接和设备的类似信息。采用这种方式,一个机制满足了多个目的。
分区与安装
磁盘布局因操作系统而异。一个磁盘可以分成多个分区,或者一个卷可以横跨多个磁盘上的数个分区。
分区可以是"生的"(或原始的,raw),即没有文件系统,或者"熟的"(cooked)即含有文件系统。"生"磁盘(raw disk)用于没有合适文件系统的地方。
引导信息能保存在各个分区中。同样,它有自己的格式,因为在引导时系统并没有文件系统设备驱动程序,所以并不能解释文件系统格式。因此,引导信息通常为一组有序块,并作为镜像文件读入内存。该镜像问阿金按预先指定的位置如第一个字节开始执行。引导信息除了包括如何启动一个特定操作系统外,还可以有其他指令。例如,可以把多个操作系统装在这样的系统上,PC和其他系统可以双引导。系统如何知道引导哪个?一个启动加载器能够知道位于引导区的多个文件系统和多个操作系统。一旦装入,它可以引导位于磁盘上的一个操作系统。磁盘可以有多个分区,每个分区包含不同类型的文件系统和不同的操作系统。
根分区(root partition)包括操作系统内核或其他系统文件,在引导时装入内存。其他卷根据不同操作系统可以在引导时自动装入或在此之后手动装入。作为成功装入操作的一部分,操作系统会验证设备上的文件系统确实有效。操作系统通过设备驱动程序读入设备目录并验证目录是否有合适的格式。如果为无效格式,那么检验分区一致性,并根据需要自动或手动地加以纠正。最后,操作系统在其位于内存的装入表中注明该文件系统已装入和该文件系统的类型。
虚拟文件系统
实现多个类型文件系统一个明显但不十分满意的方法是为每个类型编写目录和文件程序。但是,绝大多数操作系统包括UNIX都使用面向对象技术来简化,组织和模块化实现过程。使用这些方法允许不同文件系统类型可通过同样结构来实现,这也包括网络文件类型和NFS。用户可以访问位于本地磁盘的多个文件系统类型,甚至位于网络上的文件系统。
采用数据结构和子程序,可以分开基本系统调用的功能和实现细节。因此,文件系统实现包括三个主要层次,第一层为文件系统接口,包括oepn(),read(),write()和close()调用以及文件描述符。
第二层称为虚拟文件系统(VFS)层,它有两个目的:
VFS层通过定义一个清晰的VFS接口,以将文件系统的通用操作和具体实现分开。多个VFS接口的实现可以共存在同一台机器上,它允许访问已装在本地的多个类型的文件系统。
VFS提供了在网络上唯一标识一个文件的机制。VFS基于称为vnode的文件表示结构,该结构包括一个数值标识符以表示位于整个网络范围内的唯一文件。该网络范围的唯一性用来支持网络文件系统。内核中为每个活动节点(文件或目录)保存一个vnode结构。
VFS根据文件系统类型调用特定文件类型操作以处理本地请求,通过调用NFS协议程序来处理远程请求。文件句柄可以从相应的vnode中构造,并作为参数传递给程序。结构中的第三层实现文件系统类型或远程文件系统协议。
下面简要的讨论一下Linux中的VFS结构。Linux VFS定义的4中主要对象类型是:
索引节点对象(inode object)表示一个单独的文件。
文件对象(file object)表示一个打开的文件
超级块对象(superblock object)表示整个文件系统
目录条目对象(dentry object)表示一个单独的目录条目
VFS对每种类型的对象都定义了一组必须实现的操作。这些类型的每一个对象都包含了一个指向函数表的指针。函数表列出了实际上实现特定对象的操作函数。比如,文件对象的一些操作的缩写(API)包括
目录实现
线性列表
最为简单的目录实现方法是使用存储文件名和数据块指针的线性列表。这种方法编程简单但运行时较为费时。要创建新文件,必须首先搜索目录以确定没有相同名称的文件存在。接着,在目录后增加一个新条目。要删除文件时,根据给定文件名搜溹目录,接着释放分配给它的空间。
目录条目的线性列表的真正缺点是查找文件需要线性搜索。目录信息需要经常使用,用户在访问文件时会注意到实现的快慢。事实上,许多操作系统采用软件缓存来存储最近访问过的目录信息。缓存命中避免了不断从磁盘读取信息。排序列表可以使用二分搜索,并减少平均搜索时间。
哈希表
用于文件目录的另一个数据结构是哈希表。采用这种方法时,除了使用线性列表存储目录外,还使用哈希数据结构。哈希表根据文件名得到一个值,并返回一个指向线性列表中元素的指针。因此,它大大减少目录搜索时间。插入和删除也较简单,不过需要一些预备措施来避免冲突(collision)(两个文件名哈希到相同的位置)。
分配方法
磁盘的直接访问特点使大家能够灵活地实现文件。在绝大多数情况下,一个磁盘可存储许多文件。主要问题是如何为这些文件分配空间,以便有效地使用磁盘空间和快速地访问文件。常用的主要磁盘空间分配方法有三个:连续、链接和索引。每种方法都有其优点和缺点。
连续分配
连续分配(contiguous allocation)方法要求每个文件在磁盘上占有一组连续的块。磁盘地址为磁盘定义了一个线性序列。采用这种序列,假设只有一个作业访问磁盘,在访问块b后访问b+1通常不需要移动磁头。因此,用于访问连续分配文件所需要的寻道数最小。在确实需要寻道时所需要的寻道时间也最小。
对一个连续分配文件的访问很容易。要顺序访问,文件系统会记住上次访问过块的磁盘地址,如果需要可读入下一块。要直接访问一个从块b开始的文件的块i,可以直接访问块b+I,因此连续分配支持顺序访问和直接访问。
不过,连续分配也有一些问题。一个困难是为新文件找到空间。被选择来管理空闲空间的系统决定了这个任务如何完成。
连续磁盘空间分配可以作为在前面描述的通过动态存储分配(dynamic storage-allocation)问题的一个具体应用,即如何从一个空闲孔列表中寻找一个满足大小为n的空间。
连续分配的另一个问题是确定一个文件需要多少空间。
为了减少这些缺点,有的操作系统使用修正的连续分配方案。该方案开始分配一块连续空间,当空间不够时,另一块被称为扩展(extent)的连续空间会添加到原来的分配中。这样,文件块的位置就成为开始地址,块数,加上一个指向下一扩展的指针。。在有的系统上,文件用户可以设置扩展大小,但如果用户设置不正确,将会影响效率。
链接分配
链接分配(linked allocation)解决了连续分配的所有问题。采用链接分配,每个文件是磁盘块的链表;磁盘块分布在磁盘的任何地方。目录包括文件第一块的指针和最后一块的指针。每块都有一个指向下一块的指针。用户不能使用这些指针,因此,如果每块有512B,磁盘地址为4B,那么用户可以使用508B。
要创建新文件,可以简单地在目录中增加一个新条目。对于链接分配,每个目录条目都有一个指向文件首块的指针。
不过,链接分配确实有缺点。主要问题是它只能有效地用于文件的顺序访问。链接分配不能有效地支持文件的直接访问。
一个采用链接分配方法的变种是文件分配表(FAT)的使用。每个卷的开始部分用于存储该FAT。每块都在该表中有一项,该表可以通过块号码来索引。FAT的使用与链表相似。目录条目含有文件首块的块号码。根据块号码索引的FAT条目包含文件下一块的块号码。根据块号索引的FAT条目包含文件的下一块的块号码。这条链会一直继续到最后一块,该块对应FAT条目的值为文件结束值。未使用的块用0值表示。为了文件分配一个新的块只要简单地找到第一个值为0的FAT条目,用新块的地址替换前面的文件结束值,用文件结束值替换0.
如果不对FAT采用缓存,FAT分配方案可能导致大量的磁头寻道时间。
索引分配
链接分配解决了连续分配的外部碎片和大小声明问题。但是,如果不用FAT,那么链接分配就不能有效支持直接访问,这是因为快指针与块分布在整个磁盘,且必须按顺序读取。索引分配(indexed allocation)通过把所有指针放在一起,即通过索引块解决了这个问题。
每个文件都有其索引块,这是一个磁盘块地址的数组。索引块的第i个条目指向文件的第i个块。目录条目包括索引块的地址。要读第i块,通过索引块的第i个条目的指针来查找和读入所需的块。
当创建文件时,索引块的所有指针都设为nil。当首次写入第i块时,先从空闲空间管理器中得到一块,先从空闲空闲管理器中得到一块,再将其地址写到索引块的第i个条目。
如果索引块太小,那么它不能为大文件存储足够多的指针。因此,必须采用一定机制来处理这个问题。针对这一目的的机制包括如下:
链接方案:一个索引块通常为一个磁盘块。因此,它本身能直接读写。为了处理大文件,可以将多个索引块链接起来。
多层索引:链接表示的一种变种是用第一层索引指向一组第二层的索引块,第二层索引块再指向文件快。
组合方案:在UFS中使用的另一方案是将索引块的头15个指针放在文件的inode中。这其中的头12个指针指向直接块;即他们包括了能存储文件数据的块的地址。因此,(不超过12块的)小文件不需要其他的索引块。如果块的大小为4KB,那么不超过48KB的数据可以直接访问。其他3个指针指向间接地址。
空闲空间管理
因为磁盘空间有限,所以如果可能需要将删除文件的空间用于新文件。为了记录空闲磁盘空间,系统需要维护一个空闲空间链表(free-space list)。空闲空间链表记录了所有空闲磁盘空间,即未分配给文件或目录的空间。当创建文件时,搜索空闲空间链表以得到所需要的空间,并分配给新文件。这些空间会从空闲链表中删除。当删除文件时,其磁盘空间会增加到空闲空间表上。
位向量
通常,空闲空间表现为位图(bit map)或位向量(bit vector)。每块用一位表示。如果一块为空闲,那么其位为1;如果一块已分配,那么其位为0。
链表
空闲空间管理的另一个方法是将所有的空闲磁盘块用链表连接起来,并将指向第一空闲块的指针保存在磁盘的特殊位置,同时也缓存在内存中。第一块包含一个下一空闲磁盘块的指针,如此继续下去。好在遍历整个表并不是一个经常操作。通常,操作系统只不过简单地需要一个空闲块以分配给一个文件,所以分配空闲表的第一块就可以了。FAT方法将空闲块的计算结合到分配数据结构中,不再需要另外的方法。
组
对空闲链表的一个改进是将n个空闲块的地址存在第一个空闲块中。这些块中前n-1个确实为空,而最后一块包含另外n个空闲块地址。
计数
另外一种方法是利用这样一个事实:通常,有多个连续块需要同时分配或释放,尤其是在使用连续分配和采用簇时更是如此。因此,不是记录n个空闲块的地址,而是可以记录第一块的地址和紧跟第一块的连续的空闲块的数量n。这样,空闲空间表的每个条目包括磁盘地址和数量。虽然每个条目会比原来需要更多空间,但是表的总长度会更短,这是因为连续块的数量常常大于1。
效率与性能
有的系统有一块独立内存用来做缓冲缓存,位于其中的块假设马上需要使用。其他系统采用页面缓存(page cache)来缓存文件数据。页面缓存使用虚拟内存技术,将文件数据作为页面而不是面向文件系统地块来缓存。采用虚拟地址来缓存文件数据,与采用物理磁盘块来缓存相比,更为高效。许多系统都使用页面缓存来缓存进程页和文件数据。这称为统一虚拟内存(unified buffer cache)。
有的UNIX和Linux版本提供了统一缓冲缓存(unified buffer cache)。为展示统一缓冲缓存的优点,考虑文件打开和访问的两种方法。一种方法是使用内存映射,另一种方法使用标准系统调用read()和write()。如果没有统一缓冲缓存,那么情况会如下图所示。
在这种情况下,标准系统调用read()和write()会经过统一缓冲缓存。内存映调用需要使用两个缓存:页面缓存和缓冲缓存。内存映射先从文件系统中读入磁盘块并存放在缓冲缓存中。因为虚拟内存不能直接与缓冲缓存进行交流,所以缓冲缓存内的文件必须复制到页面缓存中。这种情况称为双重缓存(double caching),需要两次缓存文件数据。提供了统一缓冲缓存,内存映射与read()和write()系统调用都使用同样的页面缓存。这避免了双重缓存,且也允许用虚拟内存系统来管理文件数据。这种统一缓冲缓存如下所示。
页面缓存,文件系统和磁盘驱动程序有着有趣的联系。当数据写到磁盘文件时,页先放在缓存中,并且磁盘驱动程序会根据磁盘地址对输出队列进行排序。这两个操作允许磁盘驱动最小化磁头寻道和优化写数据。除非要求同步写,否则进行写磁盘只是写入缓存,系统在方便时异步地将数据写到磁盘中。因而,用户觉得写非常快。当从磁盘中读入数据时,块I/O系统会执行一定的预读操作;结果是,写比读更加接近于异步。因此,通过文件系统输出到磁盘,通常要比磁盘读入更加快;这与直觉相反。
恢复
一致性检查
部分目录信息保存在内存(或缓存)中以加快访问。因为缓存目录信息写到磁盘并不是马上进行的,所以内存目录信息通常要比相应磁盘信息更新。
通常,一个检查和纠正磁盘不一致对的特殊程序需要在重启时运行。一致性检查程序(consistency checker),例如UNIX下的fsck和MS-DOS下的chkdsk系统程序,将目录结构数据与磁盘数据相比较,并试图纠正所发现的不一致。分配算法和空闲空间管理算法决定了检查程序能发现什么类型的问题,及其如何成功地纠正问题。
基于日志结构的文件系统
数据库基于日志恢复算法已成功应用到一致性检查问题,这种实现称为基于日志的面向事务文件系统(log-based transaction-oriented或journaling file system)。
所有元数据都按顺序写到日志上。执行一个特殊任务的一组操作称为事务(transaction)。这些修改一旦写到这个日志上之后,就可认为已经提交,系统调用就可返回到用户进程,以允许它继续执行。同时,这些日志条目再对真实文件系统结构进行重放。随着修改的进行,可不断地更新一个指针以表示哪些操作已完成和哪些仍然没有完成。当一个完成提交事务已完成,那么就从日志文件中删除(日志文件事实上是个环形缓冲)。环形缓冲写到空间末尾的时候,会从头开始写,从而覆盖掉以前的旧值。
如果系统崩溃,日志文件可能有零个或多个事务。它所包含的任何事务已经由操作系统所提交,但是还没有(对文件系统)完成,所以必须要完成。可以执行事务直到该工作完成,因此文件系统结构仍能保持一致。唯一可能出现的问题是一个事务被中断,即在系统崩溃之前,它还没有被提交。这些事务所做文件系统的修改必须撤销,以恢复文件系统地一致性。这种恢复只是在崩溃时才需要,从而避免了与一致检查有关的所有问题。
对磁盘元数据更新采用日志的另一种好处是,这些更新比直接在磁盘上进行要快。这种改善原因是顺序I/O比随机I/O的性能要好。低效率的同步随机元数据被转换成高效的同步顺序(基于日志文件系统的记录区域)。这些修改再通过随机写而异步回放到适当数据结构。总的结构是面向元数据操作(如文件创建和文件删除)性能的提高。
小结
文件系统持久驻留在外存上,外存设计成可以持久地容纳大量数据。最常用的外存介质是磁盘。
物理磁盘可分成区,以控制介质的使用和允许在同一磁盘上支持多个可能不同的文件系统。这些文件系统安装在逻辑文件系统上,然后才可以使用。文件系统通常按层结构或模块结构来加以实现:底层处理存储设备的物理属性,高层处理符号文件名和文件逻辑属性,中间层将逻辑文件概念映射到物理设备属性。
每个文件系统类型都有其结构和算法。VFS层允许上层统一地处理每个文件系统类型。即使远程文件系统也能集成到文件系统目录结构中,通过VFS接口采用标准系统调用进行操作。
文件在磁盘上有三种不同空间分配方法:连续的,链接的或索引分配。连续分配有外部碎片问题,链接分配的直接访问效率低,索引分配可能因其索引块而浪费一定空间。连续分配可采用区域来扩展,以增加灵活性和降低外部碎片。索引分配需要为索引块提供大量的额外开销。这些算法可以用多种方式进行优化。连续分配的空间可以通过扩展来增大,从而增加灵活性和减少外部碎片。索引分配可按簇(为多个块)来进行,以增加吞吐量和降低所需索引条目的数量。按大簇的索引分配与采用区域的连续分配相似。
空闲空间分配方法也影响磁盘使用效率,文件系统性能,外存可靠性。所使用的方法包括位向量和链表。优化方法包括组合,计数和FAT(将链表放在一个连续区域内)。
目录管理程序必须考虑效率,性能和可靠性。哈希表是最为常用的方法;它快速且高效。然而,表损坏和系统崩溃可能导致目录信息与磁盘内容不一致。一致性检查程序可用来修补损坏。操作系统备份工具允许磁盘数据复制到磁带,使得用户可以恢复数据甚至整个磁盘(因硬件失败,操作系统错误或用户错误)。
由于文件系统在系统操作中的重要位置,其性能和可靠性十分关键。日志结构和缓存等技术可帮助改善性能,日志结构和RAID可提高可靠性。