虚拟文件系统(VFS)作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口。通过虚拟文件系统,程序可以利用标准的 Unix 系统调用对不同的文件系统(甚至不同介质上的文件系统)进行读写操作。
一、通用文件系统接口
VFS 可以使得用户可以直接使用 open()、read() 和 write() 等文件相关系统调用,而不需要考虑具体文件系统和实际物理介质。VFS 与块 I/O 相结合,提供抽象、接口,使得用户空间的程序调用统一的系统调用访问各种文件。
二、文件系统抽象层
内核在所有类型的文件系统接口上建立了一个抽象层,该抽象层使 Linux 能够支持各种文件系统。
VFS 之所以能衔接各种各样的文件系统,是因为它定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。而实际的文件系统通过编程提供 VFS 所期望的抽象接口和数据结构,这样,内核就可以毫不费力地连接在一起。
如下面这个例子:
ret = write(fd, buf, len)
该系统调用将 buf 指针指向的长度为 len 字节的数据写入文件描述符 fd 对应的文件的当前位置。这个系统调用首先执行 sys_write() 系统调用函数,该函数要找到 fd 所在的文件系统所实现的写操作,然后再执行该操作,数据最终通过该操作写入介质。
三、Unix 文件系统
Unix 有四种与文件系统相关的传统抽象概念:文件、目录项、索引节点和挂载点(mount point)。
从本质上讲文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。文件系统的通用操作包含创建、删除和挂载等。在 Unix 中,文件系统被安装到一个特定的挂载点上,该挂载点在全局层次结构中被称为命名空间,所有的已安装文件系统都作为根文件系统树的枝叶出现在系统中。
VFS 把目录当作文件对待,所以可以对目录执行和文件相同的操作。
Unix 系统将文件的相关信息和文件本身这两个概念加以区分,例如访问控制权限、大小、拥有者等信息。文件相关信息也被称作文件的元数据,被存储在一个单独的数据结构中,这个结构被称为索引结点(inode)。文件系统的信息则存储在超级块中,超级块是一种包含文件系统信息的数据结构。
比如说在磁盘上,文件(目录也属于文件)信息按照索引节点的形式存储在单独的块中;控制信息被集中存储在磁盘的超级块中。
四、VFS 对象及其数据结构
VFS 采用的是面向对象的设计思路,使用一组数据结构来代表通用文件对象。这些结构体包含数据的同时也包含操作这些数据的函数指针,其中的操作函数由具体的文件系统实现。
VFS 有四个主要的对象类型,分别是:
- 超级块对象,代表一个具体的已安装文件系统。、
- 索引节点对象,它代表一个具体文件。
- 目录项对象,它代表一个目录项,是路径的一个组成部分(注:目录项不同于目录,目录属于文件对象)。
- 文件对象,它代表由进程打开的文件。
上述每个对象都包含一个对应的操作对象,这些操作对象描述了内核针对主要对象可以使用的方法:
- super_operations 对象,包含内核针对特定文件系统能调用的方法,比如 write_inode() 和 sync_fs() 等方法。
- inode_operations 对象,包含内核针对特定文件能调用的方法,比如 create() 和 link()。
- dentry_operations 对象,包含内核针对特定目录所能调用的方法,如 d_compare() 和 d_delete()。
- file_operations 对象,包含进程针对已打开文件所能调用的方法,比如 read() 和 write()。
操作对象作为一个结构体指针来实现,里面包含指向操作其父对象的函数指针。对于其中许多方法来说,可以继承使用 VFS 提供的通用函数,如果通用函数的功能无法满足需要,那么就必须使用实际文件系统独有的方法来填充这些函数指针。
五、超级块对象
各种文件系统都必须实现超级块对象,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块。对于并非基于磁盘的文件系统(如基于内存的文件系统,sysfs),它们会在使用现场创建超级块并将其保存到内存中。
超级块对象由 super_block 结构体表示,定义在文件 <linux/fs.h> 中:
六、索引节点对象
索引节点对象包含了内核在操作文件或目录时需要的全部信息。索引节点对象与其对应的文件是分开存放的。索引节点对象由 inode 结构体表示,定义在文件 <linux/fs.h> 中:
一个索引节点代表文件系统中的一个文件,它也可以是设备或管道这样的特殊文件。因此索引节点结构体中有一些和特殊文件相关的项,如 i_pipe 项就指向一个代表有名管道的数据结构,i_bdev 指向块设备结构体,i_cdev 指向字符设备结构体。
索引节点对象的 inode_operations 项也非常重要,因为它描述了 VFS 用以操作索引节点对象的所有方法,这些方法由文件系统实现。
inode->inode_operations->对应操作函数
七、目录项对象
VFS 把目录当作文件来对待,所以在路径 /bin/vi 中,bin 和 vi 都属于文件,bin为目录文件而 vi 是一个普通文件,路径中的每个组成部分都由一个索引节点对象表示。
为了方便查找操作,VFS 引入了目录项,每个目录项代表路径中的一个特定部分,如 前面的 /、bin 和 vi 都属于目录项的概念。在路径中的每一个部分都是目录项对象,目录项的引入可以让文件的查找变得更加方便。
目录项对象由 dentry 结构体表示,定义在文件 <linux/dcache.h> 中:
与前面两个对象不同,目录项没有对应的磁盘数据结构,VFS 根据字符串形式的路径名现场创建它。由于目录项并非真正保存在磁盘上,所以目录项结构体没用是否被修改的标志。
目录项有三种有效状态:被使用、未被使用和负状态。
- 一个被使用的目录项对应一个有效的索引节点(即 d_inode 指向相应的索引节点),并且表明该对象存在一个或多个使用者(d_count 为正值)。
- 一个未被使用的目录项对应一个有效的索引节点(d_inode 指向一个索引节点), 但是应指明 VFS 当前并未使用它(d_count 为 0)。该目录项对象仍然指向一个有效对象,而且保留在缓存中以便需要时再使用它,所以之后再需要它时不需要重新创建。
- 一个负状态的目录项(或者说无效目录项)没有对应的有效索引节点(d_inode 为 NULL),因为索引节点已被删除了,或路径不再正确了,但目录项仍保留。
如果 VFS 遍历路径名中所有的元素并将它们逐个地解析成目录项对象,还要达到最深层目录,将是一件非常费力的工作,所以内核将目录项对象缓存再目录项缓存中(简称 dcache),以节省时间。
八、文件对象
文件对象表示进程已打开的文件。该对象由相应的 open() 系统调用创建,由 close() 系统调用撤销。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。虽然一个文件对应的文件对象不是唯一的,但对应的索引节点和目录项对象是唯一的。
文件对象由 file 结构体表示,定义在 <linux/fs.h> 中:
类似于目录项对象,文件对象实际上没有对应的磁盘数据,只有当一个文件被进程打开时才被创建。
文件对象的操作如下: