一、概述
ext系列文件系统和xfs文件系统是Linux使用最多的文件系统,也是很多发布版本默认选择的文件系统。而事实上,Linux支持的文件系统非常广泛,Minix,FAT,VFAT,NFS,NTFS…等等。我们知道一个分区代表了一个文件系统,不同的分区可以安装不同的文件系统。由此可知,Linux支持多种不同文件系统,要实现这个目的,就要将对各种不同文件系统和管理纳入到一个统一的框架中,让内核中的文件系统界面成为一条文件系统“总线”,使用户程序 可以通过同一个文件系统操作界面,也就是同一组系统调用,对各种不同的文件系统(以及 文件)进行操作。那么在Linux系统中,如何管理和调用这些文件系统的接口呢?通过VFS来实现。
1.1 VFS定义和作用
我们知道在Linux系统中一切皆文件,在Linux系统中基本上把其中的所有内容都看作为文件,除了我们普遍意义上理解的文件之外,像目录、字符设备、块设备、 套接字、进程、线程、管道等都被视为“文件”。例如对于块设备,我们通过fdisk -l显示块设备列表,其实块设备可以理解为在文件夹/dev下面的文件,只不过这些文件是特殊的文件。"一切皆文件"是Linux的基本哲学之一,实现这一行为的基础,正是Linux的虚拟文件系统机制。
VFS是一个抽象层,其向上提供了统一的文件访问接口,而向下则兼容了各种不不同类型的文件系统。不仅仅是诸如Ext2、ExtExt4、XFS、windows家族的NTFS和Btrfs等常规意义上的文件系统,还可以是比如上图的proc等伪文件系统和设备,也可以是诸如NFS、CIFS等网络文件系统。
VFS,Virtual File System虚拟文件系统,也称为虚拟文件系统开关(Virtual Filesystem Switch),就是采用标准的Linux系统调用读写位于不同物理介质上的不同文件系统,即为各类文件系统提供了一个统一的操作界面和应用编程接口,VFS是一个内核软件层。
VFS是一个可以让open()、read()、write()等系统调用不用关心底层的存储介质和文件系统类型就可以工作的抽象层,如下图所示。
举个例子,Linux用户程序可以通过read()
来读取ext3
、NFS
、XFS
等文件系统的文件,也可以读取存储在SSD
、HDD
等不同存储介质的文件,无须考虑不同文件系统或者不同存储介质的差异。
另外,VFS实现了一部分公共的功能,例如页缓存和inode缓存等,从而避免多个文件系统重复实现的问题。有关高速缓存,后续章节还有介绍。
1.2 VFS原理
VFS之所以能够衔接各种各样的文件系统,是因为它抽象了一个通用的文件系统模型,定义了通用文件系统都支持的、概念上的接口。新的文件系统只要支持并实现这些接口,并注册到Linux内核中,即可安装和使用。
举个例子,比如Linux写一个文件:
int ret = write(fd, buf, len);
调用了write()
系统调用,它的过程简要如下:
- 首先,勾起VFS通用系统调用
sys_write()
处理。 - 接着,
sys_write()
根据fd
找到所在的文件系统提供的写操作函数,比如op_write()
。 - 最后,调用
op_write()
实际的把数据写入到文件中。
操作示意图如下:
二、文件系统分类
2.1 基于磁盘的文件系统
管理本地磁盘和模拟磁盘的设备
Linux的Ext2、Ext3和ReiserFS
Unix文件系统的变种,如sysv、UFS、MINIX和VERITAS VxFS。
微软的文件系统,如MS-DOS、VFAT、NTFS
ISO9660 CD-ROM文件系统和UDF(Universal Disk Format)DVD文 件系统
其他文件系统如IBM OS/2中的HPFS、Apple’s Macintosh的HFS、AFFS 和ADFS等。
2.2 网络文件系统
它允许方便访问网络上其他计算机的文件系统。VFS支持的网络文件系统有 NFS、Coda、AFS(Andrew filesystem)、CIFS(Common Internet Filesystem) 和NCP(Novell’s NetWare Core Protocol)。
2.3 特殊文件系统
它不管理磁盘空间,/proc就是一个特殊的文件系统。
三、VFS内部结构和对象类型
VFS层通过定义一个清晰的VFS接口,以将文件系统的通用操作和具体实现分开。多个VFS接口的实现可以共存在同一台机器上,它允许访问已装在本地的多个类型的文件系统。
VFS提供了在网络上唯一标识一个文件的机制。VFS基于称为vnode的文件表示结构,该结构包括一个数值标识符以表示位于整个网络范围内的唯一文件。该网络范围的唯一性用来支持网络文件系统。内核中为每个活动节点(文件或目录)保存一个vnode结构。
VFS根据文件系统类型调用特定文件类型操作以处理本地请求,通过调用NFS协议程序来处理远程请求。文件句柄可以从相应的vnode中构造,并作为参数传递给程序。它的下一层实现文件系统类型或远程文件系统协议。
Linux为了实现这种VFS系统,采用面向对象的设计思路,主要抽象了四种对象类型:
超级块对象(superblock object)表示整个文件系统
索引节点对象(inode object)表示一个单独的文件
目录项对象(dentry object)表示一个单独的目录项(或者称作目录条目)
文件对象(file object)表示一个打开的文件
如下图所示:
每个对象都包含一组操作方法,用于操作相应的文件系统。
备注:Linux将目录当做文件对象来处理,是另一种形式的文件,它里面包含了一个或多个目录项。而目录项是单独抽象的对象,主要包括文件名和索引节点号。因为目录是可以层层嵌套,以形成文件路径,而路径中的每一部分,其实就是目录项。
接下来介绍一下各个对象的作用以及相关操作。
3.1 超级块
超级块用于存储文件系统的元信息,由super_block
结构体表示,定义在<linux/fs.h>
中,元信息里面包含文件系统的基本属性信息,比如有:
- 索引节点信息
- 挂载的标志
- 操作方法 s_op
- 安装权限
- 文件系统类型、大小、区块数
- 等等等等
其中操作方法 s_op 对每个文件系统来说,是非常重要的,它指向该超级块的操作函数表,包含一系列操作方法的实现,这些方法有:
- 分配inode
- 销毁inode
- 读、写inode
- 文件同步
- 等等
当VFS需要对超级块进行操作时,首先要在超级块的操作方法 s_op 中,找到对应的操作方法后再执行。比如文件系统要写自己的超级块:
superblock->s_op->write_supper(sb);
创建文件系统时,其实就是往存储介质的特定位置,写入超级块信息;而卸载文件系统时,由VFS调用释放超级块。
Linux支持众多不同的文件系统,file_system_type
结构体用于描述每种文件系统的功能和行为,包括:
- 名称、类型等
- 超级块对象链表
- 等
当向内核注册新的文件系统时,其实是将file_system_type
对象实例化,然后加入到Linux的根文件系统的目录树结构上。
3.2 索引
索引节点对象包含Linux内核在操作文件、目录时,所需要的全部信息,这些信息由inode
结构体来描述,定义在<linux/fs.h>
中,主要包含:
- 超级块相关信息
- 目录相关信息
- 文件大小、访问时间、权限相关信息
- 引用计数
- 等等
一个索引节点inode
代表文件系统中的一个文件,只有当文件被访问时,才在内存中创建索引节点。与超级块类似的是,索引节点对象也提供了许多操作接口,供VFS系统使用,这些接口包括:
- create(): 创建新的索引节点(创建新的文件)
- link(): 创建硬链接
- symlink(): 创建符号链接。
- mkdir(): 创建新的目录。
等等,我们常规的文件操作,都能在索引节点中找到相应的操作接口。
3.3 目录项
前面提到VFS把目录当做文件对待,比如/usr/bin/vim
,usr
、bin
和vim
都是文件,不过vim
是一个普通文件,usr
和bin
都是目录文件,都是由索引节点对象标识。
由于VFS会经常的执行目录相关的操作,比如切换到某个目录、路径名的查找等等,为了提高这个过程的效率,VFS引入了目录项的概念。一个路径的组成部分,不管是目录还是普通文件,都是一个目录项对象。/
、usr
、bin
、vim
都对应一个目录项对象。不过目录项对象没有对应的磁盘数据结构,是VFS在遍历路径的过程中,将它们逐个解析成目录项对象。
目录项由dentry
结构体标识,定义在<linux/dcache.h>
中,主要包含:
- 父目录项对象地址
- 子目录项链表
- 目录关联的索引节点对象
- 目录项操作指针
- 等等
目录项有三种状态:
- 被使用:该目录项指向一个有效的索引节点,并有一个或多个使用者,不能被丢弃。
- 未被使用:也对应一个有效的索引节点,但VFS还未使用,被保留在缓存中。如果要回收内存的话,可以撤销未使用的目录项。
- 负状态:没有对应有效的索引节点,因为索引节点被删除了,或者路径不正确,但是目录项仍被保留了。
将整个文件系统的目录结构解析成目录项,是一件费力的工作,为了节省VFS操作目录项的成本,内核会将目录项缓存起来。
3.4 文件
文件对象是进程打开的文件在内存中的实例。Linux用户程序可以通过open()
系统调用来打开一个文件,通过close()
系统调用来关闭一个文件。由于多个进程可以同时打开和操作同一个文件,所以同一个文件,在内存中也存在多个对应的文件对象,但对应的索引节点和目录项是唯一的。
文件对象由file
结构体表示,定义在<linux/fs.h>
中,主要包含:
- 文件操作方法
- 文件对象的引用计数
- 文件指针的偏移
- 打开文件时的读写标识
- 等等等等
类似于目录项,文件对象也没有实际的磁盘数据,只有当进程打开文件时,才会在内存中产生一个文件对象。
每个进程都有自己打开的一组文件,由file_struct
结构体标识,该结构体由进程描述符中的files
字段指向。主要包括:
- fdt
- fd_array[NR_OPEN_DEFAULT]
- 引用计数
- 等
fd_array数组指针指向已打开的文件对象,如果打开的文件对象个数 > NR_OPEN_DEFAULT,内核会分配一个新数组,并将 fdt 指向该数组。
除此之外,内核还为所有打开文件维持一张文件表,包括:
- 文件状态标志
- 文件偏移量
- 等
关于多进程打开同一文件以及文件共享更详细的信息,可以阅读《UNIX环境高级编程》第三章。
四、从VFS到具体文件系统
4.1 挂载
我们从上面得知,VFS可以管理各种文件系统,那么VFS和文件系统怎么关联的呢?给用户如何展示的呢?通过挂载。
如下图所示,该系统根文件系统是Ext3文件系统,而在其/mnt目录下面又分别挂载了Ext4文件系统和XFS文件系统。最后形成了一个由多个文件系统组成的文件系统树。
挂载是用户态发起的命令,就是我们知道的mount命令,该命令执行的时候需要指定文件系统的类型(本文假设Ext2)和文件系统数据的位置(也就是设备)。通过这些关键信息,VFS就可以完成Ext2文件系统的初始化,并将其关联到当前已经存在的文件系统中,也就是建立其图2所示的文件系统树。
在挂载的过程中,最为重要的数据结构是vfsmount,它代表一个挂载点。其次是dentry和inode,这两个都是对文件的表示,且都会缓存在哈希表中以提高查找的效率。
其中inode是对磁盘上文件的唯一表示,其中包含文件的元数据(管理数据)和文件数据等内容,但不含文件名称。而dentry则是为了Linux内核中查找文件方便虚拟出来的一个数据结构,其中包含文件名称、子目录(如果存在的话)和关联的inode等信息。
dentry结构体最为关键,其维护了内核中的文件目录树。其中里面比较重要的几个结构体分别是d_name、d_hash和d_subdirs。其中d_name代表一个路径节点的名称(文件夹名称)、d_hash则用于构建哈希表,d_subdirs则是下级目录(或文件)的列表。这样,通过dentry就可以形成一个非常复杂的目录树。
4.2 文件处理流程
文件处理流程包括两步:
1)我们在访问一个文件之前首先要打开它(open)文件访问
2)然后进行文件的读写操作(read或者write)
我们知道,在用户态打开一个文件是返回的是一个文件描述符,其实也就是一个整数值;同时,访问文件也是通过这个文件描述符进行的。那么操作系统是怎么通过这个整数值实现不同类型文件系统的访问呢?不同文件系统的差异其实就是inode中初始化的函数指针的差异。
在Linux操作系统中,文件的打开必须要与进程(或者线程)关联,也就是说一个打开的文件必须隶属于某个进程。
在linux内核当中一个进程通过task_struct结构体描述,而打开的文件则用file结构体描述,打开文件的过程也就是对file结构体的初始化的过程。在打开文件的过程中会将inode部分关键信息填充到file中,特别是文件操作的函数指针。在task_struct中保存着一个file类型的数组,而用户态的文件描述符其实就是数组的下标。这样通过文件描述符就可以很容易到找到file,然后通过其中的函数指针访问数据。
我们以Ext2文件系统的写数据为例来看看文件处理流程和各个层级之间的关系,如下图:
在调用用户态的写数据接口的时候,需要传入文件描述符。内核根据文件描述符找到file,然后调用函数接口(file->f_op->write)文件磁盘数据。其中file结构体的f_op指针就是在打开文件的时候通过inode初始化的。
五. 总结
Linux支持了很多种类的文件系统,包含本地文件系统ext3
、ext4
到网络文件系统NFS
、HDFS
等,VFS系统屏蔽了不同文件系统的操作差异和实现细节,提供了统一的实现框架,也提供了标准的操作接口,这大大降低了操作文件和接入新文件系统的难度。
参考资料
- 《深入理解LINUX内核》第三版
- Linux内核设计与实现
- 浅谈Linux虚拟文件系统
- https://baijiahao.baidu.com/s?id=1621555464151870974&wfr=spider&for=pc