"虚拟文件系统VFS-数据结构"

  "第一章"

Posted by Xu on December 18, 2017

虚拟文件系统VFS-数据结构


1. 个人理解

首先给出一个个人对vfs宏观的理解:

  • vfs名为虚拟文件系统,虚拟两个字就是对复杂的实现加以人类逻辑上容易接受的抽象概念方便对文件系统进行统一的理解和扩展,体现人类史上最具有实际价值和可扩展性的哲学概念:以不变应万变,以静制动。任何工程,学术及理论,只有通过人类思维不断的抽象,简化及统一标准化才能不断延续,不断升级,使人类社会的发展具有无限的可能性。

  • 单方面来说,vfs就是一种跨文件系统的通用文件模型,可以称之为一套标准的文件系统访问接口标准,能够支持大部分的文件系统(而通用的文件模型应该支持所有的文件系统),VFS的视角里,一切皆文件,而如何将这些文件通过目录树的方式整合起来就涉及到VFS的具体实现了。

  • 目前来说,VFS支持三种主要类型的文件系统:磁盘文件系统,网络文件系统,特殊文件系统,针对每个不同的文件系统的Linux函数系统调用的实现,都会有一个指针去指向相应文件系统的适当函数,简而言之,所有文件系统的适配都是通过一组合适的函数指针分配来实现的。

2. VFS架构原理

对象模型

通用文件模型可以看作是面向对象的,但Linux编码并没有采取面向对象语言c++来实现,而是出于效率上的考虑使用C语言,这里的对象你可以看作是一组数据结构,指向函数的指针字段相当于对象中的方法。

首先我们先介绍通用文件模型中的对象数据结构,其中包括:

  • 超级块对象:记录文件系统信息
  • 索引节点对象:存放具体文件的一般信息。每个索引节点都有唯一的索引节点号,这个节点号唯一标示文件系统中的文件
  • 文件对象:存放打开文件与进程之间的交互信息,当进程访问文件时,存在于内核内存中。
  • 目录项对象:存放目录项与对应文件进行链接的有关信息

其中这四个对象之间的交互关系可由下图表示:

VFS

其中目录项和索引节点具有高速缓存机制,方便再次访问。

vfs处理的系统调用

具体的系统调用可见《深入理解linux内核》459页,其中涉及到文件系统,普通文件,目录文件以及符号链接文件,另外还有几个少数几个由VFS处理的其它系统调用涉及设备文件,管道文件,最后一组系统调用socket(),connect(),bind(),属于套接字系统调用,用于实现网络功能。

vfs是应用程序与具体文件系统的一层,不过有些文件操作并不需要涉及到底层函数,只需要vfs层的操作即可,比如,close()只需要vfs释放文件对象即可。

3. VFS的实现

在代码架构的层次来看,VFS的实现主要在于构建对象模型中每个对象的数据结构之间的关联关系,所以要想把VFS的代码实现搞清楚,就需要先分析每个对象的数据结构。然后从数据结构字段中捋清之间的关系。所以首先分析每个对象数据结构:

3.1 数据结构

3.1.1 超级块对象

超级块对象数据结构名称为super_block,其中详细的字段,见书P414。

所有超级块对象都是以双向循环链表形式链接在一起:其中s_list字段存放相邻超级块元素,sb_lock自旋锁保护链表免受多处理器同时访问。

其中*s_fs_info字段指向属于具体文件系统的信息(如ext2超级块对象就会指向ext2_sb_info文件系统信息)

s_dirt:表示内存中的该文件系统超级块信息与磁盘不同步。

超级块对象中的方法也就是超级块操作,由s_op字段来描述,该字段的结构为super_operation,该结构中包含各种对超级块的操作。所有的操作函数可详见P463

3.1.2 索引节点对象

具体字段见P465 每个索引节点对象可能会出现在如下几个双向循环链表中:

  • 有效未使用的索引节点链表:没有被进程使用的索引节点,用作索引高速缓存(变量:inode_unused)
  • 正在使用的索引节点链表:这些索引节点正在被进程使用,且不为脏。(变量:inode_in_use)
  • 脏索引节点链表:与磁盘不同步的索引节点(变量:超级块对象中的s_dirty为链表头) (以上三条链表指向相邻元素的指针字段为i_list)
  • 文件系统索引链表:每个索引节点都被包涵在文件系统的双向循环链表中,链表头为文件系统超级块结构中s_inodes字段,该链表指向相邻元素的指针字段为i_sb_list
  • 散列表:索引节点存放在一个称为inode_hashtable散列表中,为防止冲突,使用字段i_hash包含前后两个指针构成由这些索引组成的hash双向链表。

索引节点操作存放在指针字段i_op,该字段由inode_operations结构来描述所有的索引操作,操作函数详见P468

3.1.3 文件对象

由于文件对象是描述进程与文件交互信息的对象,只有当进程打开文件时才会创建,其中包括文件指针描述了进程访问文件的当前位置等信息,不会保存到磁盘,所以不需要同步,没有脏bit

文件对象存放在一个名为filp的slab高速缓存分配,filp描述符地址存放在filp_cachep变量中。

其中正在被使用的文件对象包含在由具体文件系统的超级块所对应的几个链表中,链表头存放在s_files(超级块)中,文件对象中的f_list分别指向前后链表元素。

f_count字段纪录文件对象被引用的数量,f_dentry指向与文件相关的目录项对象。

打开文件操作open():当进程打开一个文件时,它会调用get_empty_filp()函数来分配新的文件对象,该函数通过调用kmem_cache_alloc()从filp高速缓存中获取空闲的文件对象,然后将索引节点从磁盘装入内存,就会把指向这些文件操作的指针存放在索引节点的i_fop字段中,并通过该字段初始化文件对象的f_op字段供对该文件进行操作。

所有对文件对象的操作函数详见P472

3.1.4 目录项对象

VFS把每个目录看做由若干子目录和文件组成的一个普通文件。目录项的结构体名称为dentry。对于进程查找的每一个分量,内核都会为其创建一个目录项对象。

目录项在磁盘上并没有对应的映像,因此也没有dirty bit,他存放在名为dentry_cache的slab分配器高速缓存中,其创建和删除是由kmem_cache和kmem_cache_free完成的。具体字段见P474

目录项的 四个状态:

  • 空闲状态:不包含任何有效信息,没有被vfs使用
  • 未使用状态:包含有效信息,指向对应索引节点,但没有被内核使用,回收内存时会被丢弃
  • 正在使用状态:正在被内核进程使用,引用计数d_count为正数,d_node指向关联索引节点
  • 负状态:指向的索引节点信息不存在或已被删除

目录项的操作函数详见P476

3.2 数据结构的关联

结合3.1中对各数据结构的分析,其之间的关联关系为如下所示:

VFSRelationship

PS:该博客要结合深入理解linux内核学习。

3.3 目录项高速缓存

由于从磁盘读入一个目录项并构造一个目录项对象需要花费大量的时间,所以在完成对目录项对象的操作后,后面可能还会使用到,因此将目录项保存在内存中具有重要意义。

Linux目录项高速缓存由两类数据结构组成:

  1. 一个处于正在使用,未使用或负状态的目录项集合
  2. 一个散列表,可快速获取与给定文件名和目录名对应的目录项对象
3.3.1 目录项集合

在内核内存中,未使用的目录项对象相关联的索引节点对象并不会被丢弃,因为目录项高速缓存还在使用它。所有未使用的目录项对象会存放在一个LRU(最近最少使用)的双向链表中,最后释放的目录项对象会插入到该链表的首部,所以最近最少使用的目录项对象总会出现在该链表的尾部。当内存空间变小时,内核就会从链表尾部删除数据。

未使用链表:存放该LRU链表的变量为dentry_unused,目录项对象中d_lru字段用于该链表指向前后链表节点。

每个 “正在使用”的目录项对象都会插入到相应索引节点对象的i_dentry指向的双向链表,其中目录项对象中d_alias用于该链表指向前后链表节点,如3.2中关系图所示。

3.3.2 散列表

散列表:是由dentry_hashtable实现的,数组中每一个元素是一个指向链表的指针,该链表存放具有相同hash值的目录项对象,而hash值的计算由目录项对象及文件名计算出来的,其中目录项对象中的d_hash用于该链表中指向相邻元素。如下图所示:

HashTable

3.3 与进程相关的文件

fs_struct:

每个进程都有自己当前的工作目录和根目录,类型为fs_struct的数据结构用于维护此类进程与文件系统交互的数据。该数据存放于进程描述符结构中的fs字段

struct task_struct->fs:fs_struct字段:

  • count:共享这个表的进程个数
  • lock:用于表中字段的读/写自旋锁
  • umask:当打开文件设置文件权限时所使用的位掩码
  • *root:根目录的目录项
  • *pwd:当前工作目录的目录项
  • *altroot:模拟根目录的目录项(在80x86结构上始终为NULL)
  • *rootmnt:根目录所安装的文件系统对象
  • *pwdmnt:当前工作目录所安装的文件系统对象
  • *altrootmnt:模拟根目录所安装的文件系统对象(在80x86结构上始终为NULL)

files_struct:

该结构记录进程当前打开的文件信息,存放在进程描述符结构体中的files字段。

struct task_struct->files:files_struct字段:

  • count 共享该表的进程数
  • file_lock 保护以下的所有域,以免在tsk->alloc_lock中的嵌套
  • max_fds 当前文件对象的最大数
  • max_fdset 当前文件描述符的最大数
  • next_fd 已分配的文件描述符加1
  • ** fd 指向文件对象指针数组的指针
  • *close_on_exec 指向执行exec( )时需要关闭的文件描述符
  • *open_fds 指向打开文件描述符的指针
  • close_on_exec_init 执行exec( )时需要关闭的文件描述符的初 值集合
  • open_fds_init 文件描述符的初值集合,已打开文件的文件描述符位图
  • *fd_array[32] 文件对象指针的初始化数组

fd字段指针指向文件对象的指针数组,多数情况下该指针数组为files_struct中的fd_array[32]字段,该字段存放文件对象。fd指向的数组(fd_array[32])中的索引index为文件描述符(fd)

通常:

  • 0号索引表示进程标准输入文件
  • 1号索引表示进程标准输出文件
  • 2号索引表示进程标准错误输出文件

fget():该函数获取文件对象,通过接受文件描述符fd作为参数,返回进程描述符中current->files->fd[fd]文件对象,同时该文件对象引用加1。

fput():接受文件对象地址作为参数,减少文件对象引用,当引用变为0时,减少索引节点相关引用值,从超级块链表中移除该文件对象,释放文件对象给slab分配器,减少对应目录项引用计数器。