跳到主要内容C++与Linux基础:虚拟文件系统 VFS 详解 | 极客日志C++
C++与Linux基础:虚拟文件系统 VFS 详解
Linux 虚拟文件系统 VFS 是内核抽象层,向上提供统一接口,向下兼容多种文件系统。核心思想是“一切皆文件”,将硬件设备、进程通信等抽象为文件操作。VFS 维护四个关键结构体:超级块对象代表已挂载文件系统,索引节点对象标识具体文件,目录项对象连接文件名与索引节点,文件对象代表进程打开的文件。通过函数指针实现多态机制,不同文件系统或设备驱动注册各自的 file_operations 结构体,VFS 根据上下文调用对应实现,屏蔽底层差异,简化系统开发复杂度。
BigDataPan2 浏览 C++与Linux基础:虚拟文件系统 VFS 详解
1. 回顾前置的知识点,继续学习
在上一篇文章中,我们学习了 fd 的本质是一个指针数组的下标,并利用 dup2 函数进行重定向,随后利用这个特性完成了 minishell 的重定向。
dup2 这个函数还是比较难的,容易搞混淆。前面是你要写入的 fd,后面是要替换的文件 fd。类似于 a = 1,把后面的参数赋值给了前面。
今天我将继续深入研究文件操作的底层,研究 Linux 的底层。
我们已经讲完了文件重定向,接下来就是特别难的一切都是文件的思想了。虽然之前已经理解过了,但这里还是要继续深入研究。
- 怎么理解一切都是文件?
- 理解 Linux 的几个结构体
- 理解 VFS 设计模式下的多态
2. 再谈一切皆文件的思想
我们之前讲 Linux 下面的所有的东西都是文件,比如键盘和鼠标或者屏幕,我们可以看到图片中将显示器和键盘都当作了文件,在 struct_file 中进行封装。
我们之前也是讲过这个图片,详细的交代了什么是 fd 是怎么来的。这里也能看到 file 结构体里的 f_op 字段,它指向 file_operations 结构体,里面定义了 read、write 等接口。
这项技术的核心思想是将计算机中的各种资源(不仅是磁盘文件,还包括硬件设备)都抽象为'文件',从而提供统一的操作接口。
-
统一抽象:在 Windows 中不是文件的东西,在 Linux 下也被视为文件。这包括:
- 硬件设备:显示器、键盘、磁盘、网卡。
- 进程通信:管道(Pipe)。
- 其他:进程信息、Socket 套接字等。
-
统一接口:开发者只需要使用一套 API(如 open, read, write, close),就可以对绝大部分系统资源进行操作。例如,读取文件和读取网卡数据都可以使用 read 函数;向文件写入和向屏幕打印都可以使用 write 函数。
2-1 为什么要这样设计
我们从 Linux 的文件和进程来看,把这些都当作一些文件,极大的方便了进程通过一系列指针来控制这些外设。无论你是把它究竟是一个屏幕还是键盘,或者是打印机,它都可以当作一个文件提供操作给文件。极大的方便了进程的统一管理。
- 读取硬盘里的文本,你需要一套'硬盘指令'。
- 读取麦克风的声音,你需要一套'音频指令'。
- 读取网络发来的数据,你又需要一套'网络指令'。
这太乱了!Linux 的做法是:把它们全都伪装成文件。
通过这种抽象(Abstraction),操作系统只需要提供一组通用的'五大金刚'接口:
- Open(打开)
- Read(读取)
- Write(写入)
- Close(关闭)
- Seek(定位)
无论你面对的是一块磁盘、一个键盘,还是一个远程服务器,你用的代码逻辑几乎是一模一样的。这种统一性极大地降低了系统开发的复杂度。
3. 一切皆文件思想的底层:VFS
要想彻底理解一切皆是文件,我们需要扒开 Linux 的底层,我们不得不得提到 VFS(虚拟文件系统)。
虚拟文件系统(Virtual File System, VFS) 是 Linux 内核中的一个抽象层。你可以把它想象成操作系统里的'万能翻译官'或者'通用接口标准'。它向上对用户程序提供统一的 open, read, write 接口,向下兼容各种千奇百怪的文件系统(ext4, NTFS, NFS, procfs 等)。
在软件工程中,这体现了 '依赖倒置原则':高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。VFS 正是这样一个'抽象层',让系统能够统一而灵活地管理各类文件系统。
无论怎么讲,它实在是太抽象了,太难以理解了。我们将分下面几点来详细谈谈:
3-1 VFS 到底包括什么
VFS 的四大核心对象(The Big Four)
VFS 在内存中维护了四个核心结构体,它们各司其职。理解了这四个,就理解了 VFS 的骨架:
1. 超级块对象 (Superblock) —— struct super_block
- 代表什么:代表一个已挂载的文件系统(比如你挂载的整个 C 盘,或者插入的一个 U 盘)。
- 包含什么:文件系统的元数据。比如:块大小、魔数、文件系统类型、根目录在哪里。
- 生命周期:挂载(mount)时创建,卸载(umount)时销毁。
2. 索引节点对象 (Inode) —— struct inode
- 代表什么:代表具体的一个文件(或者目录)。它是文件唯一的标识符。
- 包含什么:文件的元数据。比如:文件大小、拥有者、权限、时间戳、数据块在磁盘上的位置。注意:Inode 不包含文件名。
- 生命周期:当文件被访问时,内核会把磁盘上的 Inode 信息读入内存构建这个结构体。
3. 目录项对象 (Dentry) —— struct dentry
- 代表什么:代表路径中的一项。用于连接'文件名'和'Inode'。
- 为什么需要它:Inode 里没有文件名,Linux 为了解析路径(比如
/home/user/a.txt),需要把路径拆分为 home、user、a.txt 三个部分。每一部分都是一个 Dentry。
- 包含什么:文件名、指向对应 Inode 的指针、指向父目录 Dentry 的指针。
- 关键作用:Dentry Cache (dcache)。内核会缓存这些对象,下次再访问同一个路径时,不需要再去磁盘读 Inode,直接查内存里的 Dentry,速度极快。
4. 文件对象 (File) —— struct file
- 代表什么:代表进程打开的一个文件。
- 包含什么:当前的读写位置(offset)、打开模式(只读/读写)、指向对应 Dentry 的指针。
- 生命周期:
open() 时创建,close() 时销毁。它是进程级别的,不同进程打开同一个文件,会有不同的 file 结构体(因为读写进度可能不同),但它们指向同一个 inode.
我们这里详细的讲述 struct file,和我们的进程的关系。
3-2 进程启动时 VFS 起到的作用
一个程序启动,虚拟内存的内核态中会存放这个进程的 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 语言工作者,学习的设计思想。
3-3 VFS 的设计思想
这个设计思想,在这里我就直接点破了:是多态。你可能会疑惑:Linux 的底层语言不是 C 语言吗,怎么会有多态呢?待会就带你领会一下大神的设计和 coding 能力。
要想理解多态,我们要回顾一下 C++ 的多态是什么:每个有虚函数的类(类型)有一个虚函数表(不是每个对象)。每个对象有一个指向该虚函数表的指针(vptr)。在 C++ 中,我们有:
- 父类和子类各有自己的虚函数表
- 子类的虚函数表基于父类的虚函数表创建:
- 如果子类重写了虚函数,表中对应位置替换为子类的函数地址
- 如果子类没有重写,表中保留父类的函数地址
在 Linux 中靠 C 语言是如何手搓一个多态的呢:
我们先提供一个基类:struct file_operations,里面全是函数指针:
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 *);
};
里面全是函数指针,这些指针都是指向一个一个具体的文件驱动的函数:
场景 A:如果你操作的是 Ext4 文件系统上的文件(这里的这个可以暂时不用考虑 Ext4 是 什么东西)。
const struct file_operations ext4_file_operations = {
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.open = ext4_file_open,
};
你看,这里就像子类一样,给这些函数指针具体的指向了这些函数。
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) {
struct file *file = fget(fd);
ret = vfs_read(file, buf, count, &pos);
fput(file);
return ret;
}
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) {
if(file->f_op->read){
return file->f_op->read(file, buf, count, pos);
} else if(file->f_op->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 动态调用正确的实现
3-4 整体来看 VFS 的作用
他相当于一个润滑剂,抹平了不同硬件之间的差异。这就是 VFS 的威力:也是他的强悍的地方:
总结
今天的内容还是比较难的,希望大家好好想一想这个文章吧!
回顾我们今天的内容,一切皆文件不只是一句口号,而是 Linux 最核心的设计哲学。通过 VFS 这层抽象,Linux 把硬盘、键盘、显示器、网络、进程信息这些完全不同的东西,都装进了同一个框架里。
- 对上,提供统一的接口 —— 不管你是操作文件还是设备,都只用 open、read、write、close
- 对下,用函数指针实现多态 —— 不同的文件系统各自实现这些接口,VFS 负责把调用分发给正确的实现
正是这种设计,让 Linux 能够轻松支持几十种不同的文件系统,让用户程序无需关心底层细节。好的架构不是把简单的东西搞复杂,而是把复杂的东西变得简单。
希望这篇文章能帮你理解 VFS 的本质。万事开头难,能把这些概念理清楚并写出来,你已经迈出了重要的一步。继续加油!
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online