一个看似简单的问题
当你在Linux终端输入cp /home/user/file.txt /mnt/usb/backup/时,系统在做什么?源文件位于ext4格式的SSD上,目标目录在FAT32格式的U盘里。这两种文件系统的数据结构完全不同——ext4使用extent树管理块分配,FAT32依靠链式的FAT表。然而,cp命令对这种差异一无所知。
这背后是一个精妙的抽象层设计:虚拟文件系统(Virtual File System,VFS)。它让cp、ls、cat等用户态程序可以用同一套系统调用操作任何文件系统,从本地的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对象拆分为两个独立概念——inode和dentry[2]。inode代表文件的元数据(不包含文件名),dentry代表目录项(将文件名与inode关联)。
这个拆分带来了显著优势:
- 硬链接的自然支持:多个dentry可以指向同一个inode,无需额外机制
- 路径解析优化:可以单独缓存目录项(dcache),加速路径查找
- 职责清晰:元数据操作与名称操作分离
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被解析后,每个路径分量(home、user、file.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遍历链表,按名称找到ext4的file_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: 返回成功
挂载完成后,内核中会创建几个关键数据结构:
struct mount:表示一个挂载实例struct mountpoint:表示挂载点目录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-walk和ref-walk两种模式进行路径解析:
- rcu-walk:无锁、高性能模式,适用于大部分只读场景
- 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; // 前一次读取位置
};
预读算法基于两个观察:
- 空间局部性:访问某位置后,大概率会访问邻近位置
- 顺序检测:检测顺序读取模式,增大预读窗口
当检测到顺序读取时,预读窗口可以从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使用了多项优化技术:
- 无锁查找(RCU):使用RCU(Read-Copy-Update)实现无锁读取
- 序列计数:快速检测并发修改
- 热路径优化:高频访问的路径分量常驻内存
// 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面临扩展性挑战:
- 全局dcache锁:早期dcache使用全局锁,现代内核使用分区锁
- inode分配竞争:引入per-cpu缓存减少锁竞争
- 路径解析优化: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的价值不仅在于统一接口,更在于:
- 可扩展性:新文件系统可以无缝接入
- 性能优化:dcache、页缓存等机制对所有文件系统生效
- 安全隔离:mount namespace实现了容器文件隔离
- 系统可观测: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