一个看似简单的问题

当你在Linux终端输入cp /home/user/file.txt /mnt/usb/backup/时,系统在做什么?源文件位于ext4格式的SSD上,目标目录在FAT32格式的U盘里。这两种文件系统的数据结构完全不同——ext4使用extent树管理块分配,FAT32依靠链式的FAT表。然而,cp命令对这种差异一无所知。

这背后是一个精妙的抽象层设计:虚拟文件系统(Virtual File System,VFS)。它让cplscat等用户态程序可以用同一套系统调用操作任何文件系统,从本地的ext4到网络的NFS,从内存中的tmpfs到内核信息的procfs。

但VFS的价值远不止"统一接口"。它是Linux"一切皆文件"哲学的技术基石,是容器技术实现文件隔离的关键设施,也是理解现代操作系统设计的核心窗口。

从 vnode 到 VFS:一段四十年技术演进史

问题的起源

1970年代末,Unix系统面临一个新挑战:如何支持多种文件系统?最初的Unix只支持一种文件系统类型,但随着网络文件系统(NFS)的出现和不同存储介质的需求,内核需要同时处理多种文件系统实现。

1986年,Sun Microsystems的Steve Kleiman在SunOS中提出了vnode/vfs架构[1]。这是操作系统设计史上的一次范式转变:将文件系统无关的操作从文件系统相关的实现中分离出来。vnode(虚拟节点)成为所有文件表示的统一抽象,每个具体文件系统提供自己的vnode操作向量。

timeline
    title VFS技术演进时间线
    1986 : Sun提出vnode/vfs架构<br/>SunOS实现多文件系统支持
    1989 : SVR4合并AT&T与Sun技术<br/>VFS/vnode成为Unix标准
    1991 : Linux诞生<br/>早期版本无VFS抽象
    1994 : Linux 1.0引入VFS层<br/>支持ext、minix、msdos
    1999 : Linux 2.2改进dentry缓存<br/>大幅提升路径解析性能
    2001 : Linux 2.4完善页缓存集成<br/>address_space抽象
    2003 : Linux 2.6引入mount namespace<br/>容器文件隔离基础
    2010年代 : OverlayFS、overlay成为容器标准<br/>VFS支持联合挂载

Linux的VFS实现

Linux并非简单地复制Sun的设计。Linus Torvalds在实现VFS时做出了关键决策:将Sun的单一vnode对象拆分为两个独立概念——inodedentry[2]。inode代表文件的元数据(不包含文件名),dentry代表目录项(将文件名与inode关联)。

这个拆分带来了显著优势:

  1. 硬链接的自然支持:多个dentry可以指向同一个inode,无需额外机制
  2. 路径解析优化:可以单独缓存目录项(dcache),加速路径查找
  3. 职责清晰:元数据操作与名称操作分离

Linux VFS的另一个创新是address_space抽象,它将文件内容与物理存储的映射关系独立出来,使页缓存(page cache)可以统一管理所有文件系统的缓存。

四大核心数据结构:VFS的建筑基石

VFS的优雅源于四个核心数据结构的精确定义与协作。理解它们,就理解了VFS的运行机制。

Superblock:文件系统的全貌视图

每个已挂载的文件系统在VFS中由一个super_block结构体表示。它存储了该文件系统的全局信息:

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; // 操作函数向量
    
    unsigned long       s_flags;        // 挂载标志
    unsigned long       s_magic;        // 魔数(用于识别文件系统类型)
    struct dentry       *s_root;        // 文件系统根目录的dentry
    struct rw_semaphore s_umount;       // 卸载信号量
    int                 s_count;        // 引用计数
    // ... 更多字段
};

super_operations定义了文件系统级别的操作向量,核心方法包括:

struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb);
    void (*destroy_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 (*put_super)(struct super_block *);
    int (*sync_fs)(struct super_block *sb, int wait);
    int (*statfs)(struct dentry *, struct kstatfs *);
    int (*remount_fs)(struct super_block *, int *, char *);
    // ... 更多操作
};

当VFS需要分配一个新inode时,它会调用sb->s_op->alloc_inode(sb),由具体文件系统实现内存分配和初始化。这种函数指针多态是VFS实现抽象的关键机制[3]。

Inode:文件的元数据载体

inode是文件系统中最重要的概念之一。它包含了文件除文件名和实际数据外的所有信息:

struct inode {
    umode_t             i_mode;     // 文件类型和权限
    unsigned short      i_opflags;  // 操作标志
    kuid_t              i_uid;      // 所有者用户ID
    kgid_t              i_gid;      // 所有者组ID
    unsigned int        i_flags;    // 文件系统标志
    
    const struct inode_operations *i_op;  // inode操作向量
    struct super_block  *i_sb;      // 所属superblock
    
    union {
        const unsigned int i_nlink; // 硬链接计数
        unsigned int __i_nlink;
    };
    
    loff_t              i_size;     // 文件大小(字节)
    struct timespec64   i_atime;    // 最后访问时间
    struct timespec64   i_mtime;    // 最后修改时间
    struct timespec64   i_ctime;    // inode最后变更时间
    
    spinlock_t          i_lock;     // 保护inode的自旋锁
    unsigned short      i_bytes;    // 已使用的字节数(不足一个块)
    u8                  i_blkbits;  // 块大小位数
    blkcnt_t            i_blocks;   // 分配的块数
    
    union {
        struct hlist_head   i_dentry;   // 指向此inode的dentry链表
        struct rcu_head     i_rcu;
    };
    
    const struct file_operations *i_fop; // 默认文件操作
    
    void                *i_private; // 文件系统私有数据
    // ... 更多字段
};

inode_operations定义了文件级别的操作:

struct inode_operations {
    struct dentry * (*lookup)(struct inode *, struct dentry *, unsigned 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 *);
    int (*rename)(struct mnt_idmap *, struct inode *, struct dentry *,
                  struct inode *, struct dentry *, unsigned int);
    int (*permission)(struct mnt_idmap *, struct inode *, int);
    int (*getattr)(struct mnt_idmap *, const struct path *, struct kstat *, u32, unsigned int);
    int (*setattr)(struct mnt_idmap *, struct dentry *, struct iattr *);
    // ... 更多操作
};

一个关键洞察:inode不包含文件名。文件名存储在dentry中,这使得一个inode可以被多个dentry引用——这就是硬链接的本质[4]。

Dentry:路径解析的中间产物

dentry(directory entry)是VFS独有的创新。它将文件名与inode关联起来,是路径解析过程中的核心数据结构:

struct dentry {
    unsigned int d_flags;           // dentry标志
    seqcount_spinlock_t d_seq;      // 序列计数(用于RCU路径查找)
    struct hlist_bl_node d_hash;    // 哈希表节点
    struct dentry *d_parent;        // 父目录dentry
    struct qstr d_name;             // 文件名(包含哈希值)
    
    struct inode *d_inode;          // 关联的inode
    unsigned char d_iname[DNAME_INLINE_LEN]; // 短文件名内联存储
    
    struct lockref d_lockref;       // 锁和引用计数
    const struct dentry_operations *d_op; // dentry操作向量
    struct super_block *d_sb;       // 所属superblock
    unsigned long d_time;           // 重新验证时间
    void *d_fsdata;                 // 文件系统私有数据
    
    union {
        struct list_head d_lru;     // LRU链表
        wait_queue_head_t *d_wait;  // 等待队列
    };
    struct list_head d_child;       // 在父目录的子项链表中
    struct list_head d_subdirs;     // 子目录链表
    // ... 更多字段
};

dentry的核心价值在于缓存。当路径/home/user/file.txt被解析后,每个路径分量(homeuserfile.txt)都会生成一个dentry并缓存。下次访问/home/user/other.txt时,只需从user的dentry开始解析,大幅减少磁盘I/O[5]。

dentry_operations定义了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_iput)(struct dentry *, struct inode *);
    char *(*d_dname)(struct dentry *, char *, int);
    // ... 更多操作
};

对于网络文件系统(如NFS),d_revalidate尤其重要——它检查缓存的dentry是否仍然有效,因为远程文件系统可能在客户端不知情的情况下被修改[6]。

File:进程视角的文件视图

file结构体代表进程打开的一个文件实例。它是进程与inode之间的桥梁:

struct file {
    union {
        struct llist_node   fu_llist;
        struct rcu_head     fu_rcuhead;
    } f_u;
    struct path             f_path;     // 文件路径(包含dentry和挂载点)
    struct inode            *f_inode;   // 关联的inode
    const struct file_operations *f_op; // 文件操作向量
    
    spinlock_t              f_lock;     // 保护file的自旋锁
    atomic_long_t           f_count;    // 引用计数
    unsigned int            f_flags;    // 打开标志(O_RDONLY, O_WRONLY等)
    fmode_t                 f_mode;     // 访问模式
    struct mutex            f_pos_lock; // 保护文件偏移量
    loff_t                  f_pos;      // 当前文件偏移量
    struct fown_struct      f_owner;    // 异步I/O所有者
    
    // 预读状态
    struct file_ra_state    f_ra;
    
    void                    *private_data; // 私有数据(如TTY、管道等使用)
    // ... 更多字段
};

file_operations是用户最熟悉的操作集合,对应read、write、ioctl等系统调用:

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 (*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 *);
    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 datasync);
    int (*fasync)(int, struct file *, int);
    int (*lock)(struct file *, int, struct file_lock *);
    // ... 更多操作
};

一个重要区别:同一个inode可以被多个file结构体引用。每次open()调用都会创建新的file结构体,拥有独立的文件偏移量。这就是为什么父子进程可以各自独立地读写同一个文件[7]。

graph TB
    subgraph "进程视角"
        P1[进程1]
        P2[进程2]
    end
    
    subgraph "VFS核心对象"
        F1[file结构体<br/>f_pos=100]
        F2[file结构体<br/>f_pos=500]
        D1[dentry<br/>name: 'log.txt']
        I1[inode<br/>i_ino=12345<br/>i_size=1024]
    end
    
    subgraph "具体文件系统"
        EXT4[ext4磁盘布局<br/>inode表, 数据块]
    end
    
    P1 -->|fd=3| F1
    P2 -->|fd=5| F2
    F1 --> D1
    F2 --> D1
    D1 --> I1
    I1 --> EXT4
    
    style P1 fill:#e1f5fe
    style P2 fill:#e1f5fe
    style F1 fill:#fff3e0
    style F2 fill:#fff3e0
    style D1 fill:#f3e5f5
    style I1 fill:#e8f5e9

文件系统注册与挂载:VFS的动态扩展机制

register_filesystem:声明存在

一个新的文件系统要被VFS识别,首先需要调用register_filesystem()进行注册:

struct file_system_type {
    const char *name;               // 文件系统名称
    int fs_flags;                   // 标志位
#define FS_REQUIRES_DEV     1       // 需要块设备
#define FS_BINARY_MOUNTDATA 2       // 二进制挂载数据
#define FS_HAS_SUBTYPE      4       // 支持子类型
#define FS_USERNS_MOUNT     8       // 可在用户命名空间挂载
    
    // 初始化文件系统上下文
    int (*init_fs_context)(struct fs_context *);
    
    // 参数规范
    const struct fs_parameter_spec *parameters;
    
    // 销毁superblock
    void (*kill_sb)(struct super_block *);
    
    struct module *owner;           // 所属模块
    struct file_system_type *next;  // 链表下一项
    struct hlist_head fs_supers;    // 该类型的所有superblock
    
    // ... 其他字段
};

注册过程将file_system_type加入内核的单向链表。当用户执行mount -t ext4 /dev/sda1 /mnt时,VFS遍历链表,按名称找到ext4file_system_type结构体[8]。

挂载流程:从命令到内核状态

现代Linux(5.1+内核)使用fs_context API进行文件系统挂载,这是一个更灵活、更安全的替代旧mount()系统调用的方案:

sequenceDiagram
    participant User as 用户空间
    participant VFS as VFS层
    participant FS as 文件系统驱动
    participant Disk as 存储设备
    
    User->>VFS: mount -t ext4 /dev/sda1 /mnt
    VFS->>VFS: 查找file_system_type
    VFS->>FS: 调用init_fs_context()
    FS-->>VFS: 返回fs_context
    VFS->>FS: 解析挂载选项
    VFS->>FS: 调用get_tree()
    FS->>Disk: 读取磁盘超级块
    Disk-->>FS: 返回超级块数据
    FS->>FS: 验证魔数、检查完整性
    FS->>FS: 创建super_block结构体
    FS->>FS: 创建根目录dentry和inode
    FS-->>VFS: 返回super_block
    VFS->>VFS: 创建mount结构体
    VFS->>VFS: 挂载到目标挂载点
    VFS-->>User: 返回成功

挂载完成后,内核中会创建几个关键数据结构:

  1. struct mount:表示一个挂载实例
  2. struct mountpoint:表示挂载点目录
  3. struct vfsmount:嵌入在mount中,包含文件系统信息
struct mount {
    struct hlist_node m_hash;       // 哈希表节点
    struct mount *m_parent;         // 父挂载点
    struct dentry *m_mountpoint;    // 挂载点dentry
    struct vfsmount mnt;            // 嵌入的vfsmount
    union {
        struct rcu_head mnt_rcu;
        struct llist_node mnt_llist;
    };
    // ... 更多字段
};

struct vfsmount {
    struct dentry *mnt_root;        // 文件系统根目录
    struct super_block *mnt_sb;     // 关联的superblock
    int mnt_flags;                  // 挂载标志
    // ... 其他字段
};

Mount Namespace:容器技术的文件隔离基础

Linux 2.4.19引入了mount namespace,这是容器技术实现文件系统隔离的关键机制。每个mount namespace拥有独立的挂载点视图[9]。

挂载传播(mount propagation)定义了挂载事件如何在namespace之间传递:

传播类型 标志 行为
MS_SHARED 共享 接收和发送挂载事件
MS_PRIVATE 私有 不接收也不发送事件
MS_SLAVE 从属 只接收事件,不发送
MS_UNBINDABLE 不可绑定 私有 + 禁止bind mount

这些传播类型使得Docker等容器可以拥有完全独立的文件系统视图,同时又能共享某些挂载点(如/usr)以节省内存[10]。

路径解析:从字符串到内核对象

当用户调用open("/home/user/file.txt", O_RDONLY)时,VFS需要将字符串路径转换为内核对象。这个过程涉及复杂的缓存查找和目录遍历。

路径解析算法

Linux使用rcu-walkref-walk两种模式进行路径解析:

  1. rcu-walk:无锁、高性能模式,适用于大部分只读场景
  2. ref-walk:加锁、安全模式,用于需要修改或遇到复杂情况时
flowchart TD
    A[开始路径解析] --> B{检查dcache}
    B -->|命中| C[直接获取dentry]
    B -->|未命中| D[调用lookup]
    D --> E{需要磁盘I/O?}
    E -->|是| F[读取目录数据]
    E -->|否| G[从inode中查找]
    F --> H[创建新dentry]
    G --> H
    H --> I[加入dcache]
    C --> J{路径结束?}
    I --> J
    J -->|否| K[继续下一分量]
    K --> B
    J -->|是| L[返回最终dentry]
    
    style A fill:#e3f2fd
    style L fill:#e8f5e9
    style B fill:#fff3e0
    style F fill:#fce4ec

Dentry缓存(dcache)

dcache是VFS最重要的性能优化之一。它使用哈希表+LRU链表的结构:

// dentry哈希表
static struct hlist_bl_head *dentry_hashtable;

// dentry哈希函数
static inline struct hlist_bl_head *d_hash(unsigned int hash)
{
    return dentry_hashtable + (hash & d_hash_mask);
}

每个dentry根据其父目录指针和文件名哈希值进行索引。查找时间复杂度接近$O(1)$。

dcache还与**inode缓存(icache)**协同工作。当dentry被访问时,其关联的inode也会被固定在内存中。内核参数vfs_cache_pressure控制dcache和icache的回收倾向[11]:

  • 值=100:默认行为
  • 值>100:更积极地回收dentry和inode缓存
  • 值<100:保留更多缓存

快速路径与慢速路径

路径解析遵循"快速路径优先"原则:

// 简化的路径解析逻辑
static int link_path_walk(const char *name, struct nameidata *nd)
{
    while (*name) {
        // 1. 提取下一个路径分量
        const char *this = name;
        name = getname_component(name);
        
        // 2. 快速路径:dcache查找
        dentry = d_lookup(nd->path.dentry, &this);
        if (dentry) {
            // 检查有效性
            if (d_revalidate(dentry, nd->flags))
                goto found;
        }
        
        // 3. 慢速路径:调用文件系统lookup
        dentry = d_alloc(nd->path.dentry, &this);
        err = inode->i_op->lookup(inode, dentry, nd->flags);
        
found:
        // 4. 处理符号链接
        if (dentry->d_inode && S_ISLNK(dentry->d_inode->i_mode)) {
            // 跟随符号链接
        }
        
        // 5. 移动到下一级
        nd->path.dentry = dentry;
    }
    return 0;
}

实际实现更复杂,涉及符号链接循环检测、权限检查、自动挂载触发等。

文件操作:系统调用的内核旅程

open():创建文件对象

open()系统调用是所有文件操作的起点:

// 简化的open实现
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
    // 1. 从用户空间复制文件名
    struct open_flags op;
    tmp = getname(filename);
    
    // 2. 解析路径,获取dentry和inode
    path = do_filp_open(dfd, tmp, &op);
    
    // 3. 创建file结构体
    struct file *f = alloc_file(&path, flags, fop);
    
    // 4. 调用文件系统的open方法
    if (f->f_op->open)
        f->f_op->open(inode, f);
    
    // 5. 分配文件描述符
    fd = get_unused_fd_flags(flags);
    fd_install(fd, f);
    
    return fd;
}

read()/write():数据流动的核心

VFS的读写操作涉及复杂的缓存管理:

// 简化的read实现
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    ssize_t ret;
    
    // 1. 权限检查
    if (!(file->f_mode & FMODE_READ))
        return -EBADF;
    
    // 2. 调用文件系统的read或read_iter
    if (file->f_op->read)
        ret = file->f_op->read(file, buf, count, pos);
    else if (file->f_op->read_iter)
        ret = new_sync_read(file, buf, count, pos);
    else
        ret = -EINVAL;
    
    // 3. 更新访问时间
    if (ret > 0)
        add_rchar(current, ret);
    
    return ret;
}

对于普通文件,数据流经页缓存(page cache)

flowchart LR
    A[用户缓冲区] <-->|copy_to_user<br/>copy_from_user| B[内核页缓存]
    B <-->|address_space_operations| C[文件系统]
    C <-->|bio请求| D[块设备层]
    D <--> E[存储设备]
    
    subgraph "缓存命中场景"
        A <--> B
    end
    
    subgraph "缓存未命中场景"
        A <--> B
        B <--> C
        C <--> D
        D <--> E
    end

Address Space:文件内容与页缓存的桥梁

address_space是连接inode与页缓存的抽象:

struct address_space {
    struct inode *host;              // 所属inode
    struct xarray i_pages;           // 页缓存XArray
    // 按页面索引组织的页缓存
    // 支持高效的范围查找和迭代
    
    unsigned long nrpages;           // 缓存页面数
    unsigned long nrexceptional;     // 特殊条目数(如DAX、hole)
    const struct address_space_operations *a_ops; // 操作向量
    
    unsigned long flags;             // 标志位
    errseq_t wb_err;                 // 回写错误
    spinlock_t private_lock;         // 私有数据锁
    struct list_head private_list;   // 私有数据链表
    // ... 更多字段
};

struct address_space_operations {
    // 读取页面
    int (*readpage)(struct file *, struct page *);
    int (*readpages)(struct file *filp, struct address_space *mapping,
                     struct list_head *pages, unsigned nr_pages);
    
    // 写入页面
    int (*writepage)(struct page *page, struct writeback_control *wbc);
    int (*writepages)(struct address_space *, struct writeback_control *);
    
    // 脏页标记
    int (*set_page_dirty)(struct page *page);
    
    // 直接I/O
    ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);
    
    // 空洞处理
    int (*migratepage)(struct address_space *, struct page *, struct page *, enum migrate_mode);
    // ... 更多操作
};

当调用read()时,VFS首先检查页缓存。如果页面不存在,调用readpage()从磁盘读取。这个过程是透明的,用户态程序无需关心数据是否在缓存中[12]。

预读机制:预测性的性能优化

Linux实现了智能预读(readahead)机制,预测即将访问的页面并提前加载:

struct file_ra_state {
    pgoff_t start;              // 当前窗口起始
    unsigned int size;          // 当前窗口大小
    unsigned int async_size;    // 异步预读阈值
    unsigned int ra_pages;      // 最大预读页面数
    unsigned int mmap_miss;     // mmap未命中计数
    loff_t prev_pos;            // 前一次读取位置
};

预读算法基于两个观察:

  1. 空间局部性:访问某位置后,大概率会访问邻近位置
  2. 顺序检测:检测顺序读取模式,增大预读窗口

当检测到顺序读取时,预读窗口可以从4KB增长到256KB甚至更大。

特殊文件系统:VFS的扩展应用

VFS的抽象能力不仅用于传统存储设备,还支持一系列特殊文件系统。

procfs:进程信息的窗口

/proc文件系统不对应任何物理存储,而是在读取时动态生成内容:

static const struct file_operations proc_pid_cmdline_ops = {
    .read       = proc_pid_cmdline_read,
    .llseek     = generic_file_llseek,
    .read_iter  = generic_file_read_iter,
};

static ssize_t proc_pid_cmdline_read(struct file *file, char __user *buf,
                                      size_t count, loff_t *pos)
{
    // 从进程的内存布局中读取命令行参数
    struct task_struct *task = get_proc_task(file_inode(file));
    struct mm_struct *mm = get_task_mm(task);
    
    // 访问进程内存,复制到用户缓冲区
    // ...
}

procfs展示了VFS的"一切皆文件"理念:进程信息、内核参数、设备状态都可以通过文件操作访问[13]。

sysfs:设备模型的用户空间接口

/sys文件系统与内核的kobject/kset机制紧密关联:

// sysfs文件创建
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr)
{
    // 创建表示内核对象属性的文件
    // 读写操作会调用attribute的show/store方法
}

// 属性操作示例
static ssize_t cpu_show(struct device *dev, 
                        struct device_attribute *attr, char *buf)
{
    return sprintf(buf, "%u\n", cpumask_weight(cpu_online_mask));
}

static DEVICE_ATTR(online, 0444, cpu_show, NULL);

sysfs使得设备驱动的配置和状态检查可以通过简单的文件读写完成。

tmpfs:内存中的文件系统

tmpfs将文件存储在内存中,兼具速度和易失性:

static struct file_system_type tmpfs_fs_type = {
    .name       = "tmpfs",
    .init_fs_context = shmem_init_fs_context,
    .parameters = shmem_fs_parameters,
    .kill_sb    = kill_litter_super,
    .fs_flags   = FS_USERNS_MOUNT,
};

tmpfs的inode和页面都存储在内存中,没有持久化开销。/dev/shm是默认的tmpfs挂载点,用于POSIX共享内存[14]。

OverlayFS:容器镜像的基石

OverlayFS是联合挂载(union mount)的现代实现,广泛用于容器技术:

graph TB
    subgraph "OverlayFS结构"
        U[upper层<br/>可读写]
        L1[lower层1<br/>只读]
        L2[lower层2<br/>只读]
        M[Merged视图<br/>用户看到的合并结果]
    end
    
    U --> M
    L1 --> M
    L2 --> M
    
    subgraph "写入操作"
        W[写入新文件] --> U
        C[修改现有文件] -->|copy-up| U
    end
    
    subgraph "读取操作"
        R[读取文件] -->|先查upper| U
        R -->|再查lower| L1
        R --> L2
    end

OverlayFS的核心概念:

  • upper层:可读写层,存储修改和新文件
  • lower层:只读层,可以有多个
  • copy-up:修改lower层文件时,先复制到upper层

这种设计使得容器可以共享只读的基础镜像,同时拥有独立的可写层,极大节省存储空间[15]。

性能优化:VFS的加速机制

Dentry缓存优化

dcache使用了多项优化技术:

  1. 无锁查找(RCU):使用RCU(Read-Copy-Update)实现无锁读取
  2. 序列计数:快速检测并发修改
  3. 热路径优化:高频访问的路径分量常驻内存
// RCU查找示例
struct dentry *d_lookup(const struct dentry *parent, const struct qstr *name)
{
    unsigned int hash = name->hash;
    struct hlist_bl_head *b = d_hash(hash);
    
    // RCU临界区
    rcu_read_lock();
    hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
        if (dentry->d_parent == parent &&
            dentry->d_name.hash == hash &&
            d_unhashed(dentry) == 0) {
            // 名称比较
            if (!dentry->d_op || !dentry->d_op->d_compare ||
                dentry->d_op->d_compare(dentry, dentry->d_name.len,
                                       name->name, name) == 0)
                goto found;
        }
    }
    rcu_read_unlock();
    return NULL;
found:
    // 增加引用计数
    if (!lockref_get_not_dead(&dentry->d_lockref))
        dentry = NULL;
    rcu_read_unlock();
    return dentry;
}

页缓存与回写机制

Linux使用回写机制异步将脏页写入磁盘:

// 回写相关参数(/proc/sys/vm/)
dirty_ratio              = 20    // 脏页占比上限(%)
dirty_background_ratio   = 10    // 后台回写触发阈值(%)
dirty_writeback_centisecs = 500  // 回写线程唤醒间隔(厘秒)
dirty_expire_centisecs   = 3000  // 脏页过期时间(厘秒)

回写机制的演进历史:

时期 机制 特点
早期 bdflush 单线程,全局锁
2.6内核 pdflush 多线程,动态创建
现代 per-BDI flusher 每设备一个线程,减少竞争

per-BDI(Backing Device Info)flusher解决了多设备并发回写的瓶颈问题[16]。

目录锁优化

目录操作(创建、删除、重命名)需要复杂的锁定协议:

// 目录锁规则(简化版)
// 1. 获取父目录的i_rwsem
// 2. 获取目标文件/目录的i_rwsem(如果适用)
// 3. 对于跨目录重命名,需要获取s_vfs_rename_mutex

// 锁获取顺序(避免死锁):
// - 父目录 -> 子目录
// - 对于同层inode,按地址顺序锁定
// - 全局s_vfs_rename_mutex在最后

这种多层次的锁定策略确保了正确性的同时最大化并发度。

安全机制:VFS与LSM的集成

VFS是Linux安全模块(LSM)的主要接入点。每个关键操作都会调用LSM钩子:

// 安全钩子示例
int vfs_permission(struct mnt_idmap *idmap, struct inode *inode, int mask)
{
    // 1. 传统权限检查
    int ret = inode_permission(idmap, inode, mask);
    if (ret)
        return ret;
    
    // 2. LSM钩子
    return security_inode_permission(inode, mask);
}

LSM框架支持多种安全策略:

  • SELinux:基于标签的强制访问控制
  • AppArmor:基于路径的访问控制
  • Smack:简化的强制访问控制
  • TOMOYO:基于路径名的策略[17]

VFS还实现了capabilities系统,允许细粒度的权限控制:

// 文件capability检查
int cap_inode_need_killpriv(struct dentry *dentry)
{
    struct inode *inode = d_backing_inode(dentry);
    
    // 检查文件是否设置了setuid/setgid位
    if (inode->i_mode & (S_ISUID | S_ISGID))
        return 1;
    
    // 检查扩展属性中的capabilities
    if (!cap_inode_getsecurity(dentry, XATTR_NAME_CAPS, NULL, 0, true))
        return 0;
    
    return 1;
}

文件锁定:协作与强制

Linux支持两种文件锁定模型:

劝告锁(Advisory Locking)

默认模式,需要进程主动配合:

// fcntl锁
struct flock fl = {
    .l_type = F_WRLCK,    // 写锁
    .l_whence = SEEK_SET,
    .l_start = 0,
    .l_len = 0,           // 锁定整个文件
};

fcntl(fd, F_SETLK, &fl);

强制锁(Mandatory Locking)

强制锁由内核强制执行,但需要特殊配置:

# 在挂载时启用
mount -o mand /dev/sda1 /mnt

# 设置强制锁标志(关闭组执行位 + 设置setgid位)
chmod g-s,g+x file.txt

强制锁在现代系统中很少使用,因为它可能破坏应用程序的兼容性[18]。

现代挑战与演进方向

大规模并发

随着CPU核心数增长,VFS面临扩展性挑战:

  1. 全局dcache锁:早期dcache使用全局锁,现代内核使用分区锁
  2. inode分配竞争:引入per-cpu缓存减少锁竞争
  3. 路径解析优化:rcu-walk实现无锁查找

持久内存支持

NVDIMM等持久内存技术带来新挑战:

  • DAX(Direct Access):绕过页缓存直接访问
  • 一致性保证:崩溃后数据的完整性
  • 同步语义:何时保证数据持久化
// DAX相关的address_space_operations
static const struct address_space_operations ext4_dax_aops = {
    .direct_IO         = ext4_dax_direct_IO,
    .set_page_dirty    = __set_page_dirty_no_writeback,
    // DAX不需要传统的回写
};

io_uring与异步I/O

io_uring是Linux 5.1引入的新异步I/O机制,对VFS提出新要求:

  • 支持完全非阻塞的操作链
  • 减少系统调用开销
  • 与VFS缓存协同工作

小结

Linux VFS是一个历经三十年演进的精妙设计。它通过superblock、inode、dentry、file四个核心抽象,以及函数指针多态机制,实现了"一切皆文件"的Unix哲学。

VFS的价值不仅在于统一接口,更在于:

  1. 可扩展性:新文件系统可以无缝接入
  2. 性能优化:dcache、页缓存等机制对所有文件系统生效
  3. 安全隔离:mount namespace实现了容器文件隔离
  4. 系统可观测:procfs、sysfs提供了内核信息的标准访问方式

从Sun的vnode架构到现代Linux的VFS实现,这一设计模式已经成为操作系统的经典范式。理解VFS,是深入Linux内核设计的关键一步。


参考文献

[1] Kleiman, S. (1986). “Vnodes: An Architecture for Multiple File System Types in Sun UNIX”. USENIX Summer Conference. https://www.cs.fsu.edu/~awang/courses/cop5611_s2022/vnode.pdf

[2] Pate, S. (2003). “UNIX Filesystems: Evolution, Design, and Implementation”. Wiley. https://www.oreilly.com/library/view/unix-filesystems-evolution/9780471456759/chap07-sec005.html

[3] Linux Kernel Documentation. “Overview of the Linux Virtual File System”. https://docs.kernel.org/filesystems/vfs.html

[4] Star Lab. (2022). “Introduction to the Linux Virtual Filesystem (VFS): A High-Level Tour”. https://www.starlab.io/blog/introduction-to-the-linux-virtual-filesystem-vfs-part-i-a-high-level-tour

[5] Biriukov, V. “Essential Linux Page Cache theory”. https://biriukov.dev/docs/page-cache/2-essential-page-cache-theory/

[6] Linux Kernel Documentation. “Making Filesystems Exportable”. https://docs.kernel.org/filesystems/nfs/exporting.html

[7] Kerrisk, M. “file_operations structure”. man7.org. https://man7.org/linux/man-pages/man2/open.2.html

[8] Linux Kernel Labs. “File system drivers (Part 1)”. https://linux-kernel-labs.github.io/refs/heads/master/labs/filesystems_part1.html

[9] LWN.net. (2016). “Mount namespaces, mount propagation, and unbindable mounts”. https://lwn.net/Articles/690679/

[10] Red Hat. (2021). “Building a container by hand using namespaces: The mount namespace”. https://www.redhat.com/en/blog/mount-namespaces

[11] Server Fault. (2011). “tuning linux cache settings for inode caching”. https://serverfault.com/questions/338097/tuning-linux-cache-settings-for-inode-caching

[12] LWN.net. (2020). “Two new ways to read a file quickly”. https://lwn.net/Articles/813827/

[13] Linux Kernel Documentation. “The /proc Filesystem”. https://www.infradead.org/~mchehab/kernel_docs/filesystems/proc.html

[14] opensource.com. (2019). “Virtual filesystems in Linux: Why we need them and how they work”. https://opensource.com/article/19/3/virtual-filesystems-linux

[15] ops.tips. (2019). “a practical look into overlayfs”. https://ops.tips/notes/practical-look-into-overlayfs/

[16] LWN.net. (2021). “Bye-bye bdflush()”. https://lwn.net/Articles/861431/

[17] Linux Kernel Documentation. “Linux Security Module Usage”. https://docs.kernel.org/admin-guide/LSM/index.html

[18] Linux Kernel Documentation. (1996). “Mandatory File Locking For The Linux Operating System”. https://www.kernel.org/doc/html/v5.8/filesystems/mandatory-locking.html

[19] Gorman, M. “Understanding the Linux Virtual Memory Manager”. https://www.kernel.org/doc/gorman/html/understand/understand007.html

[20] LWN.net. (2009). “Scaling the Linux VFS”. https://blog.linuxplumbersconf.org/2009/slides/Nick-Piggin-presentation.pdf

[21] Medium. (2024). “Understanding the Linux Page Cache: A Beginner’s Guide”. https://medium.com/@lambdafunc/understanding-the-linux-page-cache-a-beginners-guide-0f1cc8bdb04d

[22] O’Reilly. “Understanding the Linux Kernel, 3rd Edition - Chapter 12”. https://www.oreilly.com/library/view/understanding-the-linux/0596005652/ch12s06.html

[23] IBM Developer. (2007). “Anatomy of the Linux file system”. https://developer.ibm.com/tutorials/l-linux-filesystem/

[24] Stack Overflow. (2020). “inode reference counter how does it work?”. https://stackoverflow.com/questions/62612732/inode-reference-counter-how-does-it-work

[25] LWN.net. (2011). “Object-oriented design patterns in the kernel, part 2”. https://lwn.net/Articles/446317/

[26] University of Florida. “Lecture 21: Address Space Operations”. https://www.cise.ufl.edu/~jnw/FileSystemsfa02/Lectures/21.html

[27] Linux Kernel Documentation. “Directory Locking”. https://docs.kernel.org/filesystems/directory-locking.html

[28] Wikipedia. “Sparse file”. https://en.wikipedia.org/wiki/Sparse_file

[29] LWN.net. (2011). “Punching holes in files”. https://lwn.net/Articles/415889/

[30] Linux Kernel Documentation. “The Linux Journalling API”. https://www.kernel.org/doc/html/v5.3/filesystems/journalling.html

[31] Medium. (2025). “Filesystem Journaling Explained (ext4, XFS, btrfs)”. https://medium.com/@springmusk/filesystem-journaling-explained-ext4-xfs-btrfs-4ac9a0961638

[32] Bootlin. “Block device drivers”. https://bootlin.com/doc/legacy/block-drivers/block_drivers.odp

[33] Linux Journal. (1995). “An introduction to block device drivers”. https://www.linuxjournal.com/article/2890

[34] man7.org. “xattr(7) - Extended attributes”. https://man7.org/linux/man-pages/man7/xattr.7.html

[35] ArchWiki. “Extended attributes”. https://wiki.archlinux.org/title/Extended_attributes

[36] Medium. (2017). “On Disk IO, Part 1: Flavors of IO”. https://medium.com/databasss/on-disk-io-part-1-flavours-of-io-8e1ace1de017

[37] ScyllaDB. (2017). “Different I/O Access Methods for Linux, What We Chose for Scylla”. https://www.scylladb.com/2017/10/05/io-access-methods-scylla/

[38] LWN.net. (2016). “Mount namespaces and shared subtrees”. https://lwn.net/Articles/689856/

[39] Oracle Linux Blog. (2025). “Using drop_caches probably won’t crash your system”. https://blogs.oracle.com/linux/using-dropcaches-probably-wont-crash-your-system

[40] Medium. (2025). “Virtual File System (VFS)”. https://medium.com/@vdczz.dev/virtual-file-system-vfs-45a60955a136

[41] Kuniga. (2021). “Linux Filesystems Overview”. https://www.kuniga.me/blog/2021/02/08/linux-filesystems-overview.html

[42] University of Warsaw. “VFS and filesystems in Linux”. https://www.mimuw.edu.pl/~vincent/lecture12/12-fs.pdf