Linux虚拟文件系统基础概念

发表于 讨论求助 2023-05-10 14:56:27

虚拟文件系统(VFS)作为内核子操作系统,为用户空间程序提供了文件和文件系统相关的接口。程序可以利用标准的Unix系统调用(如:open()、read()、write())对不同的文件系统,甚至不同的介质上的文件系统进行读写操作。它是一种抽象层,通过虚拟接口访问文件系统,它将各种不同的文件系统抽象后采用统一的方式进行操作。VFS之所以能衔接各种各样的文件系统,是因为它定义了所有的文件系统都支持的、基本的、概念上的接口和数据结构。同时实际文件系统也将自身的诸如“如何打开文件”,“目录是什么”等概念在形式上与VFS的定义保持一致。 

##VFS对象及其数据结构

VFS采用的面向对象的设计思路,使用一组数据结构来代表通用文件对象。因为内核纯粹使用C代码实现,没有直接利用面向对象的语言,所以内核中的数据结构都使用C语言的结构体实现,而这些结构体包含操作这些数据的函数指针,其中的操作函数由具体文件系统实现。VFS中主要有四个主要的对象类型。每个主要对象中都包含一个操作对象,这些操作对象描述了内核针对主要对象可以使用的方法。操作对象作为一个结构体指针来实现,此结构体中包含指向操作其父对象的函数指针。对于其中许多方法来说,可以继承使用VFS提供的通用函数,如果通用函数提供的基本功能无法满足需要,那么就必须使用实际文件系统的独有方法填充这些函数指针,使其指向文件系统实例。四个对象之间通过指针关联。

超级块(super_block)对象

超级块对象代表一个具体的已安装文件系统 
各种文件系统都必须实现超级块对象,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块。对于并非基于磁盘的文件系统(如基于内存的文件系统,如sysfs),它们会在使用现场创建超级块并将其保存到内存中。超级块对象由super_block结构体表示,定义在中。创建、管理和撤销超级块对象的代码位于文件fs/super.c中。超级块对象通过alloc_super()函数创建并初始化。在文件系统安装时,文件系统会调用该函数以便从磁盘读取文件系统超级块,并且将其信息填充到内存中的超级块对象中。

超级块操作

超级块对象中最重要的一个域是s_op,它指向超级块的操作函数表。超级块操作函数表由super_operations结构体表示,定义在文件中。该结构体中的每一项都是一个指向超级块操作函数的指针,超级块操作函数执行文件系统和索引节点的低层操作。当文件系统需要对其超级块执行操作时,首先要在超级块对象中寻找需要的操作方法。所有操作函数都是由VFS在进程上下文中调用。除了dirty_inode(),其他函数在必要时都可以阻塞。这其中的一些函数是可选的。在超级块操作表中,文件系统可以将不需要的函数指针设置成NULL。如果VFS发现操作函数指针是NULL,那它要么就会调用通用函数执行相应操作,要么什么也不做,如何选择取决于具体操作。

索引节点(inode)对象文件

索引节点对象代表了一个具体文件 
Unix系统将文件的相关信息和文件本身这两个概念加以区分,例如访问控制权限、大小、拥有者、创建时间等信息。文件相关信息(也叫元数据)被存储在一个单独的数据结构中,该结构被称为索引节点。 
索引节点对象包含了内核在操作文件或目录时需要的全部信息。对于Unix风格的文件系统,这些信息可以从磁盘索引节点直接读入。如果一个文件系统没有索引节点,那么,不管这些相关信息在磁盘上是怎么存放的,文件系统都必须从中提取这些信息。没有索引节点的文件系统通常将文件的描述信息作为文件的一部分来存放,有些现代文件系统使用数据库来存储文件的数据。不管哪种情况、采用哪种方式,索引节点对象必须在内存中创建,以便于文件系统使用。 
索引节点对象由inode结构体表示,它定义在文件中。一个索引节点代表文件系统中(但是索引节点仅当文件被访问时,才在内存中创建)的一个文件,它也可以是设备或管道这样的特殊文件。因此索引节点结构体中有一些和特殊文件相关的项,比如i_pipe项就指向一个代表有名管道的数据结构,i_bdev指向块设备结构体,i_cdev指向字符设备结构体。这三个指针被存放在一个公用体中,因为一个给定的索引节点每次只能表示三者之一(或三者均不)。 
有时,某些文件系统可能并不能完整地包含索引节点结构体要求的所有信息,这时,该文件系统就可以在实现中选择任意合适的办法来解决这个问题。

索引节点操作

和超级块操作一样,索引节点对象中的inode_operations项也非常重要,因为它描述了VFS用以操作索引节点对象的所有方法,这些方法由文件系统实现。与超级块类似,对索引节点的操作调用方式如下: 
i->i_op->truncate(i) 
i指向给定的索引节点,truncate()函数是由索引节点i所在的文件系统定义的。inode_operations结构体定义在文件中。

目录项(dentry)对象

VFS把目录当作文件对待,但由于VFS经常需要执行目录相关的操作,路径名查找需要解析路径中的每一个组成部分,不但要确保它有效,而且还需要再进一步寻找路径中的下一个部分。为了方便查找操作,VFS引入了目录项的概念。每个dentry代表路径中的一个特定部分。在路径中(包括普通文件在内),每一个部分都是目录项对象。解析一个路径并遍历其分量绝非简单的演练,它是耗时的、常规的字符串比较过程,执行耗时、代码繁琐。目录项对象的引入使得这个过程更加简单。目录项也可包括安装点。VFS在执行目录操作(如果需要的话)会现场创建目录项对象。 
目录项对象由dentry结构体表示,定义在文件中。与前面的两个对象不同,目录项对象没有对应的磁盘数据结构,VFS根据字符串形式的路径名现场创建它。而且由于目录项对象并非真正保存在磁盘上,所以目录项结构体没有是否被修改的标致(是否为脏)。

目录项状态

目录项有三种有效状态:被使用、未被使用和负状态。 
一个被使用的目录项对应一个有效的索引节点(即d_inode指向相应的索引节点)并且表明该对象存在一个或多个使用者(即d_count为正值)。一个目录项牌被使用状态,意味着它正被VFS使用并且指向有效的数据,因此不能被丢弃。 
一个未被使用的目录项对应一个有效的索引节点,但是应指明VFS当前并未使用它(d_count为0)。该目录项对象仍然指向一个有效的对象,而且被保留在缓存中以便需要时再使用它。由于该目录项不会过早地被撤销,所以以后再需要它时,不必重新创建,与未缓存的目录项相比,这样使路径查找更迅速。但如果要回收内存的话,可以撤销未使用的目录项。 
一个负状态的目录项没有对应的有效索引节点(d_inode为NULL),因为索引节点已被删除了,或路径不再正确了,但是目录项仍然保留,以便快速解析以后的路径查询。虽然负状态的目录项有些用处,但是如果有需要,可以撤销它,因为毕竟实际上很少用到它。 
目录项对象释放后也可以保存到slab对象缓存中去,此时,任何VFS或文件系统代码都没有指向该目录对象的有效引用。

目录项缓存

如果VFS层遍历路径名中所有的元素并将它们逐个地解析成目录项对象,还要到达最深层目录,这将是一件非常费力的工作,会浪费大师的时间。所以内核将目录项对象缓存在目录项缓存(dcache)中。 
目录项缓存包括三个主要部分:

  • “被使用的”目录项链表。该链表通过索引节点对象中的i_dentry项连接相关的索引节点,因为一个给定的索引节点可能有多个链接,所以就可能有多个目录项对象,因此用一个链表来连接它们。

  • “最近被使用的”双向链表。该链表含有未被使用的和负状态的目录项对象。该链表总是在头部插入目录项,所以链头节点的数据总比链尾的数据要新。当内核必须通过删除节点项回收内存时,会从链尾删除节点(最旧)。

  • 散列表和相应的散列函数用来快速地将给定路径解析为相关目录项对象。散列表由数组dentry_hashtable表示,其中每一个元素都是一个指向具有相同键值的目录项对象链表的指针。数组的大小取决于系统中物理内存的大小。实际的散列值由d_hash()函数计算,它是内核提供给文件系统的唯一的一个散列函数。查找散列表要通过d_lookup()函数,如果该函数在dcache中发现了与其匹配的目录项对象,则匹配的对象被返回;否则,返回NULL指针。 
    dcache在一定意义上也提供对索引节点的缓存,也就是icache。和目录项对象相关的索引节点对象不会被释放,因为目录项会让相关索引节点的使用计数为正,这样就可以确保索引节点留在内存中。只要目录项被缓存,其相应的索引节点也就被缓存了。

目录项操作

dentry_operation结构体了VFS操作目录项的所有方法。该结构定义在文件中。详细见p224

文件(file)对象

文件对象表示进程已打开的文件,该对象(不是物理文件)由相应的open()系统调用创建,由close()系统调用撤销,所有这些文件相关的调用实际上都是文件操作表中定义的方法。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已打开文件,它反过来指向目录项对象(反过来指向索引节点),其实只有目录项对象才表示实际已打开的实际文件。虽然一个文件对应的文件对象不是唯一的,但对应的索引节点和目录对象无疑是唯一的。 
文件对象由file结构体表示,定义在中。类似于目录项对象,文件对象实际上没有对应的磁盘数据。所以在结构体中没有代表其对象是否为脏、是否需要写回磁盘的标志。文件对象通过f_dentry指针指向相关的目录项对象。目录项会指向相关的索引节点,索引节点会记录文件是否为脏的。

文件操作

与file结构体相关的操作与系统调用很类似,这些操作是标准Unix系统调用的基础。文件对象的操作由file_operations结构体表示,定义在文件中。具体的文件系统可以为每一种操作做专门的实现,或者如果存在通用操作,也可以使用通用操作。

关于ioctls

现在有三个关于ioctl的方法。unlocked_ioctl()和ioctl相同,不过前者在无大内核锁(BKL)情况下被调用。因此函数的作者必须确保适当的同步。因为大内核锁是粗粒度、低效的锁,驱动程序应当实现unlocked_ioctl()而不是ioctl()。 
compat_ioctl()也在无大内核锁的情况下被调用,但是它的目的是为64位的系统提供32位ioctl的兼容方法。至于你如何实现它取决于现有的ioctl命令。早期的驱动程序隐含有确定大小的类型(如long),应该实现适用于32位应用的compat_ioctl()方法。这通常意味着把32位值转换为64位内核中合适的类型。新驱动程序重新设计ioctl命令,应该确保所有的参数和数据都有明确大小的数据类型,在32位系统上运行32位应用是安全的,在64位系统上运行32位应用也是安全的,在64位系统上运行64位应用更是安全的。这些驱动程序可以让compat_ioctl()函数指针和unlocked_ioctl()函数指针指向同一函数。

和文件系统相关的数据结构

除了以上几种VFS基础对象外,内核还使用了另外一些标准数据结构来管理文件系统的其他相关数据。第一个对象是file_system_type,用来描述各种特定文件系统类型。第二个结构体是vfsmount,用来描述一个安装文件系统的实例。 
因为Linux支持众多不同的文件系统,所以内核必须由一个特殊的结构来描述每种文件系统的功能和行为。file_system_type结构体被定义在中。每种文件系统,不管有多少个实例被实际安装到系统中,还是根本就没有安装到系统中,都只有一个file_system_type结构。当文件系统被实际安装时,将有一个vfsmount结构体在安装点被创建。该结构体用来代表文件系统的实例。vfsmount结构被定义在中。理清文件系统和所有其他安装点间的关系,是维护所有安装点链表中最复杂的工作。所以vfsmount结构体中维护的各种链表就是为了能够跟踪这些关联信息。vfsmount结构还保存了在安装时指定的标志信息,该信息存储在mnt_flages域中。如下表:

标志描述
MNT_NOSUID禁止该文件系统的可执行文件执行文件设置setuid和setgid标志
MNT_MODEV禁止访问该文件系统上的设备文件
MNT_NOEXEC禁止执行该文件系统上的可执行文件

安装那些管理员不充分信任的移动设备时,这些标志很有用处。它们和其他一些很少用的标志一起定义在中。

和进程相关的数据结构

系统中每一个进程都有自己的一组打开的文件,像根文件系统、当前工作目录、安装点等。有三个数据结构将VFS层和系统的进程紧密联系在一起,它们分别是:file_struct、fs_struct和namespace结构体。 
file_struct结构体定义在文件中,该结构体由进程描述符中的files目录项指向。所有与单个进程(per-process)相关的信息(如打开的文件及文件描述符)都包含在其中。 
fs_struct结构由进程描述符的fs域指向。它包含和进程相关的信息,定义在文件中。该结构包含了当前进程的当前工作目录(pwd)和根目录。 
namespace结构体定义在文件中,由进程描述符中的mmt_namespace域指向。2.4版内核以后,单进程命名空间被加入到内核中,它舍不得每一个进程在系统中都看到唯一的安装文件系统——不仅是唯一的根目录,而且是唯一的文件系统层次结构。

上面这些数据结构都是通过进程描述符连接起来的。对多数进程来说,它们的描述符都指向唯一的files_struct和fs_struct结构体。但是,对于那些使用克隆标志CLONE_FILES或CLONE_FS创建的进程,会共享这两个结构体。所以多个进程描述符可能指向同一个files_struct或fs_struct结构体。每个结构体都维护一个count域作为引用计数,它防止在进程正使用该结构体时,该结构被撤销。 
namespace结构体的使用方法却和前两种结构体完全不同,默认情况下,所有的进程共享同样的命名空间(也就是,它们都从相同的挂载表中看到同一个文件系统层次结构)。只有在进行clone()操作时使用CLONE_NEWS标志,才会给进程一个唯一的命名空间结构体的拷贝。因为大多数进程都不提供这个标志,所有进程都继承其父进程的命名空间。因此,在大多数系统上只有一个命名空间,不过CLONE_NEWS标志可以使这一功能失效。


发表
26906人 签到看排名