本文基于 Linux 6.4-rc1 内核源码(commit ac9a78681b92),对 VFS 层的核心数据结构、关键算法和执行路径进行深度分析。所有代码片段均直接取自内核源文件,并标注了文件路径与行号。
一、VFS 的设计理念与分层架构 Linux 内核中的虚拟文件系统(Virtual File System,VFS)是整个存储栈最核心的抽象层。它在用户空间系统调用与具体文件系统实现之间插入一个统一的接口层,使得 open(2)、read(2)、write(2) 等系统调用在逻辑上与底层是 ext4、btrfs、xfs 还是网络文件系统(NFS)完全无关。
1.1 面向对象的 C 设计模式 VFS 本质上是用 C 语言实现的面向对象设计。它通过以下手法模拟多态:
对象(Object) :inode、dentry、file、super_block 等结构体表示具体实例,持有数据。
方法集(vtable) :inode_operations、file_operations、super_operations、dentry_operations 等函数指针表,由具体文件系统在挂载时填充。
多态分发 :VFS 调用方法集中的函数指针,实际执行由具体文件系统提供的实现。
这种设计在 1991 年初版 Linux 时就已奠定。Linus 在设计之初参考了 Sun 的 VFS 接口规范,将路径解析、缓存管理与底层 I/O 彻底解耦。
1.2 分层架构全景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 用户空间 open("/etc/passwd", O_RDONLY) │ ▼ 系统调用入口(arch/x86/entry/syscalls/) ┌──────────────────────────────────┐ │ VFS 层 │ │ syscall → do_filp_open │ │ → path_openat │ │ → link_path_walk (namei) │ │ → dcache 查找 │ │ → inode_operations.lookup│ └──────────────┬───────────────────┘ │ 向下调用具体文件系统 ┌──────────────▼───────────────────┐ │ 具体文件系统层 │ │ ext4 / xfs / btrfs / tmpfs ... │ └──────────────┬───────────────────┘ │ ┌──────────────▼───────────────────┐ │ 块设备层 / 页缓存 │ │ page cache → bio → block layer │ └──────────────────────────────────┘
VFS 层负责:路径解析(namei)、dentry 缓存(dcache)、inode 缓存(icache)、权限检查、文件描述符管理、页缓存协调。
二、核心数据结构深度分析 2.1 inode — 文件的元数据对象 inode 代表一个文件系统对象(普通文件、目录、符号链接、设备文件等)的元数据。它与具体的路径名无关,是文件在文件系统中的唯一身份标识。
源文件:include/linux/fs.h,第 612 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 struct inode { umode_t i_mode; unsigned short i_opflags; kuid_t i_uid; kgid_t i_gid; unsigned int i_flags; const struct inode_operations *i_op ; struct super_block *i_sb ; struct address_space *i_mapping ; unsigned long i_ino; union { const unsigned int i_nlink; unsigned int __i_nlink; }; dev_t i_rdev; loff_t i_size; struct timespec64 i_atime ; struct timespec64 i_mtime ; struct timespec64 i_ctime ; spinlock_t i_lock; unsigned short i_bytes; u8 i_blkbits; blkcnt_t i_blocks; unsigned long i_state; struct rw_semaphore i_rwsem ; unsigned long dirtied_when; struct hlist_node i_hash ; struct list_head i_lru ; struct list_head i_sb_list ; union { struct hlist_head i_dentry ; struct rcu_head i_rcu ; }; atomic64_t i_version; atomic_t i_count; atomic_t i_writecount; union { const struct file_operations *i_fop ; void (*free_inode)(struct inode *); }; struct address_space i_data ; union { struct pipe_inode_info *i_pipe ; struct cdev *i_cdev ; char *i_link; unsigned i_dir_seq; }; void *i_private; } __randomize_layout;
重要的 i_state 标志位 (include/linux/fs.h 第 2115 行起):
1 2 3 4 5 6 7 8 9 10 #define I_DIRTY_SYNC (1 << 0) #define I_DIRTY_DATASYNC (1 << 1) #define I_DIRTY_PAGES (1 << 2) #define I_NEW (1 << 3) #define I_WILL_FREE (1 << 4) #define I_FREEING (1 << 5) #define I_CLEAR (1 << 6) #define I_SYNC (1 << 7) #define I_DIRTY_TIME (1 << 11) #define I_CREATING (1 << 15)
I_NEW 标志值得特别关注:当 iget_locked() 分配一个新 inode 并插入哈希表后,它处于 I_NEW 状态,此时其他路径若查到这个 inode 会在 wait_on_inode() 中睡眠等待,直到文件系统完成初始化并调用 unlock_new_inode() 清除该标志。
2.2 dentry — 路径到 inode 的映射 dentry(directory entry,目录项)是路径分量与 inode 之间的连接。它存储了文件名和对应 inode 的关联,形成整个目录树结构。
源文件:include/linux/dcache.h,第 82 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 struct dentry { unsigned int d_flags; seqcount_spinlock_t d_seq; struct hlist_bl_node d_hash ; struct dentry *d_parent ; struct qstr d_name ; struct inode *d_inode ; unsigned char d_iname[DNAME_INLINE_LEN]; struct lockref d_lockref ; const struct dentry_operations *d_op ; struct super_block *d_sb ; unsigned long d_time; void *d_fsdata; union { struct list_head d_lru ; wait_queue_head_t *d_wait; }; struct list_head d_child ; struct list_head d_subdirs ; union { struct hlist_node d_alias ; struct hlist_bl_node d_in_lookup_hash ; struct rcu_head d_rcu ; } d_u; } __randomize_layout;
负 dentry(negative dentry) :当 d_inode == NULL 时称为负 dentry,表示”该路径分量不存在”。这是性能优化的重要机制:访问一个不存在的文件(如 stat("/etc/nonexistent"))后,VFS 会在 dcache 中缓存这个负 dentry,下次同样的访问直接命中缓存,无需再次进入文件系统层做磁盘查找。
d_name 与内联存储 :struct qstr 包含哈希值、名称长度和指向名称字符串的指针。对于长度小于 DNAME_INLINE_LEN(通常为 36 或 40 字节)的文件名,名称直接存储在 d_iname 数组中(d_name.name 指向 d_iname),避免额外的内存分配,这对大量短文件名的目录而言有显著的内存和缓存效益。
2.3 file — 进程打开文件的运行时状态 file 结构代表一个已打开文件的实例,与进程相关。同一个 inode 可以被多个进程以不同标志打开,每次打开产生独立的 file 对象。
源文件:include/linux/fs.h,第 959 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct file { union { struct llist_node f_llist ; struct rcu_head f_rcuhead ; unsigned int f_iocb_flags; }; struct path f_path ; struct inode *f_inode ; const struct file_operations *f_op ; spinlock_t f_lock; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock ; loff_t f_pos; struct fown_struct f_owner ; const struct cred *f_cred ; struct file_ra_state f_ra ; u64 f_version; void *private_data; struct address_space *f_mapping ; errseq_t f_wb_err; errseq_t f_sb_err; } __randomize_layout __attribute__((aligned(4 )));
f_path 中同时保存了 vfsmount 和 dentry,这使得 VFS 能区分同一文件在不同挂载点下的访问路径——这对 getcwd(2) 和 openat(2) 等系统调用非常重要。
f_count 使用 atomic_long_t(而非普通 atomic_t)是因为文件描述符可以通过 dup(2)、fork(2) 等方式大量共享,在极端情况下引用计数可能超过 32 位范围(尽管实际中极为罕见)。
2.4 super_block — 文件系统实例的全局状态 super_block 代表一个已挂载文件系统的实例,是整个文件系统的顶层对象。
源文件:include/linux/fs.h,第 1153 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 struct super_block { struct list_head s_list ; 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 ; const struct dquot_operations *dq_op ; const struct export_operations *s_export_op ; unsigned long s_flags; unsigned long s_iflags; unsigned long s_magic; struct dentry *s_root ; struct rw_semaphore s_umount ; atomic_t s_active; const struct xattr_handler **s_xattr ; struct hlist_bl_head s_roots ; struct list_head s_mounts ; struct block_device *s_bdev ; struct backing_dev_info *s_bdi ; void *s_fs_info; u32 s_time_gran; time64_t s_time_min; time64_t s_time_max; char s_id[32 ]; uuid_t s_uuid; unsigned int s_max_links; const struct dentry_operations *s_d_op ; struct shrinker s_shrink ; struct list_lru s_dentry_lru ; struct list_lru s_inode_lru ; struct rcu_head rcu ; struct work_struct destroy_work ; struct mutex s_sync_lock ; struct user_namespace *s_user_ns ; };
s_shrink 是超级块注册到内核内存回收子系统的 shrinker,当系统内存紧张时,内核的 shrink_slab() 路径会调用它来回收该文件系统上的 dentry 和 inode 缓存。sysctl_vfs_cache_pressure(默认值 100)控制回收的激进程度,可通过 /proc/sys/vm/vfs_cache_pressure 调整。
三、操作方法集源码分析 3.1 file_operations — 文件 I/O 接口 源文件:include/linux/fs.h,第 1771 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 *, struct io_comp_batch *, unsigned int flags); 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 ); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t , unsigned int ); ssize_t (*splice_read) (struct file *, loff_t *, struct pipe_inode_info *, size_t , unsigned int ); loff_t (*remap_file_range)(struct file *, loff_t , struct file *, loff_t , loff_t , unsigned int ); int (*fadvise)(struct file *, loff_t , loff_t , int ); int (*uring_cmd)(struct io_uring_cmd *, unsigned int ); } __randomize_layout;
现代文件系统优先实现 read_iter/write_iter 而非 read/write。前者基于 struct kiocb(异步 I/O 控制块)和 struct iov_iter(分散/聚集缓冲区迭代器),可以统一处理同步、异步和 io_uring 三种 I/O 模式。VFS 在 vfs_read() 中会自动适配:若文件系统只实现了 read_iter,则通过 new_sync_read() 包装成同步调用。
3.2 inode_operations — 目录与元数据操作 源文件:include/linux/fs.h,第 1817 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 ); 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 (*mknod) (struct mnt_idmap *, struct inode *, struct dentry *, umode_t , dev_t ); int (*rename) (struct mnt_idmap *, struct inode *, struct dentry *, struct inode *, struct dentry *, unsigned int ); int (*setattr)(struct mnt_idmap *, struct dentry *, struct iattr *); int (*getattr)(struct mnt_idmap *, const struct path *, struct kstat *, u32, unsigned int ); int (*atomic_open )(struct inode *, struct dentry *, struct file *, unsigned open_flag, umode_t create_mode); } ____cacheline_aligned;
mnt_idmap 是 Linux 5.12 引入的挂载点 ID 映射机制,用于支持用户命名空间(user namespace)下的 UID/GID 映射,使容器内的文件访问能正确对应宿主机的权限模型。
3.3 super_operations — 文件系统生命周期管理 源文件:include/linux/fs.h,第 1903 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 *, int wait); int (*freeze_super)(struct super_block *); int (*freeze_fs) (struct super_block *); int (*thaw_super) (struct super_block *); 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 *); long (*nr_cached_objects) (struct super_block *, struct shrink_control *); long (*free_cached_objects)(struct super_block *, struct shrink_control *); };
四、路径查找算法详解 路径查找(path lookup / namei)是 VFS 中最复杂的子系统之一。将一个字符串路径(如 /usr/lib/libc.so.6)解析为一个 (vfsmount, dentry) 二元组,需要处理符号链接、挂载点穿越、并发重命名、RCU 无锁查找等诸多复杂情况。
4.1 nameidata — 路径查找的上下文 源文件:fs/namei.c,第 567 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct nameidata { struct path path ; struct qstr last ; struct path root ; struct inode *inode ; unsigned int flags, state; unsigned seq, next_seq, m_seq, r_seq; int last_type; unsigned depth; int total_link_count; struct saved { struct path link ; struct delayed_call done ; const char *name; unsigned seq; } *stack , internal[EMBEDDED_LEVELS]; struct filename *name ; struct nameidata *saved ; int dfd; vfsuid_t dir_vfsuid; umode_t dir_mode; } __randomize_layout;
4.2 三层重试机制 路径查找的入口是 filename_lookup(),它实现了三层重试机制来平衡性能与正确性(源文件:fs/namei.c,第 2500 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int filename_lookup (int dfd, struct filename *name, unsigned flags, struct path *path, struct path *root) { int retval; struct nameidata nd ; set_nameidata(&nd, dfd, name, root); retval = path_lookupat(&nd, flags | LOOKUP_RCU, path); if (unlikely(retval == -ECHILD)) retval = path_lookupat(&nd, flags, path); if (unlikely(retval == -ESTALE)) retval = path_lookupat(&nd, flags | LOOKUP_REVAL, path); if (likely(!retval)) audit_inode(name, path->dentry, 0 ); restore_nameidata(); return retval; }
RCU 模式 是路径查找的快速路径。在此模式下,整个路径遍历过程不获取任何自旋锁或引用计数,仅依赖 RCU 读锁(rcu_read_lock())和 seqlock(d_seq)来保证一致性读取。绝大多数路径查找会在 RCU 模式下成功完成,无锁路径的性能在多核系统上远优于有锁版本。
4.3 path_lookupat() — 路径查找核心 源文件:fs/namei.c,第 2467 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 static int path_lookupat (struct nameidata *nd, unsigned flags, struct path *path) { const char *s = path_init(nd, flags); int err; if (unlikely(flags & LOOKUP_DOWN) && !IS_ERR(s)) { err = handle_lookup_down(nd); if (unlikely(err < 0 )) s = ERR_PTR(err); } while (!(err = link_path_walk(s, nd)) && (s = lookup_last(nd)) != NULL ) ; if (!err && unlikely(nd->flags & LOOKUP_MOUNTPOINT)) { err = handle_lookup_down(nd); nd->state &= ~ND_JUMPED; } if (!err) err = complete_walk(nd); if (!err && nd->flags & LOOKUP_DIRECTORY) if (!d_can_lookup(nd->path.dentry)) err = -ENOTDIR; if (!err) { *path = nd->path; nd->path.mnt = NULL ; nd->path.dentry = NULL ; } terminate_walk(nd); return err; }
4.4 link_path_walk() — 逐分量解析 源文件:fs/namei.c,第 2243 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 static int link_path_walk (const char *name, struct nameidata *nd) { int depth = 0 ; int err; nd->last_type = LAST_ROOT; nd->flags |= LOOKUP_PARENT; while (*name == '/' ) name++; if (!*name) { nd->dir_mode = 0 ; return 0 ; } for (;;) { struct mnt_idmap *idmap ; const char *link; u64 hash_len; int type; idmap = mnt_idmap(nd->path.mnt); err = may_lookup(idmap, nd); if (err) return err; hash_len = hash_name(nd->path.dentry, name); type = LAST_NORM; if (name[0 ] == '.' ) switch (hashlen_len(hash_len)) { case 2 : if (name[1 ] == '.' ) { type = LAST_DOTDOT; nd->state |= ND_JUMPED; } break ; case 1 : type = LAST_DOT; } if (likely(type == LAST_NORM)) { struct dentry *parent = nd->path.dentry; nd->state &= ~ND_JUMPED; if (unlikely(parent->d_flags & DCACHE_OP_HASH)) { struct qstr this = { { .hash_len = hash_len }, .name = name }; err = parent->d_op->d_hash(parent, &this); if (err < 0 ) return err; hash_len = this.hash_len; name = this.name; } } nd->last.hash_len = hash_len; nd->last.name = name; nd->last_type = type; name += hashlen_len(hash_len); if (!*name) goto OK; do { name++; } while (unlikely(*name == '/' )); if (unlikely(!*name)) { OK: if (!depth) { nd->dir_vfsuid = i_uid_into_vfsuid(idmap, nd->inode); nd->dir_mode = nd->inode->i_mode; nd->flags &= ~LOOKUP_PARENT; return 0 ; } name = nd->stack [--depth].name; link = walk_component(nd, 0 ); } else { link = walk_component(nd, WALK_MORE); } } }
walk_component() 是每个路径分量的实际处理函数,它先调用 lookup_fast()(在 dcache 中 RCU 查找),若未命中再调用 lookup_slow()(获取目录锁后调用 i_op->lookup())。
五、dentry 缓存(dcache)机制 dcache 是 VFS 最重要的性能优化机制。它将路径分量到 inode 的映射缓存在内存中,避免每次路径查找都下沉到文件系统层。
5.1 全局哈希表与 RCU 查找 dcache 使用一个全局哈希表(dentry_hashtable),以 (parent_dentry, name_hash) 作为 key。
源文件:fs/dcache.c,第 2344 行,__d_lookup_rcu() 是 RCU 模式下的快速查找路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 struct dentry *__d_lookup_rcu (const struct dentry *parent , const struct qstr *name , unsigned *seqp ) { u64 hashlen = name->hash_len; const unsigned char *str = name->name; struct hlist_bl_head *b = d_hash(hashlen_hash(hashlen)); struct hlist_bl_node *node ; struct dentry *dentry ; hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) { unsigned seq; seq = raw_seqcount_begin(&dentry->d_seq); if (dentry->d_parent != parent) continue ; if (d_unhashed(dentry)) continue ; if (dentry->d_name.hash_len != hashlen) continue ; if (dentry_cmp(dentry, str, hashlen_len(hashlen)) != 0 ) continue ; *seqp = seq; return dentry; } return NULL ; }
无锁 RCU 查找完成后,调用者必须用 read_seqcount_retry(&dentry->d_seq, seq) 验证在查找过程中没有发生并发重命名,若返回 true 则需重试。
非 RCU 模式的 d_lookup() 使用 rename_lock seqlock 来防止并发重命名导致的漏找(false negative):
1 2 3 4 5 6 7 8 9 10 11 12 13 struct dentry *d_lookup (const struct dentry *parent, const struct qstr *name) { struct dentry *dentry ; unsigned seq; do { seq = read_seqbegin(&rename_lock); dentry = __d_lookup(parent, name); if (dentry) break ; } while (read_seqretry(&rename_lock, seq)); return dentry; }
5.2 dentry 的分配与绑定 当 dcache 未命中时,VFS 会在目录锁保护下调用 i_op->lookup(),文件系统在其内部通过以下步骤将新 dentry 与 inode 关联:
d_alloc() — 分配并链入父目录(fs/dcache.c 第 1847 行):
1 2 3 4 5 6 7 8 9 10 11 12 struct dentry *d_alloc (struct dentry *parent, const struct qstr *name) { struct dentry *dentry = __d_alloc(parent->d_sb, name); if (!dentry) return NULL ; spin_lock(&parent->d_lock); __dget_dlock(parent); dentry->d_parent = parent; list_add(&dentry->d_child, &parent->d_subdirs); spin_unlock(&parent->d_lock); return dentry; }
d_instantiate() — 将 dentry 与 inode 绑定(fs/dcache.c 第 2030 行):
1 2 3 4 5 6 7 8 9 10 11 void d_instantiate (struct dentry *entry, struct inode *inode) { BUG_ON(!hlist_unhashed(&entry->d_u.d_alias)); if (inode) { security_d_instantiate(entry, inode); spin_lock(&inode->i_lock); __d_instantiate(entry, inode); spin_unlock(&inode->i_lock); } }
5.3 dcache 回收 dcache 通过 LRU 列表和内存压力回调进行管理。shrink_dcache_sb() 在文件系统卸载时强制清除所有缓存(fs/dcache.c 第 1314 行):
1 2 3 4 5 6 7 8 9 10 void shrink_dcache_sb (struct super_block *sb) { do { LIST_HEAD(dispose); list_lru_walk(&sb->s_dentry_lru, dentry_lru_isolate_shrink, &dispose, 1024 ); shrink_dentry_list(&dispose); } while (list_lru_count(&sb->s_dentry_lru) > 0 ); }
在内存压力下,内核通过 s_shrink 注册的 shrinker 来回收 dentry,回收力度受 /proc/sys/vm/vfs_cache_pressure 控制(值越大,回收越激进)。
六、inode 缓存机制 6.1 inode 分配:alloc_inode() 源文件:fs/inode.c,第 254 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static struct inode *alloc_inode (struct super_block *sb) { const struct super_operations *ops = sb->s_op; struct inode *inode ; if (ops->alloc_inode) inode = ops->alloc_inode(sb); else inode = alloc_inode_sb(sb, inode_cachep, GFP_KERNEL); if (!inode) return NULL ; if (unlikely(inode_init_always(sb, inode))) { if (ops->destroy_inode) { ops->destroy_inode(inode); if (!ops->free_inode) return NULL ; } inode->free_inode = ops->free_inode; i_callback(&inode->i_rcu); return NULL ; } return inode; }
ext4 的 alloc_inode 实际上分配的是 struct ext4_inode_info,其中内嵌了 struct inode(vfs_inode 字段)以及 ext4 专有字段(如 extent 树、block bitmap 等)。通过 container_of() 宏可以在两者之间相互转换,这是 Linux 内核中经典的”子类继承”实现模式。
6.2 inode 查找与创建:iget_locked() iget_locked() 是文件系统 lookup 操作中最常用的 inode 获取接口(fs/inode.c 第 1267 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 struct inode *iget_locked (struct super_block *sb, unsigned long ino) { struct hlist_head *head = inode_hashtable + hash(sb, ino); struct inode *inode ; again: spin_lock(&inode_hash_lock); inode = find_inode_fast(sb, head, ino); spin_unlock(&inode_hash_lock); if (inode) { if (IS_ERR(inode)) return NULL ; wait_on_inode(inode); if (unlikely(inode_unhashed(inode))) { iput(inode); goto again; } return inode; } inode = alloc_inode(sb); if (inode) { struct inode *old ; spin_lock(&inode_hash_lock); old = find_inode_fast(sb, head, ino); if (!old) { inode->i_ino = ino; spin_lock(&inode->i_lock); inode->i_state = I_NEW; hlist_add_head_rcu(&inode->i_hash, head); spin_unlock(&inode->i_lock); inode_sb_list_add(inode); spin_unlock(&inode_hash_lock); return inode; } spin_unlock(&inode_hash_lock); destroy_inode(inode); inode = old; wait_on_inode(inode); } return inode; }
这里有一个经典的”检查后再加锁”(TOCTOU)竞争处理:先在锁外快速查找,若未命中则分配新 inode,加锁后再次检查。若此时另一个 CPU 已经插入了同一 inode,则丢弃刚分配的,使用已有的。
七、文件打开流程源码走读 当用户调用 open(2) 时,内核的完整调用链如下:
1 2 3 4 5 6 7 8 9 10 11 12 sys_openat() → do_sys_openat2() → do_filp_open() # fs/namei.c → path_openat() # fs/namei.c → alloc_empty_file() # 分配 struct file → path_init() # 初始化 nameidata → link_path_walk() # 解析路径(中间分量) → open_last_lookups() # 处理最后分量 + 创建/打开 → do_open() # 最终打开操作 → vfs_open() → do_dentry_open() → f_op->open() # 调用文件系统的 open 方法
7.1 do_filp_open() — 三层重试的文件打开 源文件:fs/namei.c,第 3810 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct file *do_filp_open (int dfd, struct filename *pathname, const struct open_flags *op) { struct nameidata nd ; int flags = op->lookup_flags; struct file *filp ; set_nameidata(&nd, dfd, pathname, NULL ); filp = path_openat(&nd, op, flags | LOOKUP_RCU); if (unlikely(filp == ERR_PTR(-ECHILD))) filp = path_openat(&nd, op, flags); if (unlikely(filp == ERR_PTR(-ESTALE))) filp = path_openat(&nd, op, flags | LOOKUP_REVAL); restore_nameidata(); return filp; }
7.2 path_openat() — 路径解析与文件打开的结合 源文件:fs/namei.c,第 3771 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 static struct file *path_openat (struct nameidata *nd, const struct open_flags *op, unsigned flags) { struct file *file ; int error; file = alloc_empty_file(op->open_flag, current_cred()); if (IS_ERR(file)) return file; if (unlikely(file->f_flags & __O_TMPFILE)) { error = do_tmpfile(nd, flags, op, file); } else if (unlikely(file->f_flags & O_PATH)) { error = do_o_path(nd, flags, file); } else { const char *s = path_init(nd, flags); while (!(error = link_path_walk(s, nd)) && (s = open_last_lookups(nd, file, op)) != NULL ) ; if (!error) error = do_open(nd, file, op); terminate_walk(nd); } if (likely(!error)) { if (likely(file->f_mode & FMODE_OPENED)) return file; WARN_ON(1 ); error = -EINVAL; } fput(file); if (error == -EOPENSTALE) { if (flags & LOOKUP_RCU) error = -ECHILD; else error = -ESTALE; } return ERR_PTR(error); }
八、VFS 读写路径分析 8.1 vfs_read() — 读取的统一入口 源文件:fs/read_write.c,第 450 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ssize_t vfs_read (struct file *file, char __user *buf, size_t count, loff_t *pos) { ssize_t ret; if (!(file->f_mode & FMODE_READ)) return -EBADF; if (!(file->f_mode & FMODE_CAN_READ)) return -EINVAL; if (unlikely(!access_ok(buf, count))) return -EFAULT; ret = rw_verify_area(READ, file, pos, count); if (ret) return ret; if (count > MAX_RW_COUNT) count = MAX_RW_COUNT; 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; if (ret > 0 ) { fsnotify_access(file); add_rchar(current, ret); } inc_syscr(current); return ret; }
8.2 vfs_write() — 写入的统一入口 源文件:fs/read_write.c,第 564 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ssize_t vfs_write (struct file *file, const char __user *buf, size_t count, loff_t *pos) { ssize_t ret; if (!(file->f_mode & FMODE_WRITE)) return -EBADF; if (!(file->f_mode & FMODE_CAN_WRITE)) return -EINVAL; if (unlikely(!access_ok(buf, count))) return -EFAULT; ret = rw_verify_area(WRITE, file, pos, count); if (ret) return ret; if (count > MAX_RW_COUNT) count = MAX_RW_COUNT; file_start_write(file); if (file->f_op->write) ret = file->f_op->write(file, buf, count, pos); else if (file->f_op->write_iter) ret = new_sync_write(file, buf, count, pos); else ret = -EINVAL; if (ret > 0 ) { fsnotify_modify(file); add_wchar(current, ret); } inc_syscw(current); file_end_write(file); return ret; }
file_start_write() / file_end_write() 成对使用,通过 sb_writers 计数防止在有写者的情况下冻结文件系统(freeze_fs 需要等待所有写者退出)。
九、实际代码示例:实现一个最简单的内核文件系统 以下示例展示了如何利用上述 VFS 接口实现一个内存文件系统的骨架。这个模式在 tmpfs、ramfs、debugfs 等内核文件系统中广泛使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <linux/fs.h> #include <linux/init.h> #include <linux/module.h> #include <linux/pagemap.h> #define MYFS_MAGIC 0x4D594653 static const struct inode_operations myfs_dir_inode_ops = { .create = NULL , .lookup = simple_lookup, .mkdir = NULL , .unlink = NULL , }; static const struct file_operations myfs_file_ops = { .read_iter = generic_file_read_iter, .write_iter = generic_file_write_iter, .mmap = generic_file_mmap, .fsync = noop_fsync, .llseek = generic_file_llseek, }; static struct inode *myfs_make_inode (struct super_block *sb, int mode, dev_t dev) { struct inode *inode = new_inode(sb); if (!inode) return NULL ; inode->i_mode = mode; inode->i_uid = current_fsuid(); inode->i_gid = current_fsgid(); inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode); inode->i_ino = get_next_ino(); switch (mode & S_IFMT) { case S_IFDIR: inode->i_op = &myfs_dir_inode_ops; inode->i_fop = &simple_dir_operations; inc_nlink(inode); break ; case S_IFREG: inode->i_op = &simple_inode_operations; inode->i_fop = &myfs_file_ops; inode->i_mapping->a_ops = &ram_aops; break ; } return inode; } static int myfs_fill_super (struct super_block *sb, void *data, int silent) { struct inode *root_inode ; struct dentry *root_dentry ; sb->s_magic = MYFS_MAGIC; sb->s_op = &simple_super_operations; sb->s_maxbytes = MAX_LFS_FILESIZE; sb->s_blocksize = PAGE_SIZE; sb->s_blocksize_bits = PAGE_SHIFT; root_inode = myfs_make_inode(sb, S_IFDIR | 0755 , 0 ); if (!root_inode) return -ENOMEM; root_dentry = d_make_root(root_inode); if (!root_dentry) return -ENOMEM; sb->s_root = root_dentry; return 0 ; }
十、总结 本文通过直接阅读 Linux 6.4-rc1 的内核源码,系统梳理了 VFS 的核心设计:
组件
源文件
核心职责
inode
include/linux/fs.h
文件元数据,独立于路径存在
dentry
include/linux/dcache.h
路径分量缓存,组成目录树
file
include/linux/fs.h
进程打开文件的运行时状态
super_block
include/linux/fs.h
文件系统实例的全局状态
link_path_walk
fs/namei.c
逐分量解析路径
__d_lookup_rcu
fs/dcache.c
RCU 无锁 dentry 查找
iget_locked
fs/inode.c
inode 的哈希查找与创建
do_filp_open
fs/namei.c
文件打开的顶层入口
vfs_read/write
fs/read_write.c
读写的统一分发
VFS 设计中有几个值得反复品味的工程决策:
RCU 无锁快速路径 :路径查找的 RCU 模式、dcache 的 __d_lookup_rcu 让绝大多数路径查找无需获取任何锁,极大提升了多核系统的可扩展性。
三层重试降级 :RCU → 引用计数 → 强制 revalidate,在性能与正确性之间找到了优雅的平衡点。
负 dentry 缓存 :缓存”不存在”这一结果,是对文件系统访问模式(大量失败查找)的精准优化。
container_of 实现继承 :文件系统将 VFS 的 inode 嵌入自己的私有结构,通过宏实现零开销的”子类转换”,是 C 语言面向对象编程的经典范式。
下一篇将深入 ext4 文件系统的内部实现,分析它如何通过 extents 树管理块分配,以及日志(journal)机制如何保证崩溃一致性。