欢迎光临散文网 会员登陆 & 注册

虚拟文件系统

2022-07-20 19:01 作者:补给站Linux内核  | 我要投稿

通常我们使用的磁盘和光盘都属于块设备,也就是说它们都是按照 数据块 来进行读写的,可以把磁盘和光盘想象成一个由数据块组成的巨大数组。但这样的读写方式对于人类来说不太友好,所以一般要在磁盘或者光盘上面挂载 文件系统 才能使用。那么什么是 文件系统 呢? 文件系统 是一种存储和组织数据的方法,它使得对其访问和查找变得容易。通过挂载文件系统后,我们可以使用如 /home/docs/test.txt 的方式来访问磁盘中的数据,而不用使用数据块编号来进行访问。

在Linux系统中,可以使用多种文件系统来挂载不同的设备,如 ext2、ext3、nfs等等。但提供给用户的文件处理接口是一致的,也就是说不管使用 ext2 文件系统还是使用 ext3 文件系统,处理文件的接口都是一样的。这样的好处是,用户不用关心使用了什么文件系统,只需要使用统一的方式去处理文件即可。那么Linux是如何做到的呢?这就得益于 虚拟文件系统(Virtual File System,简称 VFS)。

虚拟文件系统 为不同的文件系统定义了一套规范,各个文件系统必须按照 虚拟文件系统的规范 编写才能接入到 虚拟文件系统中。这有点像面向对象语言里面的 接口,当一个类实现了某个接口的所有方法时,便可以把这个类当做成此接口。VFS 主要为用户和内核架起一道桥梁,用户可以通过 VFS 提供的接口访问不同的文件系统,如下图:



下面我们开始分析 虚拟文件系统 的实现原理。

虚拟文件系统抽象数据结构

Linux奉行了Unix的理念:一切皆文件,比如一个目录是一个文件,一个设备也是一个文件等,因而文件系统在Linux中占有非常重要的地位。

因为要为不同类型的文件系统定义统一的接口层,所以 VFS 定义了一系列的规范,真实的文件系统必现按照 VFS 的规范来编写程序。VFS 抽象了几个数据结构来组织和管理不同的文件系统,分别为:超级块(super_block)、索引节点(inode)、目录结构(dentry) 和 文件结构(file),要理解 VFS 就必须先了解这些数据结构的定义和作用。


【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)      

超级块(super block)

因为Linux支持多文件系统,所以在内核中必须通过一个数据结构来描述具体文件系统的信息和相关的操作等,VFS 定义了一个名为 超级块(super_block) 的数据结构来描述具体的文件系统,也就是说内核是通过超级块来认知具体的文件系统的,一个具体的文件系统会对应一个超级块结构,其定义如下(由于super_block的成员比较多,所以这里只列出部分):

下面我们介绍一下一些比较重要的成员:

  • s_dev:用于保存设备的设备号

  • s_blocksize:用于保存文件系统的数据块大小(文件系统是以数据块为单位的)

  • s_type:文件系统的类型(提供了读取设备中文件系统超级块的方法)

  • s_op:超级块相关的操作列表

  • s_root:挂载的根目录

索引节点(inode)

索引节点(inode) 是 VFS 中最为重要的一个结构,用于描述一个文件的meta(元)信息,其包含的是诸如文件的大小、拥有者、创建时间、磁盘位置等和文件相关的信息,所有文件都有一个对应的 inode 结构。inode 的定义如下(由于inode的成员也是非常多,所以这里也只列出部分成员,具体可以参考Linux源码):

下面也介绍一下 inode 中几个比较重要的成员:

  • i_uid:文件所属的用户

  • i_gid:文件所属的组

  • i_rdev:文件所在的设备号

  • i_size:文件的大小

  • i_atime:文件的最后访问时间

  • i_mtime:文件的最后修改时间

  • i_ctime:文件的创建时间

  • i_op:inode相关的操作列表

  • i_fop:文件相关的操作列表

  • i_sb:文件所在文件系统的超级块

我们应该重点关注 i_op 和 i_fop 这两个成员。i_op 成员定义对目录相关的操作方法列表,譬如 mkdir()系统调用会触发 inode->i_op->mkdir() 方法,而 link() 系统调用会触发 inode->i_op->link() 方法。而 i_fop 成员则定义了对打开文件后对文件的操作方法列表,譬如 read() 系统调用会触发 inode->i_fop->read() 方法,而 write() 系统调用会触发 inode->i_fop->write() 方法。

目录项(dentry)

目录项的主要作用是方便查找文件。一个路径的各个组成部分,不管是目录还是普通的文件,都是一个目录项对象。如,在路径 /home/liexusong/example.c 中,目录 /, home/, liexusong/ 和文件 example.c 都对应一个目录项对象。不同于前面的两个对象,目录项对象没有对应的磁盘数据结构,VFS 在遍历路径名的过程中现场将它们逐个地解析成目录项对象。其定义如下:

文件结构(file)

文件结构用于描述一个已打开的文件,其包含文件当前的读写偏移量,文件打开模式和文件操作函数列表等,文件结构定义如下:

下图展示了各个数据结构之间的关系:



虚拟文件系统的实现

接下来我们分析一下虚拟文件系统的实现。

注册文件系统

Linux为了支持不同的文件系统而创造了虚拟文件系统,虚拟文件系统更像一个规范(或者说接口),真实的文件系统需要实现虚拟文件系统的规范(接口)才能接入到Linux内核中。

要让Linux内核能够发现真实的文件系统,那么必须先使用 register_filesystem() 函数注册文件系统,register_filesystem() 函数实现如下:

register_filesystem() 函数的实现很简单,就是把类型为 struct file_system_type 的 fs 添加到 file_systems全局链表中。struct file_system_type 结构的定义如下:

其中比较重要的字段是 read_super,用于读取文件系统的超级块结构。在Linux初始化时会注册各种文件系统,比如 ext2 文件系统会调用 register_filesystem(&ext2_fs_type) 来注册。

当安装Linux系统时,需要把磁盘格式化为指定的文件系统,其实格式化就是把文件系统超级块信息写入到磁盘中。但Linux系统启动时,就会遍历所有注册过的文件系统,然后调用其 read_super() 接口来尝试读取超级块信息,因为每种文件系统的超级块都有不同的魔数,用于识别不同的文件系统,所以当调用 read_super() 接口返回成功时,表示读取超级块成功,而且识别出磁盘所使用的文件系统。具体过程可以通过 mount_root() 函数得知:

在上面的for循环中,遍历了所有已注册的文件系统,并且调用其 read_super() 接口来尝试读取超级块信息,如果成功表示磁盘所使用的文件系统就是当前文件系统。成功读取超级块信息后,会把根目录的 dentry 结构保存到当前进程的 root 和 pwd 字段中,root 表示根目录,pwd 表示当前工作目录。

打开文件

要使用一个文件前必须打开文件,打开文件使用 open() 系统调用来实现,而 open() 系统调用最终会调用内核的 sys_open() 函数,sys_open() 函数实现如下:



sys_open() 函数的主要流程是:

  • 通过调用 get_unused_fd() 函数获取一个空闲的文件描述符。

  • 调用 filp_open() 函数打开文件,返回打开文件的file结构。

  • 调用 fd_install() 函数把文件描述符与file结构关联起来。

  • 返回文件描述符,也就是 open() 系统调用的返回值。

在上面的过程中,最重要的是调用 filp_open() 函数打开文件,filp_open() 函数的实现如下:

filp_open() 函数首先调用 get_empty_filp() 函数获取一个空闲的file结构,然后调用 open_namei() 函数来打开对应路径的文件。open_namei() 函数会返回一个 dentry结构,就是对应文件路径的 dentry结构。所以 open_namei() 函数才是打开文件的核心函数,其实现如下:

上面的代码去掉了很多权限验证的代码,open_namei() 函数首先会调用 lookup_dentry() 函数打开文件并获得文件打开后的 dentry结构,如果文件不存在并且打开文件的时候设置了 O_CREAT 标志位,那么就调用 vfs_create() 函数创建文件。我们先来看看 vfs_create() 函数的实现:


从 vfs_create() 函数的实现可知,最终会调用 inode结构 的 create() 方法来创建文件。这个方法由真实的文件系统提供,所以真实文件系统只需要把创建文件的方法挂载到 inode结构 上即可,虚拟文件系统不需要知道真实文件系统的实现过程,这就是虚拟文件系统可以支持多种文件系统的真正原因。

而 lookup_dentry() 函数最终会调用 real_lookup() 函数来逐级目录查找并打开。real_lookup() 函数代码如下:

参数 parent 是父目录的 dentry结构,而参数 name 是要打开的目录或者文件的名称。real_lookup() 函数最终也会调用父目录的 inode结构 的 lookup() 方法来查找并打开文件,然后返回打开后的子目录或者文件的 dentry结构。lookup() 方法需要把要打开的目录或者文件的 inode结构 从磁盘中读入到内存中(如果目录或者文件存在的话),并且把其 inode结构 保存到 dentry结构 的 d_inode 字段中。

filp_open() 函数会把 inode结构 的文件操作函数列表复制到 file结构 中,如下:

这样,file结构 就有操作文件的函数列表。

读写文件

读取文件内容通过 read() 系统调用完成,而 read() 系统调用最终会调用 sys_read() 内核函数,sys_read() 内核函数的实现如下:

sys_read() 函数首先会调用 fget() 函数把文件描述符转换成 file结构,然后再通过调用 file结构 的 read() 方法来读取文件内容,read() 方法是由真实文件系统提供的,所以最终的过程会根据不同的文件系统而进行不同的操作,比如ext2文件系统最终会调用 generic_file_read() 函数来读取文件的内容。

把内容写入到文件是通过调用 write() 系统调用实现,而 write() 系统调用最终会调用 sys_write() 内核函数,sys_write() 函数的实现如下:

sys_write() 函数的实现与 sys_read() 类似,首先会调用 fget() 函数把文件描述符转换成 file结构,然后再通过调用 file结构 的 write() 方法来把内容写入到文件中,对于ext2文件系统,write() 方法对应的是 ext2_file_write()函数。



虚拟文件系统的评论 (共 条)

分享到微博请遵守国家法律