虚拟文件系统
嵌入式学习Day3
1.虚拟文件系统简介
虚拟文件系统(VFS, Virtual File System),位于用户空间程序和底层具体的物理文件系统(如 ext2, ext3, FAT32, NTFS 等)之间,充当了一个抽象层。
在 Linux 中,VFS 提供了一套通用的文件系统接口。不管底层的硬件是普通的机械硬盘(ext3 格式)、老式的软盘(ext2 格式)、还是 U盘,VFS 都把它们抽象成了统一的“文件”。
2.VFS 带来的好处
- 对开发者极其友好: 程序员写应用代码时,完全不需要考虑数据最终存放在哪里。大家都用同一套
open/read/write接口,这也就是 Linux 中“一切皆文件”哲学的基础 - 跨文件系统的无缝协作: 就像图中那样,标准的系统调用可以直接跨越不同存储介质、不同文件系统格式来移动数据。
- 极强的扩展性: 如果未来发明了一种全新的存储介质或文件系统,只要在内核中为其编写了符合 VFS 规范的代码接入 VFS 层,那么现有的所有应用程序(如 cp, cat, vi 等)无需重写,甚至无需重新编译就能直接使用这个新文件系统。
3.write()调用的流程
当你在应用层写下了ret = write(fd, buf, len);其从应用层到物理介质的流程如下
- 用户空间(前端): 应用程序发起标准的 write() 系统调用,请求将 buf 里的数据写入文件描述符 fd。应用程序只管发指令,无需知道底层是哪种磁盘。
- VFS 层(路由调度): 这个调用进入内核后,首先由 VFS 层的通用系统调用 sys_write() 接管。sys_write() 会根据 fd 解析出该文件属于哪个具体的底层文件系统,并找到对应的操作函数表(就是驱动中的file_operation结构体)。
- 具体文件系统层(后端): VFS 调起该具体文件系统(比如 ext4 或 FAT)实际提供的“写方法”
- 物理介质: 最终,由这个特定的写方法将数据转化为底层的 I/O 请求,写入到具体的物理介质(如磁盘、Flash)中
4.文件系统的基本概念
4.1超级块 (Superblock)
-
内核结构体:
struct super_block -
角色定位: 文件系统中极其关键的一个数据结构。它专门用来存储这个文件系统的全局控制信息
-
存储内容: 包含了文件系统级别的整体元数据,比如块大小、最大文件长度、文件系统的魔数(Magic Number,用于识别文件系统类型),以及指向该文件系统根目录的 dentry 指针。
-
核心动作: 它包含一个 s_op 指针,指向 super_operations 结构体,里面定义了内核操作这个底层文件系统的方法(如
alloc_inode分配 inode,write_super同步超级块等)。
4.1.1超级块结构体中重要的部分
- 文件系统基础属性
s_type:记录文件系统类型(比如这是一块 ext4 还是 FAT32 格式的盘)。s_magic:文件系统的幻数。内核通过读取这个特殊数字来验证磁盘上的数据是否真的是这种类型的文件系统。s_blocksize:以字节为单位的块大小。这是文件系统进行 I/O 操作的基本单位。s_maxbytes:该文件系统支持的文件大小上限。
- 连接 VFS 其他三大对象的“枢纽”超级块是整个文件系统的源头,它通过指针连接着其他的核心结构:
s_root:目录挂载点。这是一个极其关键的指针!它指向该文件系统根目录的 dentry(目录项)。当进程访问这个文件系统时,都是从这个根节点开始“顺藤摸瓜”的。s_inodes:inodes 链表。这个链表把属于该文件系统的所有活跃的索引节点(inode)全部串联了起来,方便内核统一管理。
- 操作方法的“接口表”
s_op:超级块方法。这个指针指向一个名为super_operations的结构体(可以理解为面向对象编程里的“虚函数表”)。里面装满了针对这个底层文件系统的各种操作函数,比如怎么分配一个新 inode、怎么把超级块的变动写回磁盘等。
- 硬件层面的映射
s_bdev:相关的块设备。指向这个文件系统所驻留的底层物理设备(比如某个硬盘分区)。s_dev:设备标识符。用于唯一标识该设备。s_mtd:存储磁盘信息。通常用于像 Flash 闪存这类 MTD(Memory Technology Device)设备
- 状态维护与管理
s_list:指向所有超级块的链表。Linux 会把系统中所有挂载的文件系统的超级块通过这个链表串在一起,形成一个全局列表。s_dirt:修改(脏)标志。如果内存中的超级块数据被修改了(比如分配了新块,空闲块数量变了),这个标志会被置位。内核就知道稍后需要把它同步写回磁盘,防止数据丢失。s_fs_info:文件系统特殊信息。这是一个 void * 类型的万能指针。因为不同的文件系统(如 ext4 和 NTFS)有各自特有的私有数据,VFS 通用模型里放不下,就可以把这些“私货”挂在这个指针下面。这体现了 VFS 极强的包容性。
4.2索引节点 (Inode)
- 内核结构体:
struct inode - 角色定位: 它代表了物理磁盘上的一个实际存在的文件或目录。只要文件存在,它的 inode 就存在。
- 存储内容: 包含了文件几乎所有的元数据:拥有者(
UID/GID)、访问权限(读写执行)、文件大小、各种时间戳(创建、修改、访问时间),以及指向磁盘上实际数据块的指针。 - 关键特性: Inode 里面绝对不包含文件名。在 Linux 中,文件名只是给人看的,系统只认 inode 号。这也解释了为什么 Linux 可以有“硬链接”(多个不同的文件名指向同一个 inode)。
4.2.1索引节点结构体中重要的部分
- 基础元数据 这些是平时用 ls -l 或 stat 命令看到的最直观的信息:
i_ino:节点号(这就是档案袋的唯一编号)。i_uid和i_gid:拥有者的用户ID和组ID。i_mode:访问权限(比如经常挂在嘴边的 777 或 644)。i_size:文件大小(以字节为单位)。i_atime,i_mtime,i_ctime:分别对应最后访问时间、最后修改时间和最后改变(属性改变)时间。
i_nlink掌控文件生死
i_nlink(硬链接数):这个字段极其关键。之前提到过,你可以给同一个 inode 贴上多个不同的文件名(硬链接)。内核怎么知道什么时候该真正清空硬盘数据?就是靠它。每创建一个硬链接,i_nlink加 1;每删除一个文件名(执行 rm,底层调用 unlink),i_nlink减 1。只有当i_nlink变为 0 时,内核才会真正释放磁盘空间。
- 完美诠释“一切皆文件”的核心设计
i_pipe:管道信息i_bdev:块设备驱动i_cdev:字符设备驱动
如果你在写一个字符设备驱动(比如 V4L2 摄像头节点或某种传感器的控制节点 /dev/my_device),用户空间的程序之所以能像读写普通文件一样去操作你的硬件,就是因为 VFS 在打开这个设备文件时,会通过这个 inode 里的 i_cdev 指针,精准地路由到你注册在内核里的具体驱动代码。
- 建立联系的“纽带”
-
i_sb:相关的超级块。这个指针让 inode 能随时找到自己的“老家”(它属于哪个文件系统)。 -
i_dentry:目录项链表。因为一个 inode 可能对应多个硬链接(多个 dentry),这个链表就把所有指向它的目录项都串了起来。
4.3目录项 (Dentry)
- 内核结构体:
struct dentry - 角色定位: 它代表了路径中的一个逻辑组成部分。它的主要任务是把文件名和对应的 Inode 关联起来。
- 运行机制: 比如路径 /usr/bin/gcc,这里面的 /、usr、bin、gcc 在内核中都会被解析成一个个单独的 dentry 对象。
- 性能利器: 频繁解析路径是非常耗时的。因此,Linux 并没有把 dentry 结构持久化存储在磁盘上,而是在内存中动态创建,并维护了一个庞大的目录项缓存(dcache)。当你第二次访问同一个路径时,内核直接从内存的 dcache 中秒级命中,极大提升了文件系统的响应速度。
4.3.1目录项结构体中重要的部分
1.搭建“目录树”的结构基石
d_parent:父目录的目录项对象。它指向上级目录。当你在终端输入 cd .. 时,内核在底层很大程度上就是顺着这个指针往上找的。d_subdirs:子目录链表。如果当前 dentry 是一个目录,这个链表就挂着它下面所有的子文件和子目录。内核遍历目录时(比如执行 ls),就会去扫这个链表
- 连接“名字”与“灵魂”的桥梁(核心绑定)
*d_inode:相关联的索引节点。这是整个结构体中最关键的一个指针!它把当前的“路径名”和真正的“物理文件控制块(Inode)”死死绑定在一起。顺着它,内核就能找到文件的读写权限和数据块位置。d_name:目录项名称。记录了这个节点的字符串名字(比如 “vi” 或 “bin”)。d_iname:短文件名。这是一个极其精妙的性能优化设计。由于 Linux 中很多文件名都很短,内核为了避免为每个短名字都去单独分配一块内存(那样既慢又容易产生碎片),直接在结构体内部预留了一个字符数组。只要名字长度不超过DNAME_INLINE_LEN_MIN,就直接塞进这个数组里。
- 支撑“目录项高速缓存 (dcache)”的生命线(性能与内存管理)
-
d_count:使用计数。它记录了当前有多少个进程正在使用这个目录项。如果一个文件正被打开,它的d_count就大于 0,内核绝对不会销毁它。 -
d_lru:未使用的链表。当一个文件被关闭, -
d_count降为 0 时,内核并不会立刻在内存中删掉这个 dentry,而是把它挂到这个 LRU(最近最少使用)链表上。下次你再访问同一个路径时,内核直接从这个链表里把它“捞”出来复用,这就是 Linux 文件操作速度极快的原因。 -
*d_sb(文件的超级块)让这个目录项时刻知道自己属于哪个底层文件系统;而 *d_op(目录项操作指针)则提供了一组操作这个目录项的底层函数接口。
4.4文件对象 (File)
- 内核结构体:
struct file - 角色定位: 它代表了一个进程已经打开的文件。它是这四个概念中唯一和“用户态进程”直接绑定的。
- 存储内容: 当你在用户空间调用
open()时,内核就会在内存中创建一个 file 对象。它保存了该进程对这个文件的操作状态:比如当前的读写偏移量(f_pos)、打开模式(只读、读写等)。 - 联系: 用户空间的“文件描述符(fd)”本质上就是一个整数索引,它指向进程打开文件表中的一个项,而这个项就指向这个
struct file对象
4.4.1文件对象结构体中重要的部分
1. f_pos
- 字段定义:
f_pos(文件当前的位移量/文件指针)。 - 底层逻辑: 当你在用户空间调用 read(fd, buf, 100) 时,内核怎么知道该从哪里开始读这 100 个字节?就是靠
f_pos。每次读写成功后,内核会自动把f_pos加上实际读写的字节数。
2. *f_op
-
字段定义:
*f_op(文件操作表)。 -
底层逻辑: 这个指针指向
struct file_operations。这是 Linux 中实现面向对象编程“多态”机制的灵魂。VFS 层的通用sys_read()函数根本不知道底层是什么硬件,它只会无脑地调用 file->f_op->read(…)。
3. f_path
-
字段定义:
f_path(包含目录项)。 -
底层逻辑:
struct file对象代表的是一次“动态的交互会话”,它本身不保存文件的物理信息。f_path是一个结构体,里面包含了一个极其重要的指针:dentry。 -
开发场景: 如果你的驱动程序在执行某个操作时,突然需要获取这个文件对应的真实 inode 信息(比如想查一下这个设备节点的权限或者主次设备号),你就可以通过
file->f_path.dentry->d_inode这条路径,顺藤摸瓜地找到代表物理实体的 inode 对象。
4. *private_data
-
字段定义:
*private_data(tty 设备驱动的钩子,实际上不仅限于 tty)。 -
底层逻辑: 这是一个 void * 类型的万能指针。VFS 框架完全不关心这里面存了什么,它是专门留给驱动开发者存放私有上下文数据的。
-
开发场景: 在 RV1126 或 RK3576 这类 SoC 上写一个复杂的字符设备驱动,里面包含了很多硬件寄存器状态、自旋锁、等待队列等。为了避免使用丑陋且不安全的全局变量,标准做法是:
-
在驱动的 open 函数里,kmalloc 动态分配一个包含你所有硬件状态的自定义结构体。
-
把这个结构体的地址赋给 file->private_data。
-
随后当用户空间调用 read、write 或 ioctl 时,内核会把这个 file 指针传给你。你只需要把 file->private_data 强转回你的结构体类型,瞬间就能恢复当前的硬件上下文环境。这让你的驱动能够完美支持多线程和多实例并发打开。
-
5. f_flags 与 f_mode
-
字段定义:
f_flags(当打开文件时所指定的标志) 和 f_mode (文件的访问模式)。 -
底层逻辑: 当你在应用层调用
open("/dev/video0", O_RDWR | O_NONBLOCK)时,这里的读写意图和阻塞标志,就会被内核妥善保存在这两个字段中。 -
开发场景:
* f_mode:内核在调用你的驱动 read 函数前,会先检查 f_mode 确认用户有没有读权限(比如是不是用 O_WRONLY 只写模式打开的却尝试去读),如果没有,内核直接就拦截报错了,根本不会走到你的驱动代码里。f_flags:在实现设备驱动的 read 函数时,你往往需要检查 file->f_flags & O_NONBLOCK。如果用户指定了非阻塞模式(O_NONBLOCK),且此时硬件刚好没有数据可读,你的驱动绝不能调用休眠函数把进程挂起(这会导致系统卡死),而是必须立刻返回 -EAGAIN 错误码,让用户态程序过会儿再来试。