35 | 瞧一瞧Linux:虚拟文件系统如何管理文件?
下载APP
关闭
渠道合作
推荐作者
35 | 瞧一瞧Linux:虚拟文件系统如何管理文件?
2021-07-28 LMOS 来自北京
《操作系统实战45讲》
课程介绍
讲述:陈晨
时长14:33大小13.29M
你好,我是 LMOS。
在前面的课程中,我们已经实现了 Cosmos 下的文件系统 rfs,相信你已经感受到了一个文件系统是如何管理文件的。今天我们一起来瞧一瞧 Linux 是如何管理文件,也验证一下 Linux 那句口号:一切皆为文件。
为此,我们需要首先搞清楚什么是 VFS,接着理清为了实现 VFS 所用到的数据结构,然后看看一个文件的打开、读写、关闭的过程,最后我们还要亲自动手实践,在 VFS 下实现一个“小”且“能跑”的文件系统。
什么是 VFS
VFS(Virtual Filesystem)就像伙伴系统、SLAB 内存管理算法一样,也是 SUN 公司最早在 Sloaris 上实现的虚拟文件系统,也可以理解为通用文件系统抽象层。Linux 又一次“白嫖”了 Sun 公司的技术。
在 Linux 中,支持 EXT、XFS、JFS、BTRFS、FAT、NTFS 等多达十几种不同的文件系统,但不管在什么储存设备上使用什么文件系统,也不管访问什么文件,都可以统一地使用一套 open(), read()、write()、close() 这样的接口。
这些接口看上去都很简单,但要基于不同的存储设备设计,还要适应不同的文件系统,这并不容易。这就得靠优秀的 VFS 了,它提供了一个抽象层,让不同的文件系统表现出一致的行为。
对于用户空间和内核空间的其他部分,这些文件系统看起来都是一样的:文件都有目录,都支持建立、打开,读写、关闭和删除操作,不用关注不同文件系统的细节。
我来给你画张图,你一看就明白了。
VFS架构图
你有没有发现,在计算机科学领域的很多问题,都可以通过增加一个中间的抽象层来解决,上图中 Linux 的 VFS 层就是应用和许多文件系统之间的抽象层。VFS 向上对应用提供了操作文件的标准接口,向下规范了一个文件系统要接入 VFS 必需要实现的机制。
后面我们就会看到,VFS 提供一系列数据结构和具体文件系统应该实现的回调函数。这样,一个文件系统就可以被安装到 VFS 中了。操作具体文件时,VFS 会根据需要调用具体文件系统的函数。从此文件系统的细节就被 VFS 屏蔽了,应用程序只需要调用标准的接口就行了。
VFS 数据结构
VFS 为了屏蔽各个文件系统的差异,就必须要定义一组通用的数据结构,规范各个文件系统的实现,每种结构都对应一套回调函数集合,这是典型的面向对象的设计方法。
这些数据结构包含描述文件系统信息的超级块、表示文件名称的目录结构、描述文件自身信息的索引节点结构、表示打开一个文件的实例结构。下面我们依次探讨这些结构。
超级块结构
首先我们来看一看超级块结构,这个结构用于一个具体文件系统的相关信息,其中包含了 VFS 规定的标准信息,也有具体文件系统的特有信息,Linux 系统中的超级块结构是一个文件系统安装在 VFS 中的标识。我们来看看代码,如下所示。
上述代码中我删除了我们现在不用关注的代码,在文件系统被挂载到 VFS 的某个目录下时,VFS 会调用获取文件系统自己的超级块的函数,用具体文件系统的信息构造一个上述结构的实例,有了这个结构实例,VFS 就能感知到一个文件系统插入了。
下面我们来看看超级块函数集合。
上述代码中 super_operations 结构中所有函数指针所指向的函数,都应该要由一个具体文件系统实现。
有了超级块和超级块函数集合结构,VFS 就能让一个文件系统的信息和表示变得规范了。也就是说,文件系统只要实现了 super_block 和 super_operations 两个结构,就可以插入到 VFS 中了。但是,这样的文件系统没有任何实质性的功能,我们接着往下看。
目录结构
Linux 系统中所有文件都是用目录组织的,就连具体的文件系统也是挂载到某个目录下的。Linux 系统的目录结构逻辑示意图,如下所示。
Linux目录结构
上图中显示了 Linux 文件目录情况,也显示了一个设备上的文件系统是如何挂载到某个目录下的。那么 VFS 用什么来表示一个目录呢?我们来看看代码,如下所示。
我们可以发现,dentry 结构中包含了目录的名字和挂载子目录的链表,同时也能指向父目录。但是需要注意的是,目录也是文件,需要用 inode 索引结构来管理目录文件数据。
这个目录文件数据,你可以把它想象成一个表,表有三列,它们分别是:名称、类型(文件或者目录)、inode 号。扫描这个表,就可以找出这个目录文件中包含的所有子目录或者文件。
接着我们来看看目录函数集, 如下所示。
dentry_operations 结构中的函数,也需要具体文件系统实现,下层代码查找或者操作目录时 VFS 就会调用这些函数,让具体文件系统根据自己储存设备上的目录信息处理并设置 dentry 结构中的信息,这样文件系统中的目录就和 VFS 的目录对应了。
现在我们已经解决了目录,下面我们就去看看 VFS 怎么实现表示文件。
文件索引结点
VFS 用 inode 结构表示一个文件索引结点,它里面包含文件权限、文件所属用户、文件访问和修改时间、文件数据块号等一个文件的全部信息,一个 inode 结构就对应一个文件,代码如下所示。
inode 结构表示一个文件的全部信息,但这个 inode 结构是 VFS 使用的,跟某个具体文件系统上的“inode”结构并不是一一对应关系。
所以,inode 结构还有一套函数集合,用于具体文件系统根据自己特有的信息,构造出 VFS 使用的 inode 结构,这套函数集合如下所示。
上述代码中删除了一些我们不用关心的接口,VFS 通过定义 inode 结构和函数集合,并让具体文件系统实现这些函数,使得 VFS 及其上层只要关注 inode 结构,底层的具体文件系统根据自己的文件信息生成相应的 inode 结构,达到了 VFS 表示一个文件的目的。
下面我们再看一个实例,进一步理解 VFS 如何表示一个打开的文件。
打开的文件
如何表示应用进程打开的不同文件呢? VFS 设计了一个文件对象结构解决这个问题,文件对象结构表示进程已打开的文件。
如果我们站在应用程序的角度思考,文件对象结构会首先进入我们的视野。应用程序直接处理的就是文件,而不是超级块、索引节点或目录项。文件对象结构包含了我们非常熟悉的信息,如访问模式、当前读写偏移等。我们来看代码,如下所示。
在进程结构中有个文件表,那个表其实就是 file 结构的指针数组,进程每打开一个文件就会建立一个 file 结构实例,并将其地址放入数组中,最后返回对应的数组下标,就是我们调用 open 函数返回的那个整数。
对于 file 结构,也有对应的函数集合 file_operations 结构,下面我们再次看看它,如下所示。
file_operations 结构中的函数指针有 31 个,这里我删除了我们不需要关注的函数指针,这些函数依然需要具体文件系统来实现,由 VFS 层来调用。
到此为止,有超级块、目录结构、文件索引节点,打开文件的实例,通过四大对象就可以描述抽象出一个文件系统了。而四大对象的对应的操作函数集合,又由具体的文件系统来实现,这两个一结合,一个文件系统的状态和行为都具备了。
这样一个具体的文件系统,我们就可以安装在 VFS 中运行了。
四大对象结构的关系
我们已经了解构成文件系统的四大对象结构,但是要想完全了解它们的工作机制,还必须要搞清楚,随着 VFS 代码的运行,这些对象结构在内存中的建立和销毁以及它们之间的组织关系。
一图胜千言,我来为你画一幅全景图,你就明白四大对象结构之间的关系了。
VFS对象关系示意图
上图中展示了 spuer_block、dentry、inode、file 四大结构的关系,当然这只是打开一个文件的情况,如果打开了多个文件则相应的结构实例就会增加,不过底层逻辑还是前面图里梳理的这样,万变不离其宗。
搞清楚了四大结构的关系后,我们就可以探索文件相关的操作了。
文件操作
Linux 下常规的文件操作就是打开、读、写、关闭,让我们分别讨论一下这几种文件操作的流程。
打开文件
在对文件进行读写之前,需要先用 open 函数打开这个文件。应用程序使用标准库的 open 函数来打开一个文件。
在 x86_64 架构里,open 函数会执行 syscall 指令,从用户态转换到内核态,并且最终调用到 do_sys_open 函数,然进而调用 do_sys_openat2 函数。
我给你画一幅流程图,你一看就明白了。
打开文件流程
上图中清楚了展示了从系统调用开始,打开文件的全部主要流程,file、dentry、inode 三个结构在这个流程中扮演了重要角色。在查找路径和检查权限后,进入了具体文件系统的打开流程。
读写文件
只要打开了一个文件,就可以对文件进行进一步的读写操作了。其实读写本是两个操作,只数据流向不同:读操作是数据从文件经由内核流向进程,而写操作是数据从进程经由内核流向文件。
所以,下面我们以读操作为例,看看读操作的流程,我依然用画图的方式为你展示这一流程,如下所示。
读文件流程示意图
上图中展示了读文件操作的函数调用流程,写文件操作的流程和读文件操作的流程一样,只是数据流向不同,我就不展开了,你可以自己想一下。
关闭文件
我们打开了文件,也对文件进行了读写,然后就到了关闭文件的环节。为什么要关闭文件呢?因为打开文件时分配了很多资源,如 file、dentry、inode,内存缓冲区等,这些资源使用了都要还给系统,如若不然,就会导致资源泄漏。
下面我们就来看看关闭文件的操作流程,我同样用画图的方式为你展示这一流程,如下所示。
关闭文件流程示意图
以上就是关闭一个文件的全部流程。它回收了 file 结构,其中最重要是调用了文件系统的 flush 函数,它给了文件系统一个刷新缓冲区,把数据写回储存设备的机会,这样就保证了储存设备数据的一致性。
文件系统实例
为了进一步加深理解,我为你写了一个 400 行代码左右的最小文件系统,放在本课的目录中,它就是 trfs,这是一个内存文件系统,支持文件的建立、打开、读写、关闭等操作,通过内存块存放数据。下面仅对文件系统的注册和使用进行介绍。
注册 trfs
我们先来看看如何注册 trfs 文件系统的。由于我们的文件系统是写在 Linux 内核模块中的,所以我们要在模块初始化函数中注册文件系统 ,Linux 注册文件系统需要一个参数,即文件系统类型结构,它里面放着文件系统名字、文件系统挂载、卸载的回调函数,代码如下所示。
上面的代码只展示了注册文件系统的代码,其它代码在课程相关代码目录下。支持文件打开、读写、关闭操作,能够在内存中保存文件数据。
使用 trfs 文件系统
注册了 trfs 文件系统,这不等于可以使用这个文件系统存取文件了。那么如何使用 trfs 文件系统呢?当然首先是编译 trfs 内核模块代码,在终端中 cd 到对应的目录下执行 make,然后把编译好的内核模块插入到系统中,最后就是将这个文件系统挂载到一个具体的目录下。代码如下。
有了上述代码,挂载 trfs 到 /mnt 下,我们就可以用 touch 建立一个文件,然后用 cat 读取这个文件了。
好了,关于 trfs 我们就介绍到这里了,trfs 的代码我已经帮你写好了,你可以自己慢慢研究,有什么问题也可以和我交流。
重点回顾
至此,Linux 的虚拟文件系统就告一段落了,同时也标志着我们整个文件系统章节结束了。那么本节课中学了什么呢?我来为你梳理一下。
1.什么是 VFS。VFS 是虚拟文件系统,是 Linux 中一个中间层,它抽象了文件系统共有数据结构和操作函数集合。一个具体的文件系统只要实现这些函数集合就可以插入 VFS 中了,也因为 VFS 的存在,使得 Linux 可以同时支持各种不同的文件系统。
2.VFS 的数据结构,为了搞清楚 VFS 的实现原理,我们研究了它的数据结构,分别是表示文件系统的超级块结构、表示文件路径的目录结构、表示文件自身的索引结点结构,还有进程打开的文件实例结构,最后还了解了它们之间的关系。
3. 为了进一步了解 VFS 和具体文件系统的工作机制,我们研究了文件的打开、读写、关闭等操作流程,在这些流程我们明白了 VFS 是如何和具体文件系统打通的。
4. 为了弄懂一个具体文件系统是如何安装到 VFS 中的,我们实现了一个小的 trfs 文件系统,trfs 将文件数据保存在内存中, 将 trfs 挂载到 Linux 中某个目录下就可以让一些标准应用进行文件操作了。
你或许还想知道 EXT4 文件系统是如何划分储存设备的,还想知道 EXT4 是如何管理目录和文件索引结点的。那请你以勤奋为舟,遨游在 LInux 代码的海洋中,寻找 EXT4 这座大岛吧。
思考题
请说一说 super_block,dentry,inode 这三个数据结构 ,一定要在储存设备上对应存在吗?
欢迎你在留言区跟我交流互动,也推荐你把这节课分享给朋友一起学习进步。
我是 LMOS,我们下节课见!
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 6
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
34 | 仓库管理:如何实现文件的六大基本操作?
下一篇
36 | 从URL到网卡:如何全局观察网络数据流动?
精选留言(7)
- pedro置顶2021-07-28请说一说 super_block,dentry,inode 这三个数据结构 ,一定要在储存设备上对应存在吗? 不需要严格对应,之所以要对应是为了使用、维护更加方便,抽象是软件设计最大的魅力。
作者回复: 铁子 正确
6 - neohope2021-08-08一、数据结构 1、四大基本结构 A、超级块管理为super_block,用于描述存储设备上的文件系统,可以从super_block出发把存储设备上的内容读取出来 B、目录结构管理为dentry,通过其来组织整个目录结构 C、文件索引节点管理为inode,可以先把它看作是存储设备上的具体对象,一个inode可以对应多个dentry【比如link】 D、文件管理为file,描述进程中的某个文件对象 2、Linux在挂载文件系统时,会读取文件系统超级块super_block,然后从超级块出发读取并构造全部dentry目录结构;dentry目录结构指向存储设备文件时,是一个个的inode结构。 3、应用程序在打开文件时,在进程结构task_struct->fs_struct中,记录进程相关的文件系统信息,这样就可以对文件系统,进行新增、删除、打开、关闭等相关操作。 4、同时,在进程结构task_struct->files_struct->fdtable->file,保存全部打开的文件指针,文件指针file结构中,会保存inode指针,从而可以获取文件权限、文件访问记录、文件数据块号的信息,进一步可以从文件读取文件信息。 二、trfs demo 1、除上面的结构外,内部使用了两个结构:文件描述fileinfo,目录描述dir_entry A、fileinfo记录在了inode的私有数据中,这样通过inode就可以方便的找到fileinfo B、如果是文件,fileinfo.data中记录的就是文件内容 C、如果是文件夹,fileinfo.data记录的就是一个个dir_entry 2、trfs基于非连续内存 A、由MAX_FILES+1个fileinfo组成,记录在全局变量finfo_arr中,但第0和第MAX_FILES个好像没有使用 B、每个fileinfo中包含一个文件块,大小为MAX_BLOCKSIZE C、并没有使用单独的位图,而是通过每个fileinfo来记录其使用情况的 3、初始化 A、初始化了finfo_arr结构 trfs_init->init_fileinfo B、超级块创建,占用了finfo_arr[1] trfs_mount->mount_nodev->trfs_fill_super 4、使用 A、每次新建文件或文件夹,就占用一个空闲的fileinfo B、删除文件或文件夹,就将一个fileinfo设置为可用 C、读写文件就是通过file找到fileinfo.data D、查找和枚举就是通过file找到fileinfo.data,然后访问其中的每个dir_entry展开
作者回复: 正确 的
3 - Fan2021-07-29请说一说 super_block,dentry,inode 这三个数据结构 ,一定要在储存设备上对应存在吗? 要的。
作者回复: 不需要 只是需要对应的文件系统代码在内存中构造出相应的结构就行了
共 2 条评论1 - 蓝色梦幻2022-05-14请教一下:"有了上述代码,挂载 trfs 到 /mnt 下,我们就可以用 touch 建立一个文件,然后用 cat 读取这个文件了。" 这里具体要怎么操作呀?我可以用findmnt 查看到trfs系统的挂载,后续要怎么操作呢? TARGET SOURCE FSTYPE OPTIONS ├─/mnt none trfs rw,relatime展开
作者回复: 直接 用touch cat 啊
- 艾恩凝2022-05-13打卡
编辑回复: 加油咯
- Geek_cb2b432021-11-13请问在一个新建的空文件,从100兆的位置写200兆数据,操作的流程是什么,文件的大小是300兆吗?
作者回复: 不一定的
- 不及胜于过之2021-08-08文件系统就是 super_block/和super_operations,dentry和dentry_operations,inode和inode_operations,file 和 file_operations,真的是醍醐灌顶,大佬可以按这个表述风格,详细说下mount 嘛, 一直理解的不是很深刻,尤其是容器用到的union mount
作者回复: 好的 敬请期待