Linux 内核在用户进程(或者说 C 标准库)和文件系统之间引入了一个抽象层——VFS,它是一个内核子系统
Virtual File System(虚拟文件系统)是一种抽象的文件系统概念,它提供了一种将多个不同的文件系统(例如本地文件系统、网络文件系统、压缩文件系统等)整合成一个统一的视图的方式。虚拟文件系统通过将不同的文件系统组合在一起,并且将它们的特性抽象为标准的文件操作,从而使得用户和应用程序可以通过相同的方式访问它们。
Virtual File System通常由两部分组成:虚拟文件系统层和文件系统实现层。虚拟文件系统层提供了一种统一的文件系统接口,允许应用程序通过标准的文件操作(如读、写、创建、删除等)来访问不同的文件系统。文件系统实现层则是具体实现各种文件系统的代码,它们被整合到虚拟文件系统中。
虚拟文件系统的优势在于可以提供统一的文件访问接口,隐藏底层不同文件系统的细节。这样一来,应用程序就可以在不知道底层文件系统的情况下进行文件操作,这大大提高了软件的可移植性和可扩展性。此外,虚拟文件系统还可以支持各种文件系统的共享、挂载和卸载等功能,从而使得不同的文件系统之间可以方便地进行数据交换和共享。
底层实现
VFS 中的主要的对象和数据结构定义在这里:<include/linux/fs.h>
- superblock: 超级块对象,代表一个已挂载的具体文件系统,存储文件系统的元信息。
- inode: 索引节点对象,代表一个文件,存储文件的元信息。
- dentry: 目录项对象,代表一个目录项,是文件路径的其中一个组成部分。
- file: 文件对象,代表由进程打开的一个文件,目录也是一个文件
Superblock
超级块对象用于存储特定文件系统的信息以及描述该文件系统
- 通常位于磁盘特定扇区的文件系统超级块或者文件系统控制块中
- 不是基于磁盘的文件系统(例如 sysfs)会实时生成超级块对象并存储在内存中
struct super_block {
struct list_head s_list; // 指向所有其他相同文件系统类型的超级块的链表。
dev_t s_dev; // 设备标识符
unsigned long s_blocksize; // 块大小,以 byte 为单位
loff_t s_maxbytes; // 块大小,以 bit 为单位
struct file_system_type *s_type; // 文件系统类型,包括名字、属性和其他信息
const struct super_operations *s_op; // 内核可以调用的超级块方法
uuid_t s_uuid; // 这个文件系统的唯一标识 UUID
struct list_head s_inodes; // 这个文件系统中的所有 inodes 列表
unsigned long s_magic; // 文件系统的魔数
struct dentry *s_root; // 目录挂载点
int s_count; // 超级块的引用计数
void *s_fs_info; // 文件系统的一些特殊信息
const struct dentry_operations *s_d_op; // 目录项的默认操作对象
...
};
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*free_inode)(struct inode *);
void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, struct writeback_control *wbc);
int (*drop_inode) (struct inode *);
void (*evict_inode) (struct inode *);
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_super) (struct super_block *, enum freeze_holder who);
int (*freeze_fs) (struct super_block *);
int (*thaw_super) (struct super_block *, enum freeze_holder who);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*umount_begin) (struct super_block *);
...
};
Inode
inode 对象包含了内核在操作文件或目录时需要的全部的信息,文件的 inode 包含指向文件内容的数据块的指针,目录索引节点包含指向存储名称 —— 索引节点关联的块的指针。
文件系统中的对象类型通常包括以下几种:
- socket
- symbolic link
- regular file
- block device
- directory
- character device
- FIFO
目录项对象和目录项缓存 (DENTRY & DCACHE)
dentry 是 directory entry 的缩写,也就是目录项,它通过将 inode 索引节点编号与文件名关联,将 inode 和文件关联起来。主要是解决 inode 是一个整数类型对人类不友好的问题
dentry 不同于 super_block 和 inode,它在磁盘上没有对应的数据结构,所以使用文件路径名作为索引文件对象的时候,VFS 需要遍历整个文件路径中的所有部分并从块设备 (比如磁盘) 读取相关的数据实时解析成 dentry 对象,同时还要对路径中的每一部分进行验证和字符串比较。因此VFS 引入了目录项缓存,也就是 dcache。
dcache 主要由以下几个部分组成:
- "被使用的" dentry 链表。该链表通过 inode 中的
i_dentry
字段把相关的 dentries 链接在一起,这是因为一个 inode 可能有多个指向它的链接 (比如有多个硬链接指向它) ,也就会对应多个 dentries,因此就用一个链表把这些对象串在一起。 - "最近被使用的"双向链表。所有"未被使用的"和"负状态"的 dentries 都存放在这个链表中,该链表按照插入时间倒序排列,也就是说最新的数据总是从链表头部插入,所以越靠近链表尾部的对象越旧。当内核决定要回收内存的时候,会使用 LRU 算法优先对链表尾部进行清理,也就是最近最少使用的数据会被优先清理掉。
- Hash 表。通过 hash 函数可以快速地根据给定文件路径获取对应的 dentry 对象,如果不在缓存中则返回空值。
dcache 在一定程度上还实现了 inode 的高速缓存 (inode cache, icache)。因为在内核缓存中,那些"未被使用的" dentries 对应的 inodes 也会被保存在缓存中,因为那些 dentries 还保留在 dcache 中,所以那些 inode 的引用计数就会变成正数,也就会一起留在缓存中。未来如果 VFS 需要查找那些 inodes 的时候,就能借助 dcache 快速找到它们。
FILE
VFS 的最后一个重要对象是 file 文件对象,它表示由用户空间进程打开的文件。这个对象将用户空间的文件和内核空间的 inode 对象关联起来,如果我们从用户空间的视角去看待 VFS,file 就是 VFS 提供的沟通用户空间和具体文件系统的标准接口,因为用户程序直接处理的就是文件而非超级块、索引节点和目录项。
所有打开的 file 对象都存储在内核中的 fd_array
数组,用户空间持有的文件描述符 fd 本质上是这个数组的索引,fd 传递回内核进行相关的文件操作时,内核就是通过它从 fd_array
数组中取出真正的 file 对象进行相应的操作。