由于现在大多数资料都是基于 v2.6 - v4.x 版本内核的, v6.x 内核的相关资料较少,而经过这么多年的发展 Linux 内核对文件系统的实现发生了多次变化,笔者将结合现有资料和 Linux v6.8.0 的代码编写本篇文章,水平有限,如有错误跪请留言指正 Orz

Linux 利用 VFS 抽象出了通用的文件系统模型,进而可以借助 VFS 的转换来快速方便的兼容各种不同的文件系统格式,同时给上层应用提供统一的操作接口,屏蔽实际文件系统格式的差异。本篇文章将从整体架构和实现细节两个层面介绍 Linux 文件系统模型以及 VFS 的设计,通过内核代码中对 VFS 核心对象的声明来讲明 Linux 是如何看待文件以及操作文件的,同时避免了引入过多的内核代码的实现细节,减少额外的代码负担,方便想要理解 Linux 文件系统实现原理或入门相关领域的读者学习研究。

# Linux 文件系统

文件系统定义

计算机的文件系统是一种存储和组织计算机数据的方法,它使得对其访问和查找变得容易,文件系统使用文件树形目录的抽象逻辑概念代替了硬盘和光盘等物理设备使用数据块的概念,用户使用文件系统来保存数据不必关心数据实际保存在硬盘(或者光盘)的地址为多少的数据块上,只需要记住这个文件的所属目录和文件名。在写入新数据之前,用户不必关心硬盘上的那个块地址没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中。

严格地说,文件系统是一套实现了数据的存储、分级组织、访问和获取等操作的抽象数据类型(Abstract data type)。

文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。不同的文件系统有不同的特点,比如目录结构、权限控制、性能优化等。那么怎样在支持多种文件系统的同时,给用户和应用程序提供统一的接口呢?正如那句「计算机科学中的所有问题都可以通过增加一个额外的间接层来解决」一样,Linux 内核在用户层和文件系统层之间抽象出了 VFS(Virtual File 虚拟文件系统)层来解决上述问题,正如下图所示。

架构图

该结构图可以明显看出,当应用程序或库函数调用系统调用后,如果涉及到文件操作,会先进入 VFS 层,现由 VFS 预先处理(某些优化)后交由对应的文件系统去处理,而文件系统由维护自己的缓冲区(也是为了 I/O 优化)并直接跟存储介质打交道。

VFS 是一个重要的抽象层,他定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。无论底层使用的是 ext4、NTFS,还是 FAT32,应用程序都可以通过 VFS 提供的接口直接操作存储设备上的文件。通过这种设计,文件系统不仅更加灵活,还更易于扩展和维护。例如,当需要支持一种新的文件系统时,只需在 VFS 层实现对应的接口适配,而不必修改用户态的应用程序。

因此,抽象出 VFS 不仅提升了系统的兼容性和扩展性,还简化了文件系统的开发复杂度,使得不同文件系统之间能够以模块化的方式协同工作。这种设计理念也反映了现代操作系统的一个重要特点:通过分层架构和抽象接口实现复杂系统的高效管理和灵活适应。

# VFS 入门

由于只是简单入门,讲解 VFS 时只会涉及一些最基本的、形成文件系统所必需的部分,一些与优化、特定文件系统或特殊功能相关的部分都暂且不涉及。

更进一步的讲解可以看 Linux 内核自带的 VFS 文档,位于 linux/Documentation/filesystems/vfs.rst

我们可以把 VFS 层理解为 Linux 对文件模型的抽象,他抽象了一些结构(文件结构,目录结构)和一些方法(读、写、挂载等等),对应到 C 语言中就是他定义了一些结构体和一些函数原型,只要某个文件系统 “实现” 了这些必需的结构体和函数,并按照 VFS 的要求向 VFS 注册,那就可以正确被 VFS 层调用,也即可以被 Linux 系统兼容。

这些结构体和函数原型都是抽象与建模后的产物,想理解他们为什么这么抽象,必需先说说 VFS 的核心设计 —— 统一文件模型。目前的 Linux 系统的 VFS 都是源于 Unix 家族,因此这里所说的 VFS 对所有 Unix 家族的系统都适用。Unix 家族的 VFS 的文件模型定义了四种对象,这四种对象构建起了统一文件模型

注意,因为 VFS 是一个抽象的概念,而有些文件系统又是根据 VFS 的理念来组织文件的,所以很多 VFS 中的元素和具体的文件系统中的元素名称相同,在本文中,如果没有将具体的文件系统,那么所说的名词(例如 iNode/Superblock)都是描述 VFS,而不是具体的文件系统,请注意区分这二者的不同。在介绍完 VFS 的 4 个对象后会举一个具体的例子帮助理解。

⚠️ 虽然 Linux 内核是用 C 语言实现的,没有 class 关键字,但是内核用 struct 事实上构建了一些类,他包括一些特定的属性和方法,所以下面用 “对象” 来描述对应的数据结构。当说到 “创建某个对象时”,实际上是说创建了一个对应的结构体并对特定属性、方法进行初始化了。

这四种对象分别是:

  • superblock:存储文件系统基本的元数据。如文件系统类型、大小、状态,以及其他元数据相关的信息(元元数据)
  • index node(inode):保存一个文件相关的元数据。包括文件的所有者(用户、组)、访问时间、文件类型等,但不包括这个文件的名称,文件和目录均有具体的 inode 对应。
  • directory entry(dentry):保存了文件(目录)名称和具体的 inode 的对应关系,用来粘合二者,同时可以实现目录与其包含的文件之间的映射关系。另外也作为缓存的对象,缓存最近最常访问的文件或目录,提升系统性能
  • file:一组逻辑上相关联的数据,被一个进程打开并关联使用
元数据 v.s. 数据

元数据(metadata)指的是具有某些管理职能的数据,他虽然也是数值,但一般特定数值会代表特定含义,从而在逻辑上表达某种含义。而数据则是只关注值的不同,一般是连续的、具体的。数据关注具体值,而元数据关注数据的属性或特征,所以也被称为 “数据的数据”。在文件系统中,文件就是由数据组成的,而记录文件的属性、大小等的数据就是元数据,他是数据(文件)的特征数据。

# file

在 Linux 中,“一切皆文件” 是一个核心设计理念。无论是普通文件、设备、管道、套接字等,都可以通过文件操作接口(如 openreadwrite )进行访问,并在 VFS 层面进行统一管理。虽然从用户态来看,这些对象都可以像文件一样操作,但它们在内核中的具体数据结构可能不同。例如,普通文件和管道使用 struct file 表示状态,而目录使用 struct dentrystruct inode 表示。在本文中, file 结构应当作被理解为描述普通文件的状态,但在 Linux 其他部分(如设备文件、管道)其实也是类似的抽象机制,只是底层实现有所不同。

为方便理解,我们自底向上的讲,从我们最直观的文件说起。 file 对象是内核在进程打开文件时创建的一个内存数据结构,它代表了文件的当前访问状态(如文件指针位置、文件描述符等),并与具体的进程和用户相联系,file 的其具体的定义位于 linux/include/linux/fs.h ,这里只讲解一些基本的也是比较重要的结构体成员。

struct file {
	union { 
		/* fput() uses task work when closing and freeing file (default). */
		struct callback_head 	f_task_work;
		/* fput() must use workqueue (most kernel threads). */
		struct llist_node	f_llist;
		unsigned int 		f_iocb_flags;
	};
	...
	fmode_t			f_mode; /* 用户期望的文件打开方式,在打开文件时指定 */
	atomic_long_t		f_count; /* 文件的引用计数,当计数为 0 时可以安全地释放文件对象 */
	...
	loff_t			f_pos; /* 跟踪文件中当前的读写位置(偏移量) */
	unsigned int		f_flags; /* 文件自身的标志位,打开文件后可以获取 */
	struct fown_struct	f_owner; /* 标识当前打开文件所属的进程信息 */
	...
	struct path		f_path; /* 包含了文件系统挂载点信息 vfsmount,同时链接到当前文件对应的目录项 dentry */
	struct inode		*f_inode;	/* 文件对象缓存的索引节点信息 */
	const struct file_operations	*f_op; /* 指向文件操作表,记录文件操作函数指针(读、写等) */
	...
	struct address_space	*f_mapping; /* 用于将文件直接映射到内存中,以便快速、有效地访问文件数据 f_mapping 结构包含了与文件映射相关的各种信息,例如文件的起始位置、大小、访问权限等 */
	...
} __randomize_layout __attribute__((aligned(4)));

其中 union 内的内容是在较新版本的内核中出现的,主要是用于任务调度和性能优化,暂且不提。有两个成员需要重点关注:

  • f_path 是一个指向 struct path 结构体的指针,该结构体的定义是:

    struct path {
    	struct vfsmount *mnt;
    	struct dentry *dentry;
    } __randomize_layout;

    其中 struct vfsmount 存储文件系统挂载点信息, struct dentry 是当前文件对应的目录项,file 对象正是通过 path 结构来链接到对应的目录中的。

  • f_op 是一个指向 const struct file_operations 结构体的指针,该结构体记录了当前文件所支持的所有操作(函数)的函数指针,其定义如下:

    struct file_operations {
    	struct module *owner;
    	loff_t (*llseek) (struct file *, loff_t, int);
    	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    	int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
    			unsigned int flags);
    	int (*iterate_shared) (struct file *, struct dir_context *);
    	__poll_t (*poll) (struct file *, struct poll_table_struct *);
    	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    	int (*mmap) (struct file *, struct vm_area_struct *);
    	unsigned long mmap_supported_flags;
    	int (*open) (struct inode *, struct file *);
    	int (*flush) (struct file *, fl_owner_t id);
    	...
    } __randomize_layout;

每次打开一个文件时内核都会创建一个 file 对象,即使打开的是同一个文件,也会创建多个 file 对象,只不过这些对象会指向同一个 inode(即 f_inode 指向同一个 inode 对象)。

❗️注意区分 file 对象和文件的区别❗️

file 对象描述的是一个被进程打开的文件的状态而非是文件这个概念,对于存储在磁盘上的文件来说,最重要的是文件的二进制数据,而在内核中,文件在磁盘上的数据是通过 inode 对象来间接寻找的,file 对象只有在某个文件被进程打开时才动态创建,用于跟踪该文件的状态,而非表示磁盘上的文件或其内容,请务必区分这两个概念。

# inode

inode(index node,索引块)主要存储了文件的元数据,如权限、所有者、大小、修改时间等信息。一个文件对应一个 inode,而多个 file 对象(即多个打开文件的文件描述符)可以引用同一个 inode。对于文件系统来说,创建文件系统时会生成 inode,保存在具体存储设备上,记录文件的元信息;而对于 VFS 来说,inode 对应一个 inode 数据结构,来管理文件系统的 file 对象,记录 file 对象的元数据,部分字段与关联的 file 对象有关,会动态创建,其具体定义位于 linux/include/linux/fs.h ,这里同样只列出了一些基本的、重要的成员。

struct inode {
	umode_t			i_mode; /* inode 类型:文件、目录、链接等等 */
	...
	kuid_t			i_uid; /* 用户 id */
	kgid_t			i_gid; /* 用户组 id */
	unsigned int		i_flags; /* inode 标志位 */
	const struct inode_operations	*i_op; /* 指向 inode 操作表,内部是 inode 函数指针 */
	struct super_block	*i_sb; /* 指向所属的超级块 */
	struct address_space	*i_mapping; /* 磁盘上的 inode 数据以 page 为单位保存在内存缓冲区,通过 i_mapping 实现这种映射关系 */
	unsigned long		i_ino; /* inode 编号,每个文件系统实例内唯一 */
	...
	loff_t			i_size; /* 文件大小,若为目录,i_size 是一个块大小,默认为 1024 字节 */
	struct timespec64	__i_atime; /* 时间戳 */
	struct timespec64	__i_mtime;
	struct timespec64	__i_ctime;
	...
	unsigned short          i_bytes; /* 文件最后一个块的字节数,注意块单位为 512 字节为一块 */
	u8			i_blkbits;               /* 以位为单位的块的大小 */
	... 
	blkcnt_t		i_blocks;            /* 分配给文件的磁盘块数,文件使用块的数目 */
	/* Misc */
	unsigned long		i_state;         /* inode 关联的各种状态标志 */
	...
	struct hlist_node	i_hash;        /* 将当前 inode 链接到 inode 哈希表(链表) */
	....
	union {
		struct hlist_head	i_dentry;    /* 链接到应用此 inode 的目录项单链表表头 */
		struct rcu_head		i_rcu;
	};	
  ...
	union {
		const struct file_operations	*i_fop;	/* 链接到文件对象 file 的操作表 */
		void (*free_inode)(struct inode *);
	};
	...
	struct address_space	i_data; /* inode 数据地址空间的地址空间对象,作为页缓存的中间媒介 */
	...
} __randomize_layout;

同样有两个成员比较重要,我们单独拿出来说说

  • i_dentry 字段指定当前 inode 对应的的 file 对象的名称,也就是 dentry,该结构内部是一个指向单链表的指针(链表头),因为可能由多个 dentry 都指向这个 inode(硬链接)。

    struct hlist_head {
    	struct hlist_node *first;
    }; /* list 前的 h 是 hash 的缩写,hlist_head 用于实现哈希表中的链表头。 */
    struct hlist_node {
    	struct hlist_node *next, **pprev;
    };

    pprev :是一个 指向指针的指针,它指向当前节点的前一个节点的 next 指针。 pprev 的设计是为了 快速删除 链表中的节点。在普通的链表中,如果想删除一个节点,通常需要遍历到该节点的前一个节点并修改其 next 指针。使用 pprev 可以直接修改前一个节点的 next 指针,从而加速节点的删除操作。对一个单向列表而言,删除当前节点的操作不需要遍历链表查找前驱节点,所以删除操作的时间复杂度为 O(1)

  • i_fopi_op 是除了文件的一些权限信息、访问时间、大小等信息之外,最重要的。他们记录了 inode 和 file 对象所提供的操作, i_fopstruct file_operations 类型的结构体,上面已经讲过了。而 inode 支持的操作是用 struct inode_operations 结构来记录的,定义如下:

    struct inode_operations {
    	struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
    	const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *);
    	int (*permission) (struct mnt_idmap *, struct inode *, int);
    	struct posix_acl * (*get_inode_acl)(struct inode *, int, bool);
    	int (*readlink) (struct dentry *, char __user *,int);
    	int (*create) (struct mnt_idmap *, struct inode *,struct dentry *,
    		       umode_t, bool);
    	int (*link) (struct dentry *,struct inode *,struct dentry *);
    	int (*unlink) (struct inode *,struct dentry *);
    	int (*symlink) (struct mnt_idmap *, struct inode *,struct dentry *,
    			const char *);
    	int (*mkdir) (struct mnt_idmap *, struct inode *,struct dentry *,
    		      umode_t);
    	int (*rmdir) (struct inode *,struct dentry *);
    	...
    } ____cacheline_aligned;
inode寻找文件数据

在某些文件系统(如 ext2/ext3/ext4)的具体实现中,inode 结构中会定义 i_block 数组,它存储该 inode 对应文件的数据所在的块号,或者指向间接块号的指针。通过该数组,可以获取文件的实际数据块的位置。在 VFS 的 struct inode 结构中, i_mapping 指向 struct address_space ,该结构用于管理文件数据在内存中的映射(页缓存),从而加速数据访问。某些文件系统可能会在 i_data 字段中存储具体的文件系统元数据,例如 ext2/ext3/ext4 可能使用它来存储 i_blockstruct address_space 主要用于管理内核页缓存(Page Cache),文件系统的 inode 结构会通过 i_mapping 访问已缓存的页,而不是每次直接访问磁盘数据,从而提高读写性能,涉及到缓存的内容比较复杂,这里暂时不过多讨论。

# dentry

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

dentry 一般没有在磁盘等底层持久化存储设备上存储,而是一个动态创建的内存数据结构,所有的文件系统对象都有对应的目录项,不论是目录、常规文件,还是符号链接、块设备文件、字符设备文件等,目录项反映的是文件系统对象在文件系统树中的位置,便于 VFS 快速寻找。dentry 创建之后会被操作系统进行缓存,目的是为了提升对文件系统进行操作的性能。dentry 的结构如下示意,具体定义于 <linux/dcache.h>dcachedentry cache 的缩写),同样这里只列出了部分基本成员。

struct dentry {
	unsigned int d_flags;		/* 目录项的标志位,用于标识 dentry 的状态和属性 */
	...
	struct hlist_bl_node d_hash;	/* 用于在哈希表中进行目录项的快速查找 */
	struct dentry *d_parent;	/* 父目录的 dentry */
	struct qstr d_name;       /* dentry 名称 */
	struct inode *d_inode;		/* dentry 对应文件的 inode */
	unsigned char d_iname[DNAME_INLINE_LEN];	/* 短名称,避免动态分配内存 */
  ...
	const struct dentry_operations *d_op; /* 指向 dentry 的操作表 */
	struct super_block *d_sb;	/* 指向 dentry 对象所属文件系统超级块的实例 */
	unsigned long d_time;		  /* 最后一次验证时间	 */
	...
	struct hlist_node d_sib;	/* 一个单链表,存储当前 dentry 的兄弟节点 */
	struct hlist_head d_children;	/* 一个单链表的链表头,内部是该 dentry 的子目录项 */
  ...
};

其中最重要的有两个字段:

  • d_inode 指针指向了当前 dentry 关联的 inode。

  • d_op 指向了一系列 dentry 支持的操作的集合,该结构体定义如下:

    struct dentry_operations {
    	int (*d_revalidate)(struct dentry *, unsigned int);
    	int (*d_weak_revalidate)(struct dentry *, unsigned int);
    	int (*d_hash)(const struct dentry *, struct qstr *);
    	int (*d_compare)(const struct dentry *,
    			unsigned int, const char *, const struct qstr *);
    	int (*d_delete)(const struct dentry *);
    	int (*d_init)(struct dentry *);
    	void (*d_release)(struct dentry *);
    	void (*d_prune)(struct dentry *);
    	void (*d_iput)(struct dentry *, struct inode *);
    	char *(*d_dname)(struct dentry *, char *, int);
    	struct vfsmount *(*d_automount)(struct path *);
    	int (*d_manage)(const struct path *, bool);
    	struct dentry *(*d_real)(struct dentry *, const struct inode *);
    }  ____cacheline_aligned;

这里给出一个简单目录树结构,来说明 dentry 在内存中的关联关系:

/
 ├── home
      └── ubuntu
      	   └── test.txt
 ├── etc
 └── usr
      └── bin

d_children 只指向第一个子目录的 dentry&dentry ),然后通过 d_sib 来遍历所有子目录,最后一个子目录的 d_sib 指针为 NULL 。同理 d_parent 指向父目录的 dentry ,便于从子目录向上索引,示意图如下:

dentry关系图

# SuperBlock

SuperBlock 用于存储一个文件系统最重要、最基本的元数据,例如文件系统名称、大小、块大小、inode 大小、有多少 inode 已用和未用、有多少空闲 block 等。当 SuperBlock 被某个文件系统具体实现时,他一般会存储在文件系统的第一个块中;而对于 VFS 来说,SuperBlock 对应着 super_block 结构体,每次操作系统启动时,都要重新生成 VFS 的 SuperBlock 信息并常驻内存。操作系统和 VFS 通过注册的 SuperBlock,就可以获取挂载点信息,进行挂载管理、进行文件系统操作、以及在卸载时释放资源。一个 SuperBlock 对应一个文件系统,同样的,一个文件系统也只有一个 SuperBlock。SuperBlock 的具体定义位于 linux/include/linux/fs/fs.h ,其定义如下:

struct super_block {
	struct list_head	s_list;		/* 一个双向链表结构,将多个 superblock 组织成链表 */
	dev_t			s_dev;		/* 设备标识符 */
	unsigned char		s_blocksize_bits;
	unsigned long		s_blocksize; /* 字节为单位的块大小 */
	loff_t			s_maxbytes;	/* 文件的最大长度 */
	struct file_system_type	*s_type; /* 文件系统类型 */
	const struct super_operations	*s_op; /* superblock 的操作函数集合,供 VFS 调用 */
	...
  unsigned long		s_magic;     /* 文件系统的魔术数 */
	struct dentry		*s_root;     /* 文件系统的根目录项对象 */
	...
	struct block_device	*s_bdev; /* 块设备驱动程序描述符指针 */
	...
	struct list_head	s_inodes;  /* 一个双向链表,用于存储与 superblock 相关的所有 inode */
  ...
}

有几个成员比较重要,单独解释下:

  • s_list 是一个双向链表,由于 Linux 系统支持同时挂载多个文件系统,因此 s_list 字段用于在内存中构建 superblock 链表来支持挂载多个文件系统。

    struct list_head {
    	struct list_head *next, *prev;
    };
  • s_magic 每个文件系统的特殊标识,定义在 linux/include/uapi/linux/magic.h

    #define EXT2_SUPER_MAGIC	0xEF53
    #define EXT3_SUPER_MAGIC	0xEF53
    #define XENFS_SUPER_MAGIC	0xabba1974
    #define EXT4_SUPER_MAGIC	0xEF53
    #define BTRFS_SUPER_MAGIC	0x9123683E
    ...
  • s_root 标识该文件系统的根目录项对象, s_bdev 标识该文件系统所在的设备信息

  • s_op 指向该文件系统 superblock 所支持的各种操作的函数集合

    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 *);
    	...
    };
内核与文件系统中的同名但不同义的概念辨析,以superblock和inode为例
  1. VFS 中的 inode 和具体文件系统中的 inode

    • 在 VFS 中,inode 是一个抽象概念,它描述了文件的元数据,例如文件大小、权限、时间戳等。它是 VFS 用来统一管理文件的逻辑结构,和底层具体文件系统无关。

    • 而在具体的文件系统(例如 ext4)中,inode 是物理存在的,它是存储在磁盘上的一段结构化数据,记录了该文件在磁盘上的位置以及其他相关信息。它和 VFS 的 inode 概念一致,但实现方式取决于具体的文件系统。

  2. VFS 中的 Superblock 和具体文件系统中的 Superblock

    • 在 VFS 中,Superblock 是一个虚拟结构,用于描述一个文件系统的全局信息,例如文件系统的大小、块大小、可用空间等。这些信息是 VFS 管理文件系统的必要抽象。

    • 而在具体文件系统中(比如 ext4 或 FAT32),Superblock 是磁盘上的实际数据块,存储了文件系统的全局元数据。这些元数据的组织方式可能和 VFS 的抽象一致,但也可能有所不同。

同样名字不同含义的原因在于:名字相同是因为 VFS 的抽象是基于通用的文件系统理念设计的,它试图统一底层文件系统的逻辑,因此抽象层使用的名字与具体文件系统中常见的名字一致。然而,含义不同是因为 VFS 的内容是逻辑抽象,而具体文件系统中的元素是这个抽象的具体实现。一个是概念层面,一个是物理层面,两者的作用和实现细节都可能有差异。

# 文件系统原理

# VFS 中如何找到文件

我们先来举一个简单的例子,来梳理下 VFS 的索引文件的工作流程(找到 /home/ubuntu/test.txt ):

  1. 从根目录开始:通过 super_block 中的 s_root 获取根目录的 dentry
  2. 逐级解析路径:VFS 会将路径字符串分解为多个部分(如目录和文件名),然后通过每个部分查找对应的 dentry 。每个目录的 dentry 都有一个 d_inode ,指向该目录的 inode
  3. 查找目录项( dentry:VFS 会检查 dentry 是否存在于内存中,如果存在,则返回对应的 inode 。否则,VFS 会向文件系统请求加载该 inode
  4. 返回 inode 并执行操作:当 VFS 找到文件的 inode 时,操作系统可以通过该 inode 访问文件的元数据、数据块等,然后再执行如打开、读取、写入等操作。

# 用户态如何操作文件

每个打开的文件在内核进程中是以文件描述符(file descriptor)存在的,每个进程维护一个数组,这个数组的下标就是给应用返回的文件描述符,数组中的每个元素都是一个 file 对象, struct file 中保存了 struct dentry , 而 struct dentry 中就实际包括文件的 inode 的信息。具体的关系如下:

用户态操作文件

VFS 层提供标准文件操作的函数接口,具体的文件操作函数是由文件实际所在的文件系统提供。所以在一次文件操作中会经历如下步骤,以在 ext4 文件系统上执行 write() 为例(简化版本的步骤,只是为了展示调用过程)。

write调用过程

这张 write() 函数调用的路径图也正好和文章最开始的 Linux 架构图相对应,可以一起参考着看。

# 参考

  • https://blog.csdn.net/jinking01/article/details/105095087

  • https://www.bitsobject.com/filesystems/2024/11/163.html

  • https://www.bitsobject.com/filesystems/2024/11/211.html

  • https://www.bitsobject.com/filesystems/2024/11/179.html

  • https://cloud.tencent.com/developer/article/2074596


更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Gality 微信支付

微信支付

Gality 支付宝

支付宝