3. VFS(虚拟文件系统)基础

VFS(Virtual File System,虚拟文件系统)是 Linux 内核中一个至关重要的抽象层。它为上层应用程序(如 open()、read()、write())提供了一套统一的系统调用接口,而将底层不同物理文件系统(如 ext4、F2FS、FUSE)的具体实现细节隐藏起来。在 Android 系统中,VFS 是连接用户空间与底层存储设备(如 UFS、eMMC)的桥梁,也是文件系统驱动开发者必须掌握的核心知识。

核心理解: VFS 的存在使得用户空间程序无需关心文件实际存储在何种介质或文件系统上。对于驱动开发者而言,VFS 定义了四个核心对象:超级块(super_block)、inode、dentry 和 file,它们共同构成了文件系统的“骨架”。

3.1 VFS 概念与架构

VFS 本质上是一个内核软件层,它定义了一组标准的文件系统操作接口(如 struct file_operations、struct inode_operations、struct super_operations)。任何想要被 Linux 支持的文件系统,都必须实现这些接口中的部分或全部函数指针。

下图展示了 VFS 在 Android 系统中的位置及其与各组件的关系:

VFS 在 Android 内核中的架构位置 VFS 在 Android 内核中的架构位置 用户空间 (User Space) Android Framework / Native 进程 (open, read, write 系统调用) VFS 虚拟文件系统层 (Virtual File System) 超级块 (super_block) | inode | dentry | file 结构体 统一接口: file_operations, inode_operations, super_operations 具体文件系统层 (Specific File Systems) ext4 | F2FS | FUSE | proc | sysfs | tmpfs 块设备层 / 存储驱动层 UFS 驱动 | eMMC 驱动 | IO 调度器
Android 特有场景: 在 Android 系统中,VFS 层之上运行着大量的 Binder 通信和 ART 虚拟机。VFS 的性能直接影响 App 的启动速度、文件读写效率以及 OTA 升级的稳定性。例如,f2fs 文件系统就是通过优化 VFS 层的缓存策略来提升随机读写性能的。

3.2 四大核心数据结构关系总览

VFS 的核心由四个主要数据结构组成,它们之间的关系可以用“文件路径查找”这一经典场景来串联:

数据结构 内核定义 核心作用 生命周期
super_block struct super_block 代表一个已挂载的文件系统实例,管理全局信息(块大小、操作函数表) 从挂载(mount)开始,到卸载(umount)结束
inode struct inode 代表一个文件或目录的元数据(权限、大小、时间戳、数据块指针) 从文件被创建或首次访问时加载,到 inode 被回收或文件被删除
dentry struct dentry 代表路径中的一个分量(目录项),负责路径解析和目录缓存 由路径查找创建,可被 dentry cache 缓存,直至被回收
file struct file 代表一个打开的文件描述符上下文(当前读写位置、标志、私有数据) 从 open() 系统调用开始,到 close() 系统调用结束

下图清晰地展示了这四个结构体在文件打开过程中的协作关系:

VFS 四大核心数据结构关系图 VFS 四大核心数据结构关系图(以 open() 为例) 进程 (Process) task_struct 文件描述符表 fd_array[] struct file f_pos (读写位置) f_flags (打开标志) f_op (操作函数表) 指向 struct dentry d_name (文件名) d_parent (父目录) f_path.dentry struct inode i_ino (inode 号) i_mode (权限/类型) d_inode struct super_block s_fs_info (私有数据) i_sb 路径查找流程 /data/app/ → 逐级查找 dentry 最终关联到 inode

3.3 超级块(super_block)

struct super_block 是 VFS 中最顶层的结构体,它代表一个已经挂载的文件系统实例。每个挂载点(如 /、/data、/system)都对应一个唯一的 super_block 对象。

3.3.1 关键字段

struct super_block {
    struct list_head    s_list;            // 全局超级块链表
    dev_t               s_dev;             // 块设备标识符
    unsigned long       s_blocksize;       // 文件系统块大小 (如 4096)
    loff_t              s_maxbytes;        // 文件最大大小
    struct file_system_type *s_type;       // 文件系统类型 (如 ext4_fs_type)
    struct super_operations *s_op;         // 超级块操作函数表
    struct dentry       *s_root;           // 挂载点的根 dentry
    void                *s_fs_info;        // 指向具体文件系统私有数据 (如 ext4_sb_info)
    // ... 其他字段
};

3.3.2 生命周期

  • 创建: 当用户执行 mount 系统调用时,内核通过 s_type->mount() 回调函数创建 super_block 实例。
  • 活跃: 在文件系统挂载期间,super_block 始终存在于内存中,管理着该文件系统的所有 inode 和 dentry。
  • 销毁: 当执行 umount 系统调用时,内核调用 s_op->kill_sb() 来释放 super_block 及其关联的所有资源。
驱动开发注意: 在实现自定义文件系统时,s_fs_info 是连接 VFS 与具体文件系统私有数据的关键。你通常需要在这里存储文件系统的超级块副本、日志句柄、设备信息等。务必在 kill_sb 中正确释放 s_fs_info 指向的内存,否则会导致内存泄漏。

3.4 inode 结构体

struct inode 是 VFS 中用于描述文件或目录元数据的核心结构体。每个文件或目录在磁盘上都有一个唯一的 inode 编号,在内存中则对应一个 inode 对象。

3.4.1 关键字段

struct inode {
    umode_t             i_mode;            // 文件类型与权限 (S_IFREG, S_IFDIR 等)
    uid_t               i_uid;             // 文件所有者 UID
    gid_t               i_gid;             // 文件所属组 GID
    loff_t              i_size;            // 文件大小 (字节)
    struct timespec64   i_atime;           // 最后访问时间
    struct timespec64   i_mtime;           // 最后修改时间
    struct timespec64   i_ctime;           // 最后状态改变时间
    unsigned long       i_ino;             // inode 编号
    struct super_block  *i_sb;             // 所属超级块
    struct address_space *i_mapping;       // 地址空间 (用于页缓存)
    struct inode_operations *i_op;         // inode 操作函数表
    const struct file_operations *i_fop;   // 默认文件操作函数表
    union {
        struct pipe_inode_info  *i_pipe;   // 管道
        struct block_device     *i_bdev;   // 块设备
        struct cdev             *i_cdev;   // 字符设备
    };
    // ... 其他字段
};

3.4.2 生命周期

阶段 触发条件 关键操作
分配 文件创建 (create) 或首次路径查找 alloc_inode() 分配内存,s_op->alloc_inode() 可被重载
读取 路径查找时 inode 不在缓存中 s_op->read_inode() 从磁盘读取元数据
使用 文件被打开或元数据被访问 通过 i_count 引用计数保护,i_lock 自旋锁保护字段
回收 引用计数降为 0,且没有 dentry 引用 destroy_inode() 释放内存,s_op->destroy_inode() 清理私有数据

3.5 dentry 结构体

struct dentry(目录项)是 VFS 路径查找的核心组件。它不直接对应磁盘上的数据结构,而是内存中用于加速路径解析的缓存实体。每个 dentry 代表路径中的一个分量,例如路径 /data/app/com.example 会分解为根 dentry、data dentry、app dentry 和 com.example dentry。

3.5.1 关键字段

struct dentry {
    struct qstr         d_name;            // 文件名 (快速比较用)
    struct dentry       *d_parent;         // 父目录 dentry
    struct inode        *d_inode;          // 关联的 inode (可为 NULL)
    struct list_head    d_child;           // 父目录的子 dentry 链表
    struct list_head    d_subdirs;         // 子 dentry 链表 (仅目录)
    struct dentry_operations *d_op;        // dentry 操作函数表
    unsigned int        d_flags;           // 标志位 (DCACHE_DISCONNECTED 等)
    // ... 其他字段
};

3.5.2 dentry 状态与缓存

dentry 在内存中有三种状态,理解这些状态对于调试文件系统问题至关重要:

状态 说明 d_inode 值 是否在 LRU 链表
被使用 (Used) dentry 正在被一个或多个 file 结构体或进程引用 非 NULL 否
未被使用 (Unused) dentry 仍然有效,但当前没有被引用,保留在缓存中 非 NULL 是 (可被回收)
负状态 (Negative) dentry 对应的文件或目录不存在(用于缓存“不存在”的结果) NULL 是 (可被回收)
性能优化: dentry cache(dcache)是 VFS 性能的关键。Android 系统中,频繁的文件路径查找(如 App 启动时的 dex2oat 操作)高度依赖 dcache 的命中率。如果 dcache 被频繁颠簸,会导致路径查找变慢,进而影响 App 启动速度。驱动开发者可以通过 /proc/sys/fs/dentry-state 监控 dcache 的使用情况。

3.6 file 结构体

struct file 代表一个进程打开的文件描述符。它与进程的文件描述符表(fd_array)一一对应,是进程与文件系统交互的直接接口。

3.6.1 关键字段

struct file {
    struct path         f_path;            // 包含 dentry 和 mount 的路径信息
    struct inode        *f_inode;          // 缓存 f_path.dentry->d_inode
    const struct file_operations *f_op;    // 文件操作函数表
    loff_t              f_pos;             // 当前读写位置 (文件偏移量)
    unsigned int        f_flags;           // 打开标志 (O_RDONLY, O_SYNC 等)
    fmode_t             f_mode;            // 打开模式 (FMODE_READ, FMODE_WRITE)
    void                *private_data;     // 私有数据指针 (驱动常用)
    struct address_space *f_mapping;       // 地址空间映射
    // ... 其他字段
};

3.6.2 生命周期

  1. 分配与初始化: 当进程调用 open() 系统调用时,内核通过 get_empty_filp() 分配 struct file 实例,然后调用 do_dentry_open() 进行初始化,最终调用 f_op->open() 回调。
  2. 使用: 进程通过 read()、write()、ioctl() 等系统调用操作文件,这些调用最终会通过 f_op 中的函数指针分发到具体文件系统的实现。
  3. 释放: 当所有进程关闭文件描述符(close())后,内核调用 fput() 递减引用计数。当引用计数降为 0 时,调用 f_op->release() 回调,然后释放 struct file 内存。
Android 驱动开发关键点: private_data 字段是驱动开发者最常用的工具。在 open() 回调中,你可以分配驱动私有数据结构(如设备上下文、缓冲区),并将其地址赋值给 private_data。在后续的 read()、write()、ioctl() 回调中,通过 filp->private_data 直接访问,无需全局变量。务必在 release() 中释放 private_data 指向的内存。

3.7 四者关系总结与生命周期对比

为了更直观地理解这四个结构体的生命周期差异,下表总结了它们的创建与销毁时机:

结构体 创建时机 销毁时机 生命周期跨度 典型引用关系
super_block 文件系统挂载 文件系统卸载 最长(与挂载点共存亡) 被所有 inode 和根 dentry 引用
inode 文件创建或首次访问 inode 缓存回收或文件删除 较长(可被多个 dentry 和 file 共享) 被 dentry 的 d_inode 和 file 的 f_inode 引用
dentry 路径查找时创建 dcache 回收或父 dentry 被释放 中等(可被缓存) 被 file 的 f_path.dentry 引用
file open() 系统调用 close() 系统调用 最短(与进程打开的文件描述符共存亡) 引用 dentry 和 inode

下图以时间轴的形式展示了这四个结构体在典型场景下的生命周期变化:

VFS 核心结构体生命周期时间轴 VFS 核心结构体生命周期时间轴 t1: mount t2: open() t3: close() t4: umount super_block inode dentry file 生命周期

3.8 驱动开发中的常见陷阱

  • 引用计数管理: 在 open() 回调中,如果增加了一个 inode 或 dentry 的引用计数,务必在 release() 中对应减少。否则会导致结构体无法被回收,造成内存泄漏。
  • 并发访问: struct file 的 f_pos 字段在多线程并发读写时可能被竞争。如果驱动需要维护独立的文件位置,建议使用 private_data 存储每个文件描述符的私有上下文。
  • dentry 的负缓存: 当查找一个不存在的文件时,VFS 会创建一个负 dentry(d_inode == NULL)。如果后续该文件被创建,需要手动使负 dentry 失效,否则路径查找会一直认为文件不存在。
  • super_block 的私有数据: 在 fill_super() 回调中分配的 s_fs_info,必须在 kill_sb() 中释放。这是驱动开发中最常见的内存泄漏来源之一。
调试建议: 在 Android 内核开发中,可以使用 /proc/slabinfo 查看 dentry、inode_cache、filp 等 slab 缓存的使用情况。如果某个缓存的对象数量持续增长而不下降,通常意味着存在引用计数泄漏。使用 trace_printk 或 ftrace 跟踪 dput()、iput()、fput() 的调用栈,可以快速定位泄漏点。