Linux文件存储抽象VFS

概念
VFS(Virtual Filesystem Switch)称为虚拟文件系统或虚拟文件系统转换,是一个内核软件层,在具体的文件系统之上抽象的一层,用来处理与Posix文件系统相关的所有调用,表现为能够给各种文件系统提供一个通用的接口,使上层的应用程序能够使用通用的接口访问不同文件系统,同时也为不同文件系统的通信提供了媒介。

架构

VFS在整个Linux系统中的架构视图如下:

Linux系统的User使用GLIBC(POSIX标准、GUN C运行时库)作为应用程序的运行时库,然后通过操作系统,将其转换为系统调用SCI(system-call interface),SCI是操作系统内核定义的系统调用接口,这层抽象允许用户程序的I/O操作转换为内核的接口调用。VFS提供了一个抽象层,将POSIX API接口与不同存储设备的具体接口实现进行了分离,使得底层的文件系统类型、设备类型对上层应用程序透明。

接口适配示例

用户写入一个文件,使用POSIX标准的write接口,会被操作系统接管,转调sys_write这个系统调用(属于SCI层)。然后VFS层接受到这个调用,通过自身抽象的模型,转换为对给定文件系统、给定设备的操作,这一关键性的步骤是VFS的核心,需要有统一的模型,使得对任意支持的文件系统都能实现系统的功能。这就是VFS提供的统一的文件模型(common file model),底层具体的文件系统负责具体实现这种文件模型,负责完成POSIX API的功能,并最终实现对物理存储设备的操作。


VFS这一层建模和抽象是有必要的,如果放在SCI层会导致操作系统的系统调用的功能过于复杂,易出bug。那么就只能让底层文件系统都遵循统一实现,这对于已经出现的各种存储设备来说天然就有不同的特性,也是无法实现的。因此VFS这样一层抽象是有其必要性的。

跨设备/文件系统示例

VFS为不同设备或文件系统间的访问提供了媒介,下面的示意图和代码中,用户通过cp命令进行文件的拷贝,对用户来说是不用关心底层是否跨越文件系统和设备的,具体都通过VFS抽象层实现对不同文件系统的读写操作。

VFS的抽象接口

上述示例中提到VFS也有自己的文件模型,用来支持操作系统的系统调用。下面是VFS抽象模型支持的所有Linux系统调用:
  • 文件系统相关:mount, umount, umount2, sysfs,  statfs,  fstatfs,  fstatfs64, ustat
  • 目录相关:chroot,pivot_root,chdir,fchdir,getcwd,mkdir,rmdir,getdents,getdents64,readdir,link,unlink,rename,lookup_dcookie
  • 链接相关:readlink,symlink
  • 文件相关:chown, fchown,lchown,chown16,fchown16,lchown16,hmod,fchmod,utime,stat,fstat,lstat,acess,oldstat,oldfstat,oldlstat,stat64,lstat64,lstat64,open,close,creat,umask,dup,dup2,fcntl, fcntl64,select,poll,truncate,ftruncate,truncate64,ftruncate64,lseek,llseek,read,write,readv,writev,sendfile,sendfile64,readahead

Linux系统VFS支持的文件系统

  • Disk-based 文件系统Ext2, ext3, ReiserFS,Sysv, UFS, MINIX, VxFS,VFAT, NTFS,ISO9660 CD-ROM, UDF DVD,HPFS, HFS, AFFS, ADFS,
  • Network 文件系统:NFS, Coda, AFS, CIFS, NCP
  • 特殊文件系统:/proc,/tmpfs等

统一文件模型(common file model)

VFS为了提供对不同底层文件系统的统一接口,需要有一个高度的抽象和建模,这就是VFS的核心设计——统一文件模型。目前的Linux系统的VFS都是源于Unix家族,因此这里所说的VFS对所有Unix家族的系统都适用。Unix家族的VFS的文件模型定义了四种对象,这四种对象构建起了统一文件模型。
  • superblock:存储文件系统基本的元数据。如文件系统类型、大小、状态,以及其他元数据相关的信息(元元数据)
  • index node(inode):保存一个文件相关的元数据。包括文件的所有者(用户、组)、访问时间、文件类型等,但不包括这个文件的名称。文件和目录均有具体的inode对应
  • directory entry(dentry):保存了文件(目录)名称和具体的inode的对应关系,用来粘合二者,同时可以实现目录与其包含的文件之间的映射关系。另外也作为缓存的对象,缓存最近最常访问的文件或目录,提示系统性能
  • file:一组逻辑上相关联的数据,被一个进程打开并关联使用
统一文件模型是一个标准,各种具体文件系统的实现必须以此模型定义的各种概念来实现。

Superblock

静态:superblock保存了一个文件系统的最基础的元信息,一般都保存在底层存储设备的开头;动态:挂载之后会读取文件系统的superblock并常驻内存,部分字段是动态创建时设置的。superblock的具体定义见linux/include/fs/fs.h,下图展示了内存中维护的superblock:

由于Linux系统支持同时挂载多个文件系统,因此s_list字段用于在内存中构建superblock链表来支持挂载多个文件系统。s_root字段标识该文件系统的根目录,s_bdev标识该文件系统所在的设备信息。其中最重要的字段是s_op,这个指针指向该文件系统所支持的各种操作的结构体,称为“super_operations”,具体定义如下:
struct super_operations {
struct inode * ( * alloc_inode ) ( struct super_block * sb ) ;
void ( * destroy_inode ) ( struct inode * ) ;
void ( * dirty_inode ) ( struct inode * ) ;
int ( * write_inode ) ( struct inode * , int ) ;
void ( * drop_inode ) ( struct inode * ) ;
void ( * delete_inode ) ( struct inode * ) ;
void ( * put_super ) ( struct super_block * ) ;
void ( * write_super ) ( struct super_block * ) ;
int ( * sync_fs ) ( struct super_block * sb , int wait ) ;
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 ) ;
} ;
这个结构体的每个成员都一个函数指针,用来代表这个操作具体应该执行的底层操作。例如需要写入数据时,VFS会通过superblock中的s_op字段最终去调用write_super来执行文件系统的具体操作: sb->s_op->write_super(sb)。所有这些调用都由VFS完成,向上对接了操作系统的sys_write系统调用,向下转交到具体文件系统的底层操作。

每一个文件系统使用前必须进行挂载(mount),superblock包含的s_type字段定义了这个文件系统的类型。通过系统调用 register_filesystem()、 unregister_filesystem() 可以实现对具体文件系统的挂载和卸载,它们都只有一个参数,就是文件系统类型“  file_system_type ”。本质上就是告诉操作系统挂载的文件系统信息。对于2.6.18之后的内核版本,该结构体定义如下:
struct file_system_type {
const char * name ; //
int fs_flags ;
struct super_block * ( * get_sb ) ( struct file_system_type * ,
int , char * , void * , struct vfsmount * ) ; //kernelsuperblock
void ( * kill_sb ) ( struct super_block * ) ; //kernelsuperblock
struct module * owner ;
struct file_system_type * next ;
struct list_head fs_supers ;
struct lock_class_key s_lock_key ;
struct lock_class_key s_umount_key ;
} ;
另外,get_sb的最后一个参数是vfsmount类型,这是系统用来记录挂载信息的数据结构。它保存了挂载点、设备、挂载选项等信息。对于每个一个打开的进程来说,都会在其内核部分维护两个数据结构:fs_struct和file;分别用来描述关联的文件系统信息和打开的文件信息。

Index node

静态:创建文件系统时生成inode,保存在具体存储设备上,记录了文件系统的元信息;动态:VFS在内存中使用inode数据结构,来管理文件系统的文件对象,记录了文件对象的详细信息,部分字段与关联的文件对象有关,会动态创建。具体信息如下:

i_dentry字段指定当前inode标识的文件对象的名称,也就是dentry,是一个链表的,因为可能由多个dentry都指向这个inode(硬链接)。然后除了文件的一些权限信息、访问时间、大小等信息之外,最重要的就是记录了inode和file对象所提供的操作,分别是i_fop和i_op。其中inode支持的操作示例如下:
struct inode_operations {
int ( * create ) ( struct inode * , struct dentry * , int ) ;
struct dentry * ( * lookup ) ( struct inode * , struct dentry * ) ;
int ( * link ) ( struct dentry * , struct inode * , struct dentry * ) ;
int ( * unlink ) ( struct inode * , struct dentry * ) ;
int ( * symlink ) ( struct inode * , struct dentry * , const char * ) ;
int ( * mkdir ) ( struct inode * , struct dentry * , int ) ;
int ( * rmdir ) ( struct inode * , struct dentry * ) ;
int ( * mknod ) ( struct inode * , struct dentry * , int , dev_t ) ;
int ( * rename ) ( struct inode * , struct dentry * , struct inode * , struct dentry * ) ;
int ( * readlink ) ( struct dentry * , char * , int ) ;
int ( * follow_link ) ( struct dentry * , struct nameidata * ) ;
void ( * truncate ) ( struct inode * ) ;
int ( * permission ) ( struct inode * , int ) ;
int ( * setattr ) ( struct dentry * , struct iattr * ) ;
int ( * getattr ) ( struct vfsmount * mnt , struct dentry * , struct kstat * ) ;
int ( * setxattr ) ( struct dentry * , const char * , const void * , size_t , int ) ;
ssize_t ( * getxattr ) ( struct dentry * , const char * , void * , size_t ) ;
ssize_t ( * listxattr ) ( struct dentry * , char * , size_t ) ;
int ( * removexattr ) ( struct dentry * , const char * ) ;
} ;
inode在内存中创建后会有inode cache进行缓存,并执行延迟的write back策略保存到底层存储设备。

Directory entry

dentry是用来记录具体的文件名与对应的inode间的对应关系的,同时可以用来实现硬链接、缓存、多级目录等树状文件系统的特性。VFS的dentry设计上就是为了实现整个文件系统树状层次结构的,每个文件系统拥有一个没有父dentry的根目录(root dentry),这个dentry会被superblock引用,用来作为进行树形结构的查找入口。其余的所有dentry都是有唯一的父dentry,并可以由若干个孩子dentry。示例:对于一个文件"/home/user/a”,会存在“/”、“home”、“user”、“a”四个dentry,依次构成父子关系,每个dentry也都有一个inode与之关联,存储了具体的数据。

dentry没有在磁盘等底层持久化存储设备上存储,是一个动态创建的内存数据结构,主要是为了构建出树状组织结构而设计,用来进行文件、目录的查找。dentry创建之后会被操作系统进行缓存,目的是为了提升对文件系统进行操作的性能。dentry的结构如下示意,具体定义于“”。

其中最重要的有两个字段,一个是d_inode指针指向了当前dentry关联的inode。另一个就是d_op字段,指向了一系列dentry支持的操作的集合。典型的dentry支持的操作集合如下:
struct dentry_operations {
int ( * d_revalidate ) ( struct dentry * , int ) ;
int ( * d_hash ) ( struct dentry * , struct qstr * ) ;
int ( * d_compare ) ( struct dentry * , struct qstr * , struct qstr * ) ;
int ( * d_delete ) ( struct dentry * ) ;
void ( * d_release ) ( struct dentry * ) ;
void ( * d_iput ) ( struct dentry * , struct inode * ) ;
} ;
dentry在需要使用时动态创建,并会被缓存。每个dentry有三种状态:
  • used:与一个inode关联,正处于被VFS使用的状态,不能被损坏和丢弃
  • unused:与inode关联,但处于被缓存状态,没有被VFS使用
  • negative:没有与具体的inode关联(相当于是一个无效的路径)
dentry由于会被动态创建,为了提升系统性能,设计了一个dentry cache进行缓存,包括三个部分:
  • used dentries 链表:记录每个正在使用的dentry,将其关联的inode的i_dentry字段指向的dentry链表连接起来形成一个dentry链表的链表
  • LRU双向环链表:用于维护unused和negative状态的dentry对象,从头部插入,离头部越近就是最近访问过的。当需要删除dentry时,从队列尾部删除最旧的dentry
  • hash table和hash function:用来快速查询一个给定的路径到dentry对象

File

文件对象是打开一个具体文件之后创建的一个内存数据结构,与具体的进程和用户相联系。一个文件对象包括的内容就是编程语言支持设置的各种文件打开的flag、mode,文件名称、当前的偏移等,其中非常重要的一个字段就是f_op,指向了当前文件所支持的操作集合。
struct file {
struct dentry * f_dentry ;
struct vfsmount * f_vfsmnt ;
struct file_operations * f_op ;
mode_t f_mode ;
loff_t f_pos ;
struct fown_struct f_owner ;
unsigned int f_uid , f_gid ;
unsigned long f_version ;
...
}
基本的操作集合如下,这也是使用应用程序可感知到的一系列接口。
struct file_operations {
struct module * owner ;
loff_t ( * llseek ) ( struct file * , loff_t , int ) ;
ssize_t ( * read ) ( struct file * , char * , size_t , loff_t * ) ;
ssize_t ( * aio_read ) ( struct kiocb * , char * , size_t , loff_t ) ;
ssize_t ( * write ) ( struct file * , const char * , size_t , loff_t * ) ;
ssize_t ( * aio_write ) ( struct kiocb * , const char * , size_t , loff_t ) ;
int ( * readdir ) ( struct file * , void * , filldir_t ) ;
unsigned int ( * poll ) ( struct file * , struct poll_table_struct * ) ;
int ( * ioctl ) ( struct inode * , struct file * , unsigned int , unsigned long ) ;
int ( * mmap ) ( struct file * , struct vm_area_struct * ) ;
int ( * open ) ( struct inode * , struct file * ) ;
int ( * flush ) ( struct file * ) ;
int ( * release ) ( struct inode * , struct file * ) ;
int ( * fsync ) ( struct file * , struct dentry * , int datasync ) ;
int ( * aio_fsync ) ( struct kiocb * , int datasync ) ;
int ( * fasync ) ( int , struct file * , int ) ;
int ( * lock ) ( struct file * , int , struct file_lock * ) ;
ssize_t ( * readv ) ( struct file * , const struct iovec * , unsigned long , loff_t * ) ;
ssize_t ( * writev ) ( struct file * , const struct iovec * , unsigned long , loff_t * ) ;
ssize_t ( * sendfile ) ( struct file * , loff_t * , size_t , read_actor_t , void * ) ;
ssize_t ( * sendpage ) ( struct file * , struct page * , int , size_t , loff_t * , int ) ;
unsigned long ( * get_unmapped_area ) ( struct file * , unsigned long , unsigned long , unsigned long , unsigned long ) ;
} ;

VFS抽象组件间的关系

从用户角度来看VFS的时候,可以通过下图很容易的理解各个抽象组件间的协作关系:

每个打开的文件对用户来说有一个文件描述符,也就是VFS抽象的file object。file object指向一个dentry,dentry指向新的dentry或一个inode,inode最终代表了一个具体存储设备上的数据。

参考文献:

1、http://learnlinuxconcepts.blogspot.com/2014/10/the-virtual-filesystem.html

2、https://www.ibm.com/developerworks/library/l-virtual-filesystem-switch/

3、http://www.tldp.org/LDP/khg/HyperNews/get/fs/vfstour.html

4、https://www.win.tue.nl/~aeb/linux/lk/lk-8.html

5、https://www.kernel.org/doc/Documentation/filesystems/vfs.txt

你可能感兴趣的:(Linux)