预计阅读本页时间:-
10.6 Linux文件系统
在包括Linux在内的所有操作系统中,最可见的部分是文件系统。在本节的以下部分,我们将介绍隐藏在Linux文件系统、系统调用以及文件系统实现背后的基本思想。这些思想中有一些来源于MULTICS,虽然有很多已经被MS-DOS、Windows和其他操作系统使用过了,但是其他的都是UNIX类操作系统特有的。Linux的设计非常有意思,因为它忠实地秉承了“小的就是美好的”(Small is Beautiful)的设计原则。虽然只是使用了最简的机制和少量的系统调用,但是Linux却提供了强大的和优美的文件系统。
10.6.1 基本概念
最初的Linux文件系统是MINIX 1文件系统。但由于它只能支持14字节的文件名(为了和UNIX Version 7兼容)和最大64MB的文件(这在只有10MB硬盘的年代是足够强大的),在Linux刚被开发出来的时候,开发者就意识到需要开发更好的文件系统(开始于MINIX 1发布的5年后)。对MINIX 1文件系统进行第一次改进后的文件系统是ext文件系统。ext文件系统能支持255个字符的文件名和2GB的文件大小,但是它的速度比MINIX 1慢,所以仍然有必要对它进行改进。最终,ext2文件系统被开发出来,它能够支持长文件名和大文件,并且具有更好的性能,这使得它成为了Linux主要的文件系统。不过,Linux使用虚拟文件系统(VFS)层支持很多类型的文件系统(VFS将在下文介绍)。在Linux链接时,用户可以选择要构造到内核中的文件系统。如果需要其他文件系统,可以在运行时作为模块动态加载。
Linux中的文件是一个长度为0或多个字节的序列,可以包含任意的信息。ASCII文件、二进制文件和其他类型的文件是不加区别的。文件中各个位的含义完全由文件所有者确定,而文件系统不会关心。文件名长度限制在255个字符内,可以由除了NUL以外的所有ASCII字符构成,也就是说,一个包含了三个回车符的文件名也是合法的(但是这样命名很不方便)。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
按照惯例,许多程序能识别的文件包含一个基本文件名和一个扩展名,中间用一个点连接(点也被认为是占用了文件名的一个字符)。例如一个名为prog.c的文件是一个典型的C源文件,prog.f90是一个典型的FORTRAN 90程序文件,而prog.o通常是一个object文件(编译器的输出文件)。这个惯例不是操作系统要求的,但是一些编译器和程序希望是这样,比如一个名为prog.java.gz的文件可能是一个gzip压缩的Java程序。
为了方便,文件可以被组织在一个目录里。目录存储成文件的形式并且在很大程度上可以作为文件处理。目录可以包含子目录,这样可以形成有层次的文件系统。根目录表示为“/”,它通常包含了多个子目录。字符“/”还用于分离目录名,所以/usr/ast/x实际上是说文件x位于目录ast中,而目录ast位于/usr目录中。图10-23列举了根目录下几个主要的目录及其内容。

在Linux中,不管是对shell还是一个打开文件的程序来说,都有两种方法表示一个文件的文件名。第一种方法是使用绝对路径,绝对路径告诉系统如何从根目录开始查找一个文件。例如/usr/ast/books/mos3/chap-10,这个路径名告诉系统在根目录里寻找一个叫usr的目录,然后再从usr中寻找ast目录……依照这种方式,最终找到chap-10文件。
绝对路径的缺点是文件名太长并且不方便。因为这个原因,Linux允许用户和进程把他们当前工作的目录标识为工作目录,这样路径名就可以相对于工作目录命名,这种方式命名的目录名叫做相对路径。例如,如果/usr/ast/books/mos3是工作目录,那么shell命令
cp chap-10 backup-10
和长命令cp/usr/ast/books/mos3/chap-10/usr/ast/books/mos3/backup-10的效果是一样的。
一个用户要使用属于另一个用户的文件或者使用文件树结构里的某个文件的情况是经常发生的。例如,两个用户共享一个文件,这个文件位于其中某个用户所拥有的目录中,另一个用户需要使用这个文件时,必须通过绝对路径才能引用它(或者通过改变工作目录的方式)。如果绝对路径名很长,那么每次输入时将会很麻烦。为了解决这个问题,Linux提供了一种指向已存在文件的目录项,称作链接(link)。
以图10-24a为例,两个用户Fred和Lisa一起工作来完成一个项目,他们需要访问对方的文件。如果Fred的工作目录是/usr/fred,他可以使用/usr/lisa/x来访问Lisa目录下的文件x。Fred也可以如图10-24b所示的方法,在自己目录下创建一个链接,然后他就可以用x来代替/usr/lisa/x了。

在上面的例子中,我们说在创建链接之前,Fred引用Lisa的文件x的惟一方法是使用绝对路径。实际上这并不正确,当一个目录被创建出来时,有两个目录项“.”和“..”被自动创建出来存放在该目录中,前者代表工作目录自身,而后者表示该目录的父目录,也就是该目录所在的目录。这样一来,在/usr/fred目录中访问Lisa的文件x的另一个路径是:../lisa/x。
除了普通的文件之外,Linux还支持字符特殊文件和块特殊文件。字符特殊文件用来建模串行I/O设备,比如键盘和打印机。如果打开并从/dec/tty中读取内容,等于从键盘读取内容,而如果打开并向/dev/lp中写内容,等于向打印机输出内容。块特殊文件通常有类似于/dev/hd1的文件名,它用来直接向硬盘分区中读取和写入内容,而不需要考虑文件系统。一个偏移为k字节的read操作,将会从相应分区开始的第k个字节开始读取,而完全忽略i节点和文件的结构。原始块设备常被一些建立(如mkfs)或修补(如fsck)文件系统的程序用来进行分页和交换。
许多计算机有两块或更多的磁盘。银行使用的大型机,为了存储大量的数据,通常需要在一台机器上安装100个或更多的磁盘。甚至在PC上也至少有两块磁盘——一块硬盘和一个光盘驱动器(如DVD)。当一台机器上安装了多个磁盘的时候,如何处理它们就是一个问题。
一个解决方法是在每一个磁盘上安装自包含的文件系统,使它们之间互相独立。考虑如图10-25a所示的解决方法,有一个硬盘C:和一个DVD D:,它们都有自己的根目录和文件。如果使用这种解决方法,除了默认盘外,使用者必须指定设备和文件,例如,要把文件x复制到目录d中(假设C:是默认盘),应该使用命令
cp D:/x/a/d/x
这种方法被许多操作系统使用,包括MS-DOS、Windows 98和VMS。

Linux的解决方法是允许一个磁盘挂载到另一个磁盘的目录树上,比如,我们可以把DVD挂载在目录/b上,构成如图10-25b所示的文件系统。挂载之后,用户能够看见一个目录树,而不再需要关心文件在哪个设备上,上面提到的命令就可以变成
cp/b/x/a/d/x
和所有文件都在硬盘上是一样的。
Linux文件系统的另一个有趣的性质是加锁(locking)。在一些应用中会出现两个或更多的进程同时使用同一个文件的情况,可能导致竞争条件(race condition)。有一种解决方法是使用临界区,但是如果这些进程属于相互不认识的独立的用户,这种解决方法是不方便的。
考虑这样的一个例子,一个数据库组织许多文件在一个或多个目录中,它们可以被不相关的用户访问。可以通过设置信号量来解决互斥的问题,在每个目录或文件上设置一个信号量,当程序需要访问相应的数据时,在相应的信号量上做一个down操作。但这样做的缺点是,尽管进程只需要访问一条记录却使得整个目录或文件都不能访问。
由于这种原因,POSIX提供了一种灵活的、细粒度的机制,允许一个进程使用一个不可分割的操作对小到一个字节、大到整个文件加锁。加锁机制要求加锁者标识要加锁的文件、开始位置以及要加锁的字节数。如果操作成功,系统会在表格中添加记录说明要求加锁的字节(如数据库的一条记录)已被锁住。
系统提供了两种锁,共享锁和互斥锁。如果文件的一部分已经被加了共享锁,那么在上面尝试加共享锁是允许的,但是加互斥锁是不会成功的;如果文件的一部分已经被加了互斥锁,那么在互斥锁解除之前加任何锁都不会成功。为了成功地加锁,请求加锁的部分的所有字节都必须是可用的。
在加锁时,进程必须指出当加锁不成功时是否阻塞。如果选择阻塞,则当已经存在的锁被删除时,进程被放行并在文件上加锁;如果选择不阻塞,系统调用在加锁失败时立即返回,并设置状态码表明加锁是否成功,如果不成功,由调用者决定下一步动作(比如,等待或者继续尝试)。
加锁区域可以是重叠的。如图10-26a所示,进程A在第4字节到第7字节的区域加了共享锁,之后,进程B在第6字节到第9字节加了共享锁,如图10-26b所示,最后,进程C在第2字节到第11字节加了共享锁。由于这些锁都是共享锁,是可以同时存在的。

此时,如果一个进程试图在图10-26c中文件的第9个字节加互斥锁,并设置加锁失败时阻塞,那么会发生什么?由于该区域已经被进程B和进程C两个进程加锁,这个进程将会被阻塞,直到进程B和进程C释放它们的锁为止。