Linux(一) VFS虚拟文件系统

一、先了解一下什么是挂载

Linux有自己的一套文件系统,例如Ext2、Ext3,但是外部其他文件系统时,由于各个文件系统都各自有一套的文件管理体系,是无法通过Linux本身访问文件的方式直接访问的,这个时候挂载就产生了。

挂载,指的就是将设备文件中的*目录连接到 Linux 根目录下的某一目录(最好是空目录),访问此目录就等同于访问设备文件。

举个例子:将U盘插入Linux系统中,虽然可以通过图形界面查看设备信息,但是无法通过命令方式访问数据,访问此目录只会提供给你此设备的一些基本信息(比如容量),如下图所示,两边的文件系统结构是不相同的。

Linux(一) VFS虚拟文件系统

Linux根目录的/dev目录下存放的是所有的硬件设备文件,事实上,当 U 盘插入 Linux 后,系统也确实会给 U 盘分配一个目录文件(比如 sdb1),就位于 /dev/ 目录下(/dev/sdb1)。

将/sdb1挂载到/sdb-u文件目录下

mount /dev/sdb1 /sdb-u

Linux(一) VFS虚拟文件系统

可以看到,U 盘文件系统已经成为 Linux 文件系统目录的一部分,此时访问 /sdb-u/ 就等同于访问 U 盘。

二、VFS基本概念

之所以挂载后可以使Linux成功访问U盘,是因为存在一种机制:VFS虚拟文件系统

它屏幕了底层各个文件系统的差异,提供一套通用的接口,所有文件系统都需要先转换为VFS结构才能为用户所调用。当上面将/dev/sbd1挂载到/sbd-u下时,就相当于将U盘特有的文件系统转换成了VFS结构

VFS本身只存在于内存中,它需要将硬盘上的文件系统抽象到内存中,这个过程主要是由以下几个重要结构来实现的

1、超级快 supper_block

超级块(super_block)代表了整个文件系统本身,超级块的内容需要读取具体文件系统在硬盘上的超级块结构获得,所以超级块是具体文件系统超级块的内存抽象

超级块的结构非常负责,将其简化后的数据结构如下所示

struct super_block {
    unsigned long s_blocksize;
    unsigned char s_blocksize_bits;
    unsigned long s_maxbytes;
    struct file_system_type *s_type;
    struct super_operations *s_op;
    unsigned long s_magic;
    struct dentry *s_root;
    struct list_head s_inodes;
    struct list_head s_dirty;
    struct block_device *s_bdev;
    void *s_fs_info;
}

❑ s_blocksize 指定了文件系统的块大小

❑ s_maxbytes 指定文件系统中最大文件的尺寸

❑ s_type 指向file_system_type结构的指针

❑ s_magic 是魔术数字,每个文件系统都有一个魔术数字

❑ s_root 是指向文件系统根dentry的指针

-----超级块对象还定义了一些链表头,用来链接文件系统内的重要成员-----

❑ s_inodes 指向文件系统内所有的inode,通过它可以遍历inode对象

❑ s_dirty 指向所有dirty的inode对象

❑ s_bdev 指向文件系统存在的块设备指针

-----超级块结构包含一些函数指针-----

❑ super_operations 提供了最重要的超级块操作,它的成员函数read_inode提供了读取inode信息的功能。每个具体的文件系统一般都要提供这个函数来实现对inode信息的读取,例如ext2文件系统提供的具体函数是ext2_read_inode。我们可以理解为这就是:VFS提供了架构,而具体文件系统必须按照VFS的架构去实现

超级块链表

每个超级块都要链接到一个超级块链表super_blocks,而文件系统内的每个文件在打开时都需要在内存分配一个inode结构,这些inode结构都要链接到超级块,最后形成类似以下的结构,遍历super_blocks就能找到操作系统打开过的所有inode。

Linux(一) VFS虚拟文件系统

2、Dentry

可以是目录或文件,说白了就是输入ls命令时展示出来的列表信息,一个目录(文件)就是一个Dentry

对于一个通常的文件系统来说,文件和目录一般按树状结构保存,目录项(dentry)就是反映了文件系统的这种树状关系。在文件系统中,无论是目录还是文件均视为文件,而每个文件都有一个dentry(可能有多个),比如根目录就有一个dentry,二级目录里的文件和目录对应的dentry就链接到根目录的dentry上,三级目录里的dentry又链接到二级目录,从而形成了一颗dentry树,从顶部可以遍历完整的树结构

dentry的结构定义同样很庞杂,和超级块类似,我们当前只分析最重要的部分

struct dentry { 
    struct inode *d_inode;
    struct hlist_node d_hash;
    struct dentry *d_parent;
    struct qstr d_name; 
    union { 
        struct list_head d_child;
        struct rcu_head d_rcu; 
    } d_u;
    struct list_head d_subdirs;
    struct dentry_operations *d_op; 
    struct super_block *d_sb;
    int dsemounted;
}

❑ d_inode指向一个inode结构。这个inode和dentry共同描述了一个普通文件或者目录文件

❑ dentry结构里有d_subdirs成员和d_child成员。d_subdirs是子项(子项可能是目录,也可能是文件)的链表头,所有的子项都要链接到这个链表。d_child是dentry自身的链表头,需要链接到父dentry的d_subdirs成员。当移动文件的时候,需要把一个dentry结构从旧的父dentry的链表上脱离,然后链接到新的父dentry的d_subdirs成员。这样dentry结构之间就构成了一颗目录树

❑ d_parent是指针,指向父dentry结构

❑ d_hash是链接到dentry cache的hash链表

❑ d_name成员保存的是文件或者目录的名字。打开一个文件的时候,根据这个成员和用户输入的名字比较来搜寻目标文件

❑ d_mounted用来指示dentry是否是一个挂载点。如果是挂载点,该成员不为零

dentry cache

每个文件都要对应一个inode节点与至少一个dentry项。假设我们有一个100G的硬盘,上面写满了空文件,那个需要多少内存才能重建VFS呢?

文件最少要占用1个block(一般是4K)。假一个dentry与一个inode需要100byte,则dentry与inode需要占用1/40的空间。1G硬盘则需要2.5G空间。最近都开始换装1T硬盘了,需要 25G的内存才能放下inode与dentry,相信没有几台电脑可以承受。

为了避免资源浪费,VFS采用了dentry cache的设计。

当有用户用ls命令查看某一个目录或用open命令打开一个文件时,VFS会为这里用的每个目录项与文件建立dentry项与inode,即按需创建。然后dentry会被链接到hash链表结构的dentry_hashtable上,后续再次访问该文件的时候,就会从dentry_hashtable里去找,避免了再次读硬盘,这就是dentry cache概念。当Linux认为VFS占用太多资源时,VFS会释放掉长时间没有被使用的dentry项与inode项。

那当dentry_hashtable上没有该dentry需要创建时是如何创建的呢?

首先,会通过当前文件目录对应的dentry找到对应的inode节点,有了inode节点就可以读取目录中的信息,其中包含了下一级目录与文件列表信息(name和inode号),用is -i会显示文件的inode号

ls -i
975248 subdir0  975247 subdir1   975251 file0

然后,根据inode号重建inode节点,再根据目录(文件)数据和新建的inode节点来重建子目录(文件)的dentry节点,dentry就是最终我们能够看见的目录(文件)了

整体dentry的结构大致如下

Linux(一) VFS虚拟文件系统

3、inode

inode与磁盘文件一一对应,一般来说,找到Inode后就可以访问磁盘上的文件了。inode保存了文件的大小、创建时间、文件的块大小等参数,以及对文件的读写函数、文件的读写缓存等信息,一个文件可能会有多个dentry,但是只有一个inode

因为inode的定义非常庞大,将其结构定义大大简化,以重点突出几个结构成员

struct inode {
    struct list_head i_list;
    struct list_head i_sb_list;
    struct list_head i_dentry;
    unsigned long i_ino;
    atomic_t i_count;
    loff_t i_size;
    unsigned int i_blkbits;
    struct inode_operations *i_op;
    const struct file_operations *i_fop;
    struct address_space *i_mapping;
    struct block_device *i_bdev;
}

❑ i_list、i_sb_list、i_dentry分别是三个链表头。成员i_list用于链接描述inode当前状态的链表,当创建一个新的inode的时候,要链接到inode_in_use这个链表,表示inode处于使用状态,同时成员i_sb_list也要链接到文件系统超级块的s_inodes链表头。由于一个文件可以对应多个dentry,这些dentry都要链接到成员i_dentry这个链表头

❑ i_ino 是inode的号,而i_count是inode的引用计数。成员i_size是以字节为单位的文件长度

❑ i_blkbits 是文件块的位数

❑ i_fop 是一个struct file_operations类型的指针。文件的读写函数和异步io函数都在这个结构中提供。每一个具体的文件系统,基本都要提供各自的文件操作函数

❑ i_mapping 是一个重要的成员。这个结构目的是缓存文件的内容,对文件的读写操作首先要在i_mapping包含的缓存里寻找文件的内容。如果有缓存,对文件的读就可以直接从缓存中获得,而不用再去物理硬盘读取,从而大大加速了文件的读操作。写操作也要首先访问缓存,写入到文件的缓存。然后等待合适的机会,再从缓存写入硬盘

❑i_bdev 是指向块设备的指针。这个块设备就是文件所在的文件系统所绑定的块设备

inode cache

系统内核提供了一个hash链表数组inode_hashtable,所有的inode结构都要链接到数组里面的某个hash链表。这种用法和前文介绍的hash链表数组dentry_hashtable的用法很类似,这里就不再分析了

关于 inode, 有几个需要注意的特性(划重点!!!)

  1. inode 随着文件的存在而存在,创建文件时系统会创建相应的 inode
  2. inode 都有一个编号(i_ino),操作系统内部使用 inode 号来识别不同的文件
  3. inode 中保存文件数据块(data block)的指针,且数据块与 inode 一一对应,因此 inode 号才是文件的唯一标识而非文件名

需要注意的是,VFS实际上是按照 Ext 的方式进行构建的,所以两者非常相似(毕竟 Ext 是 Linux 的原生文件系统)。比如 inode 节点,Ext 与 VFS 中都把文件管理结构称为 inode,但实际上它们是不一样的。Ext的 inode 节点在磁盘上;VFS 的 inode节点在内存里。ext-inode 中的一些成员变量其实是没有用的,如引用计数等。保留它们的目的是为了与 vfs-node 保持一致。这样在用 ext-inode 节点构造 vfs-inode节点时,就不需要一个一个赋值,只需一次内存拷贝即可。如果是非Ext格式的磁盘,就没有这么幸运了,所以 mount 非 Ext 磁盘会慢一些。

4、文件对象

文件对象的作用是描述进程和文件交互的关系。这里需要指出的是,**硬盘上并不存在一个文件结构,进程打开一个文件,内核就动态创建一个文件对象。**同一个文件,在不同的进程中有不同的文件对象。

**这里的“文件”指的是在硬盘上具体存在的文件,而“文件对象”是在内存里存在的文件结构。**当读写文件的时候,通过文件号就可以获得文件的对象。这也就是读写文件必须先打开文件的原因,如果不执行打开的过程,是不能完成文件读写的。

文件对象中部分核心的属性介绍

struct file {
    struct dentry *f_dentry;
    struct vfsmount *f_vfsmnt;
    const struct file_operations *f_op;
    loff_t f_pos;
    struct fown_struct f_owner;
    unsigned int f_uid, f_gid;
    struct file_ra_state f_ra;
    struct address_space *f_mapping;
}

❑ f_dentry和f_vfsmnt分别指向文件对应的dentry结构和文件所属于的文件系统的vfsmount对象

❑ f_pos表示进程对文件操作的位置。例如对文件读取前10字节,f_pos就指示第11字节位置

❑ f_uid和f_gid分别表示文件的用户ID和用户组ID

❑ f_ra用于文件预读的设置

❑ f_mapping指向一个address_space结构。这个结构封装了文件的读写缓存页面

当打开同一个文件多次时,每次都会构造新的文件对象,所以多个进程打开同一个文件或一个进程打开同一个文件多次时,都具有独立的文件对象,从而f_pos也是独立的,所以读取互不影响,但是对一个文件的内容进行修改其他进程可以立即感知

这里只对文件对象的简单介绍,对于文件对象在实际意义以及操作会在后续关于文件的部分具体展开

三、软硬连接

1、软连接

创建一个软连接命令(参数-s就代表创建的是软连接)

ln -s [源文件或目录] [目标文件或目录]

软连接类似于Windows系统中的快捷方式,其实就是普通的文件,只是数据块中存放的内容是另一文件的路径名的文本指向

Linux(一) VFS虚拟文件系统

软连接特点:

  1. 允许链接到不存在的目标
  2. 链接对象可为目录
  3. 即使删除所有链接文件,对源文件也无影响。但如果删除被指向的原文件,则相关软连接被称为死链接,若重新创建被指向路径文件,死链接可恢复为正常的软链接

2、 硬连接

创建一个硬连接

ln [源文件或目录] [目标文件或目录]

创建硬连接时,相当于多创建了一个dentry指向同一个inode,对指向同一个inode来说,修改任何一个dentry都是对源文件的直接修改

inode中会有个计数器,记录有几个dentry指向它,删除任意一个dentry都不会导致inode删除,直到所有dentry均删除inode才会删除,所有的dentry从某种意义上讲,都是指向inode的硬连接

Linux(一) VFS虚拟文件系统

硬连接特点:

  1. 只允许链接到已存在的文件,而不能是目录
  2. 文件至少要有一个硬链接

四、进程控制块对文件的管理

进程控制块task_struct中有两个变量与文件有关:fs和files

struct task_struct {
    // ... 其它n个变量
    struct fs_struct *fs;
    struct files_struct *files;
}

1、fs

存储着rootpwd两个指向dentry项的指针,用户定位路径时,绝对路径会通过root进行定位,相对路径会通过pwd进行定位

struct fs_struct {
    atomic_t count;
 	rwlock_t lock;
 	int umask;
 	struct dentry * root, * pwd, * altroot;
 	struct vfsmount * rootmnt, * pwdmnt, * altrootmnt; 
}

❑ count 指共享这个表的进程个数

❑ lock 用于表中字段的读/写自旋锁

❑ umask 当打开文件设置文件权限时所使用的位掩码

❑ root 根目录的目录项,pwd 当前工作目录的目录项,altroot 模拟根目录的目录项

❑ root 根目录所安装的文件系统对象,pwdmnt 当前工作目录所安装的文件系统对象,alrootmnt 模拟根目录所安装的文件系统对象

2、files

struct files_struct {
 	atomic_t count;
 	struct fdtable *fdt;
 	struct fdtable fdtab;
 	spinlock_t file_lock ____cacheline_aligned_in_smp;
 	int next_fd;
 	struct embedded_fd_set close_on_exec_init;
 	struct embedded_fd_set open_fds_init;
 	struct file *fd_array[NR_OPEN_DEFAULT];
}

在这里,有一个比较重要的属性:fd_array(file descriptor),是一个指针数组,会保存被当前进程打开的文件对象的指针,这里的文件对象指的就是上文提到的文件对象

注意:fd_array只是保存指针而已,不是文件对象本身,具体会在后面进行介绍

五、Open文件的过程

假设目前进程已经打开了两个文件

Linux(一) VFS虚拟文件系统

接下来我们再打开一个新文件

第一步:找到文件

定位到文件的inode节点,找到了inode也就找到了文件(如何找到inode节点上面已经说过了)

Linux(一) VFS虚拟文件系统

第二步:建立文件对象

创建一个新的文件对象,放入open files list中,并把它指向文件的inode节点

Linux(一) VFS虚拟文件系统

第三步:建立file descriptor

file descriptor就是进程控制块task_struct中files中维护的fd_array。因为是数组,所以实际上已经预先分配好空间了。这里这是需要把某个空闲的pointer与file 关联起来。这个fd_array中的索引号就是open文件时得到的文件fd

Linux(一) VFS虚拟文件系统

当同一个文件open多次时

Linux(一) VFS虚拟文件系统

Linux还提供了dup功能,用于复制file descriptor,但是新建的file descriptor会与原file descriptor同时指向同一个file object(也就是说,共享file object中的所有属性,当然包括了f_pos,因此如果使用dup功能复制一个新的fie descriptor时,读取是会相互影响的)

Linux(一) VFS虚拟文件系统

六、Fork对文件的影响

下图是已有父进程的文件结构

Linux(一) VFS虚拟文件系统

fork一个子进程其实和dup操作很类似,同样不创建新的文件对象,也就是说,文件对象列表不是进程的一部分,而是一个全局性的资源链表,因此不会被复制,进程维护的只是一个指针列表fd_array,所以被复制的只是指针列表。这样的话,无论在父进程还是子进程对文件进行更改的话,另一方都是能够及时感应的

Linux(一) VFS虚拟文件系统
http://www.hanyehong.com/2021/05/23/Linux-%E4%B8%80-VFS%E8%99%9A%E6%8B%9F%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F/

上一篇:文件与日志


下一篇:深入理解Linux文件系统与日志分析