为支持各种本机文件系统,且在同时允许访问其他操作系统的文件,Linux内核在用户进程(或C标准库)和文件系统实现之间引入了一个抽象层。该抽象层称之为虚拟文件系统(Virtual File System),简称VFS。VFS的任务并不简单。一方面,它用来提供一种操作文件、目录及其他对象的统一方法。另一方面,它必须能够与各种方法给出的具体文件系统的实现达成妥协。
图1 用作文件系统抽象的VFS层
1 文件系统类型
文件系统一般可以分为下面3种。
(1) 基于磁盘的文件系统(Disk-based Filesystem)是在非易失介质上存储文件的经典方法,用以在多次会话之间保持文件的内容。实际上,大多数文件系统都由此演变而来。比如,一些众所周知的文件系统,包括Ext2/3、Reiserfs、FAT和iso9660。
(2)虚拟文件系统(Virtual Filesystem)在内核中生成,是一种使用户应用程序与用户通信的方法。proc文件系统是这一类的最佳示例。它不需要在任何种类的硬件设备上分配存储空间。相反,内核建立了一个层次化的文件结构,其中的项包含了与系统特定部分相关的信息。
(3)网络文件系统(Network Filesystem)是基于磁盘的文件系统和虚拟文件系统之间的折中。这种文件系统允许访问另一台计算机上的数据,该计算机通过网络连接到本地计算机。在这种情况下,数据实际上存储在一个不同系统的硬件设备上。
2 通用文件模型
VFS不仅为文件系统提供了方法和抽象,还支持文件系统中对象(或文件)的统一视图。VFS提供一种结构模型,包含了一个强大文件系统所应具备的所有组件。但该模型只存在于虚拟中,必须使用各种对象和函数指针与每种文件系统适配。所有文件系统的实现都必须提供与VFS定义的结构配合的例程,以弥合两种视图之间的差异。当然,虚拟文件系统的结构并非是幻想出来的东西,而是基于描述经典文件系统所使用的结构。VFS抽象层的组织显然也与Ext2文件系统类似。
在处理文件时,内核空间和用户空间使用的主要对象是不同的。对用户程序来说,一个文件由一个文件描述符标识。该描述符是一个整数,在所有有关文件的操作中用作标识文件的参数。文件描述符是在打开文件时由内核分配,只在一个进程内部有效。两个不同进程可以使用同样的文件描述符,但二者并不指向同一个文件。基于同一个描述符来共享文件是不可能的。
内核处理文件的关键是inode。每个文件(和目录)都有且只有一个对应的indoe,其中包含元数据(如访问权限、上次修改的日期,等等)和指向文件数据的指针。但inode并不包含一个重要的信息项,即文件名,这看起来似乎有些古怪。通常,假定文件名称是其主要特征之一,因此应该被归入用于管理文件的对象(inode)中。
2.1 inode
2.1.1 基本概念
如何用数据结构表示目录的层次结构?如前所述,inode对文件实现来说是一个主要的概念,但它也用于实现目录。换句话说,目录只是一种特殊的文件,它必须正确地解释。inode的成员可能分为下面两类。
① 描述文件状态的元数据。例如,访问权限或上次修改的日期。
② 保存实际文件内容的数据段(或指向数据的指针)。就文本文件来说,用于保存文本。
2.1.2 举个栗子
为阐明如何用inodes来构造文件系统的目录层次结构,我们来考察内核查找对应于/usr/bin/ emacs的inode过程。
查找起始于inode,它表示根目录/,对系统来说必须总是已知的。该目录由一个inode表示,其数据段并不包含普通数据,而是根目录下的各个目录项。这些项可能代表文件或其他目录。每个项由两个成员组成。
① 该目录项的数据所在inode的编号。
② 文件或目录的名称。
系统中所有inode都有一个特定的编号,用于唯一地标识各个inode。文件名和inode之间的关联即通过该编号建立。
(1)查找操作中的第一步是查找子目录usr的inode。这一步会扫描根inode的数据段,直至找到一
个名为usr的目录项(如果查找失败,则返回File not found错误)。相关的inode可以根据inode编号定位。
(2)重复上述步骤,但这一次在usr对应inode的数据段中查找名为bin的目录项,以便根据其inode编号定位inode。下一步在bin的inode数据段中,将查找名为emacs的目录项。这仍然会返回一个inode编号,这一次的inode表示文件而非目录。图8-2给出了查找过程结束时的情形(所经由的路径由对象之间的指针表示)。
(3)最后一个inode的文件内容,与前三个inode不同。前三个inode都表示目录,其文件内容是目录项的一个列表,包括子目录和文件。与emacs文件关联的inode,其数据段存储了文件的内容。
图2 查找/usr/bin/emacs的操作
2.2 链接
链接(link)用于建立文件系统对象之间的联系。linux中有两种类型的链接,分别是符号链接(软链接)与硬链接。
符号链接可以认为是“方向指针”(至少从用户程序来看是这样),表示某个文件存在于特定的位置。当然我们都知道,实际的文件在其他地方。符号链接可以认为是一个目录项,其中除了指向文件名的指针,并不存在其他数据。目标文件删除时,符号链接仍然继续保持。对每个符号链接都使用了一个独立的inode。相应inode的数据段包含一个字符串,给出了链接目标的路径。
对于符号链接,可以区分原始文件和链接。对于硬链接,情况不是这样。在硬链接已经建立后,无法区分哪个文件是原来的,哪个是后来建立的。在硬链接建立时,创建的目录项使用了一个现存的inode编号。删除符号链接并不困难,但硬链接的处理有一点技巧。
我们假定硬链接(B)与原始文件(A)共享同一个inode。一个用户现在想要删除A。这通常会销毁相关的inode连同其数据段,以便释放存储空间供后续使用。那么接下来B就不能继续访问了,因为相关的inode和文件信息不再存在了。当然,这不是我们想要的行为。在inode中加入一个计数器,即可防止这种情况。每次对文件创建一个硬链接时,都将计数器加1。如果其中一个硬链接或原始文件被删除(不可能区分这两种情况),那么将计数器减1。只有在计数器归0时,我们才能确认该inode不再使用,可以从系统删除。
2.3 编程接口
用户进程和内核的VFS实现之间的接口照例由系统调用组成,其中大多数涉及对文件、目录和一般意义上的文件系统的操作。对上述的操作,内核提供了50多个系统调用。我们只考察最重要的调用,以阐明关键原则。
文件使用之前,必须用open或openat系统调用打开。在成功打开文件之后,内核向用户层返回一个非负的整数。这种分配的文件描述符起始于3。我们知道,尽管没有明确规定,这个标识符号之所以不从0开始,是因为所有的进程都分配了前3个标识符(0~2)。0表示标准输入,1表示标准输出,2表示标准错误输出。
在文件已经打开后,其名称就没什么用处了。它现在由其文件描述符唯一标识,所有其他库函数都需要传递文件描述符作为一个参数(进一步传递到系统调用)。尽管传统上文件描述符在内核中足以标识一个文件,但现在情况不再如此。由于多个命名空间和容器的引入,具有相同数值的多个文件描述符可以共存于内核中。对文件的唯一表示由一个特殊的数据结构(struct file)提供,我将在下文讨论。我们在示例程序调用close的部分会看到文件描述符,该调用关闭与文件的“连接”(释放文件描述符,以便在后续打开其他文件时使用)。 read也需要将文件描述符作为第一个参数,以标识读取数据的来源。
在一个打开文件中的当前位置保存在文件位置指针(file pointer)中,这是一个整数,指定了当前位置与文件起始点的偏移量。对随机存取文件而言,该指针可以设置为任何值,只要不超出文件存储容量范围即可。这用于支持对文件数据的随机访问。其他文件类型,如命名管道或字符设备的设备文件,不支持这种做法。它们只能从头至尾顺序读取。在文件打开时,可以指定各种标志(如O_RDONLY),用来规定文件的存取模式。
2.4 将文件作为通用接口
UNIX是基于少量审慎选择的范型而建立的。一个非常重要的隐喻贯穿内核的始终(特别是VFS),尤其是在有关输入和输出机制的实现方面。
万物皆文件
好,我们承认:当然该规则有少数例外(例如,网络设备),但大多数内核导出、用户程序使用的函数都可以通过VFS定义的文件接口访问。以下是使用文件作为其主要通信手段的一部分内核子系统:
- 字符和块设备;
- 进程之间的管道;
- 用于所有网络协议的套接字;
- 用于交互式输入和输出的终端。
要注意,上述的某些对象不一定联系到文件系统中的某个项。例如,管道是通过特殊的系统调用生成,然后由内核在VFS的数据结构中管理,管道并不对应于一个可以用通常的rm、ls等命令访问的真正的文件系统项。我们特别感兴趣的是访问块设备和字符设备的设备文件。这些是真正的文件,通常位于/dev目录。其内容是在进行读写操作时由相关的设备驱动程序动态生成的。
3 VFS的结构
既然我们已经熟悉了VFS的基本结构和用户接口,下面我们将重点讨论其实现细节。在VFS接口的实现中,涉及大量数据结构,有些非常冗长。因此最好草拟出各个组成部分的一个大体概观,并说明其联结方式。
图3 各个VFS组件的相互关系
未完待续 ...
参考书目:
《深入Linux内核架构》