linux虚拟文件系统综述

Linux延续了Unix的一个哲学观点:一切皆文件,应用看到的所有对象都是文件,里面除了socket不那么典型之外所有都是文件。而实际情况非常复杂,内核管理着非常多的设备:字符设备、块设备、网络设备,还支持许多类型的文件系统:ext2、ext3、ext4、fuse、ntfs等,另外还有很多的伪文件系统:proc、devfs、sys、debugfs等,特殊的节点类型:netlink、notify、epoll等。不过在软件上没有什么是不能通过增加一个抽象层解决的,VFS就作为linux中的这一抽象层完成了一切皆文件的哲学观点实现。
文件这一概念在不同的层级中需要不同的载体来实现,对于linux应用来说就是fd文件描述符,glibc在fd基础上又封装成了fp实现POSIX接口(在多操作系统上是可移植的),对于内核来说,文件就是struct file对象。
另一个是多种多样的文件路径以一种树的形式存在,其中必不可少的是mount,允许不同的文件系统挂接到同一颗树下,形成统一的视图。这种树型管理结构衍生出了一些其他的概念:绝对路径、相对路径、挂载点、根文件系统等,如果没有挂载点的概念,linux的文件视图可能长得和windows的C盘、D盘一样,而下面的各种设备文件和伪文件系统可能又是另一番面貌。

整体架构

vfs
应用层通过系统调用陷入内核中,访问内核的资源。其中大部分的事务是以文件为对象的,VFS作为通用文件系统接口,后端通过各种文件系统实现,主要包括基于块设备的物理文件系统ext2、ext3、ext4、vfs、bfrfs等,基于网络的文件系统nfs、smbfs、ceph,用户空间文件系统fuse及其用户空间文件系统实现(例如ntfs、9P),还有堆栈式文件系统ecryptfs、overlayfs、redirfs、unionfs等,还有伪文件系统proc、sys、debugfs,基于内存的文件系统tmpfs、ramfs。

linux系统中VFS是最面向对象设计的部分,VFS是基类,提供了抽象接口并且提供了默认实现,具体文件系统实现了接口层,并且可以覆写基类的方法。

VFS也是通过不断的演进支持更多的文件系统,向上主要提供文件对象操作的抽象接口,包括文件的打开、读写、文件位移、mmap映射、lock、ioctl操作等,这些是在文件的基础上抽象而来的,有些文件类型的操作是没有意义的,例如普通文件进行readdir操作。

VFS它主要是接口的实现和各种共性操作的封装,后端还需要各种文件系统来实现具体的功能。

组件及其关系

下面我们先来简单看一下典型的基于块设备的文件系统是如何组织的,VFS作为这些块设备文件系统在内存中的代表,体现出这些文件系统的哪些共性?

superblock,dentry,inode

首先来看一下块设备上一个简单的文件系统数据是如何分布的:
fs layout
一般硬盘是分区管理的,在硬盘头部区域预留一小块来保存分区信息,它可以是MBR或者GPT类型的分区表。一块硬盘当成线性区域分成多个区,这样不同用途的分区可以逻辑隔离处理出来,分区内的操作不会波及到其他分区,每个分区有自己的文件系统类型和挂载点。
我们可以通过mkfs -t ext4 /dev/sda1来格式化一块分区,写入一些文件系统的元数据,如上图所示,文件系统格式化将分区格式化成上图所示的布局。
格式化一般是通过用户空间工具直接操作硬盘的,但是需要将分区挂载到系统上,这样所有的用户和进程都可以访问,所以挂载操作放在了内核中,这样对所有的用户进程都可见。

  1. 文件系统最重要的数据是superblock,它是文件系统数据的总控,通常记录了文件系统的block size,分区总的大小,数据块bitmap的位置,inode表的位置,inode数据存储的位置等,通过它可以找到所有的数据,包括文件内容。

  2. 数据块bitmap记录了分区内块的使用情况,当创建新文件、扩张文件内容都会从bitmap中申请,当删除文件、裁剪文件会将空闲块释放回bitmap中。

  3. inode bitmap用来存储inodes表的使用情况,一个分区内划分出一些数据块来存储文件元数据inode信息,通过该bitmap记录inode的使用情况。

  4. inodes中存储了文件inode的元数据,通常有:文件的inode编号,文件类型,文件大小,设备标识符,用户标识符,用户组标识符,文件模式,扩展属性,文件读取或修改的时间戳,链接数量,以及存储文件内容的磁盘区块的位置等。

  5. 数据块存储了文件内容信息,包括目录项和普通文件,软链接文件等的内容。

当分区被挂载时,会将硬盘上的信息读取到内存中,并且抽象成通用的对象,主要有super_block和inode对象。
文件系统对象
注意:inode有两种,一种是VFS的inode,一种是具体文件系统的inode;同时superblock也有两种,VFS的superblock和具体文件系统的superblock。VFS层的对象包含共性的数据,例如superblock中的块size,存储inode的链表,不过也通过s_fs_info指向了具体文件系统的超级块ext2_sb_info,这样在进行ext2的特殊操作时不必再从硬盘上读取就能获取它特定的信息。另外VFS不太关心data block bitmap,inode_bitmap等这些具体信息,它只关注文件系统能够提供的接口和对象。

dentry目录项,它代表每层目录,例如一个目录/tmp/linux/answer,那么就有四个目录项,对应着/, tmp,linux,answer每一项,这里/是最特殊的一个,只有绝对路径中才有意义,代表根文件系统的根。

  1. 硬盘上没有实际的内容和它对应,它只是内存中的对象;inode和superblock是有实际的硬盘内容和它一一对应的。
  2. dentry和VFS inode都是为了加速而存在的,dentry形成树的关系来加速路径查找,而inode是硬盘上inode信息在内存上的缓存。

file_system_type

文件系统要使用,必须要通过register_filesystem先向系统注册,每个文件系统需要定义自己的注册信息file_system_type,其中主要有:文件系统名称,挂载函数和卸载函数和一些辅助标记。

所有的文件系统都挂在file_systems头指针上,读取或者修改该指针上内容需要获取读写锁file_systems_lock。当挂载块设备分区时,需要指定文件系统名称,这样根据文件系统名称找到对应注册的file_system_type从而找到挂载函数。

挂载函数负责读取块设备上的superblock数据创建VFS的superblock,根据文件系统中的root inode信息创建VFS中的root inode和对应的dentry。

当卸载分区时,卸载函数主要负责的就是脏数据的回写,包括文件数据和文件系统元数据,另外销毁所有的dentry和inode,最后销毁VFS中的superblock这样消除在内存中的最后痕迹。

path,mount,vfsmount,address_space

下面的挂载行为以mount -t ext2 /dev/sda1 /mnt/media为例,不再单独列出。

path代表一个路径,它通过当前子树信息和在子树中的dentry来唯一表示路径,子树信息是vfsmount

mount:代表一个子树,关键信息有设备路径、mountpoint、挂载点的dentry。
每次挂载行为都会有一个mount实例,vfsmount作为mount的一个成员存在,它里面存储有父mount的信息。/mnt/media对应一个挂载点,对于同一个挂载点进行多次挂载会有多个实例。早期设计中vfsmount才是正牌的子树对象,不过它里面的对象基本都是给core VFS使用的,后来不想让module访问这些信息,所以做了限制:发明了mount结构,将vfsmount中的大部分成员都转移到mount对象,在vfsmount中只保留了mnt_root/mnt_sb/mnt_flags三个成员,然后mount定义放到了fs/mount.h中,这样以module形式存在的内核模块因为找不到这个头文件就访问不到这个对象的定义了,最后只留了一个空壳子vfsmount给内核模块访问;留下的vfsmount作为mount的成员存在,这样子树还是那个子树,就是内核模块只能访问vfsmount的三个虾兵蟹将了。

mountpoint:挂载点,当断开mount时,需要清除挂载点dentry的DCACHE_MOUNTE标记,但是不知道是否当前没有挂载点指向它,需要扫描mount_hashtable表,比较费时。创建了mountpoint对象,通过引用计数的方式记录dentry是否还作为挂载点存在,当不再有mount指向它时就可以清除该标记了。

nameidata它表示一个查找过程的结果,在进行路径查找时,通常会根据当前进程的fs_struct也就是根路径和当前工作路径,这样就能查找绝对路径和相对路径了,查找的过程中path代表当前查找到的路径,最终代表查找的结果:目标路径或者NULL。
address_space表示一个文件和它在磁盘上的映射关系,常用的带page cache的方式读写文件就是借助于address_space维护的page cache结构。

文件

文件有各种的类型:普通文件、目录文件、软链接文件,其他的几种特殊文件这里暂时不讨论。

其中普通文件是最常见的,它的数据区存储的就是文件内容,而目录文件也是一种文件,除了标识是文件夹之外,它有自己的数据块,只是它里面存储的是inode和对应的文件名称,而且它里面前两项通常是...,对于软链接文件而言,除了标识它是软链接类型之外,它也有自己的数据块,只是数据块中存储的是链接文件路径。
下面绿色标识数据块内容,其中dir inode中每行代表一对数据,例如第一行7和.标识当前目录项的inode编号为7。
文件内容
对于硬链接,它是两个不同文件名指向同一个inode编号,上面test2和test5文件名的inode号都是12,即他们都指向同一个inode元数据,并且inode的引用计数从1增加到了2,只有当两个文件都删除才最最终删除inode的信息和数据。

下面意图简单示意了内核中文件的各种对象address_space和file,dentry,inode的关系:
文件系统中的各种对象

  1. 多个fd可能会对应同一个file对象,例如dup系统调用
  2. 每个file对应一个dentry,它代表路径
  3. 可能多个dentry指向同一个inode,硬链接文件多个文件路径对应同一个编号的文件
  4. dentry可能没有对应的inode,内核中称之为negative dentry;这是为了加速查找,对于某些不存在的路径也为其创建dentry,只是查找到的时候没有对应的inode,文件打开失败。
  5. 每个inode对应一个address_space,它通过一颗基数树来维护文件偏移对应的page。当以O_DIRECT方式打开文件的时候仍然会关联inode的address_space,但是bypass掉了它的page cache操作,直接搞块设备。

推荐可以分析一个超级简单的文件系统,它没有追求性能而是作为教学工具来阐述文件系统在内核中的组织,有什么问题可以私信我。
https://github.com/psankar/simplefs

接下来会具体分析一下几个场景的内容:设备的挂载,文件的打开,读写,O_DIRECT和O_SYNC打开方式的区别和两种方式的一致性保持,sync操作,readahead,page cache到BIO,最后以simplefs为例看一下各种文件操作的联动关系。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页