从零写OS内核 | VFS——虚拟文件系统,Linux的”一切皆文件”是怎么实现的
你用 cat /proc/cpuinfo 读 CPU 信息,用 ls /dev/null 查设备节点,用 echo "hello" > /tmp/a.txt 写文件——三个完全不同的操作,却都可以用 open/read/write 这几个系统调用搞定。为什么?
答案就是 VFS(Virtual File System Switch)——Linux 内核用来统一管理所有”看起来像文件的东西”的一层抽象。它让磁盘文件、管道、设备、进程信息、网络 socket 都用同一套接口操作,实现了 Unix 最经典的设计哲学:一切皆文件。
今天,我们来搞清楚 VFS 是怎么把完全不同的事物用同一个框架统一起来的。
1. 为什么需要 VFS
没有 VFS 的话:
没有 VFS 时:
→ 应用程序需要知道文件在哪个具体的文件系统(ext4、btrfs、FAT)
→ 读磁盘用 ext4 的 API,读网络用 socket API,读设备用 ioctl
→ 每个应用程序都要写不同代码处理不同后端有了 VFS:
有 VFS 时:
→ 应用程序只调用 open/read/write
→ VFS 层统一处理,路由到具体文件系统的实现(ext4、btrfs...)
→ 写代码时不需要知道底层是什么VFS 在系统中的位置:
用户程序
↓
系统调用(open/read/write)
↓
VFS 层(统一接口)
↓ ┌──────────────┬──────────────┬──────────────┬──────────────┐
→ │ ext4 │ btrfs │ procfs │ devtmpfs │
│ (磁盘文件) │ (磁盘文件) │ (进程信息) │ (设备节点) │
└──────────────┴──────────────┴──────────────┴──────────────┘
↓ ↓ ↓ ↓
磁盘 磁盘 /proc /dev2. VFS 核心数据结构:inode 和 dentry
VFS 有两个最核心的结构:inode(代表一个文件)和 dentry(代表目录项)。
2.1 inode:代表一个具体的文件/对象
inode(索引节点)是 VFS 的核心抽象:
每个文件(磁盘、设备、管道等)在内核里都有一个 inode
inode 包含:
- 文件类型(regular / dir / char / block / link / socket)
- 文件大小
- 时间戳(atime/mtime/ctime)
- 权限位(mode)
- 文件操作函数表(struct file_operations *f_op)
- 指向具体文件系统私有数据的指针
inode 不是文件名——文件名在 dentry 里
inode 是文件本身(内容、元数据、操作)// VFS inode 结构(简化)
// 源码:include/linux/fs.h
struct inode {
umode_t i_mode; // 文件类型 + 权限位
kuid_t i_uid; // 所有者 UID
kgid_t i_gid; // 所有者 GID
loff_t i_size; // 文件大小
struct timespec i_atime; // 访问时间
struct timespec i_mtime; // 修改时间
struct timespec i_ctime; // 状态改变时间
const struct inode_operations *i_op; // inode 操作函数表
const struct file_operations *i_fop; // 文件操作函数表
void *i_private; // 文件系统私有数据
// ...
};2.2 dentry:文件名到 inode 的桥梁
dentry(目录项)是"文件名到 inode 的映射":
/tmp
↓ dentry: "tmp" → inode(tmp目录)
/tmp/a.txt
↓ dentry: "a.txt" → inode(a.txt文件)
dentry 组成:
- d_name: 文件名("a.txt")
- d_parent: 父目录的 dentry
- d_inode: 指向这个文件的 inode
- d_hash: dentry 缓存的 hash 表
- d_op: dentry 操作函数表(指针)
VFS 维护一个 dentry cache(dcache)来加速路径解析:
"从根目录 / 解析 /tmp/a.txt" 的过程:
→ 先找 / 的 dentry → inode → 读目录内容
→ 找 "tmp" → dentry → inode
→ 找 "a.txt" → dentry → inode
→ 找到 inode 后缓存 dentry,下次更快2.3 file:进程打开的文件实例
file(文件实例)是"进程打开的文件":
同一个文件可以被多个进程同时打开
每个进程有一个 struct file(文件描述符 fd 的底层结构)
每个进程有独立的:
- 文件偏移(f_pos)
- 文件描述符标志(O_NONBLOCK 等)
- 读写缓冲区状态
但共享同一个:
- inode(同一个磁盘文件)
- struct file_operations(操作函数)// VFS file 结构
struct file {
struct path f_path; // 路径(dentry + vfsmount)
const struct file_operations *f_op; // 文件操作函数表
loff_t f_pos; // 当前文件偏移
void *private_data; // 文件系统私有数据
struct fown_struct f_owner; // 用于 SIGIO
unsigned int f_flags; // open 时的标志
// ...
};3. file_operations:操作函数表
这是 VFS 最巧妙的设计——函数指针表。
// file_operations 结构(每个 inode 有一份)
// 源码:include/linux/fs.h
struct file_operations {
// 读/写
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
// 打开/释放
int (*open) (struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
// 目录操作
int (*iterate)(struct file *, struct dir_context *);
// 偏移
loff_t (*llseek)(struct file *, loff_t, int);
// I/O 控制
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
// mmap
int (*mmap) (struct file *, struct vm_area_struct *);
// 其他
int (*fsync) (struct file *, loff_t, loff_t, int);
// ...
};为什么函数指针表能统一不同文件系统?
ext4 的 file_operations:
ext4_read() = ext4 文件系统读
ext4_write() = ext4 文件系统写
procfs 的 file_operations:
proc_read() = 从内核数据结构读进程信息
proc_write() = 不支持(写入 /proc 一般是错误)
devtmpfs 的 file_operations:
dev_read() = 从设备驱动读数据
dev_write() = 写数据到设备驱动
VFS 统一调用:
file->f_op->read(file, ...) ← 不管是 ext4/procfs/dev
file->f_op->write(file, ...) ← VFS 不需要知道具体是哪种文件系统4. VFS 路径解析:从路径名到 inode
当你说 open("/tmp/a.txt", O_RDONLY) 时,VFS 做了以下事情:
路径解析流程(以 /tmp/a.txt 为例):
Step 1: 解析 / 的 dentry
→ 从当前进程的 root 目录开始
→ 在 dcache 里找 / 的 dentry
→ 找到 / inode
Step 2: 查找 "tmp"(在 / 目录下)
→ 调用 / inode 的 lookup() 方法(目录读)
→ ext4_lookup() 读 / 目录的内容
→ 找到 "tmp" 对应的 dentry → inode(tmp目录)
Step 3: 查找 "a.txt"(在 /tmp 目录下)
→ 调用 tmp inode 的 lookup() 方法
→ ext4_lookup() 读 /tmp 目录的内容
→ 找到 "a.txt" dentry → inode(a.txt)
Step 4: 分配 file 结构
→ 把 file->f_op = inode->i_fop
→ file->private_data = inode 的私有数据
Step 5: 返回 fd(文件描述符)// VFS open 系统调用简化流程
int do_sys_open(int dfd, const char *pathname, int flags, mode_t mode) {
struct filename *name = getname(pathname);
// 解析路径到 dentry
struct path path;
path.dentry = path_lookup(name, flags, &path);
// 获取 inode
struct inode *inode = path.dentry->d_inode;
// 分配 file 结构
struct file *f = do_filp_open(dfd, name, flags, mode);
// 设置 file->f_op = inode->i_fop
f->f_op = inode->i_fop;
// 调用文件系统的 open
if (f->f_op->open)
error = f->f_op->open(inode, f);
// 返回 fd
fd = get_unused_fd_flags(flags);
fdinstall(fd, f);
return fd;
}5. 特殊文件系统:/proc 和 /dev
5.1 /proc:进程信息的文件系统接口
/proc 的特点:
目录结构:
/proc/1/ → 进程 PID=1 的信息
/proc/meminfo → 系统内存信息
/proc/cpuinfo → CPU 信息
/proc/diskstats → 磁盘状态
...
/proc 的 inode_operations 和 file_operations:
proc_lookup() → 读 /proc/PID/stat 之类的文件
proc_read() → 从内核数据结构(task_struct)读出进程信息
所以读 /proc/cpuinfo 的流程:
open("/proc/cpuinfo") → VFS 路径解析 → proc inode
→ proc_file_read() → 读取内核变量 boot_cpu_data
→ 返回给用户5.2 /dev:设备节点的文件系统接口
/dev 的特点:
/dev/null → 设备驱动(null_write 直接丢弃,null_read 返回0)
/dev/zero → 读返回全0,写直接丢弃
/dev/urandom → 读返回随机数(加密安全)
/dev/tty → 当前终端设备
设备文件有两种:
字符设备(c):按字符流处理(键盘、串口)
块设备(b):按块处理(磁盘)
设备号:
- 主设备号(major):标识设备驱动
- 次设备号(minor):标识驱动管理的具体设备
/dev/null 的主设备号 = 1,次设备号 = 3
内核里注册了 misc 驱动,major=1,处理所有 misc 设备6. Linux 实践:观察 VFS
# 查看文件系统类型
df -T
# 查看具体文件的 inode 信息
ls -li /tmp/a.txt
# 查看某文件系统的挂载信息
mount | grep ext4
# 查看所有文件系统(/proc/mounts)
cat /proc/mounts# 用 strace 观察 VFS 操作
strace -e trace=open,read,write cat /etc/hostname
# 观察目录项缓存(dcache)命中率
cat /proc/sys/vr/dentry-state
# 观察 inode 缓存
cat /proc/sys/vr/inode-state# 用 lsof 观察进程打开的文件
lsof -p $$
# 观察具体文件被哪些进程打开
lsof /tmp/a.txt# 用 debugfs 观察 ext4 内部结构(需要 root)
# debugfs -w /dev/sda1
# debugfs: stat /tmp/a.txt
# 显示 inode 号、块位置、时间戳等7. 从零实现:wandos 的 VFS
⚠️ wandos 当前状态:kernel/fs/ 目录下有 VFS 实现,wandos 用简化版的 inode + file 结构管理所有文件系统。
7.1 VFS 层结构(kernel/fs/vfs.c)
// wandos VFS 结构
// 源码:kernel/fs/vfs.c
// 文件描述符表(每个进程一份)
struct file_descriptor {
bool used; // 是否占用
struct vnode *node; // 指向 VFS 节点
int flags; // O_RDONLY, O_WRONLY 等
loff_t pos; // 当前偏移
};
struct process_file_table {
struct file_descriptor fd[64]; // 最多 64 个 fd
};
// VFS 节点(inode 的简化版)
struct vnode {
char name[256]; // 文件名
enum vnode_type type; // FILE / DIR / DEVICE / PIPE
struct file_operations *fops; // 操作函数表
void *private_data; // 文件系统私有数据
uint64_t size; // 文件大小
uint64_t inode_num; // inode 号
struct dentry *parent; // 父目录
struct list_head children; // 子目录/文件
};
// file_operations(和 Linux 类似)
struct file_operations {
int (*open)(struct vnode *, struct file_descriptor *);
int (*close)(struct file_descriptor *);
ssize_t (*read)(struct file_descriptor *, char *buf, size_t count);
ssize_t (*write)(struct file_descriptor *, const char *buf, size_t count);
loff_t (*lseek)(struct file_descriptor *, loff_t off, int whence);
};7.2 VFS open 实现
// wandos VFS open(简化版)
int vfs_open(const char *pathname, int flags) {
// 路径解析(简化版:不支持相对路径)
struct vnode *node = path_walk(pathname);
if (!node) return -ENOENT; // 文件不存在
// 检查权限
if ((flags & O_WRONLY) && (node->type == VNODE_DIR))
return -EISDIR; // 不能写目录
// 分配 fd
int fd = allocate_fd();
if (fd < 0) return -EMFILE; // fd 表满
// 初始化 fd
fd_table[fd].node = node;
fd_table[fd].flags = flags;
fd_table[fd].pos = 0;
fd_table[fd].used = true;
// 调用文件系统的 open
if (node->fops->open)
node->fops->open(node, &fd_table[fd]);
return fd;
}7.3 设备驱动注册
// wandos 设备驱动注册(简化版)
void devtmpfs_init(void) {
// 注册 /dev/null (major=1, minor=3)
struct vnode *null = alloc_vnode();
null->name = "null";
null->type = VNODE_DEVICE;
null->fops = &null_fops; // null_fops 指向 null_read/null_write
null->private_data = NULL;
devtmpfs_register(null);
// 注册 /dev/zero
struct vnode *zero = alloc_vnode();
zero->name = "zero";
zero->type = VNODE_DEVICE;
zero->fops = &zero_fops;
devtmpfs_register(zero);
}
// /dev/null 的读写实现
ssize_t null_read(struct file_descriptor *fd, char *buf, size_t count) {
return 0; // 读 null 返回 0(EOF)
}
ssize_t null_write(struct file_descriptor *fd, const char *buf, size_t count) {
return count; // 写 null 丢弃,返回写入字节数
}
static struct file_operations null_fops = {
.open = NULL, // 设备节点不需要 open
.read = null_read,
.write = null_write,
};7.4 wandos 和 Linux 的主要差异
- 没有 dentry cache:Linux 维护完整的 dcache 来加速路径解析;wandos 的 path_walk 每次都重新解析,没有缓存
- 没有页缓存(page cache):Linux 把磁盘文件缓存在内存里,读写先操作 page cache;wandos 直接操作磁盘,没有缓存层
- 没有 writeback:Linux 有延迟写机制(dirty page);wandos 写操作直接落盘(慢,但可靠)
- 没有权限检查:Linux 在 VFS 层做权限检查(capability、ACL);wandos 没有这层
8. 动手环节:实现一个简单的字符设备驱动
今天的目标:在 os-kernel-from-scratch 里实现一个 /dev/hello 设备节点,读它返回 “Hello from kernel!”,写它打印到 VGA 屏幕。
任务 1:定义 file_operations
struct file_operations hello_fops = {
.read = hello_read,
.write = hello_write,
};任务 2:实现 hello_read / hello_write
hello_read 把 “Hello from kernel!n” 写入用户 buffer。hello_write 把用户 buffer 内容打印到 VGA 屏幕。
任务 3:注册设备到 VFS
在模块初始化时调用 register_device("hello", &hello_fops),在 /dev 下创建 hello 节点。
验收标准
cat /dev/hello输出 “Hello from kernel!”echo "world" > /dev/hello在屏幕上显示 “world”ls -l /dev/hello显示设备类型(c 或 b)
9. 踩坑与注意事项
坑 1:硬链接和软链接的区别
硬链接(ln)是在同一个文件系统里创建另一个 dentry,指向同一个 inode。没有”原始文件”和”链接文件”的区别——它们是平等的。删除任何一个,inode 引用计数减 1,直到计数为 0 才真正删除文件。软链接(ln -s)是一个特殊文件,里面存了路径字符串,可以跨文件系统,删除原文件后软链接仍然存在(指向不存在的文件)。
坑 2:目录也是 inode
目录也是一种 inode,有自己的 file_operations。目录的 read() 读出来的是 dirent 结构(目录项列表)。ls 就是通过读目录的 dirent 列表来显示文件名。
坑 3:page cache 和 buffer cache 混淆
Linux 有两层缓存:page cache(页缓存,用于文件 I/O)和 buffer cache(缓冲区缓存,用于块设备元数据)。两者可以重叠——读文件时先查 page cache,块设备读写时用 buffer cache。现代 Linux 统一用 page cache,buffer cache 已被淘汰。不要混淆。
坑 4:fd 和 inode 混淆
文件描述符(fd)是进程打开文件表里的索引(0-1023),每个进程有自己的 fd 表。inode 是文件系统里的唯一标识,两个进程可以各自打开同一个文件(得到不同的 fd),但共享同一个 inode。
写在最后
VFS 是 Linux 最优雅的设计之一——它用函数指针表把完全不同的事物(磁盘文件、进程信息、设备节点、网络 socket)用同一套接口(open/read/write/ioctl)统一起来,让应用程序不需要知道底层是什么。”一切皆文件”不是一句口号,而是通过 VFS 的 inode/dentry/file 三层结构实实在在实现的。
理解 VFS,你才算真正理解为什么 cat /proc/cpuinfo 和 cat /dev/zero 能用同一个 read 系统调用搞定,以及为什么 Unix 的哲学如此强大。
下篇预告:同步原语——spinlock 与信号量的实现。
相关阅读
- Linux Kernel Source:
fs/namei.c(路径解析) - Linux Kernel Source:
fs/inode.c(inode 操作) - Linux Kernel Source:
fs/open.c(文件操作) - wandos:
kernel/fs/vfs.c - wandos:
kernel/fs/devtmpfs.c - 本文 Demo: https://github.com/golang12306/os-kernel-from-scratch (demos/vfs/)
- https://github.com/zhangfuwen/wandos — Linux 内核教程
- 下一篇:《从零写OS内核 | 同步原语——spinlock与信号量的实现》
动手环节
想深入理解本文内容?动手实践是最好的方式:
今天的目标:下载 wandos 代码仓库,添加一个自定义设备驱动,理解 Linux vs wandos 的差异。
-
下载 wandos:
git clone https://github.com/zhangfuwen/wandos.git cd wandos -
找到对应模块:查看
kernel/fs/vfs.c -
实现作业:根据文中”动手环节”章节的要求,完成代码编写
-
提交作业:Fork 仓库,提交你的改动,在 GitHub 上开一个 Pull Request
评论