预计阅读本页时间:-
10.6.3 Linux文件系统的实现
在本节中,我们首先研究虚拟文件系统(Virtual File System,VFS)层支持的抽象。VFS对高层进程和应用程序隐藏了Linux支持的所有文件系统之间的区别,以及文件系统是存储在本地设备,还是需要通过网络访问的远程设备。设备和其他特殊文件也可以通过VFS访问。接下来,我们将描述第一个被Linux广泛使用的文件系统ext2(second extended file system)。随后,我们将讨论ext3文件系统中所作的改进。所有的Linux都能处理有多个磁盘分区且每个分区上有一个不同文件系统的情况。
1.Linux虚拟文件系统
为了使应用程序能够与在本地或远程设备上的不同文件系统进行交互,Linux采用了一个被其他UNIX系统使用的方法:虚拟文件系统。VFS定义了一个基本的文件系统抽象以及这些抽象上允许的操作集合。调用上节中提到的系统调用访问VFS的数据结构,确定要访问的文件所属的文件系统,然后通过存储在VFS数据结构中的函数指针调用该文件系统的相应操作。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
图10-30总结了VFS支持的四个主要的文件系统结构。其中,超级块包含了文件系统布局的重要信息,破坏了超级块将会导致文件系统无法访问。每个i节点(i-node,index-node的简写,但是从来不这样称呼它,而一些人省略了“-”并称之为i节点)表示某个确切的文件。值得注意的是在Linux中,目录和设备也当作是文件,所以它们也有自己对应的i节点。超级块和i节点都有相应的结构,由文件系统所在的物理磁盘维护。

为了便于目录操作及路径(比如/usr/ast/bin)的遍历,VFS支持dentry数据结构,它表示一个目录项。这个数据结构由文件系统在运行过程中创建。目录项被缓存在dentry_cache中,比如,dentry_cache会包含/,/usr,/usr/ast的目录项。如果多个进程通过同一个硬连接(即相同路径)访问同一个文件,它们的文件对象都会指向这个cache中的同一个目录项。
file数据结构是一个打开文件在内存中的表示,并且在调用open系统调用时被创建。它支持read、write、sendfile、lock等上一节中提到的系统调用。
在VFS下层实现的实际文件系统并不需要在内部使用与VFS完全相同的抽象和操作,但是必须实现跟VFS对象所指定的操作在语义上等价的文件系统操作。这四个VFS对象中的operations数据结构的元素都是指向底层文件系统函数的指针。
2.Linux ext2文件系统
接下来,我们介绍在Linux中最流行的磁盘文件系统:ext2。第一个Linux操作系统使用MINIX文件系统,但是它限制了文件名长度并且文件长度最大只能是64MB。后来MINIX被第一个扩展文件系统,ext文件系统取代。ext可以支持长文件名和大文件,但由于它的效率问题,ext被ext2代替,ext2在今天还在广泛使用。
ext2的磁盘分区包含了一个如图10-31所示的文件系统。块0不被Linux使用,而通常用来存放启动计算机的代码。在块0后面,磁盘分区被划分为若干个块组,划分时不考虑磁盘的物理结构。每个块组的结构如下:
第一个块是超级块,它包含了该文件系统的信息,包括i节点的个数、磁盘块数以及空闲块链表的起始位置(通常有几百个项)。下一个是组描述符,存放了位图(bitmap)的位置、空闲块数、组中的i节点数,以及组中目录数等信息,这个信息很重要,因为ext2试图把目录均匀地分散存储到磁盘上。

两个位图分别记录空闲块和空闲i节点,这是从MINIX1文件系统继承的(大多数UNIX文件系统不使用位图,而使用空闲列表)。每一个位图的大小是一个块。如果一个块大小是1KB,那么就限制了块数和i节点数只能是8192个。块数是一个严格的限制,但是在实际应用中,i节点数并不是。
在超级块之后是i节点存储区域,它们被编号为1到某个最大值。每个i节点的大小是128字节,并且每一个i节点恰好描述一个文件。i节点包含了统计信息(包含了stat系统调用能获得的所有信息,实际上stat就是从i节点读取信息的),也包含了所有存放该文件数据的磁盘块的位置。
在i节点区后面是数据块区,所有文件和目录都存放在这个区域。对于一个包含了一个以上磁盘块的文件和目录,这些磁盘块是不需要连续的。实际上,一个大文件的块有可能遍布在整个磁盘上。
目录对应的i节点散布在磁盘块组中。如果有足够的空间,ext2会把普通文件组织到与父目录相同的块组上,而把同一个块上的数据文件组织成初始文件i节点。这个思想来自Berkeley的快速文件系统(McKusick等人,1984)。位图用于快速确定在什么地方分配新的文件系统数据。在分配新的文件块时,ext2也会给该文件预分配许多(8个)额外的数据块,这样可以减少将来向该文件写入数据时产生的文件碎片。这种策略在整个磁盘上实现了文件系统负载平衡,而且由于排列和缩减文件碎片,它的性能也很好。
要访问文件,必须首先使用一个Linux系统调用,例如open,该调用需要文件的路径名。解析路径名以解析出单独的目录。如果使用相对路径,则从当前进程的当前目录开始查找,否则就从根目录开始。在以上两种情况中,第一个目录的i节点很容易定位:在进程描述符中有指向它的指针;或者在使用根目录的情况下,它存储在磁盘上预定的块上。
目录文件允许不超过255个字符的文件名,如图10-32所示。每一个目录都由整数个磁盘块组成,这样目录就可以整体写入磁盘。在一个目录中,文件和子目录的目录项是未排序的,并且一个紧挨着一个。目录项不能跨越磁盘块,所以通常在每个磁盘块的尾部会有部分未使用的字节。

图10-32中的每个目录项由四个固定长度的域和一个可变长度的域组成。第一个域是i节点号,文件colossal的i节点号是19,文件voluminous的i节点号是42,目录bigdir的i节点号是88。接下来是rec_len域,标明该目录项的大小(以字节为单位),可能包括名字后面的一些填充。在名字以未知长度填充时,这个域被用来寻找下一个目录项。这也是图10-32中箭头的含义。接下来是类型域:文件、目录等。最后一个固定域是文件名的长度(以字节为单位),在例子中是8、10和6。最后是文件名,文件名以字节0结束,并被填充到32字节边界。额外的填充可以在此之后。
在图10-32b中,我们看到的是文件voluminous的目录项被移除后同一个目录的内容。这是通过增加colossal的域的长度,将voluminous以前所在的域变为第一个目录项的填充。当然,这个填充可以用来作为后续的目录项。
由于目录是按线性顺序查找的,要找到一个位于大目录末尾的目录项会耗费相当长的时间。因此,系统为近期访问过的目录维护一个缓存。该缓存使用文件名进行查找,如果命中,那么就可以避免费时的线性查找。组成路径的每个部分都在目录缓存中保存一个dentry对象,并且通过它的i节点查找到后续的路径元素的目录项,直到找到真正的文件i节点。
例如,要通过绝对路径名来查找一个文件(如:/usr/ast/file),需要经过如下步骤。首先,系统定位根目录,它通常使用2号i节点(特别是当1号i节点被用来处理磁盘坏块的时候)。系统在目录缓存中存放一条记录以便将来对根目录的查找。然后,在根目录中查找字符串“usr”,得到/usr目录的i节点号。/usr目录的i节点号同样也存入目录缓存。然后这个i节点被取出,并从中解析出磁盘块,这样就可读取/usr目录并查找字符串“ast”。一旦找到这个目录项,目录/usr/ast的i节点号就可以从中获得。有了/usr/ast的i节点号,就可以读取i节点并确定目录所在的磁盘块。最后,从/usr/ast目录查找“file”并确定其i节点号。因此,使用相对地址不仅对用户来说更加方便,而且也为系统节省了大量的工作。
如果文件存在,那么系统提取其i节点号并以它为索引在i节点表(在磁盘上)中定位相应的i节点,并装入内存。i节点被存放在i节点表中,其中i节点表是一个内核数据结构,用于保存所有当前打开的文件和目录的i节点。i节点表项的格式至少要包含stat系统调用返回的所有域,以保证stat正常运行(见图10-28)。图10-33中列出了i节点结构中由Linux文件系统层支持的一些域。实际的i节点结构包含更多的域,这是由于该数据结构也用于表示目录、设备以及其他特殊文件。i节点结构中还包含了一些为将来的应用保留的域。历史已经表明未使用的位不会长时间保持这种方式。

现在来看看系统如何读取文件。对于调用了read系统调用的库函数的一个典型使用是:
n=read(fd,buffer,nbytes);
当内核得到控制权时,它需要从这三个参数以及内部表中与用户有关的信息开始。内部表中的项目之一是文件描述符数组。文件描述符数组用文件描述符作为索引并为每一个打开的文件保存一个表项(最多达到最大值,通常默认是32个)。
这里的思想是从一个文件描述符开始,找到文件对应的i节点为止。考虑一个可能的设计:在文件描述符表中存放一个指向i节点的指针。尽管这很简单,但不幸的是这个方法不能奏效。其中存在的问题是:与每个文件描述符相关联的是用来指明下一次读(写)从哪个字节开始的文件读写位置,它该放在什么地方?一个可能的方法是将它放到i节点表中。但是,当两个或两个以上不相关的进程同时打开同一个文件时,由于每个进程有自己的文件读写位置,这个方法就失效了。
另一个可能的方法是将文件读写位置放到文件描述符表中。这样,每个打开文件的进程都有自己的文件读写位置。不幸的是,这个方法也是失败的,但是其原因更加微妙并且与Linux的文件共享的本质有关。考虑一个shell脚本s,它由顺序执行的两个命令p1和p2组成。如果该shell脚本在命令行
s>x
下被调用,我们预期p1将它的输出写到x中,然后p2也将输出写到x中,并且从p1结束的地方开始。
当shell生成p1时,x初始是空的,从而p1从文件位置0开始写入。然而,当p1结束时就必须通过某种机制使得p2看到的初始文件位置不是0(如果将文件位置存放在文件描述符表中,p2将看到0),而是p1结束时的位置。
实现这一点的方法如图10-34所示。实现的技巧是在文件描述符表和i节点表之间引入一个新的表,叫做打开文件描述表,并将文件读写位置(以及读/写位)放到里面。在这个图中,父进程是shell而子进程首先是p1然后是p2。当shell生成p1时,p1的用户结构(包括文件描述符表)是shell的用户结构的一个副本,因此两者都指向相同的打开文件描述表的表项。当p1结束时,shell的文件描述符仍然指向包含p1的文件位置的打开文件描述。当shell生成p2时,新的子进程自动继承文件读写位置,甚至p2和shell都不需要知道文件读写位置到底是在哪里。
然而,当不相关的进程打开该文件时,它将得到自己的打开文件描述表项,以及自己的文件读写位置,而这正是我们所需要的。因此,打开文件描述表的重点是允许父进程和子进程共享一个文件读写位置,而给不相关的进程提供各自私有的值。
再来看读操作,我们已经说明了如何定位文件读写位置和i节点。i节点包含文件前12个数据块的磁盘地址。如果文件位置是在前12个块,那么这个块被读入并且其中的数据被复制给用户。对于长度大于12个数据块的文件,i节点中有一个域包含一个一级间接块的磁盘地址,如图10-34所示。这个块含有更多的磁盘块的磁盘地址。例如,如果一个磁盘块大小为1KB而磁盘地址长度是4字节,那么这个一级间接块可以保存256个磁盘地址。因此这个方案对于总长度在268KB以内的文件适用。

除此之外,还使用一个二级间接块。它包含256个一级间接块的地址,每个一级间接块保存256个数据块的地址。这个机制能够处理10+216 个块(67 119 104字节)。如果这样仍然不够,那么i节点为三级间接块留下了空间,三级间接块的指针指向许多二级间接块。这个寻址方案能够处理大小为224 个1KB块(16GB)的文件。对于块大小是8KB的情况,这个寻址方案能够支持最大64TB的文件。
3.Linux Ext3文件系统
为了防止由系统崩溃和电源故障造成的数据丢失,ext2文件系统必须在每个数据块创建之后立即将其写出到磁盘上。必需的磁盘磁头寻道操作导致的延迟是如此之长以至于性能差得无法让人接受。因此,写操作被延迟,对文件的改动可能在30秒内都不会提交给磁盘,而相对于现代的计算机硬件来说,这是一段相当长的时间间隔。
为了增强文件系统的健壮性,Linux依靠日志文件系统。Ext3,作为Ext2文件系统的改进,就是一个日志文件系统的例子。
这种文件系统背后的基本思想是维护一个日志,该日志顺序记录所有文件系统操作。通过顺序写出文件系统数据或元数据(i节点,超级块等)的改动,该操作不必忍受随机磁盘访问时磁头移动带来的开销。最后,这些改动将被写到适当的磁盘地址,而相应的日志项可以被丢弃。如果系统崩溃或电源故障在改动提交之前发生,那么在重启动过程中,系统将检测到文件系统没有被正确地卸载。然后系统遍历日志,并执行日志记录所描述的文件系统改动。
Ext3设计成与Ext2高度兼容,事实上,两个系统中所有的核心数据结构和磁盘布局都是相同的。此外,一个作为ext2系统被卸载的文件系统随后可以作为ext3系统被加载并提供日志能力。
日志是一个以环形缓冲器形式组织的文件。日志可以存储在主文件系统所在的设备上也可以存储在其他设备上。由于日志操作本身不被日志记录,这些操作并不是被日志所在的ext3文件系统处理的,而是使用一个独立的日志块设备(Journaling Block Device,JBD)来执行日志的读/写操作。
JBD支持三个主要数据结构:日志记录、原子操作处理和事务。一个日志记录描述一个低级文件系统操作,该操作通常导致块内变化。鉴于系统调用(如write)包含多个地方的改动——i节点、现有的文件块、新的文件块、空闲块列表等,所以将相关的日志记录按照原子操作分成组。Ext3将系统调用过程的起始和结束通知JBD,这样JBD能够保证一个原子操作中的所有日志记录或者都被应用,或者没有一个被应用。最后,主要从效率方面考虑,JBD将原子操作的汇集作为事务对待。一个事务中日志记录是连续存储的。仅当一个事务中的所有日志记录都被安全提交到磁盘后,JBD才允许日志文件的相应部分被丢弃。
把每个磁盘改动的日志记录项写到磁盘可能开销很大,ext3可以配置为保存所有磁盘改动的日志或者仅仅保存文件系统元数据(i节点、超级块、位映射等)改动的日志。只记录元数据会使系统开销更小,性能更好,但是不能保证文件数据不会损坏。一些其他的日志文件系统仅仅维护关于元数据操作的日志(例如,SGI的XFS)。
4./proc文件系统
另一个Linux文件系统是/proc(process)文件系统。其思想来自于Bell实验室开发的第8版UNIX,后来被4.4BSD和System V采用。不过,Linux在几个方面对该思想进行了扩充。其基本概念是为系统中的每个进程在/proc中创建一个目录。目录的名字是进程PID的十制数值。例如,/proc/619是与PID为619的进程相对应的目录。在该目录下是进程信息的文件,如进程的命令行、环境变量和信号掩码等。事实上,这些文件在磁盘上并不存在。当读取这些文件时,系统按需从进程中抽取这些信息,并以标准格式将其返回给用户。
许多Linux扩展与/proc中其他的文件和目录相关。它们包含各种各样的关于CPU、磁盘分区、设备、中断向量、内核计数器、文件系统、已加载模块等信息。非特权用户可以读取很多这样的信息,于是就可以通过一种安全的方式了解系统的行为。其中的部分文件可以被写入,以达到改变系统参数的目的。