《Linux内核的设计与实现》第十三章笔记

本文详细阐述了Linux虚拟文件系统(VFS)的作用,它作为内核核心,提供统一接口处理不同文件系统,支持文件操作的透明性。VFS对象及其数据结构,如超级块、索引节点和文件对象,展示了其面向对象设计和文件系统抽象层。关键组件如super_operations、inode_operations和file_operations解释了VFS如何协调底层操作。
摘要由CSDN通过智能技术生成

虚拟文件系统(有时也称作虚拟文件交换,更常见的是简称VFS)作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口。系统中所有文件系统不但依赖VFS共存,而且也依靠VFS系统协同工作。通过虚拟文件系统,程序可以利用标准的Uinx系统调用对不同的文件系统,甚至不同介质上的文件系统进行读写操作,如图13-1所示。
在这里插入图片描述

13.1 通用文件系统接口

VFS使得用户可以直接使用open(),read0() 和write()这样的系统调用而无须考虑具体文件系统和实际物理介质。

VFS存在的意义

  1. 向上,对应用层提供一个标准的文件操作接口;
  2. 对下,对文件系统提供一个标准的接口,以便其他操作系统的文件系统可以方便的移植到Linux上;

在本章中,我们将讨论VFS,它把各种不同的文件系统抽象后采用统一的方式进行操作。在第14章中,我们将讨论块1O层,它支持各种各样的存储设备-从CD到蓝光光盘,从硬件设备再到压缩闪存。VFS与块10相结合,提供抽象、接口以及交融,使得用户空间的程序调用统一的系统调用访问各种文件,不管文件系统是什么,也不管文件系统位于何种介质,采用的命名策略是统一的。

13.2 文件系统抽象层

之所以可以使用这种通用接口对所有类型的文件系统进行操作,是因为内核在它的底层文件系统接口上建立了一个抽象层。该抽象层使Linux能够支持各种文件系统,即便是它们在功能和行为上存在很大差别。为了支持多文件系统,VFS提供了一个通用文件系统模型,该模型囊括了任何文件系统的常用功能集和行为。

VFS抽象层之所以能衔接各种各样的文件系统,是因为它定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。同时实际文件系统也将自身的诸如“如何打开文件",“目录是什么”等概念在形式上与VFS的定义保持一致。实际文件系统通过编程提供VFS所期望的抽象接口和数据结构,这样,内核就可以毫不费力地和任何文件系统协同工作。

因为VFS,Linux可以方便的支持新的文件系统。只要该文件系统按照Linux约定的模式重新编写一下该文件系统的基本类型结构,然后通过register_filesystem函数注册一下即可。

其实在内核中,除了文件系统本身外,其他部分并不需要了解文件系统的内部细节。比如一个简单的用户空间程序执行如下的操作:

ret = write(fd,buf,len);

该系统调用将buf指针指向的长度为len字节的数据写入文件描述符fd对应的文件的当前位置。这个系统调用首先被一个通用系统调用sys_write()处理,然后VFS层接受到这个调用,通过自身抽象的模型,转换为对给定文件系统、给定设备的操作。图13-2描述了从用户空间的write)调用到数据被写入磁盘介质的整个流程。
在这里插入图片描述

13.4 VFS对象及其数据结构

VFS其实采用的是面向对象 的设计思路,使用一组数据结构来代表通用文件对象。这些数据结构类似于对象。因为内核纯粹使用C代码实现,没有直接利用面向对象的语言,所以内核中的数据结构都使用C语言的结构体实现,而这些结构体包含数据的同时也包含操作这些数据的函数指针,其中的操作函数由具体文件系统实现。

VFS中有四个主要的对象类型,它们分别是:

  • 超级块对象,它代表一个具体的已安装文件系统。
  • 索引节点对象,它代表一个具体文件。
  • 目录项对象,它代表一个目录项,是路径的一个组成部分。
  • 文件对象,它代表由进程打开的文件。

每个主要对象中都包含一个操作对象,这些操作对象描述了内核针对主要对象可以使用的方法:

  • super_operations对象,其中包括内核针对特定文件系统所能调用的方法,比如write_inode()和sync_fs()等方法。
  • inode_operations对象,其中包括内核针对特定文件所能调用的方法,比如create()和link()等方法。
  • dentry_operations对象,其中包括内核针对特定目录所能调用的方法,比如d_compare()和d_delete()等方法。
  • file_operations对象,其中包括进程针对已打开文件所能调用的方法,比如read()和write()等方法。

操作对象作为一个结构体指针来实现,此结构体中包含指向操作其父对象的函数指针。对于其中许多方法来说,可以继承使用VFS提供的通用函数,如果通用函数提供的基本功能无法满足需要,那么就必须使用实际文件系统的独有方法填充这些函数指针,使其指向文件系统实例。再次提醒,我们这里所说的对象就是指结构体。但是这些结构体的确代表的是一个对象,它含有相关的数据和对这些数据的操作,所以可以说它们就是对象。

值得一提的是,VFS使用了大量结构体对象,它所包括的对象远远多于上面提到的这几种主要对象。

13.5 超级块对象

对于每一个具体的文件系统,在它注册时,都会给它分配一个super block结构,存放其重要资料。

各种文件系统都必须实现超级块对象,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块(所以称为超级块对象)。对于并非基于磁盘的文件系统(如基于内存的文件系统,比如sysfs),它们会在使用现场创建超级块并将其保存到内存中。

超级块对象由super_block结构体表示,定义在文件<linux/fs.h>中,下面给出它的结构和各个域的描述:
在这里插入图片描述
在这里插入图片描述
创建、管理和撒销超级块对象的代码位于文件fs/super.c中。超级块对象通过alloc_super()函数创建并初始化。在文件系统安装时,文件系统会调用该函数以便从磁盘读取文件系统超级块,并且将其信息填充到内存中的超级块对象中。

13.6 超级块操作

超级块对象中最重要的一个域是s_op,它指向超级块的操作函数表。超级块操作函数表由super_operations结构体表示,定义在文件<linux/fs.h>中,其形式如下:
在这里插入图片描述
这个结构体的每个成员都是一个函数指针,用来代表这个操作具体应该执行的底层操作,例如需要写入数据时,VFS会通过super block 中的s_op字段最终去调用write_super来执行文件系统具体操作:

sb->s_op->write_super(sb);

所有这些调用都由VFS完成,向上对接了操作系统的系统调用,向下转交到具体文件系统的底层操作。

所有以上函数都是由VFS在进程上下文中调用。除了dirty_node(),其他函数在必要时都可以阻塞。

这其中的一些函数是可选的。在超级块操作表中,文件系统可以将不需要的函数指针设置成NULL。如果VFS发现操作函数指针是NULL,那它要么就会调用通用函数执行相应操作,要么什么也不做,如何选择取决于具体操作。

13.7 索引节点对象

索引节点对象包含了内核在操作文件或目录时需要的全部信息。对于Unix风格的文件系统来说,这些信息可以从磁盘索引节点直接读入。如果一个文件系统没有索引节点,那么,不管这些相关信息在磁盘上是怎么存放的,文件系统都必须从中提取这些信息。不管哪种情况、采用哪种方式,索引节点对象必须在内存中创建,以便于VFS使用。

索引节点对象由inode结构体表示,它定义在文件<linux/fs.h>中,下面给出它的结构体和各项的描述
在这里插入图片描述
在这里插入图片描述
一个素引节点代表文件系统中(但是索引节点仅当文件被访问时,才在内存中创建)的一个文件,它也可以是设备或管道这样的特殊文件。因此索引节点结构体中有一些和特殊文件相关的项,比如i_pipe项就指向一个代表有名管道的数据结构,i_bdev指向块设备结构体,i_cdev指向字符设备结构体。这三个指针被存放在一个公用体中,因为一个给定的素引节点每次只能表示三者之一(或三者均不)。

有时,某些文件系统可能并不能完整地包含索引节点结构体要求的所有信息。举个例子,有的文件系统可能并不记录文件的访问时间,这时,该文件系统就可以在实现中选择任意合适的办法来解决这个问题。它可以在i_atime中存储0,或者让i_atime等于i_mtime,或者只在内存中更新i_atime而不将其写回磁盘,或者由文件系统的实现者来决定。

13.8 索引节点操作

和超级块操作一样,索引节点对象中的inode_operations项也非常重要,因为它描述了VFS用以操作索引节点对象的所有方法,这些方法由文件系统实现。
在这里插入图片描述

13.9 目录项对象

VFS把目录当作文件对待,所以在路径 /bin/vi 中,bin和vi都属于文件——bin是特殊的目录文件而vi是一个普通文件,路径中的每个组成部分都由一个索引节点对象表示。虽然它们可以统一由素引节点表示,但是VFS经常需要执行目录相关的操作,比如路径名查找等。路径名查找需要解析路径中的每一个组成部分,不但要确保它有效,而且还需要再进一步寻找路径中的下一个部分为了方

便查找操作,VFS引入了目录项的概念。每个dentry代表路径中的一个特定部分。对前一个例子来说,/、bin和vi都属于目录项对象。前两个是目录,最后一个是普通文件。必须明确一点:在路径中(包括普通文件在内),每一个部分都是目录项对象。解析一个路径并遍历其分量绝非简单的演练,它是耗时的、常规的字符串比较过程,执行耗时、代码繁琐。目录项对象的引入使得这个过程更加简单。

目录项对象由dentry结构体表示,定义在文件<linux/dcache.h>中。下面给出该结构体和其中各项的描述:

在这里插入图片描述
与前面的两个对象不同,目录项对象没有对应的磁盘数据结构,VFS根据字符串形式的路径名现场创建它。而且由于目录项对象并非真正保存在磁盘上,所以目录项结构体没有是否被修改的标志(也就是是否为脏、是否需要写回磁盘的标志)。

13.9.1 目录项状态

目录项对象有三种有效状态:被使用、未被使用和负状态。

一个被使用的目录项对应一个有效的索引节点(即d_inode指向相应的素引节点)并且表明该对象存在一个或多个使用者(即d_count为正值),一个目录项处于被使用状态,意味着它正被VFS使用并且指向有效的数据,因此不能被丢弃。

一个未被使用的目录项对应一个有效的索引节点(d_inode指向一个素引节点),但是应指明VFS当前并未使用它(d_count为0)。该目录项对象仍然指向一个有效对象,而且被保留在缓存中以便需要时再使用它。由于该目录项不会过早地被撤销,所以以后再需要它时,不必重新创建,与未缓存的目录项相比,这样使路径查找更迅速。但如果要回收内存的话,可以撤销未使用的目录项

一个负状态的目录项没有对应的有效索引节点(d_inode为NULL),因为索引节点已被删除了,或路径不再正确了,但是目录项仍然保留。

13.9.2 目录项缓存

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

目录项缓存包括三个主要部分:

  • “被使用的”目录项链表。
  • “最近被使用的”双向链表。
  • 散列表和相应的散列函数用来快速地将给定路径解析为相关目录项对象。

13.10 目录项操作

dentry_operation结构体指明了VFS操作目录项的所有方法。

该结构定义在文件<linux/dcache.h>中。

在这里插入图片描述

13.11 文件对象

VFS的最后一个主要对象是文件对象。文件对象表示进程已打开的文件。如果我们站在用户角度来看待VFS,文件对象会首先进入我们的视野。进程直接处理的是文件,而不是超级块、索引节点或目录项。所以不必奇怪:文件对象包含我们非常熟悉的信息(如访问模式,当前偏移等),同样道理,文件操作和我们非常熟悉的系统调用read() 和write() 等也很类似。

文件对象是已打开的文件在内存中的表示。该对象(不是物理文件)由相应的open() 系统调用创建,由close() 系统调用撤销,所有这些文件相关的调用实际上都是文件操作表中定义的方法。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已打开文件,它反过来指向目录项对象(反过来指向素引节点),其实只有目录项对象才表示已打开的实际文件。虽然一个文件对应的文件对象不是唯一的,但对应的索引节点和目录项对象无疑是唯一的。

文件对象由file结构体表示,定义在文件<linux/fs.h>中,下面给出该结构体和各项的描述。
在这里插入图片描述
在这里插入图片描述
类似于目录项对象,文件对象实际上没有对应的磁盘数据。所以在结构体中没有代表其对象是否为脏、是否需要写回磁盘的标志。文件对象通过f_dentry指针指向相关的目录项对象。目录项会指向相关的索引节点,索引节点会记录文件是否是脏的。

13.12 文件操作

和VFS的其他对象一样,文件操作表在文件对象中也非常重要。跟fie结构体相关的操作与系统调用很类似,这些操作是标准Unix系统调用的基础。

文件对象的操作由file_operations结构体表示,定义在文件<linux/fs.h>中:
在这里插入图片描述
在这里插入图片描述
具体的文件系统可以为每一种操作做专门的实现,或者如果存在通用操作,也可以使用通用操作。一般在基于Unix的文件系统上,这些通用操作效果都不错。并不要求实际文件系统实现文件操作函数表中的所有方法——虽然不实现最基础的那些操作显然是很不明智的,对不感兴趣的操作完全可以简单地将该函数指针置为NULL。

13.14 和进程相关的数据结构

系统中的每一个进程都有自己的一组打开的文件,像根文件系统、当前工作目录、安装点等。有三个数据结构将VFS层和系统的进程紧密联系在一起,它们分别是:file_struct,fs_struct和namespace结构体。

file_struct结构体定义在文件<linux/fdtable.h>中。该结构体由进程描述符中的files目录项指向。所有与单个进程(per-process)相关的信息(如打开的文件及文件描述符)都包含在其中,其结构和描述如下
在这里插入图片描述

fd_array数组指针指向已打开的文件对象。因为NR_OPEN_DEFAULT等于BITS_PER_LONG,在64位机器体系结构中这个宏的值为64,所以该数组可以容纳64个文件对象。如果一个进程所打开的文件对象超过64个,内核将分配一个新数组,并且将fdt指针指向它。所以对适当数量的文件对象的访问会执行得很快,因为它是对静态数组进行的操作;如果一个进程打开的文件数量过多,那么内核就需要建立新数组。所以如果系统中有大量的进程都要打开超过64个文件,为了优化性能,管理员可以适当增大NR_OPEN_DEFAULT的预定义值。

和进程相关的第二个结构体是fs_struct。该结构由进程描述符的fs域指向。它包含文件系统和进程相关的信息,定义在文件<linux/fs_struct.h>中,下面是它的具体结构体和各项描述:
在这里插入图片描述
在这里插入图片描述
该结构包含了当前进程的当前工作目录(pwd)和根目录。

第三个也是最后一个相关结构体是namespace结构体。它定义在文件<linux/mmt_namespace.h>中,由进程描述符中的mmt_namespace域指向。2.4版内核以后,单进程命名空间被加入到内核中,它使得每一个进程在系统中都看到唯一的安装文件系统——不仅是唯一的根目录,而且是唯一的文件系统层次结构。下面是其具体结构和描述:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值