1. 引言:为什么需要虚拟文件系统

在操作系统领域,文件系统是持久化数据的基石。一个现代操作系统往往需要同时支持几十种甚至上百种不同的文件系统:本地磁盘上的 ext4、XFS、Btrfs、NTFS、FAT32,网络文件系统 NFS、CIFS,特殊文件系统 procfs、sysfs、tmpfs、devfs,以及用户态文件系统 FUSE 等等。如果每种文件系统的接口都直接暴露给上层应用,那么应用开发者将不得不为每一种文件系统编写不同的读写逻辑,这显然是不现实的。

为了解决这个问题,操作系统在用户态系统调用层与实际文件系统实现之间引入了一层抽象,这就是虚拟文件系统(Virtual File System,简称 VFS)。VFS 也被称为虚拟文件切换层(Virtual Filesystem Switch)。它向上提供统一的文件操作接口,向下定义了一组标准化的操作集合,每一种具体文件系统只需要实现这些接口,就能无缝地接入操作系统。

从概念上讲,VFS 是一种面向对象思想在操作系统内核中的经典应用。它把「文件」「目录」「索引节点」「超级块」等概念抽象成带有方法指针的结构体,具体文件系统通过填充这些方法指针来提供自己的实现。这种设计使得内核可以做到「一次编写,处处挂载」——同一个系统调用 open(),既可以打开 ext4 分区上的普通文件,也可以打开 procfs 中的内核参数文件,还可以打开 NFS 服务器上的远程文件。

理解 VFS 的重要性在于:它是打通「文件系统理论」和「操作系统内核实践」的关键桥梁。无论是做存储引擎开发、分布式文件系统设计,还是进行 Linux 内核调试与性能调优,对 VFS 的深入理解都是不可或缺的基本功。本文将以 Linux 内核的 VFS 实现为主线,系统性地讲解 VFS 的架构原理、核心数据结构、关键代码路径以及实战调试方法,帮助读者建立从概念到源码的完整认知。

2. VFS 的核心概念与设计目标

在深入具体实现之前,我们先厘清 VFS 的几个核心概念与设计目标。这些概念是理解后续所有内容的基础。

统一抽象:VFS 的第一目标是把所有文件系统统一为四类核心对象——超级块(superblock)、索引节点(inode)、目录项(dentry)和文件对象(file)。无论底层是磁盘文件系统还是伪文件系统,内核都用这些对象来描述和管理它们。

接口标准化:VFS 为每类核心对象定义了一组操作函数表,例如 super_operationsinode_operationsfile_operationsdentry_operations。具体文件系统只需实现自己关心的函数,未实现的函数由 VFS 提供默认行为或返回错误。

缓存与性能:VFS 层维护了全局的 dentry 缓存(dcache)和 inode 缓存(icache),避免频繁访问底层存储。同时,页缓存(page cache)在 VFS 层统一管理文件数据页,使得所有文件系统都能受益于 Linux 成熟的缓存机制。

延迟与按需:VFS 大量使用惰性求值策略。例如路径解析时按需创建 dentry,inode 在真正需要时才从磁盘读取,数据页在访问时才被载入。这种策略显著减少了不必要的 I/O。

可扩展性:VFS 的设计允许动态注册和卸载文件系统类型。内核模块可以在运行时通过 register_filesystem() 注册新的文件系统,FUSE 更是把这种扩展性发挥到了用户态。

从设计模式角度看,VFS 是「策略与机制分离」的典范:VFS 提供机制(缓存、锁、引用计数、通用路径解析),具体文件系统提供策略(如何从磁盘读写、如何组织目录、如何分配空间)。这种分离使得内核能够保持核心代码的稳定性,同时容纳不断增长的各类文件系统。

3. VFS 的历史演进

理解 VFS 的历史演进,有助于把握其设计取舍的来龙去脉。

VFS 的概念最早可以追溯到 1980 年代的 SunOS。Sun 公司在 1985 年发布的 SunOS 2.0 中引入了 vnode/vfs 接口,以便在同一个系统中同时支持本地磁盘文件系统 UFS 和网络文件系统 NFS。这一设计后来被 System V Release 4(SVR4)采纳并标准化,称为 vnode 接口。

Linux 的 VFS 深受 SunOS 和 SVR4 的影响。1991 年,Linus Torvalds 在最初的 Linux 0.01 中实现了一个非常简单的文件系统层,只支持 Minix 文件系统。1992 年,Linux 引入了基于 inode 的 VFS 抽象,随后逐步加入了 ext 系列文件系统的支持。1994 年 Linux 1.0 发布时,VFS 已经具备了支持 ext2、procfs 等多种文件系统的能力。

此后的重要里程碑包括:

  • dcache 引入:Linux 2.1 时代引入目录项缓存(dcache),极大提升了路径查找性能。
  • 页缓存统一:Linux 2.4 时代将 buffer cache 与 page cache 统一,所有文件 I/O 都经过页缓存。
  • RCU 路径查找:Linux 2.6 引入 RCU 机制优化路径查找,使得并发打开文件的开销大幅下降。
  • FUSE 合入:2005 年 FUSE 被合入内核,允许在用户态实现文件系统,VFS 的扩展边界被进一步打开。
  • 多队列与异步 I/O:现代内核不断优化 VFS 的并发能力,引入 io_uring 等新接口,但 VFS 的基本架构保持稳定。

这段历史告诉我们:VFS 的核心抽象——superblock、inode、dentry、file——自 1990 年代确立以来几乎没有改变,这充分证明了该设计的生命力。后续的演进更多是在性能、可扩展性和新硬件适应方面做增强。

4. VFS 整体架构与对象模型

VFS 的架构可以概括为「四类对象、三张接口表、两层缓存」。下面逐一展开。

4.1 四类核心对象

超级块对象(super_block):代表一个已挂载的文件系统实例。它记录了文件系统类型、挂载选项、块大小、最大文件大小、根 inode 指针等信息。一个文件系统分区被挂载一次,就对应一个 super_block;如果同一个分区被挂载到多个位置(bind mount),则可能有多个 super_block 实例。

索引节点对象(inode):代表文件系统中的一个具体文件或目录。inode 保存了文件的元数据,如权限、大小、时间戳、所有者,以及指向数据块的指针。在 VFS 层,inode 是一个内存对象,它通过 i_sb 指针关联到所属的 super_block。

目录项对象(dentry):代表路径中的一个组成部分。dentry 是路径解析的产物,它把文件名和 inode 关联起来。需要注意的是,dentry 不是磁盘上的概念,而是纯粹的内存缓存对象,用于加速路径查找。一个文件可以有多个 dentry(硬链接),一个 dentry 对应一个文件名。

文件对象(file):代表一个已打开的文件实例。每次调用 open() 成功,内核都会创建一个 file 对象,它记录了当前文件偏移量、打开标志(如 O_RDONLY)以及对应的 file_operations 指针。同一个 inode 可以被多个进程同时打开,产生多个 file 对象,它们共享同一个 inode。

4.2 三张核心接口表

VFS 通过接口表实现多态。最核心的三张表是:

  • super_operations:文件系统级操作,如 alloc_inodewrite_inodeput_superstatfs
  • inode_operations:inode 级操作,如 createlookupmkdirunlinksymlink
  • file_operations:文件级操作,如 readwritellseekmmapiterate

此外还有 dentry_operations(目录项操作)、address_space_operations(地址空间操作,对接页缓存)等。每一张表本质上都是 C 语言中的「函数指针结构体」,这是 C 语言实现面向对象多态的经典手法。

4.3 两层缓存

VFS 层维护了全局的 dcache 和 inode cache。dcache 以哈希表加 LRU 链表的形式缓存 dentry,inode cache 缓存 inode 对象。这两层缓存的引入,使得绝大多数路径解析和元数据访问都不需要触达底层存储。我们将在第 14 节详细讨论这两层缓存的实现细节。

5. 核心数据结构源码解析

本节深入分析 VFS 核心数据结构在 Linux 内核源码中的定义。以下代码基于 Linux 5.x/6.x 内核,做了必要的简化和注释。

5.1 struct super_block

struct super_block {
    struct list_head    s_list;         /* 链入全局 super_blocks 链表 */
    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; /* 超级块操作表 */
    unsigned long       s_flags;        /* 挂载标志 */
    unsigned long       s_magic;        /* 文件系统魔数 */
    struct dentry       *s_root;        /* 根目录的 dentry */
    struct list_head    s_inodes;       /* 所有 inode 链表 */
    struct hlist_bl_head s_roots;       /* 所有挂载点的根 */
    void                *s_fs_info;     /* 文件系统私有数据 */
    struct address_space *s_bdev_mapping; /* 块设备映射 */
    ...
};

关键字段说明:s_type 指向文件系统类型,s_op 是操作表,s_root 指向根目录 dentry,s_fs_info 是具体文件系统(如 ext4)存放私有数据的位置。这种「通用结构 + 私有指针」的模式在 VFS 中反复出现。

5.2 struct inode

struct inode {
    umode_t             i_mode;         /* 文件类型与权限 */
    kuid_t              i_uid;          /* 所有者 */
    kgid_t              i_gid;          /* 所属组 */
    unsigned int        i_flags;        /* 文件系统标志 */
    const struct inode_operations *i_op; /* inode 操作表 */
    struct super_block  *i_sb;          /* 所属超级块 */
    struct address_space *i_mapping;    /* 地址空间(页缓存) */
    loff_t              i_size;         /* 文件大小 */
    struct timespec64   i_atime;        /* 访问时间 */
    struct timespec64   i_mtime;        /* 修改时间 */
    struct timespec64   i_ctime;        /* 状态改变时间 */
    unsigned long       i_ino;          /* inode 号 */
    dev_t               i_rdev;         /* 设备号 */
    struct list_head    i_sb_list;      /* 链入 sb 的 inode 链表 */
    union {
        struct hlist_head i_dentry;     /* 引用此 inode 的 dentry 列表 */
        struct callback_head i_rcu;
    };
    void                *i_private;     /* 文件系统私有数据 */
    ...
};

注意 i_mapping 字段:页缓存实际上挂在 inode 上,而不是 file 上。这意味着即使文件被关闭,只要 inode 还在缓存中,页缓存就仍然有效,下次打开时可以直接命中。

5.3 struct dentry

struct dentry {
    unsigned int        d_flags;        /* 目录项标志 */
    seqcount_spinlock_t d_seq;          /* 序列号,RCU 保护 */
    struct hlist_bl_node d_hash;        /* dcache 哈希链 */
    struct dentry       *d_parent;      /* 父目录 dentry */
    struct qstr         d_name;         /* 目录项名称 */
    struct inode        *d_inode;       /* 关联的 inode */
    unsigned char       d_iname[DNAME_INLINE_LEN]; /* 短名称内联存储 */
    const struct dentry_operations *d_op; /* 目录项操作表 */
    struct super_block  *d_sb;          /* 所属超级块 */
    struct list_head    d_child;        /* 父目录子项链表 */
    struct list_head    d_subdirs;      /* 子目录项链表头 */
    ...
};

dentry 的 d_name 是一个 qstr,包含名称字符串、长度和哈希值。短文件名直接内联存储在 d_iname 中,避免额外内存分配。dentry 通过 d_parentd_child 形成树形结构,与磁盘目录结构对应。

5.4 struct file

struct file {
    union {
        struct llist_node   f_llist;
        struct callback_head f_rcuhead;
    };
    struct path         f_path;         /* 文件路径(dentry + vfsmount) */
    struct inode        *f_inode;       /* 缓存 f_path.dentry->d_inode */
    const struct file_operations *f_op; /* 文件操作表 */
    unsigned int        f_flags;        /* 打开标志 */
    fmode_t             f_mode;         /* 访问模式 */
    struct mutex        f_pos_lock;     /* 偏移量锁 */
    loff_t              f_pos;          /* 当前偏移量 */
    atomic_long_t       f_count;        /* 引用计数 */
    void                *private_data;  /* 私有数据 */
    ...
};

每个进程的 task_struct 中有一个 files_struct,其中的 fdtable 把文件描述符(fd)映射到 file 对象。f_pos 是每次 read() / write() 会推进的偏移量。注意 f_pos 的并发保护:多个线程共享同一个 file 对象时,读写偏移量需要用 f_pos_lock 保护。

6. 文件系统类型与注册机制

VFS 通过 file_system_type 结构来描述一种文件系统类型,并维护一个全局链表 file_systems。内核启动时,内置文件系统通过 initcall 机制调用 register_filesystem() 完成注册;模块化的文件系统则在模块加载时注册,卸载时注销。

struct file_system_type {
    const char *name;      /* 文件系统名称,如 "ext4" */
    int fs_flags;          /* 文件系统标志 */
    struct dentry *(*mount)(struct file_system_type *, int,
                            const char *, void *);  /* 挂载函数 */
    void (*kill_sb)(struct super_block *);          /* 卸载函数 */
    struct module *owner;  /* 所属模块 */
    struct file_system_type *next;  /* 下一个文件系统类型 */
    struct list_head fs_supers;     /* 此类型的 super_block 列表 */
    ...
};
int register_filesystem(struct file_system_type *fs);
int unregister_filesystem(struct file_system_type *fs);

注册完成后,可以通过 /proc/filesystems 查看当前内核支持的文件系统列表。文件系统类型名称是全局唯一的,mount() 函数是核心回调,负责创建 super_block 和根 dentry。

现代内核还对 mount API 做了重构,引入了 fs_context 抽象。传统 mount() 回调正在逐步被 fs_context_operations 取代,目的是让挂载参数的解析更加灵活,并支持更干净的错误处理与日志。在 fs_context 模型中,挂载流程分为「参数解析」「上下文创建」「挂载完成」三个阶段,每个阶段都有独立的回调。

以下是一个简化版的文件系统注册示例,展示如何注册一个最小的伪文件系统:

static struct dentry *demo_mount(struct file_system_type *fs_type,
                                 int flags, const char *dev_name, void *data)
{
    /* 调用 mount_nodev 用于无设备文件系统 */
    return mount_nodev(fs_type, flags, data, demo_fill_super);
}
static struct file_system_type demo_fs_type = {
.name    = "demo",
.mount   = demo_mount,
.kill_sb = kill_litter_super,
};
static int __init demo_init(void)
{
return register_filesystem(&demo_fs_type);
}
static void __exit demo_exit(void)
{
unregister_filesystem(&demo_fs_type);
}
module_init(demo_init);
module_exit(demo_exit);

7. 挂载机制与命名空间

挂载(mount)是把一个文件系统实例接入全局目录树的过程。Linux 的挂载机制比最初的 VFS 设计要复杂得多,因为它需要支持挂载命名空间(mount namespace)、bind mount、递归挂载、挂载传播等高级特性。

7.1 传统挂载流程

当用户执行 mount -t ext4 /dev/sda1 /mnt/data 时,系统调用 mount() 会经过如下简化流程:

  1. 根据文件系统类型名称 "ext4" 在 file_systems 链表中查找对应的 file_system_type
  2. 调用该类型的 mount() 回调,由具体文件系统读取设备上的超级块信息,创建 super_block 对象和根 inode、根 dentry。
  3. 内核在目标挂载点 /mnt/data 上创建 struct mount 对象。struct mount 是 VFS 内部对挂载实例的描述,关联了 super_block、挂载点 dentry 和父挂载。
  4. 把新挂载插入到命名空间的挂载树中,完成路径可见性更新。

7.2 vfsmount 与 struct mount

历史上,VFS 用 vfsmount 结构表示一个挂载实例。后来为了支持命名空间等特性,内核对挂载管理进行了重构,引入了 struct mount 作为内部结构,vfsmount 成为其第一个字段的别名:

struct vfsmount {
    struct dentry *mnt_root;    /* 挂载点根 dentry */
    struct super_block *mnt_sb; /* 对应 super_block */
    int mnt_flags;              /* 挂载标志 */
};
struct mount {
struct vfsmount mnt;
struct mount mnt_parent;   / 父挂载 */
struct dentry mnt_mountpoint; / 挂载点 dentry /
struct list_head mnt_child; / 子挂载链表 */
...
};

引入 struct mount 的意义在于:同一个 super_block 可能对应多个挂载实例(bind mount 或多次挂载),每个挂载实例都可以有不同的挂载标志和命名空间归属。struct path 结构同时封装了 dentry 和 vfsmount,确保路径始终携带其所属的挂载上下文。

7.3 挂载命名空间

挂载命名空间是 Linux 容器技术的基石之一。每个进程都通过 task_struct 中的 nsproxy 指向一个挂载命名空间。在命名空间内执行挂载、卸载操作,不会影响其他命名空间中的挂载视图。Docker 容器就是利用挂载命名空间来隔离文件系统视图的。

挂载传播(mount propagation)控制挂载事件在命名空间之间的传播方向,包括 shared、slave、private、unbindable 四种模式。这些模式通过 mnt_flags 中的传播标志实现,pivot_root 和容器镜像分层都依赖这些机制。

7.4 挂载信息查看

内核通过 procfs 向用户态暴露挂载信息:/proc/self/mountinfo 提供了每个挂载点的详细属性,包括挂载 ID、父挂载 ID、传播类型、挂载选项等。以下是一个典型条目:

36 24 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw

从左到右依次是:挂载 ID、父挂载 ID、设备号、根、挂载点、挂载标志、传播类型、分隔符、文件系统类型、源设备、超级块级挂载选项。

8. 路径解析与 dcache 详解

路径解析(path lookup)是 VFS 中最频繁、最关键的操作。每次打开文件、访问目录、执行程序,都需要把字符串路径解析为具体的 dentry 和 inode。Linux 的路径解析机制经过多年优化,使用了 RCU、顺序锁和哈希表等并发技术,使其能够在多核系统上高效扩展。

8.1 dcache 的基本结构

dcache(dentry cache)是一个全局的哈希表,以「父 dentry 指针 + 文件名哈希」为键索引 dentry。查找时先计算文件名哈希,再在对应哈希桶中遍历比较,命中后递增引用计数返回。

static struct hlist_bl_head *d_hash(const struct dentry *parent,
                                    unsigned int hash)
{
    /* 根据父目录 dentry 和名称哈希选择哈希桶 */
    return dentry_hashtable + hash_32(hash ^ (unsigned long)parent,
                                      d_hash_shift);
}

dcache 同时维护 LRU 链表用于回收不活跃的 dentry。当内存紧张时,内核会从 LRU 尾部回收 dentry,释放对应的 inode 引用,腾出内存。被回收的 dentry 进入一个「负状态」(negative dentry),即 d_inode 为空,用于缓存「文件不存在」的查找结果,避免重复访问磁盘。

8.2 路径解析流程

以解析 /home/user/docs/report.txt 为例,path_lookupat() 的简化流程如下:

  1. / 开始,获取进程根目录或当前工作目录的 dentry(取决于路径是否绝对)。
  2. 逐个解析路径分量:"home"、"user"、"docs"、"report.txt"。
  3. 对每个分量,执行 link_path_walk():先在 dcache 中查找,命中则继续;未命中则调用父目录的 inode_operations->lookup() 让具体文件系统在磁盘上查找。
  4. 遇到符号链接时,根据是否允许跟随(LOOKUP_FOLLOW)决定是否递归解析链接目标,并防止循环链接。
  5. 到达最后一个分量后,返回对应的 dentry 和 vfsmount。

8.3 RCU 路径查找

传统路径查找需要持有 rename_lock 等锁,在高并发下会成为瓶颈。Linux 2.6 引入了 RCU-walk 模式:路径查找全程不持锁,通过 RCU 保证对象在查找期间不被释放,并通过 d_seq 序列号检测数据竞争。如果中途检测到目录被修改(序列号变化),则退化为持锁的 REF-walk 模式重新查找。

这种「乐观查找 + 失败回退」的策略在锁竞争激烈的场景下能带来数量级的性能提升。多核服务器上大量并发 open() 不同文件时,RCU-walk 的优势尤为明显。

8.4 negative dentry 与路径缓存

negative dentry 是 dcache 中一个容易被忽视但非常重要的设计。当查找一个不存在的文件时,内核不会立即销毁 dentry,而是将其标记为负状态并保留在缓存中。这样再次查找同一不存在的文件时可以直接返回 ENOENT,无需访问磁盘。在编译大型项目、扫描目录等频繁查找不存在文件的场景中,negative dentry 显著减少了磁盘 I/O。

负 dentry 也是一把双刃剑:如果应用反复查找大量不存在的文件,负 dentry 会占满 dcache。内核通过 /proc/sys/vm/vfs_cache_pressure 参数调节 dcache 和 inode cache 的回收压力,管理员可以根据工作负载调整此参数。

9. 文件打开与关闭的完整流程

本节以用户态 open() 调用为起点,追踪其在 VFS 层的完整执行路径。这是理解 VFS 协作机制的最佳切入点。

9.1 open() 系统调用总览

用户态调用 open("/home/user/file.txt", O_RDWR | O_CREAT, 0644) 后,经过 libc 封装进入内核 do_sys_openat2(),主要经历以下阶段:

  1. 路径解析:调用 path_lookupat() 解析路径,获得目标文件的 dentry 和 vfsmount。
  2. 权限检查:根据 inode 的权限位、进程的 uid/gid、ACL 等执行访问控制。
  3. inode 获取与创建:如果文件不存在且指定了 O_CREAT,调用父目录的 inode_operations->create() 创建新文件的 inode。
  4. file 对象分配:从 filp 缓存中分配 file 对象,设置 f_pathf_opf_flags 等字段。
  5. 调用 open 回调:执行 file_operations->open()(如果存在),让具体文件系统完成初始化,如分配 ext4 的 extent 状态等。
  6. 分配文件描述符:在进程的 fdtable 中找到最小可用 fd,把 file 对象指针填入。
  7. 返回 fd:将 fd 返回给用户态。

9.2 核心源码路径

以下是简化后的关键调用链:

SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename,
                int, flags, umode_t, mode)
{
    return do_sys_open(dfd, filename, flags, mode);
}
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_how how = build_open_how(flags, mode);
return do_sys_openat2(dfd, filename, &how);
}
static long do_sys_openat2(int dfd, const char __user *filename,
struct open_how *how)
{
struct file *file;
int fd;
file = do_filp_open(dfd, filename, how);
if (IS_ERR(file)) return PTR_ERR(file);
fd = get_unused_fd_flags(how->flags);
fd_install(fd, file);
return fd;
}

do_filp_open() 内部使用 struct nameidata 保存路径查找上下文,然后调用 path_openat() 完成实际打开。在 path_openat() 中,对于 O_CREAT 路径,会走「查找失败后创建」的 lookup_open() 分支。

9.3 close() 与引用计数

close() 相对简单:从 fdtable 中移除 fd,调用 filp_close(),进而调用 file_operations->release(),最后通过 fput() 递减 file 对象的引用计数。当引用计数归零时,file 对象被释放,同时 dput() 递减 dentry 引用,iput() 递减 inode 引用。

这里体现了 VFS 的引用计数层次:进程 fdtable 持有 file 引用,file 持有 dentry 引用,dentry 持有 inode 引用。每一层引用释放时都会触发下一层的引用递减,最终可能触发对象回收。inode 只有在引用计数归零且内存压力大时才真正从 icache 中移除。

9.4 文件描述符与文件对象的关系

需要澄清一个常见误区:文件描述符(fd)不是文件对象本身。fd 只是进程 files_structfdtable 数组的下标,数组元素是指向 file 对象的指针。多个进程打开同一文件,会有多个 file 对象指向同一个 inode;一个进程内多次打开同一文件,也会产生多个 file 对象,各有独立的文件偏移量。而 dup() 复制 fd 时,新旧 fd 共享同一个 file 对象,因此共享文件偏移量。

10. 文件读写路径与页缓存

文件读写是 VFS 的另一条核心路径。理解这条路径,需要同时理解页缓存(page cache)的工作原理。

10.1 读路径

用户态调用 read(fd, buf, count) 后,执行流程如下:

  1. ksys_read() 通过 fd 找到 file 对象,检查 f_mode 是否允许读。
  2. 调用 vfs_read(),它执行权限检查后调用 file_operations->read()
  3. 对于普通文件,read() 通常指向 generic_file_read_iter()
  4. generic_file_read_iter() 将目标偏移量对应的页映射到页缓存中。如果页已在缓存中(命中),直接拷贝数据到用户缓冲区。
  5. 如果页未命中,调用 address_space_operations->readpage()readpages() 从磁盘读取数据填充页,然后拷贝给用户。
  6. 可能需要预读(readahead):内核提前把相邻页载入缓存,减少后续磁盘 I/O 次数。

10.2 写路径

写操作比读操作更复杂,因为它涉及「写回延迟」(write-back deferral)策略:

  1. write() 调用链进入 generic_file_write_iter()
  2. 数据首先被拷贝到页缓存中对应的页,页被标记为 dirty。
  3. 系统调用立即返回。此时数据尚未写入磁盘。
  4. 内核的 writeback 线程(如 flusher / bdi_writeback)在满足条件时(如 dirty 页比例超过阈值、页驻留时间过长)批量调用 address_space_operations->writepages() 把 dirty 页刷写到磁盘。
  5. 用户调用 fsync() 时,内核强制把该文件的所有 dirty 页写回,并等待完成。

10.3 address_space 与 address_space_operations

address_space 是页缓存的管理结构,它把文件偏移量与内存页关联起来。每个 inode 的 i_mapping 指向自己的 address_space。关键操作表如下:

struct address_space_operations {
    int (*writepage)(struct page *page, struct writeback_control *wbc);
    int (*readpage)(struct file *, struct page *);
    int (*writepages)(struct address_space *, struct writeback_control *);
    int (*readpages)(struct file *, struct address_space *,
                     struct list_head *, unsigned nr_pages);
    int (*write_begin)(struct file *, struct address_space *mapping,
                       loff_t pos, unsigned len, unsigned flags,
                       struct page **pagep, void **fsdata);
    int (*write_end)(struct file *, struct address_space *mapping,
                     loff_t pos, unsigned len, unsigned copied,
                     struct page *page, void *fsdata);
    int (*readahead)(struct readahead_control *rac);
    ...
};

具体文件系统通过实现这些回调来定义数据如何与磁盘交互。例如 ext4 的 readpage() 会先查询 extent 树确定数据块位置,再触发块设备 I/O。

10.4 直接 I/O 与缓存 I/O

默认情况下,文件 I/O 经过页缓存,称为缓存 I/O(buffered I/O)。当应用以 O_DIRECT 标志打开文件时,读写绕过页缓存,数据直接在内核缓冲区与用户缓冲区之间传输,适用于数据库等自管理缓存的应用。

直接 I/O 的实现更复杂,需要处理对齐、并发与延迟分配等问题。VFS 通过 file_operations->read_iter/write_iter 中的 iocb->ki_flags 判断是否走直接 I/O 路径,具体文件系统也提供对应的 direct_IO 实现。

11. inode 操作与文件系统实现接口

具体文件系统接入 VFS 的核心工作,就是实现 inode_operationsfile_operations 这两张表。下面分别详解。

11.1 inode_operations 详解

struct inode_operations {
    struct dentry *(*lookup)(struct inode *, struct dentry *,
                             unsigned int);              /* 目录查找 */
    int (*create)(struct inode *, struct dentry *, umode_t, bool); /* 创建文件 */
    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 *, umode_t);  /* 创建目录 */
    int (*rmdir)(struct inode *, struct dentry *);       /* 删除目录 */
    int (*rename)(struct inode *, struct dentry *,
                  struct inode *, struct dentry *, unsigned int); /* 重命名 */
    int (*setattr)(struct dentry *, struct iattr *);     /* 设置属性 */
    int (*getattr)(const struct path *, struct kstat *, u32, unsigned int); /* 获取属性 */
    ssize_t (*listxattr)(struct dentry *, char *, size_t); /* 列出扩展属性 */
    ...
};

create() 为例,当内核需要在一个目录中创建新文件时,会获取父目录 inode 的 i_op->create() 并调用。ext4 的 ext4_create() 需要完成:分配新 inode 号、初始化 inode 磁盘结构、把新目录项写入目录块、更新目录日志等。这些细节对 VFS 层完全透明。

lookup() 是目录解析的回调。当 dcache 未命中时,VFS 调用父目录的 lookup(),具体文件系统在磁盘目录中搜索目标文件名,找到后实例化对应的 inode 并关联到 dentry。

11.2 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 (*iterate)(struct file *, struct dir_context *);  /* 读取目录 */
    int (*mmap)(struct file *, struct vm_area_struct *);  /* 内存映射 */
    int (*open)(struct inode *, struct file *);           /* 打开 */
    int (*flush)(struct file *, fl_owner_t id);           /* 关闭前冲刷 */
    int (*release)(struct inode *, struct file *);        /* 关闭 */
    int (*fsync)(struct file *, loff_t, loff_t, int);     /* 同步到磁盘 */
    ...
};

现代内核推荐使用 read_iter()write_iter() 替代传统的 read()write(),因为迭代器接口能更好地支持向量 I/O、异步 I/O 和跨页操作。iterate() 用于目录枚举,取代了早期的 readdir(),支持更灵活的用户态缓冲填充。

11.3 一个最小 VFS 文件实现

下面是一个简化示例,展示如何实现一个只读伪文件系统的 file_operations:

static ssize_t demo_read(struct file *file, char __user *buf,
                         size_t count, loff_t *ppos)
{
    const char *content = "Hello from VFS demo!\n";
    size_t len = strlen(content);
if (*ppos >= len) return 0;    /* 已读到末尾 */
if (count > len - *ppos) count = len - *ppos;
if (copy_to_user(buf, content + *ppos, count))
return -EFAULT;
*ppos += count;
return count;
}
static const struct file_operations demo_file_ops = {
.read = demo_read,
.iterate = generic_read_dir,
.llseek = generic_file_llseek,
};

这个例子展示了 VFS 抽象的精髓:具体文件系统只关心如何产生数据(demo_read 返回固定字符串),路径解析、权限、页交互等机制全部由 VFS 层处理。

12. VFS 与具体文件系统:以 ext4 为例

本节以 Linux 最常用的 ext4 文件系统为例,分析具体文件系统如何与 VFS 层协作。

12.1 ext4 的 VFS 挂载

ext4 的内核模块加载时注册 file_system_type

static struct file_system_type ext4_fs_type = {
    .owner    = THIS_MODULE,
    .name     = "ext4",
    .mount    = ext4_mount,
    .kill_sb  = kill_block_super,
    .fs_flags = FS_REQUIRES_DEV,
};
static int __init ext4_init_fs(void)
{
register_as_ext3();
register_as_ext2();
ext4_kset = kset_create_and_add("ext4", NULL, fs_kobj);
if (!ext4_kset)
    return -ENOMEM;
return register_filesystem(&ext4_fs_type);
}

注意 ext4 同时注册为 ext3 和 ext2,实现向后兼容。挂载时 ext4_mount() 会读取块设备上的超级块,验证魔数 EXT4_SUPER_MAGIC,并根据挂载选项初始化 ext4 私有数据结构 ext4_sb_info,将其指针保存到 super_block->s_fs_info

12.2 ext4 的 inode 创建

创建文件时,ext4 的 ext4_create() 核心流程为:

  1. 调用 ext4_new_inode() 分配新的 inode 号,初始化磁盘 inode 结构。
  2. 调用 ext4_add_nondir() 把新目录项写入父目录的数据块。
  3. 处理日志(jbd2):把元数据变更写入日志,保证崩溃一致性。
  4. 返回新建 dentry 给 VFS。

12.3 ext4 的数据读写

ext4 使用 extent 树管理文件数据块。读写时:

  1. ext4_file_read_iter() 调用 generic_file_read_iter() 走页缓存。
  2. 页未命中时,ext4_readpage() 通过 ext4_map_blocks() 查询 extent 树,把文件逻辑偏移转换为磁盘物理块号。
  3. 向块设备层提交 bio 请求,数据读入页缓存后返回。

写操作采用延迟分配(delayed allocation)策略:数据先写入页缓存,extent 的分配推迟到 writeback 阶段。这样可以把多个小写合并为大的连续分配,减少碎片和元数据开销。代价是 fsync() 的开销可能更高,因为需要在写回时同步分配 extent。

12.4 从 ext4 看 VFS 的价值

ext4 实现了数百个回调函数来满足 VFS 接口,但它不需要关心:路径如何解析、dcache 如何管理、文件描述符如何分配、权限位如何检查(大部分)、页缓存如何淘汰。这些通用逻辑全部由 VFS 提供。反过来,VFS 通过固定的接口约束,保证任意文件系统的接入都不会破坏内核的整体行为。

13. 特殊文件系统与 VFS 的边界

VFS 不仅服务于传统磁盘文件系统,还支撑了大量特殊文件系统。这些文件系统往往没有真实的磁盘存储,而是动态生成内容。理解它们有助于把握 VFS 抽象的边界。

13.1 procfs 与 sysfs

procfs 挂载在 /proc,以文件形式暴露内核运行状态。例如读取 /proc/cpuinfo 时,procfs 的 cpuinfo_read() 动态扫描 CPU 信息并格式化为文本返回。sysfs 挂载在 /sys,以目录树形式表达设备模型,每个目录和文件都对应内核中的一个 kobject。两者都实现了 VFS 接口,但数据在读取时才生成。

13.2 tmpfs 与 ramfs

tmpfs 和 ramfs 把文件存储在内存中。tmpfs 本质上是一个改进的 ramfs,它支持大小限制、swap 交换和更精细的内存管理。从 VFS 角度看,tmpfs 实现了 shmem 文件系统,其「磁盘」就是页缓存本身。写入 tmpfs 的数据直接变成 dirty 页,不经过块设备层。

13.3 FUSE:用户态文件系统

FUSE(Filesystem in Userspace)允许在用户态实现完整的文件系统。其工作原理是:用户态 FUSE 守护进程通过 /dev/fuse 字符设备与内核 FUSE 模块通信;当 VFS 收到针对 FUSE 挂载点的操作请求时,FUSE 模块把请求封装后转交给用户态守护进程处理,处理结果再返回 VFS 层。

# 使用 sshfs 挂载远程文件系统(FUSE 的典型应用)
sshfs user@remote:/home/user /mnt/remote
使用 s3fs 将 S3 存储桶挂载为本地目录
s3fs my-bucket /mnt/s3 -o passwd_file=~/.passwd-s3fs

FUSE 的出现验证了 VFS 接口设计的完整性:它把文件系统实现从内核态解放到用户态,虽然性能略逊于内核态实现,但开发效率和调试便利性大幅提升。

13.4 网络文件系统

NFS(Network File System)通过 VFS 接入内核,使得远程文件与本地文件对应用透明。NFS 的 inode 操作在本地缓存和远程服务器之间维护一致性,其 file 操作编码为 RPC 请求发送给 NFS 服务器。CIFS/SMB 也是类似的设计。网络文件系统对 VFS 提出了缓存一致性、超时重试、分布式锁等额外挑战,但 VFS 的核心接口仍然适用。

14. 缓存与内存回收机制

VFS 缓存的内存管理是系统性能的关键。Linux 把文件系统缓存与内存管理子系统深度整合,形成了著名的「页缓存即文件缓存」模型。

14.1 页缓存的统一模型

Linux 的一个经典设计是把页缓存(page cache)作为文件 I/O 和内存管理的统一缓存。当文件被读入内存时,数据所在的物理页同时被文件系统的 address_space 管理。如果应用通过 mmap() 将文件映射到虚拟地址空间,这些物理页也被页表引用。因此,同一份文件数据页同时服务于文件 I/O 和内存映射访问,避免了双重缓存。

14.2 内存回收与 shrinker

VFS 的 dcache 和 inode cache 注册了 shrinker 回调,参与内核内存回收。当系统内存紧张时,内存回收子系统会调用 shrinker 来缩减这些缓存:

  • dcache shrinker:扫描 LRU 链表,释放不活跃的 dentry。先尝试释放负 dentry 和未使用的 dentry,因为它们的回收成本最低。
  • inode shrinker:回收引用计数为零的 inode。回收前如果 inode 是 dirty 的(有未写回数据),需要先触发写回。

管理员可以通过 /proc/sys/vm/vfs_cache_pressure 控制 dcache 和 inode cache 相对页缓存的回收倾向。值越大,回收文件系统缓存的压力越大。

14.3 缓存命中率的观察

可以通过以下方式观察 VFS 缓存命中情况:

# 查看 slab 缓存中 dentry 和 inode 的使用量
slabtop -o | grep -E "dentry|inode_cache"
查看系统缓存内存占用
free -h
output 中的 buff/cache 列包含页缓存、dcache、inode cache 等
通过 /proc/sys/vm 查看相关参数
cat /proc/sys/vm/vfs_cache_pressure   # 默认 100
cat /proc/sys/vm/dirty_ratio          # dirty 页比例阈值

在数据库、Web 服务器等 I/O 密集型场景中,理解并优化这些参数可以显著提升性能。例如,对于频繁创建和删除大量小文件的构建系统,适当调低 vfs_cache_pressure 可以保留更多 dentry 缓存,减少磁盘目录查找。

15. VFS 的并发与锁机制

VFS 需要在多核并发环境下正确且高效地运行。本节分析 VFS 中的关键并发控制机制。

15.1 inode 锁

inode 内部有一把 i_rwsem 读写信号量,保护 inode 的元数据和数据一致性。read() 路径持有读锁,write() 路径持有写锁。这把锁允许并发读、串行写,在保证一致性的同时提供较好的并发性能。需要注意的是,不同 inode 之间的操作是天然并行的,锁只作用于单个 inode。

15.2 dentry 的 RCU 保护

dcache 的查找支持 RCU 模式,无需持有任何锁。dentry 的释放通过 call_rcu() 延迟到所有读临界区结束后执行。同时 d_seq 顺序锁用于检测在 RCU 遍历期间目录树是否发生了变更。这种设计使得路径查找在绝大多数情况下是无锁的。

15.3 rename 与目录锁

重命名是文件系统中最复杂的原子操作之一,因为它涉及两个目录的修改。传统实现使用全局 rename_lock 读锁保护路径遍历,结合 s_vfs_rename_mutex 保护同一文件系统内的 rename 串行化。现代内核逐步把锁粒度细化到目录级别,减少全局竞争。

15.4 引用计数

VFS 对象都有引用计数:inode 使用 i_count,dentry 使用 d_lockref,file 使用 f_count。引用计数采用原子操作,确保在并发环境下正确。不同对象之间的引用关系(file 引用 dentry,dentry 引用 inode)构成了生命周期管理的层次。

16. VFS 的调试与问题排查

了解 VFS 理论后,实战调试能力同样重要。本节介绍常用的 VFS 调试工具与技术。

16.1 常见问题类型

  • 打开文件失败:权限错误(EACCES)、文件不存在(ENOENT)、打开文件数超限(EMFILE/ENFILE)。
  • 读写性能差:缓存未命中、预读不足、dirty 页写回不及时。
  • 挂载失败:文件系统损坏、魔数不匹配、设备不存在。
  • 内存过高:dcache/inode cache 膨胀、页缓存占用过多。
  • 一致性错误:崩溃后文件数据与元数据不一致。

16.2 strace 追踪系统调用

# 追踪进程的文件系统相关系统调用
strace -e trace=file,desc -p 1234
追踪 open 调用并显示路径
strace -e trace=open,openat -o open_trace.log ./my_program
追踪文件 I/O 的字节数
strace -c -e trace=read,write ./my_program

strace 是观察 VFS 用户态入口的最佳工具。通过它可以确认应用究竟发出了哪些系统调用、返回了什么错误码、读写了多少数据。对于「应用说文件打不开」类问题,strace 往往能一步定位原因。

16.3 跟踪 VFS 内核事件

# 使用 trace-cmd 或 perf 追踪文件系统事件
trace-cmd record -e vfs_read -e vfs_write -e vfs_open ./my_program
trace-cmd report
使用 bpftrace 观察 open 调用
bpftrace -e 'kprobe:do_sys_openat2 { printf("%s opened: %s\n", comm, str(args->filename)); }'

trace-cmdbpftrace 利用内核 tracepoint 和 eBPF 技术,能够深入到 VFS 内部观察函数的调用参数和返回值,帮助定位性能瓶颈和异常路径。

16.4 文件系统一致性检查

# ext4 文件系统检查
fsck.ext4 -n /dev/sda1    # 只检查不修复
fsck.ext4 -f /dev/sda1    # 强制检查并修复
查看超级块信息
dumpe2fs -h /dev/sda1 | head -30
查看 inode 使用情况
df -i
tune2fs -l /dev/sda1 | grep -i "inode count"

当怀疑文件系统损坏时,fsck 是最后的修复手段。对于使用中的生产系统,应先在备用副本上验证。df -i 用于检查 inode 是否耗尽,这是「磁盘有空间但无法创建文件」类问题的常见原因。

16.5 性能观测

# 使用 iostat 观察设备 I/O
iostat -x 1
使用 pidstat 观察进程 I/O
pidstat -d 1
使用 filetop(BCC 工具)观察文件级 I/O
filetop -C

这些工具从不同层面观测 I/O:iostat 看设备吞吐和延迟,pidstat -d 看进程级 I/O 量,filetop 看具体文件的读写热度。结合使用可以快速定位是哪个进程、哪个文件、哪一层(VFS 缓存还是磁盘)出了问题。

17. VFS 与其他操作系统的对比

VFS 并非 Linux 独有,主流操作系统都有类似的抽象层。横向对比有助于理解设计取舍。

17.1 Solaris 与 SVR4 的 VFS/vnode

Solaris 的 VFS/vnode 架构是 Linux VFS 的重要源头。Solaris 把 vnode 作为所有文件对象的统一视图,vnode 操作表(vnodeops)定义了读写、属性等接口。Solaris 的 vnode 更强调与内核其他子系统的集成(如 VM 子系统),其 seg_map 缓存与 Linux 页缓存功能类似但实现差异较大。

17.2 Windows 的 I/O Manager 与文件系统过滤器

Windows 采用 I/O Manager 加文件系统驱动(FSD)的架构。应用通过 Win32 API 或 NT API 发起 I/O,I/O Manager 构造 IRP(I/O Request Packet)发送给文件系统驱动栈。Windows 的一大特色是强大的文件系统过滤器(filter driver)生态,允许在文件系统驱动上方或下方插入过滤器,实现加密、防病毒、备份等功能。Linux 的 eBPF 和 LSM 部分承担了类似职责,但生态和成熟度不同。

17.3 BSD 与 macOS

BSD 系的 VFS 设计与 Linux 类似但有细节差异。macOS 基于 Mach 内核加 BSD 层,文件系统通过 VFS KEXT(内核扩展)接入。现代 macOS 的 APFS 文件系统在 VFS 层之上实现了快照、克隆、加密等高级特性。

17.4 对比小结

尽管实现细节不同,主流系统的文件系统抽象都遵循「统一接口 + 具体实现」的基本模式。Linux VFS 的优势在于:开源带来的可研究性、页缓存统一模型的简洁高效、以及活跃社区带来的持续优化(如 RCU 路径查找、io_uring 集成)。对学习者而言,Linux VFS 的源码是理解文件系统抽象最有效的教材。

18. VFS 的演进趋势与前沿话题

VFS 并非停滞不前的旧技术。随着存储硬件和应用的演进,VFS 也在不断吸收新思想。

18.1 io_uring 与异步 I/O

传统 read() / write() 是同步阻塞调用。异步 I/O 接口 io_uring 在 Linux 5.1 引入后快速发展,它通过共享内存环形队列减少系统调用开销,实现对文件 I/O 的高效异步提交和完成。VFS 层为 io_uring 提供了 read_iter / write_iter 回调适配,使所有文件系统都能自动受益。io_uring 在高性能存储(如 NVMe SSD)上能够支撑每秒数百万次 I/O 操作。

18.2 DAX 与持久内存

持久内存(PMEM)的兴起推动了 DAX(Direct Access)模式的发展。DAX 允许应用直接访问持久内存上的文件数据,绕过页缓存和块设备层,大幅降低延迟。VFS 通过 IS_DAX(inode) 判断文件是否启用 DAX,走特殊的地址空间操作路径。ext4 和 XFS 都已支持 DAX。

18.3 网络存储与分布式文件系统

随着云存储和分布式存储的发展,VFS 在网络文件系统和分布式文件系统中继续扮演关键角色。Ceph、GlusterFS 等都通过内核客户端(基于 VFS)或 FUSE 提供 POSIX 兼容接口。VFS 的缓存策略与分布式一致性之间的平衡,是这些系统持续优化的重点。

18.4 形式化验证与安全性

文件系统是实现漏洞的高发区。近年来,用形式化方法验证 VFS 和具体文件系统的正确性成为研究热点。同时,VFS 与安全模块(LSM)、完整性验证(IMA/EVM)、文件加密(fscrypt)的集成不断加深,安全性在 VFS 演进中的权重持续提升。

19. 动手实践:构建一个最小 VFS 文件系统

纸上得来终觉浅。本节引导读者基于 Linux 内核模块构建一个最小可用的 VFS 文件系统。完整代码需配合内核源码树编译,这里给出关键部分。

19.1 工程准备

首先准备内核头文件和构建环境:

sudo apt install linux-headers-$(uname -r) build-essential
mkdir demo_fs && cd demo_fs

创建 Makefile:

obj-m += demo_fs.o
demo_fs-objs := demo.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

19.2 实现文件系统骨架

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/mount.h>
#include <linux/slab.h>
#define DEMO_MAGIC 0x64656d6f  /* "demo" */
/* 文件系统的根 inode */
static struct inode *demo_get_inode(struct super_block *sb,
const struct inode *dir,
umode_t mode, dev_t dev)
{
struct inode *inode = new_inode(sb);
if (!inode) return NULL;
inode-&gt;i_ino = get_next_ino();
inode_init_owner(&amp;init_user_ns, inode, dir, mode);
inode-&gt;i_atime = inode-&gt;i_mtime = inode-&gt;i_ctime = current_time(inode);
inode-&gt;i_mode = mode;
inode-&gt;i_sb = sb;
inode-&gt;i_op = &amp;simple_dir_inode_operations;
inode-&gt;i_fop = &amp;simple_dir_operations;
set_nlink(inode, 2);
return inode;
}
/* 申请超级块并创建根目录 */
static int demo_fill_super(struct super_block *sb, void *data, int silent)
{
struct inode *root_inode;
sb-&gt;s_blocksize = PAGE_SIZE;
sb-&gt;s_blocksize_bits = PAGE_SHIFT;
sb-&gt;s_magic = DEMO_MAGIC;
sb-&gt;s_op = &amp;simple_super_operations;
root_inode = demo_get_inode(sb, NULL, S_IFDIR | 0755, 0);
if (!root_inode) return -ENOMEM;
sb-&gt;s_root = d_make_root(root_inode);
if (!sb-&gt;s_root) {
iput(root_inode);
return -ENOMEM;
}
return 0;
}
/* 无设备文件系统的挂载入口 */
static struct dentry *demo_mount_fs(struct file_system_type *fs_type,
int flags, const char *dev_name,
void *data)
{
return mount_nodev(fs_type, flags, data, demo_fill_super);
}
static struct file_system_type demo_fs_type = {
.owner   = THIS_MODULE,
.name    = "demofs",
.mount   = demo_mount_fs,
.kill_sb = kill_litter_super,
.fs_flags = FS_USERNS_MOUNT,
};
static int __init demo_init(void)
{
int ret = register_filesystem(&demo_fs_type);
if (ret)
pr_err("demofs: register failed: %d\n", ret);
else
pr_info("demofs: registered successfully\n");
return ret;
}
static void __exit demo_exit(void)
{
unregister_filesystem(&demo_fs_type);
pr_info("demofs: unregistered\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("VFS Tutorial");
MODULE_DESCRIPTION("A minimal VFS demo filesystem");

19.3 编译、加载与验证

# 编译内核模块
make
加载模块
sudo insmod demo_fs.ko
检查是否注册成功
cat /proc/filesystems | grep demofs
挂载
sudo mkdir -p /mnt/demo
sudo mount -t demofs none /mnt/demo
查看挂载结果
mount | grep demofs
ls -la /mnt/demo/
卸载
sudo umount /mnt/demo
sudo rmmod demo_fs

这个最小文件系统成功挂载后,可以在挂载点看到空的根目录模型。虽然还不能创建文件,但它完整演示了「注册文件系统类型 → 填充超级块 → 创建根 inode → 接入 VFS」的流程。在此基础上扩展 inode_operationsfile_operations,即可逐步实现支持创建文件和读写的完整文件系统。

20. 总结与学习建议

VFS 是操作系统内核中最优雅的抽象之一。它用 super_block、inode、dentry、file 四个核心对象,加上以函数指针实现多态的接口表,成功统一了种类繁多的文件系统。VFS 同时整合了 dcache、inode cache 和页缓存三层缓存,并通过 RCU、读写信号量等并发机制支持多核扩展。对 VFS 的深入理解,既能帮助开发者写出更高效的文件 I/O 代码,也为内核驱动开发、分布式文件系统设计和存储性能调优奠定了坚实基础。

学习 VFS 的建议路径如下:

  • 第一步:通读内核源码 include/linux/fs.h 中的核心结构体定义,建立对象模型的直观认知。
  • 第二步:追踪 open()read() 的系统调用路径,在 fs/open.cfs/read_write.c 中阅读关键函数。
  • 第三步:研究 fs/namei.cfs/dcache.c,理解路径解析和目录项缓存。
  • 第四步:选择一个简单的文件系统(如 procfs 或 tmpfs)源码,观察它如何实现 VFS 接口。
  • 第五步:动手编写一个最小 VFS 文件系统内核模块,把理论转化为实践。
  • 第六步:结合性能观测工具,对真实工作负载进行 I/O 分析和调优。

VFS 的学习曲线陡峭但回报丰厚。建议在阅读源码时始终带着「这段代码扮演 VFS 的哪个角色」的问题意识,并积极动手实验。希望本文能成为你深入 VFS 世界的一份可靠地图。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐