Linux 存储栈深度解析(二):VFS 虚拟文件系统源码剖析

本文基于 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)inodedentryfilesuper_block 等结构体表示具体实例,持有数据。
  • 方法集(vtable)inode_operationsfile_operationssuper_operationsdentry_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; // 文件类型 + 权限位(S_IFREG, S_IFDIR 等)
unsigned short i_opflags; // 内部优化标志,如 IOP_FASTPERM
kuid_t i_uid; // 文件所有者 UID(内核内部类型,非原始 uid_t)
kgid_t i_gid; // 文件所有者 GID
unsigned int i_flags; // 文件系统挂载标志(S_SYNC, S_NOATIME 等)

const struct inode_operations *i_op; // inode 方法集(lookup, create, unlink…)
struct super_block *i_sb; // 所属超级块,指向文件系统实例
struct address_space *i_mapping; // 页缓存映射,管理文件数据的缓存页

/* 统计数据,路径遍历不访问以下字段 */
unsigned long i_ino; // inode 编号(在文件系统内唯一)
union {
const unsigned int i_nlink; // 硬链接数(只读访问,修改须用辅助函数)
unsigned int __i_nlink;
};
dev_t i_rdev; // 设备号(仅对设备文件有效)
loff_t i_size; // 文件大小(字节)
struct timespec64 i_atime; // 最后访问时间(access time)
struct timespec64 i_mtime; // 最后修改时间(modification time)
struct timespec64 i_ctime; // 最后状态变化时间(change time)
spinlock_t i_lock; // 保护 i_blocks、i_bytes、i_size
unsigned short i_bytes; // 最后一个块使用的字节数
u8 i_blkbits; // 块大小的位数(log2)
blkcnt_t i_blocks; // 分配的 512 字节块数

unsigned long i_state; // inode 状态位(I_NEW、I_DIRTY、I_FREEING 等)
struct rw_semaphore i_rwsem; // inode 读写信号量(保护目录操作等)

unsigned long dirtied_when; // 首次被标脏的 jiffies 时间
struct hlist_node i_hash; // inode 哈希表节点(用于按 sb+ino 快速查找)
struct list_head i_lru; // inode LRU 链表节点(回收时使用)
struct list_head i_sb_list; // 同一超级块下所有 inode 的链表
union {
struct hlist_head i_dentry; // 指向此 inode 的所有 dentry(别名链表)
struct rcu_head i_rcu; // RCU 释放回调
};
atomic64_t i_version; // inode 版本号(NFS、目录遍历使用)
atomic_t i_count; // 引用计数
atomic_t i_writecount; // 写者计数(防止 mmap+write 竞争)
union {
const struct file_operations *i_fop; // 默认文件操作集
void (*free_inode)(struct inode *); // 释放回调(与 i_fop 复用空间)
};
struct address_space i_data; // 该 inode 自身的页缓存(i_mapping 通常指向它)
union {
struct pipe_inode_info *i_pipe; // 管道(FIFO)私有数据
struct cdev *i_cdev; // 字符设备私有数据
char *i_link; // 内联符号链接目标
unsigned i_dir_seq; // 目录版本序列号(用于目录项迭代)
};
void *i_private; // 文件系统私有指针(各 fs 自由使用)
} __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)  // inode 元数据脏,需 sync 写回
#define I_DIRTY_DATASYNC (1 << 1) // 数据脏,datasync 时需写回
#define I_DIRTY_PAGES (1 << 2) // 页缓存中有脏页
#define I_NEW (1 << 3) // 刚分配,尚未初始化完成
#define I_WILL_FREE (1 << 4) // 即将释放(i_count 已降到 0)
#define I_FREEING (1 << 5) // 正在释放流程中
#define I_CLEAR (1 << 6) // 已清除,等待 RCU 宽限期后释放
#define I_SYNC (1 << 7) // 正在进行回写(writeback)
#define I_DIRTY_TIME (1 << 11) // 时间戳脏(lazytime 特性)
#define I_CREATING (1 << 15) // 正在 atomic_open 创建中

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 {
/* RCU 路径查找时访问的字段,集中在前部以利用缓存行 */
unsigned int d_flags; // DCACHE_* 标志位,受 d_lock 保护
seqcount_spinlock_t d_seq; // 每 dentry 的 seqlock(并发重命名保护)
struct hlist_bl_node d_hash; // 哈希桶链表节点(全局 dentry 哈希表)
struct dentry *d_parent; // 父目录的 dentry
struct qstr d_name; // 文件名(含哈希值和长度)
struct inode *d_inode; // 关联的 inode(负 dentry 时为 NULL)
unsigned char d_iname[DNAME_INLINE_LEN]; // 短文件名的内联存储(避免额外分配)

/* 引用计数与锁(热路径也会访问) */
struct lockref d_lockref; // 自旋锁 + 引用计数(合并在一个 u64 中)
const struct dentry_operations *d_op; // dentry 操作集(可为 NULL)
struct super_block *d_sb; // 所属超级块

unsigned long d_time; // 由 d_revalidate 使用(网络 fs 检查有效性)
void *d_fsdata; // 文件系统私有数据

union {
struct list_head d_lru; // dentry LRU 链表(负 dentry 或非使用中)
wait_queue_head_t *d_wait; // 仅用于 in-lookup 状态的等待队列
};
struct list_head d_child; // 父目录的子链表节点
struct list_head d_subdirs; // 本目录的子 dentry 链表头

union {
struct hlist_node d_alias; // inode 别名链表(同 inode 的多个 dentry)
struct hlist_bl_node d_in_lookup_hash; // in-lookup 期间的临时哈希桶节点
struct rcu_head d_rcu; // 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; // 用于延迟 put 的无锁链表节点
struct rcu_head f_rcuhead; // RCU 延迟释放回调
unsigned int f_iocb_flags; // io_uring 命令标志
};
struct path f_path; // 包含 vfsmount* 和 dentry*(挂载点+路径)
struct inode *f_inode; // 缓存的 inode 指针(等同 f_path.dentry->d_inode)
const struct file_operations *f_op; // 文件操作集(在 open 时从 inode 复制)

spinlock_t f_lock; // 保护 f_ep 和 f_flags
atomic_long_t f_count; // 文件引用计数(fget/fput 操作)
unsigned int f_flags; // open 时的 O_* 标志(O_RDONLY、O_NONBLOCK 等)
fmode_t f_mode; // FMODE_READ、FMODE_WRITE 等运行时模式
struct mutex f_pos_lock; // 保护 f_pos 的互斥锁(多线程并发 read/write)
loff_t f_pos; // 当前文件偏移量(read/write 的游标)
struct fown_struct f_owner; // 用于 SIGIO/SIGURG 信号投递的所有者信息
const struct cred *f_cred; // 打开文件时的进程凭据(权限检查用)
struct file_ra_state f_ra; // 预读状态(记录预读窗口大小、历史等)

u64 f_version; // 版本号(目录遍历时检测目录是否被修改)
void *private_data; // 文件系统或驱动私有数据(tty 驱动等使用)

struct address_space *f_mapping; // 页缓存映射(通常 == f_inode->i_mapping)
errseq_t f_wb_err; // 记录上次检查后发生的写回错误(用于 fsync 错误传播)
errseq_t f_sb_err; // 记录超级块级写回错误(用于 syncfs)
} __randomize_layout
__attribute__((aligned(4)));

f_path 中同时保存了 vfsmountdentry,这使得 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; // 全局超级块链表(keep this first)
dev_t s_dev; // 设备号(用于哈希查找)
unsigned char s_blocksize_bits; // 块大小(2 的幂次)
unsigned long s_blocksize; // 块大小(字节)
loff_t s_maxbytes; // 文件最大字节数限制

struct file_system_type *s_type; // 指向文件系统类型(ext4_fs_type 等)
const struct super_operations *s_op; // 超级块操作集
const struct dquot_operations *dq_op; // 磁盘配额操作集
const struct export_operations *s_export_op; // NFS 导出操作集

unsigned long s_flags; // MS_RDONLY、MS_NOSUID 等挂载标志
unsigned long s_iflags; // 内部 SB_I_* 标志(SB_I_CGROUPWB 等)
unsigned long s_magic; // 魔数(EXT4_SUPER_MAGIC = 0xEF53 等)
struct dentry *s_root; // 文件系统根目录的 dentry

struct rw_semaphore s_umount; // 卸载保护信号量
atomic_t s_active; // 活跃引用计数(挂载数)

const struct xattr_handler **s_xattr; // 扩展属性处理器数组

struct hlist_bl_head s_roots; // NFS 使用的备用根 dentry 链表
struct list_head s_mounts; // 此 sb 上的挂载点链表
struct block_device *s_bdev; // 底层块设备(非块设备 fs 为 NULL)
struct backing_dev_info *s_bdi; // 后备设备信息(预读、writeback 调度)

void *s_fs_info; // 文件系统私有数据(ext4_sb_info 等)
u32 s_time_gran; // 时间戳精度(纳秒,不能比 1 秒差)
time64_t s_time_min; // 支持的最小时间戳
time64_t s_time_max; // 支持的最大时间戳

char s_id[32]; // 可读的文件系统标识(如 "sda1")
uuid_t s_uuid; // 文件系统 UUID

unsigned int s_max_links; // 允许的最大硬链接数

const struct dentry_operations *s_d_op; // 默认 dentry 操作集

struct shrinker s_shrink; // 内存压力时用于回收 dentry/inode 的 shrinker

struct list_lru s_dentry_lru; // dentry LRU 列表(按 NUMA 节点分组)
struct list_lru s_inode_lru; // inode LRU 列表
struct rcu_head rcu; // RCU 延迟释放
struct work_struct destroy_work; // 异步销毁工作项

struct mutex s_sync_lock; // 防止并发 sync 的互斥锁
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); // 进程关闭 fd 时(非最后一次)
int (*release)(struct inode *, struct file *); // 最后一次关闭时
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int); // 异步通知(SIGIO)注册
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); // io_uring 直通命令
} __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);
// ^ 在目录 inode 中查找名为 dentry->d_name 的子项,最核心的接口

const char * (*get_link)(struct dentry *, struct inode *, struct delayed_call *);
// ^ 读取符号链接目标(替代已废弃的 follow_link/put_link)

int (*permission)(struct mnt_idmap *, struct inode *, int);
// ^ 检查访问权限,mask 是 MAY_READ | MAY_WRITE | MAY_EXEC 的组合

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);
// ^ 允许文件系统将 lookup + create + open 原子地合并为一次操作(ext4 等使用)
} ____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);
// ^ 分配 inode,文件系统可以嵌入私有数据(如 ext4_inode_info 包含 struct inode)

void (*destroy_inode)(struct inode *);
void (*free_inode)(struct inode *); // RCU 延迟释放路径调用

void (*dirty_inode)(struct inode *, int flags); // inode 被标脏时回调
int (*write_inode)(struct inode *, struct writeback_control *wbc);
int (*drop_inode) (struct inode *); // 引用计数降到 0 时,返回 1 则立即删除
void (*evict_inode)(struct inode *); // inode 被驱逐时(如 unlink 后最后一次 iput)
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 *); // df 命令的底层实现
int (*remount_fs)(struct super_block *, int *, char *);
void (*umount_begin)(struct super_block *); // NFS 强制卸载时的回调

long (*nr_cached_objects) (struct super_block *, struct shrink_control *);
long (*free_cached_objects)(struct super_block *, struct shrink_control *);
// ^ 配合 s_shrink 实现文件系统层的内存回收
};

四、路径查找算法详解

路径查找(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; // 当前已解析到的路径(mnt + dentry)
struct qstr last; // 当前正在处理的路径分量(名称 + 哈希)
struct path root; // 查找的根目录("/" 或 chroot 后的根)
struct inode *inode; // path.dentry->d_inode 的缓存(避免反复解引用)
unsigned int flags, state; // LOOKUP_* 标志和 ND_* 状态位
unsigned seq, next_seq, m_seq, r_seq; // 并发保护用的序列号
int last_type; // LAST_NORM / LAST_DOT / LAST_DOTDOT / LAST_ROOT
unsigned depth; // 当前符号链接递归深度
int total_link_count; // 全局符号链接跟随计数(防止循环,上限 40)
struct saved {
struct path link; // 符号链接路径
struct delayed_call done;
const char *name; // 符号链接展开后的字符串
unsigned seq;
} *stack, internal[EMBEDDED_LEVELS]; // 符号链接嵌套栈(内联数组避免分配)
struct filename *name; // 完整路径字符串
struct nameidata *saved; // 嵌套 nameidata 链(用于 execve 等场景)
int dfd; // 相对路径的起始目录 fd(AT_FDCWD 或具体 fd)
vfsuid_t dir_vfsuid; // 当前目录所有者(权限检查优化)
umode_t dir_mode; // 当前目录的 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);

// 第一次尝试:RCU 模式(完全无锁,最快)
retval = path_lookupat(&nd, flags | LOOKUP_RCU, path);

// 若 RCU 模式遇到需要阻塞的情况(-ECHILD),回退到引用计数模式
if (unlikely(retval == -ECHILD))
retval = path_lookupat(&nd, flags, path);

// 若缓存数据过期(-ESTALE,NFS 场景),强制重新验证
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)
{
// path_init 初始化起始点:绝对路径从 root 开始,相对路径从 cwd 或 dfd 开始
const char *s = path_init(nd, flags);
int err;

// 处理 LOOKUP_DOWN(从挂载点向下穿越,openat2 的新特性)
if (unlikely(flags & LOOKUP_DOWN) && !IS_ERR(s)) {
err = handle_lookup_down(nd);
if (unlikely(err < 0))
s = ERR_PTR(err);
}

// 核心循环:link_path_walk 解析路径的中间分量(非最后一个)
// lookup_last 处理最后一个分量(可能触发符号链接展开,返回新路径字符串继续迭代)
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;
}

// complete_walk 完成最终验证(检查权限、revalidate 等)
if (!err)
err = complete_walk(nd);

// 如果要求目标必须是目录,检查最终 dentry 是否可查找
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;
}

源文件: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;

// 跳过开头的多个 '/'("/////usr" 等同于 "/usr")
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_name 利用 CPU 字对齐优化)
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;
// 如果文件系统注册了自定义哈希函数(如大小写不敏感的 casefold),调用它
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) {
// 缓存目录的 uid 和 mode 供后续权限检查用(避免重复访问 inode)
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;

// 在 RCU 读侧遍历哈希桶链表(无锁)
hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
unsigned seq;

// raw_seqcount_begin 不等待序列号稳定(允许后续重试),保证性能
seq = raw_seqcount_begin(&dentry->d_seq);

if (dentry->d_parent != parent)
continue;
if (d_unhashed(dentry)) // 跳过已从哈希表删除的 dentry
continue;
if (dentry->d_name.hash_len != hashlen)
continue;
if (dentry_cmp(dentry, str, hashlen_len(hashlen)) != 0)
continue;

*seqp = seq; // 返回序列号,调用者后续需用 read_seqcount_retry 验证
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); // LSM 安全检查
spin_lock(&inode->i_lock);
__d_instantiate(entry, inode); // 将 dentry 加入 inode->i_dentry 别名链表
spin_unlock(&inode->i_lock);
}
// 若 inode == NULL,产生负 dentry(表示文件不存在)
}

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);
// 从 LRU 中隔离最多 1024 个 dentry 到 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;

// 优先使用文件系统自定义的分配函数(如 ext4_alloc_inode 分配更大的结构)
if (ops->alloc_inode)
inode = ops->alloc_inode(sb);
else
// 否则从通用的 inode_cachep slab 缓存分配
inode = alloc_inode_sb(sb, inode_cachep, GFP_KERNEL);

if (!inode)
return NULL;

// 初始化 inode 的通用字段(spinlock、引用计数、mapping 等)
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 inodevfs_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); // 等待 I_NEW 状态清除(其他 CPU 正在初始化)
if (unlikely(inode_unhashed(inode))) {
iput(inode);
goto again; // inode 正在被回收,重试
}
return inode; // 命中缓存,直接返回
}

// 未命中:分配新 inode
inode = alloc_inode(sb);
if (inode) {
struct inode *old;

spin_lock(&inode_hash_lock);
old = find_inode_fast(sb, head, ino); // TOCTOU 检查:重新确认
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); // 加入超级块的 inode 链表
spin_unlock(&inode_hash_lock);
// 返回带 I_NEW 标志的 inode,调用者负责填充磁盘数据后调用 unlock_new_inode()
return inode;
}
// 竞争:另一个 CPU 抢先创建了同一 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);

// 同样采用三层重试策略(RCU → 引用计数 → 强制 revalidate)
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)) {
// O_TMPFILE:创建匿名临时文件(不在目录中出现)
error = do_tmpfile(nd, flags, op, file);
} else if (unlikely(file->f_flags & O_PATH)) {
// 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); // 执行最终 open
terminate_walk(nd);
}

if (likely(!error)) {
if (likely(file->f_mode & FMODE_OPENED))
return file; // 成功
WARN_ON(1);
error = -EINVAL;
}
fput(file);
// 将 RCU 特有的错误码转换为标准错误码
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); // 检查文件锁(mandatory lock)
if (ret)
return ret;
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT; // 单次最大 2GB

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); // 通过 kiocb 包装
else
ret = -EINVAL;

if (ret > 0) {
fsnotify_access(file); // 触发 inotify/fanotify 的 IN_ACCESS 事件
add_rchar(current, ret); // 更新进程 I/O 统计
}
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); // 增加 sb->s_writers 引用,防止并发 freeze
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); // 触发 IN_MODIFY 事件
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file); // 释放 sb->s_writers 引用
return ret;
}

file_start_write() / file_end_write() 成对使用,通过 sb_writers 计数防止在有写者的情况下冻结文件系统(freeze_fs 需要等待所有写者退出)。


九、实际代码示例:实现一个最简单的内核文件系统

以下示例展示了如何利用上述 VFS 接口实现一个内存文件系统的骨架。这个模式在 tmpfsramfsdebugfs 等内核文件系统中广泛使用:

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 /* "MYFS" */

/* inode 操作集:目录 */
static const struct inode_operations myfs_dir_inode_ops = {
.create = NULL, /* 实际实现中填充 */
.lookup = simple_lookup, /* VFS 提供的通用实现 */
.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,
};

/* 创建 inode 的辅助函数 */
static struct inode *myfs_make_inode(struct super_block *sb,
int mode, dev_t dev)
{
struct inode *inode = new_inode(sb); /* 分配并链入 sb->s_inodes */
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(); /* 分配全局唯一 inode 编号 */

switch (mode & S_IFMT) {
case S_IFDIR:
inode->i_op = &myfs_dir_inode_ops;
inode->i_fop = &simple_dir_operations;
/* 目录初始引用计数为 2("." 和父目录的 ".." 各一个) */
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; /* VFS 提供的通用 super_ops */
sb->s_maxbytes = MAX_LFS_FILESIZE;
sb->s_blocksize = PAGE_SIZE;
sb->s_blocksize_bits = PAGE_SHIFT;

/* 创建根目录 inode */
root_inode = myfs_make_inode(sb, S_IFDIR | 0755, 0);
if (!root_inode)
return -ENOMEM;

/* 创建根目录 dentry 并与 inode 绑定 */
root_dentry = d_make_root(root_inode); /* d_alloc_anon + d_instantiate */
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 设计中有几个值得反复品味的工程决策:

  1. RCU 无锁快速路径:路径查找的 RCU 模式、dcache 的 __d_lookup_rcu 让绝大多数路径查找无需获取任何锁,极大提升了多核系统的可扩展性。
  2. 三层重试降级:RCU → 引用计数 → 强制 revalidate,在性能与正确性之间找到了优雅的平衡点。
  3. 负 dentry 缓存:缓存”不存在”这一结果,是对文件系统访问模式(大量失败查找)的精准优化。
  4. container_of 实现继承:文件系统将 VFS 的 inode 嵌入自己的私有结构,通过宏实现零开销的”子类转换”,是 C 语言面向对象编程的经典范式。

下一篇将深入 ext4 文件系统的内部实现,分析它如何通过 extents 树管理块分配,以及日志(journal)机制如何保证崩溃一致性。