Linux内核原理之虚拟文件系统(中)
VFS对象及数据结构
VFS的四个对象类型:
- 超级块对象:代表具体的文件系统
- 索引节点对象:代表具体文件
- 目录项对象:代表目录项,是路径的一个组成部分
- 文件对象:代表进程打开的文件
注意:VFS将目录作为一个文件来处理,不存在目录对象;目录项不同于目录
每个对象中都包含一个操作对象,其中描述了内核针对主要对象可以使用的方法
-
super_operations
对象:内核针对特定文件系统调用的方法,如write_inode(), sync_fs()
-
inode_operations
对象:内核针对特定文件调用的方法,如create(), link()
-
dentry_operations
对象:内核针对特定目录项所能调用的方法,如d_compare(), d_delete()
-
file_operations
对象:进程针对已打开文件所能调用的方法,如read(), write()
注意:操作对象作为结构体,其中包含操作父对象的函数指针,实际的文件系统可以继承VFS提供的通用函数。
超级块
各种文件系统都必须实现超级块对象,该对象存储特定文件系统的信息,对应于存放在磁盘特定扇区中文件系统超级块或者文件系统控制块。(非基于磁盘的文件系统,会在使用现场创建超级块并保存在内存中)
- 超级块结构super_block
主要成员:
- s_dirty:脏inode的链表,在同步内存数据和底层存储介质时,使用该链表更加高效
- s_files:包含一系列file结构,列出了该超级块表示的文件系统所有打开的文件,内核在卸载文件系统时会参考该链表
- s_dev和s_bdev指定了底层文件系统的数据所在的块设备,前者使用了内核内部编号,后者指向block_device结构
- s_root:用于检查文件系统是否装载,如果为NULL,该文件系统是一个伪文件系统,只在内核可见,否则,在用户空间可见
注意:超级块对象通过alloc_super()
函数创建并初始化,在安装文件系统时,文件系统会调用这个函数从磁盘读取文件系统超级块,并将其中的数据填充到内存中的超级块对象对应的结构体中。
-
超级块操作
超级块对象中s_op指针,指向超级块的操作函数表,由
super_operations()
表示,如下图:
该结构中的操作会控制底层文件系统实现获取和返回inode的方法,不会改变inode的内容
inode对象
索引节点对象:包含内核在操作文件系统或者目录时需要的全部信息(对于Unix风格的系统,直接从磁盘的索引节点读入),索引节点对象必须在内存中创建
inode的结构如下:
上述的inode结构是在内存进行管理,其中包含实际介质中存储的inode没有的成员
inode结构主要成员:
- i_ino:唯一标识inode的编号
- i_count:访问该inode结构的进程数
- i_nlink:使用该inode的硬链接数目
- i_mode、i_uid和i_gid:文件访问权限和所有权
- i_rdev:设备号,用于找到struct block_device的一个实例
- 联合体
- i_bdev:指向块设备结构block_device
- i_pipe:实现管道的inode相关信息pipe_inode_info
- i_cdev:指向字符设备结构cdev
- i_devices:这个成员作为链表元素,使得块设备或者字符设备维护一个inode的链表
-
索引节点操作
inode结构包含两个指针:i_op和i_fop,前者指向inode_operation结构,提供了inode相关的操作,后者指向file_operations,提供了文件操作(inode和file结构都包含了指向file_operations结构的指针)
-
inode链表
每个inode都有一个ilist成员,将inode存储到一个链表中
根据状态,inode分为三种:
-
inode存在于内存中,未关联到任何文件,不处于活动状态
-
inode存在于内存中,正在由一个或多个进程使用,两个计数器i_nlink和i_count都大于0,且文件内容和inode元数据和底层块设备相同
-
inode处于使用活动状态,内容已经改变,与存储介质上内容不同,inode是脏的
在fs/inode.c文件中内核定义了两个全局变量表头,inode_unused用于有效但非活动的inode,inode_in_use用于正在使用但是未改变的inode。脏的inode保存在一个特定于超级块的链表中
-
目录项缓存
Linux使用目录项缓存(dentry缓存)来快速访问文件的查找结果
VFS在执行目录项操作时,会现场创建dentry实例,dentry实例没有对应的磁盘数据结构,并非保存在磁盘上,dentry
结构体中没有是否被修改的标志(是否为脏、是否需要写会磁盘)
-
dentry结构
dentry结构定义如下:
每个dentry代表路径中的一个特定部分,比如路径/bin/vi,其中/,bin,vi都是目录项,前两个是目录,最后一个是普通文件。在路径中,包含普通文件在内,每一项都是目录项对象。给定目录下的所有文件和子目录相关联的dentry实例,都归入到d_subdirs链表中,子目录节点的d_child作为链表元素
dentry结构的主要作用:建立文件名到inode之间映射关系的缓存,涉及的结构有三个成员:
- d_inode:指向inode实例的指针
- d_name:指定文件的名称,qstr结构存储了实际的文件名、文件长度和散列值
- d_iname:如果文件名由少量字符组成,保存在d_iname中(不是d_name),以加速访问
内存中所有活动的dentry实例保存在一个散列表中,使用fs/dcache.c中的全局变量dentry_hashtable实现,d_hash实现溢出链,用于解决哈希冲突,每个元素指向具有相同键值的目录项组成的链表头指针,这个散列表称为全局dentry散列表(内核系统提供给文件系统唯一的散列函数)
注意:dentry对象不是表示文件的主要对象,这一职责分配给inode
-
dentry缓存的组织
每个由VFS发送到底层实现的请求,都会导致创建一个新的dentry对象,并保存请求结果
dentry的三种状态:被使用、未使用和负状态
- 被使用的目录项:对应一个有效的索引节点,
d_node
指向相应的索引节点,d_count
代表使用者的数量;不能被丢弃 - 未被使用的目录项:对应有效的索引节点,但是
d_count
为0,仍然指向一个有效对象,被保存在缓存中 - 负状态的目录项:没有对应的有效索引节点,
d_node
为NULL,索引节点已被删除,或者路径不不再正确
dentry对象在内存中的组织,涉及三个部分:
-
一个全局散列表(dentry_hashtable)包含的所有dentry对象
-
“被使用的” 目录项链表:索引节点中
i_dentry
链接相关的目录项(一个索引节点可能有多个链接,对应多个目录项),因此用一个链表连接他们 -
“最近被使用的” 双向链表:包含未被使用和负状态的目录项对象(总是在头部插入新的目录项,需要回收内存时,会再尾部删除旧的目录项)
注意:目录项释放后也可以保存在slab缓存中。
-
dcache
一定意义上提供了对于索引节点的缓存(icache
),和目录项相关的索引节点对象不会被释放(因为索引节点的使用计数>0),这样确保了索引节点留在内存中 - 文件访问呈现空间和时间的局部性:时间局部性体现在程序在一段时间内可能会访问相同的文件;空间局部性体现在同一个目录下的文件很可能都被访问。
- 被使用的目录项:对应一个有效的索引节点,
-
dentry操作
dentry_operations结构中保存了一些对dentry对象执行的函数指针,在不同的文件系统中可以各自实现
特定于进程的信息
三种结构体:
file_struct
,fs_struct
,namespace
主要成员包括:
- fs指向的
fs_struct
结构,保存进程的文件系统相关数据 - files指向的
files_struct
结构,包含当前进程的各个文件描述符 - mnt_ns指向的
namespace
结构,包含命名空间相关信息
-
结构体 files_struct
结构定义如下:
主要成员:
-
fd_array
数组指针:指向已打开的文件对象,NR_OPEN_DEFAULT
默认64,如果进程打开的文件超过64,内核将分配一个新数组,并且将fdt
指针指向它 - 当访问的文件对象的数量小于64时,执行比较快,因为是对静态数组的操作;如果大于64,内核需要建立新数组,访问速度相对慢一些
- 管理员可以增大
NR_OPEN_DEFAULT
选项优化性能 - fdtable结构定义如下:
-
-
结构体 fs_struct
由进程描述符的
fs
域指向,包含文件系统和进程相关的信息,包含进程的当前工作目录(pwd
)和根目录其中dentry类型的成员指向目录名称,vfsmount类型成员表示已经装载的文件系统,成员如下:
- umask成员:标准掩码,用于设置文件的权限
- root和rootmnt指定相关进程的根目录和文件系统
- pwd和pwdmnt指定当前目录和文件系统的vfsmount结构,在进程改变当前目录时,二者会动态改变(仅当进入一个新的挂载点的时候,pwdmnt才会变化)
-
VFS命名空间
单一的系统可以提供多个容器,容器彼此相互独立,从VFS角度来看,需要针对每个容器分别跟踪装载的文件系统
VFS命名空间是所有已经装载、构成某个容器目录树的文件系统集合。通过fork或clone建立的进程会继承父进程的命名空间,可以通过设置CLONE_NEWS标志,建立新的命名空间
进程描述符中的
nsproxy
成员管理命名空间的处理,其主要成员是mmt_namespace
域,它使得每个进程在系统中看到唯一的文件系统(唯一的根目录和文件系统结构层次)- count:使用计数器,指定了使用该命名空间的进程数
- root:指向根目录挂载的vfsmount实例
- list:一个双向链表的表头,保存了命名空间中所有文件系统的vfsmount实例,链表元素是vfsmount中的mnt_list成员
对于容器来说,命名空间的操作(如mount和umount)并不作用于内核的全局vfsmount结构,只操作当前容器中进程的命名空间实例。同时,改变会影响共享同一个命名空间实例的所有进程(容器)
注意:
-
对于多数进程,它们的描述符都指向自己独有的
files_struct
和fs_struct
,除非使用克隆标志CLONE_FILES
或者CLONE_FS
创建的进程会共享这两个结构体-
namespace
结构体使用方法和前两种结构完全不同,默认情况下,所有进程共享同样的命名空间(都从相同的挂载表中看到同一个文件系统层次结构,除非在cloen()
操作时使用CLONE_NEWS
标志,才会给进程一个命名空间结构体的拷贝)
-
文件
文件对象是已打开的文件在内存中的表示
-
结构体
file
表示 -
由
open()
系统调用创建,close()
撤销 -
多个进程可以打开同一个文件,所以同一个文件存在多个对应的file实例
-
file对象仅仅在观点上代表已打开文件,它反过来指向dentry对象,而dentry对象反过来指向inode
类似于目录项对象,文件对象没有对应的磁盘数据,通过file
结构体中f_dentry
指针指向相关的目录项对象,而目录项对象指向相关的索引节点,索引节点会记录文件是否为脏
结构体file_operation
定义如下:
文件相关的操作方法和系统调用很类似,具体的文件系统可以为每一种操作方法实现各自的代码,如果存在通用操作,则使用通用操作
文件系统
所有文件系统都保存在一个单链表中,每个文件系统的名字存储为字符串。在文件系统注册到内核时,将逐个元素扫描该链表,直至到达尾部或者找到指定的文件系统
- 结构体 file_system_type
主要成员:
-
name:文件系统的名称,字符串
-
fs_flags:使用的标志,标明只读装载等
-
next:指向下一个file_system_type结构
-
get_sb函数:从磁盘读取超级块,在文件系统安装时,在内存中组装超级块对象
-
kill_sb函数:在不在需要某个文件系统类型时执行清理工作
-
fs_supers:同一类型文件系统的所有超级块结构存储在一个链表中,fs_supers是这个链表的表头
注意:相同类型的多个文件系统实例,都只有一个对应的file_system_type
实例
-
结构体 vfsmount
在文件系统实际被安装时,会有一个
vfsmount
结构体在安装点创建,每个装载的文件系统对应一个vfs_mount实例(代表一个安装点),vfsmount
结构体中维护了各种链表,用于跟踪文件系统和所有安装点之间的关系系统使用了散列表mount_hashtable(定义在fs/namespace.c中),链表元素是vfs_mount类型,vfs_mount实例的地址和相关的dentry实例的地址用来计算散列和(哈希值)
参考资料
- Linux内核设计与实现
- 深入Linux内核架构