C++与Linux基础:虚拟文件系统 VFS 详解
1. 回顾前置的知识点,继续学习
在上一篇文章中,我们学习了 fd 的本质是一个指针数组的下标,并利用 函数进行重定向,随后利用这个特性完成了 minishell 的重定向。
Linux 虚拟文件系统 VFS 是内核抽象层,向上提供统一接口,向下兼容多种文件系统。核心思想是“一切皆文件”,将硬件设备、进程通信等抽象为文件操作。VFS 维护四个关键结构体:超级块对象代表已挂载文件系统,索引节点对象标识具体文件,目录项对象连接文件名与索引节点,文件对象代表进程打开的文件。通过函数指针实现多态机制,不同文件系统或设备驱动注册各自的 file_operations 结构体,VFS 根据上下文调用对应实现,屏蔽底层差异,简化系统开发复杂度。

在上一篇文章中,我们学习了 fd 的本质是一个指针数组的下标,并利用 函数进行重定向,随后利用这个特性完成了 minishell 的重定向。
dup2
dup2这个函数还是比较难的,容易搞混淆。前面是你要写入的 fd,后面是要替换的文件 fd。类似于a = 1,把后面的参数赋值给了前面。
今天我将继续深入研究文件操作的底层,研究 Linux 的底层。

我们已经讲完了文件重定向,接下来就是特别难的一切都是文件的思想了。虽然之前已经理解过了,但这里还是要继续深入研究。
学完今天的文章,我们将理解:
我们之前讲 Linux 下面的所有的东西都是文件,比如键盘和鼠标或者屏幕,我们可以看到图片中将显示器和键盘都当作了文件,在 struct_file 中进行封装。

我们之前也是讲过这个图片,详细的交代了什么是 fd 是怎么来的。这里也能看到 file 结构体里的 f_op 字段,它指向 file_operations 结构体,里面定义了 read、write 等接口。
这项技术的核心思想是将计算机中的各种资源(不仅是磁盘文件,还包括硬件设备)都抽象为'文件',从而提供统一的操作接口。
统一抽象:在 Windows 中不是文件的东西,在 Linux 下也被视为文件。这包括:
统一接口:开发者只需要使用一套 API(如 open, read, write, close),就可以对绝大部分系统资源进行操作。例如,读取文件和读取网卡数据都可以使用 read 函数;向文件写入和向屏幕打印都可以使用 write 函数。
我们从 Linux 的文件和进程来看,把这些都当作一些文件,极大的方便了进程通过一系列指针来控制这些外设。无论你是把它究竟是一个屏幕还是键盘,或者是打印机,它都可以当作一个文件提供操作给文件。极大的方便了进程的统一管理。
你可以相信,万一没有这样实现:
这太乱了!Linux 的做法是:把它们全都伪装成文件。
通过这种抽象(Abstraction),操作系统只需要提供一组通用的'五大金刚'接口:
无论你面对的是一块磁盘、一个键盘,还是一个远程服务器,你用的代码逻辑几乎是一模一样的。这种统一性极大地降低了系统开发的复杂度。
要想彻底理解一切皆是文件,我们需要扒开 Linux 的底层,我们不得不得提到 VFS(虚拟文件系统)。
我们简单介绍一下这个系统:
虚拟文件系统(Virtual File System, VFS) 是 Linux 内核中的一个抽象层。你可以把它想象成操作系统里的'万能翻译官'或者'通用接口标准'。它向上对用户程序提供统一的 open, read, write 接口,向下兼容各种千奇百怪的文件系统(ext4, NTFS, NFS, procfs 等)。
在软件工程中,这体现了 '依赖倒置原则':高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。VFS 正是这样一个'抽象层',让系统能够统一而灵活地管理各类文件系统。
无论怎么讲,它实在是太抽象了,太难以理解了。我们将分下面几点来详细谈谈:
VFS 的四大核心对象(The Big Four) VFS 在内存中维护了四个核心结构体,它们各司其职。理解了这四个,就理解了 VFS 的骨架:
struct super_blockstruct inodestruct dentry/home/user/a.txt),需要把路径拆分为 home、user、a.txt 三个部分。每一部分都是一个 Dentry。struct fileopen() 时创建,close() 时销毁。它是进程级别的,不同进程打开同一个文件,会有不同的 file 结构体(因为读写进度可能不同),但它们指向同一个 inode.我们这里详细的讲述 struct file,和我们的进程的关系。
我们先来看调用的流程:
一个程序启动,虚拟内存的内核态中会存放这个进程的 PCB (struct_task),这里面有一个结构体指针,指向 File table (file_struct),这个里面有一个关键的数组 (fd_array) 里面记录了 struct_file 的指针,通过这个,我们能找到 struct_file。在 struct file 里,有一个名为 f_op 的指针。它指向的是 struct file_operations。这里给出文件的操。这里以读写为主。尽管是不同的文件,但是 struct file_operations 里面的函数指针都是一致的。
这里只是我们简单的来讲讲这个流程,前面的 task_struct (进程) → files_struct (文件表) → fd_array (数组) → struct file (打开的文件对象)。在之前就讲了,后面才是 VFS 设计的最为精彩的,最值得我们每一个 C++/C 语言工作者,学习的设计思想。
这个设计思想,在这里我就直接点破了:是多态。你可能会疑惑:Linux 的底层语言不是 C 语言吗,怎么会有多态呢?待会就带你领会一下大神的设计和 coding 能力。
要想理解多态,我们要回顾一下 C++ 的多态是什么:每个有虚函数的类(类型)有一个虚函数表(不是每个对象)。每个对象有一个指向该虚函数表的指针(vptr)。在 C++ 中,我们有:
在 Linux 中靠 C 语言是如何手搓一个多态的呢:
我们先提供一个基类:struct file_operations,里面全是函数指针:
// <linux/fs.h>
struct file_operations {
struct module *owner;
// 读文件的函数指针
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 *);
// ... 其他操作如 poll, mmap, flush 等
};
里面全是函数指针,这些指针都是指向一个一个具体的文件驱动的函数:
场景 A:如果你操作的是 Ext4 文件系统上的文件(这里的这个可以暂时不用考虑 Ext4 是 什么东西)。
// ext4 的'多态'实现
const struct file_operations ext4_file_operations = {
.read_iter = ext4_file_read_iter, // 指向 Ext4 具体的读函数
.write_iter = ext4_file_write_iter,
.open = ext4_file_open, // ...
};
你看,这里就像子类一样,给这些函数指针具体的指向了这些函数。
场景 B:如果你操作的是鼠标 (字符设备)
鼠标驱动里会写好另一个结构体:
// 鼠标驱动的'多态'实现
const struct file_operations mousedev_fops = {
.read = mousedev_read, // 指向鼠标驱动具体的读函数
.write = mousedev_write,
.open = mousedev_open, // ...
};
同样姓名的函数指针,通过在不同的结构体里面,指向了不同的地方,驱动给出不同但是具体的函数,这些函数实现了怎么用,怎么写,怎么读。我们这里不需要管。
那怎么用呢,或者将它是怎么动起来的(注意多态的感觉)?
就拿 read 来说吧!当我们的进程动起来的时候,他会通过 fd 找到指定的 struct_file,这里面有指定的 f_op,这里的 f_op 里面在 open 的时候就被绑定了不同的设备,如果是什么他就绑定什么。
再来看看 read,通过这个 VFS 来调用 read 函数。
// 系统调用入口
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count) {
// 1. 根据 fd 找到 file 结构体
struct file *file = fget(fd); // 从 current->files->fd_array[fd] 获取
// 2. 调用 VFS 层的通用 read 函数
ret = vfs_read(file, buf, count, &pos);
fput(file);
return ret;
}
// VFS 层的 vfs_read(统一接口)
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) {
// 权限检查等...
// 3. 关键的一行:多态调用
if(file->f_op->read){
return file->f_op->read(file, buf, count, pos);
} else if(file->f_op->read_iter){
// 或者使用 read_iter(新接口)
// ...
}
return -EINVAL;
}
这就是多态! vfs_read 函数根本不在乎它读的是什么,它只管调用 f_op->read。
file 是 a.txt,f_op->read 实际上跳到了 ext4_file_read_iter。file 是 /dev/mouse,f_op->read 实际上跳到了 mousedev_read。多态的感觉 对外:统一的
read()接口 对内:file->f_op->read指向不同函数 运行时:根据f_op动态调用正确的实现
他相当于一个润滑剂,抹平了不同硬件之间的差异。这就是 VFS 的威力:也是他的强悍的地方:

今天的内容还是比较难的,希望大家好好想一想这个文章吧!
回顾我们今天的内容,一切皆文件不只是一句口号,而是 Linux 最核心的设计哲学。通过 VFS 这层抽象,Linux 把硬盘、键盘、显示器、网络、进程信息这些完全不同的东西,都装进了同一个框架里。
VFS 做了什么?说到底就两件事:
正是这种设计,让 Linux 能够轻松支持几十种不同的文件系统,让用户程序无需关心底层细节。好的架构不是把简单的东西搞复杂,而是把复杂的东西变得简单。
希望这篇文章能帮你理解 VFS 的本质。万事开头难,能把这些概念理清楚并写出来,你已经迈出了重要的一步。继续加油!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online