Linux内核设计与实现(七)| 虚拟文件系统
时间:2023-04-21 08:37:00
文章目录
- 虚拟文件系统
-
- 1.通用文件系统接口
- 2.文件系统抽象层
- 3.Unix文件系统
- 4.VFS对象及其数据结构
- 5.超级块对象
- 6.索引节点对象
- 7.目录项对象
-
- 7.1 目录项状态
- 7.2 目录项缓存
- 8.文件对象
- 9.与文件系统相关的数据结构
- 10.与过程相关的数据结构
虚拟文件系统
- 概述
虚拟文件系统又称虚拟文件交换,简称虚拟文件交换VFS,系统中的文件系统依赖和与文件系统相关的界面为用户空间提供VFS协同工作,通过VFS我们可以操作不同的文件系统,甚至在不同的介质上读写文件系统
- 工作结构图
使用cp(1)命令从ext硬盘件系统格式的硬盘复制数据ext2.可移动磁盘上的文件系统格式。两个不同的文件系统,两个不同的介质,连接到同一个 VFS 上
1.通用文件系统接口
- 概述
VFS用户可以直接使用open()、read()和write()这样的系统调用而不考虑具体的文件系统和实际的物理介质。现在听起来并不新奇(我们早就认为这是理所当然的)。然而,使这些通用系统调用能够跨越各种文件系统和不同媒体,绝不是微不足道的结果。更重要的是,系统调用可以在这些不同的文件系统和介质之间执行——我们可以使用从一个文件系统复制或移动数据到另一个文件系统的标准系统。老式操作系统(如DOS)上述工作无法完成,任何访问非本地文件系统都必须依靠特殊工具。抽象层是由现代操作系统引入的,比如Linux,只有通过虚拟接口访问文件系统,才能使这种协作和泛型访问成为可能。
2.文件系统抽象层
- 概述
这种通用接口可以用来操作所有类型的文件系统,因为核心在其底层文件系统接口上建立了抽象层。该抽象层使Linux即使它们在功能和行为上有很大的不同,也能支持各种文件系统。支持多文件系统,VFS包括任何文件系统常用功能集和行为(接口和数据结构)的通用文件系统模型。
抽象层可以方便、简单地支持各种类型的文件系统。通过编程提供实际文件系统VFS预期的抽象接口和数据结构可以毫不费力地与任何文件系统合作,为用户提供空间的接口也可以与任何文件系统无缝连接,完成实际工作。
- write()调用各级配合
系统调用将buf指针的长度为len文件描述字节的数据fd相应文件的当前位置。该系统首先被一个通用系统调用sys_write()处理,sys_write()找到函数fd文件系统实际上给出了哪个写作操作,然后执行操作。
ret = write (fd, buf, len);
3.Unix文件系统
- 概述
Unix与文件系统相关的传统抽象概念有四种:
文件、目录项、索引节点和安装点( mount point)。
- Unix文件系统的特点
文件系统本质上是一种包含文件、目录和相关控制信息的特殊数据分层存储结构。文件系统的一般操作包括创建、删除和安装。Unix文件系统安装在具体的安装点上,安装点在整体层次结构上°它被称为命名空间,所有安装的文件系统都作为根文件系统树的枝叶出现在系统中。与这种单一统一的树形成鲜明对比的是DOS和 Windows它们将文件的命名空间分类为驱动字母,例如C:。这种将命名空间划分为设备和分区的做法相当于将硬件细节泄露到文件系统的抽象层。对于用户来说,这样的描述有点随意甚至混淆Linux 统一命名空间不屑一顾。
- 文件、目录和目录项目
文件由目录组织。文件目录就像一个文件夹,用来容纳相关文件。由于目录还可以包含其他目录,即子目录,目录可以层层嵌套,形成文件路径。路径的每个部分都被称为目录条目。
“/home/wolfman/butter”
根目录是文件路径的例子/
,目录home
,wolfman
和文件butter
都是目录条目,统称为目录项。在Unix其中,目录属于普通文件,含的所有文件都列出。由于VFS将目录视为文件,因此可以执行与文件相同的目录。
- 索引节点
Unix系统将相关信息和文件本身区分这两个概念,如访问控制权、大小、所有者、创建时间等信息。文件相关信息,有时被称为文件元数据(即文件相关数据),存储在一个单独的数据结构中,称为索引节点( inode),它其实是index node缩写,但最近的术语inode更常用。
4.VFS对象及其数据结构
- 概述
VFS使用面向对象的设计理念,使用一组数据结构来代表一般文件对象,有四种主要对象类型,每个主要对象包含一个操作对象,描述了核心可以用于主要对象,即:
- 超级块对象代表特定的已安装文件系统。
- 索引节点对象的索引节点对象。
- 它代表一个目录项,是路径的一部分。
- 文件对象打开的文件对象(VFS将目录视为文件)。
5.超级块对象
- 概述
各种文件系统必须实现存储特定文件系统信息的超级块对象,通常对应于存储在磁盘特定风扇区域的超级块或文件系统控制块(所以称为超级块对象)。对于不基于磁盘的文件系统(如基于内存的文件系统,例如sysfs),它们在使用现场创建超级块并将其保存在内存中。
- 对象结构
struct super_block {
struct list_head s_list; /*指向所有超级块的链表*/
dev_t s_dev ; /*设备标识符*/
unsigned long s_blocksize; /*以字节为单位的块大小*/
unsigned char s_blocksize_bits; /*以位为单位的块大小*/
unsigned char s_dirt; /*修改(脏)标志*/
unaigned long long s_maxbytes; /*文件大小上限*/
struct file_system_type s_type; /*文件系统类型*/
struct super _operatiors s_op; /*超级块方法*/
struct dquot_operations *dq_op; /*磁盘限额方法*/
struct quotactl_ops *s_qcop; /*限额控制方法*/
struct export_operations s_export_op; /*导出方法*/
unsigned long s_flags ; /*桂载标志*/
unsigned long s_magic; /*文件系统的幻数*/
struct dentry s_root ; /*目录挂载点*/
struct rw_semaphore s_umount ; /*卸载信号量*/
struct semaphore s_lock; /*超级块信号量*/
int s_count ; /*超级块引用计数*/
int s__need_sync; /*尚未同步标志*/
atomic_t s_active; /*活动引用计数*/
void s_security; /*安全模块*/
struct xattr_handler s_xattr; /*扩展的属性操作*/,
struct list_head s_inodes; /*inodes链表*/
struct list_head s_dirty; /*脏数据链表*/
struct list_head s_io; /*回写链表*/
struct list_head s_more_io; /*更多回写的链表*/
struct hlist_head s_anon; /*匿名目录项*/
struct list__head s_files; /*被分配文件链表*/
struct list_head s_dentry_lru; /*未被使用目录项链表*/
int s_nr_dentry_unused; /*链表中目录项的数目*/
struct block_device *s_bdev ; /*相关的块设备*/
struct mtd_info *s_mtd; /*存储磁盘信息*/
struct list_head s_instances; /*该类型文件系统*/
struct quota.info s_dquot ; /*限额相关选项*/
int s_frozen ; /*frozen标志位*/
wait_queue_head_t s_wait_unfrozen; /*冻结的等待队列*/
char s_id[32]; /*文本名字*/
void *s_fs_info; /*文件系统特殊信息*/
fmode_t e_mode; /*安装权限*/
struct semaphore s_vfs_rename_sem; /*重命名信号量*/
u32 s_tire_gran; /*时间戮粒度*/
char s_subtype; /*子类型名称*/
char s_options ; /*已存安装选项*/
};
超级块操作
struct super_operations { //在给定的超级块下创建和初始化一个新的索引节点对象。 struct inode * ( *alloc_inode) (struct super_block *sb); //释放给定的索引节点 void (*destroy_inode ) (struct inode * ) ; //VFS在索引节点脏(被修改)时会调用此函数。日志文件系统(如ext3和ext4)执行该函数进行日志更新。 void (*dirty_inode) (struct inode * ); //用于将给定的索引节点写入磁盘。wait参数指明写操作是否需要同步。 int (*write_inode) (struct inode * , int); //在最后一个指向索引节点的引用被释放后,VFS会调用该函数。VFS只需要简单地删除这个索引节点后,普通Unix文件系统就不会定义这个函数了。 void ( *drop_inode) (struct inode * ) ; //用于从磁盘上删除给定的索引节点。 void (*delete_inode) (struct inode *) ; //在卸载文件系统时由VFS调用,用来释放超级块。调用者必须一直持有s_lock
锁。 void (*put_super) (struct super_block * ); //用给定的超级块更新磁盘上的超级块。VFS通过该函数对内存中的超级块和磁盘中的超级块进行同步。调用者必须一直持有s_lock 锁。 void (*write_super)(struct super_block * ); //使文件系统的数据元与磁盘上的文件系统同步。wait参数指定操作是否同步。 int ( sync_fs ) (struct super_block *sb, int wait) ; //首先禁止对文件系统做改变,再使用给定的超级块更新磁盘上的超级块。目前LVM(逻辑卷标管理)会调用该函数。 int (*freeze_fs) (struct super_block * ); int (unfreeze_fs)(struct super_block * ) ; int (*statfs) (struct dentry *, struct kstatfs *); int (*remount_fs) (struct super_block *, int * , char *) ; void (*clear_inode) (struct inode *) ; void (*umount_begin) (struct super_.block *); int (*show_options ) ( struct seq_file *, struct vfsmount *) ; int (show_stats ) (struct seq_file *, struct vfsmount * ) ; ssize_t (*quota_read)(struct super_block *, int,char *, size_t, loff_t); ssize_t (*quota_write)(struct super_block *,int,const char *,size_t, loff_t); int (*bdev_try_to_free _page)(struct super_block*,struct page*,gfp_t); };
6.索引节点对象
- 概述
索引节点对象包含了内核在操作文件或目录时需要的全部信息。对于Unix 风格的文件系统来说,这些信息可以从磁盘索引节点直接读入。如果一个文件系统没有索引节点,那么,不管这些相关信息在磁盘上是怎么存放的,文件系统都必须从中提取这些信息。没有索引节点的文件系统通常将文件的描述信息作为文件的一部分来存放。这些文件系统与Unix风格的文件系统不同,没有将数据与控制信息分开存放。有些现代文件系统使用数据库来存储文件的数据。不管哪种情况、采用哪种方式,索引节点对象必须在内存中创建,以便于文件系统使用。
-
数据结构
-
注意
对于操作这里就不展示了
7.目录项对象
- 概述
VFS把目录当作文件对待,所以在路径
/bin/vi
中,bin和vi都属于文件——bin是特殊的目录文件而vi是一个普通文件,路径中的每个组成部分都由一个索引节点对象表示。虽然它们可以统一由索引节点表示,但是VFS经常需要执行目录相关的操作,比如路径名查找等。路径名查找需要解析路径中的每一个组成部分,不但要确保它有效,而且还需要再进一步寻找路径中的下一个部分。
为了方便查找操作,VFS引入了目录项的概念。每个dentry代表路径中的一个特定部分。对前一个例子来说,
/、bin和vi
都属于目录项对象。前两个是目录,最后一个是普通文件。必须明确一点:在路径中(包括普通文件在内),每一个部分都是目录项对象。解析一个路径并遍历其分量绝非简单的演练,它是耗时的、常规的字符串比较过程,执行耗时、代码繁琐。目录项对象的引入使得这个过程更加简单。
目录项也可包括安装点。在路径
/mnt/edrom/foo
中,构成元素/、mnt、cdrom和foo
都属于目录项对象。VFS在执行目录操作时(如果需要的话)会现场创建目录项对象。
- 注意
目录项对象由dentry结构体表示,与前面的两个对象不同,目录项对象没有对应的磁盘数据结构,VFS根据字符串形式的路径名现场创建它。而且由于目录项对象并非真正保存在磁盘上,所以目录项结构体没有是否被修改的标志(也就是是否为脏、是否需要写回磁盘的标志)。
- dentry结构体
7.1 目录项状态
- 概述
目录项对象有三种状态:被使用、未被使用和负状态;对象被释放后可以加入slab对象缓存中去(没有任何有效引用指向该对象)
被使用
一个被使用的目录项对应一个有效的索引节点(即d_inode指向相应的索引节点〉并且表明该对象存在一个或多个使用者(即d_count为正值)。一个目录项处于被使用状态,意味着它正被VFS使用并且指向有效的数据,因此不能被丢弃。
未被使用
一个未被使用的目录项对应一个有效的索引节点(d_inode 指问一个索5卫尽),但龙应相明VFS当前并未使用它(d_count为0)。该目录项对象仍然指问一个有双对象,而且故休面仕拔存中以便需要时再使用它。由于该目录项不会过早地被撤销,所以以后再需要它时,不必重新创
建,与未缓存的目录项相比,这样使路径查找更迅速。但如果要回收内存的佑,可以撤销木使用的目录项。
负状态
一个负状态的目录项没有对应的有效索引节点(d_inode为NULL),因为索引节点已被删除了,或路径不再正确了,但是目录项仍然保留,以使大还所训似调田不断地返回 ENOENT,护进程不断地去试图打开并读取一个不存在的配置文件。open()余统调用个地L肌俑这直到内核构建了这个路径、遍历磁盘上的目录结构体开检查这个文件的明个仔住为止。本史肃个失败的查找很浪费资源,但是将负状态缓存起来还是非常值得的。虽然负状态的目录项有些用
处,但是如果有需要,可以撤销它,因为毕竟实际上很少用到它。
7.2 目录项缓存
- 概述
如果VFS层遍历路径名中所有的元素并将它们逐个地解析成目录项对象,还要到达最深层目录,将是-件非常费力的工作,会浪费大量的时间。所以内核将目录项对象缓存在目录项缓存(简称dcache)中。
- 缓存的三个部分
- “被使用的”目录项链表:该链表通过索引节点对象中的i_dentry项连接相关的索引节点,因为一个给定的索引节点可能有多个链接,所以就可能有多个目录项对象,因此用一个链表来连接它们。
- “最近被使用的”双向链表:该链表含有未被使用的和负状态的目录项对象。由于该链总是在头部插入目录项,所以链头节点的数据总比链尾的数据要新。当内核必须通过删除节点项回收内存时,会从链尾删除节点项,因为尾部的节点最旧,所以它们在近期内再次被使用的可能性最小。
- 散列表和相应的散列函数用来快速地将给定路径解析为相关目录项对象。
- 访问一个文件的缓存例子
举例说明,假设你需要在自己目录中编译一个源文件,
/home/dracula/src/the_sun_sucks.c
,每一次对文件进行访问(比如说,首先要打开它,然后要存储它,还要进行编译等),VFS都必须沿着嵌套的目录依次解析全部路径:/、home、dracula、src和最终的the_sun_sucks.c
。为了避免每次访向该路径名都进行这种耗时的操作,VFS 会先在目录项缓存中搜索路径名,如果找到了,就无须花费那么大的力气了。相反,如果该目录项在目录项缓存中并不存在,VFS就必须自己通过遍历文件系统为每个路径分量解析路径,解析完毕后,再将目录项对象加入dcache中,以便以后可以快速查找到它。
- 缓存与索引节点的联系
而dcache在一定意义上也提供对索引节点的缓存,也就是
icache
。和目录项对象相关的索引节点对象不会被释放,因为目录项会让相关索引节点的使用计数为正,这样就可以确保索引节点留在内存中。只要目录项被缓存,其相应的索引节点也就被缓存了。所以像前面的例子,只要路径名在缓存中找到了,那么相应的索引节点肯定也在内存中缓存着。
8.文件对象
- 概述
VFS的最后一个主要对象是文件对象。文件对象表示进程已打开的文件。如果我们站在用户角度来看待VFS,文件对象会首先进入我们的视野。进程直接处理的是文件,而不是超级块、索引节点或目录项。所以不必奇怪;文件对象包含我们非常熟悉的信息(如访问模式,当前偏移等),同样道理,文件操作和我们非常熟悉的系统调用read()和write()等也很类似。
文件对象是已打开的文件在内存中的表示。该对象(不是物理文件)由相应的open()系统调用创建,由close()系统调用撤销,所有这些文件相关的调用实际上都是文件操作表中定义的方法。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已打开文件,它反过来指向目录项对象(反过来指向索引节点),其实只有目录项对象才表示已打开的实际文件。虽然一个文件对应的文件对象不是唯一的,但对应的索引节点和目录项对象无疑是唯一的。
- 文件对象的结构
类似于目录项对象,文件对象实际上没有对应的磁盘数据。所以在结构体中没有代表其对象是否为脏、是否需要写回磁盘的标志。文件对象通过
f_dentry
指针指向相关的目录项对象。目录项会指向相关的索引节点,索引节点会记录文件是否是脏的。
9.和文件系统相关的数据结构
- 概述
除了我们上述的四种常用的对象结构,内核还会使用另外一些标准数据结构 来管理文件系统的其他数据。
- 第一个对象是file_system_type,用来描述各种特定文件系统类型,比如ext3、ext4或UDF。每种文件系统,不管有多少个实例安装到系统中,还是根本就没有安装到系统中,都只有一个file_system_type结构。
- 第二个结构体是
vfsmount
,用来描述一个安装文件系统的实例。更有趣的事情是,当文件系统被实际安装时,将有一个vfsmount
结构体在安装点被创建该结构体用来代表文件系统的实例——换句话说,代表一个安装点。
10.和进程相关的数据结构
- 概述
系统中的每一个进程都有自己的一组打开的文件,像根文件系统、当前工作目录、安装点等。有三个数据结构将VFS层和系统的进程紧密联系在–起,它们分别是:
file_struct 、 fs_struct和namespace
结构体。这些数据结构连接起来的桥梁就是进程描述符,多数进程都会指向唯一的前两个结构体(每个进程都有唯一的),对于后面的命名空间默认下所有进程共享同样的命名空间(因为子进程会继承父进程的命名空间)
- 注意
唯一的files_struct和 fs_struct结构体。但是,对于那些使用克隆标志CLONE_FILES或CLONEFS 创建的进程,会
共享
这两个结构体。所以多个进程描述符可能指向同一个files_struct或fs_struct结构体。每个结构体都维护一个count域作为引用计数,它防止在进程正使用该结构时,该结构被撤销。
file_struct
该结构体由进程描述符中的files目录项指向。所有与单个进程(per-process)相关的信息(如打开的文件及文件描述符)都包含在其中,其结构和描述如下:
- 对于
fd_array
数组来说当超过64个已打开文件对象,会新建新的数组,数组内的文件打开速度是非常快的
fs_struct
和进程相关的第二个结构体是fs_struct。该结构由进程描述符的fs域指向。它包含文件系统和进程相关的信息
namespace
第三个也是最后一个相关结构体是namespace 结构体,由进程描述符中的mmt_namespace域指向。2.4版内核以后,单进程命名空间被加入到内核中,它使得每一个进程在系统中都看到唯一的安装文件系统——不仅是唯一的根目录,而且是唯一的文件系统层次结构。