Skip to content

Commit 2af9868

Browse files
committed
keep learning vfs
1 parent 0b67c51 commit 2af9868

File tree

1 file changed

+195
-16
lines changed

1 file changed

+195
-16
lines changed

为了工作/Linux/内核层/Linux Virtual Filesystem.md

Lines changed: 195 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ categories:
55
- 内核层
66
abbrlink: f548d964
77
date: 2024-12-24 16:05:00
8-
updated: 2024-12-27 17:40:00
8+
updated: 2024-12-30 16:05:00
99
---
1010

1111
<meta name="referrer" content="no-referrer"/>
@@ -48,23 +48,23 @@ Linux 存储栈的整体结构图如下。从上到下分别是:**VFS、通用
4848

4949
# 常见的文件系统模型
5050

51-
任何实现的文件系统都需要包含这几种明确定义的类型:superblock、inode、file 和 dentry。这些也是文件系统的元数据。
51+
任何实现的文件系统都需要包含这几种明确定义的类型:super_block、inode、file 和 dentry。这些也是文件系统的元数据。
5252

5353
模型实体间通过某些 VFS 子系统或内核子系统进行交互:dentry cache(目录项缓存)、inode cache(索引节点缓存)和 buffer cache(缓冲区缓存)。每个实体都被视为对象,具有关联的数据结构和指向方法表的指针。通过替换关联的方法来为每个组件引入特定的行为(类似于 C++ 的多态)。
5454

55-
## superblock
55+
## super_block
5656

57-
**superblock 超级块存储挂载文件系统需要的信息。**具体如下:
57+
**super_block 超级块存储挂载文件系统需要的信息。**具体如下:
5858

5959
1. inode 和块的位置。
6060
2. 文件系统块大小。
6161
3. 最大文件名长度。
6262
4. 最大文件大小。
6363
5. 根 inode 的位置。
6464

65-
磁盘上的 superblock 通常存储在磁盘的第一个块中,即文件系统控制块。
65+
磁盘上的 super_block 通常存储在磁盘的第一个块中,即文件系统控制块。
6666

67-
在 VFS 中,superblock 实体都保留在类型为 `struct super_block` 的结构列表中,方法则保留在类型为 `struct super_operations` 的结构中。
67+
在 VFS 中,super_block 实体都保留在类型为 `struct super_block` 的结构列表中,方法则保留在类型为 `struct super_operations` 的结构中。
6868

6969
## inode
7070

@@ -161,7 +161,7 @@ struct file_system_type
161161

162162
内核中的所有 file_system_type 都是通过一根单向链表组织起来的,register_filesystem() 函数负责将新的 file_system_type 加入到这个链表中。
163163

164-
每个文件系统类型下都挂载了多个文件系统,比如 sda、sdb 都是 ext4 文件系统,这些 superblock 以链表的形式连接到 `file_system_type->fs_supers` 字段中。系统中所有的 superblock 也是通过一根双向链表进行连接。
164+
每个文件系统类型下都挂载了多个文件系统,比如 sda、sdb 都是 ext4 文件系统,这些 super_block 以链表的形式连接到 `file_system_type->fs_supers` 字段中。系统中所有的 super_block 也是通过一根双向链表进行连接。
165165

166166
<img src="https://img-blog.csdnimg.cn/direct/2bdd2062e61a41b182de277573ab32f4.png" alt="image-20241227120407496" style="zoom:75%;" />
167167

@@ -224,7 +224,7 @@ struct dentry *minix_mount(struct file_system_type *fs_type, int flags, const ch
224224
}
225225
```
226226
227-
# VFS 中的超级块
227+
# super_block
228228
229229
## struct super_block
230230
@@ -269,7 +269,7 @@ struct super_block
269269

270270
另外,`void *s_fs_info` 可用于存储文件系统的私有数据,具体实现时候可加入自己的数据。类似于 `struct file``void *private_data`
271271

272-
## 超级块操作
272+
## super_block 操作
273273

274274
超级块操作由 super_block 描述,定义如下:
275275

@@ -413,7 +413,7 @@ inode 用于引用磁盘上的文件。对于进程打开的文件,使用 `str
413413

414414
inode 既存在于内存中的 VFS 结构,也存在于磁盘中(UNIX、HFS 以及 NTFS 等)。VFS 中的 inode 由 `struct inode` 结构表示。和 VFS 中的其他结构一样,`struct inode` 是通用结构,涵盖了所有支持的文件类型的选项,甚至包括那些没有关联磁盘实体的文件类型(例如 FAT 文件系统)。
415415

416-
## inode 结构
416+
## struct inode
417417

418418
`struct inode` 定义如下:
419419

@@ -539,7 +539,7 @@ struct inode
539539
} __randomize_layout;
540540
```
541541

542-
每个文件系统都缓存了一定的 inode 数量到内存中。同一个文件系统的 inode 以双向链表连接起来,挂在 `superblock->s_inodes` 字段中。同时,内核中所有的 inode 被组织在了一个哈希表 inode_hashtable 上。
542+
每个文件系统都缓存了一定的 inode 数量到内存中。同一个文件系统的 inode 以双向链表连接起来,挂在 `super_block->s_inodes` 字段中。同时,内核中所有的 inode 被组织在了一个哈希表 inode_hashtable 上。
543543

544544
<img src="https://img-blog.csdnimg.cn/direct/c2c096ac10cd49878899428229ebfe98.png" alt="image-20241227141716401" style="zoom:75%;" />
545545

@@ -628,24 +628,94 @@ inode 索引节点的相关操作由 `struct inode_operations` 结构描述。
628628
629629
**mount 代表了一个文件系统被挂载到了某个地方。**只有被挂载的文件系统,才能通过 VFS 的目录树进行访问。
630630
631-
一个文件系统可能被多次 mount 到不同的地方,这样一个 superblock 会对应多个不同的 mount 结构,这些 mount 以双向链表的形式组织起来,挂在 superblock->s_mounts 字段。
631+
一个文件系统可能被多次 mount 到不同的地方,这样一个 super_block 会对应多个不同的 mount 结构,这些 mount 以双向链表的形式组织起来,挂在 super_block->s_mounts 字段。
632632
633633
被 mount 的目录称为一个 mount 点。目录也是一个 dentry,mount 通过 mnt_mountpoint 字段指向该 dentry。
634634
635635
<img src="https://img-blog.csdnimg.cn/direct/7b045be8277948d1a013fbc7538c9764.png" alt="image-20241227145405084" style="zoom:75%;" />
636636
637+
挂载点用 mountpoint 结构体表示。所有的挂载点被放到一个哈希表 mountpoint_hashtable 中,以 dentry 为键(Key),mountpoint 为值(T)。
638+
639+
<img src="https://img-blog.csdnimg.cn/direct/3ad9f59e871b42a8b5a7209a9dfe07e9.png" alt="image-20241230094606623" style="zoom:75%;" />
640+
641+
> 注:这里的 mountpoint 是一个结构体。与上面的 mount->mnt_mountpoint 不一样,上面的是一个指针,指向 dentry。
642+
643+
```c
644+
struct mount {
645+
struct hlist_node mnt_hash;
646+
struct mount *mnt_parent;
647+
struct dentry *mnt_mountpoint;
648+
struct vfsmount mnt;
649+
union {
650+
struct rcu_head mnt_rcu;
651+
struct llist_node mnt_llist;
652+
};
653+
#ifdef CONFIG_SMP
654+
struct mnt_pcp __percpu *mnt_pcp;
655+
#else
656+
int mnt_count;
657+
int mnt_writers;
658+
#endif
659+
struct list_head mnt_mounts; /* list of children, anchored here */
660+
struct list_head mnt_child; /* and going through their mnt_child */
661+
struct list_head mnt_instance; /* mount instance on sb->s_mounts */
662+
const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
663+
struct list_head mnt_list;
664+
struct list_head mnt_expire; /* link in fs-specific expiry list */
665+
struct list_head mnt_share; /* circular list of shared mounts */
666+
struct list_head mnt_slave_list;/* list of slave mounts */
667+
struct list_head mnt_slave; /* slave list entry */
668+
struct mount *mnt_master; /* slave is on master->mnt_slave_list */
669+
struct mnt_namespace *mnt_ns; /* containing namespace */
670+
struct mountpoint *mnt_mp; /* where is it mounted */
671+
union {
672+
struct hlist_node mnt_mp_list; /* list mounts with the same mountpoint */
673+
struct hlist_node mnt_umount;
674+
};
675+
struct list_head mnt_umounting; /* list entry for umount propagation */
676+
#ifdef CONFIG_FSNOTIFY
677+
struct fsnotify_mark_connector __rcu *mnt_fsnotify_marks;
678+
__u32 mnt_fsnotify_mask;
679+
#endif
680+
int mnt_id; /* mount identifier */
681+
int mnt_group_id; /* peer group identifier */
682+
int mnt_expiry_mark; /* true if marked for expiry */
683+
struct hlist_head mnt_pins;
684+
struct hlist_head mnt_stuck_children;
685+
} __randomize_layout;
686+
687+
struct mountpoint {
688+
struct hlist_node m_hash;
689+
struct dentry *m_dentry;
690+
struct hlist_head m_list;
691+
int m_count;
692+
};
693+
```
694+
695+
同一个目录可被多个文件系统 mount。这些文件系统会相互覆盖,通过 VFS 只能看到最近那个被 mount 的文件系统。
696+
697+
为了处理这种情况,文件系统中所有的 mount 都被组织到同一个哈希表 mount_hashtable 中。哈希表以 `<mount, dentry>` 为键(Key),以新的 mount 作为值(Value),将其组织起来。
698+
699+
<img src="https://img-blog.csdnimg.cn/direct/d1d769f8a5074874ba5ebe8dfb3724de.png" alt="image-20241230101507978" style="zoom:70%;" />
700+
701+
将上述整理以后,能得到一个整体的架构图:
702+
703+
<img src="https://img-blog.csdnimg.cn/direct/4f0432315c2b4ca8a40b4532ca8f1632.png" alt="image-20241230102023524" style="zoom:70%;" />
704+
637705
# file
638706

639707
**file 结构对应于由进程打开的文件,仅存在于内存中,并与 inode 索引节点关联。**它是最接近用户空间的 VFS 实体。结构字段包含用户空间文件的熟悉信息(访问模式、文件位置等),与之相关的操作由已知的系统调用(read, write 等)执行。
640708

641709
文件操作由 `struct file_operations` 结构描述。文件系统的文件操作使用 `struct inode` 结构中的 i_fop 字段进行初始化。在打开文件时,VFS 使用 inode->i_fop 的地址初始化 `struct file` 结构的 f_op 字段。后续的系统调用使用存储在 file->f_op 中的值。
642710

711+
file 与 inode 的区别在于,一个文件的 inode 在内核中是唯一的,但 file 可以有多个。
712+
713+
<img src="https://img-blog.csdnimg.cn/direct/1329e49089bf44bdacd1fe4b41b1596a.png" alt="image-20241230104309655" style="zoom:70%;" />
714+
643715
# 常规文件索引节点
644716

645717
使用索引节点必须要填充 inode 结构的 i_op 和 i_fop 字段。索引节点的类型决定了他要实现的操作。
646718

647-
## 常规文件索引节点操作
648-
649719
一个例子是 minix 文件系统的对象实例 minix_file_operations 和 minix_file_inode_operations。
650720

651721
Linux 内核实现了 generic_file_llseek()、generic_file_read_iter()、generic_file_write_iter()、generic_file_mmap() 函数,定义了一些通用的 file 操作,具体做了哪些处理可参见源码。
@@ -719,7 +789,7 @@ static int minix_setattr(struct dentry *dentry, struct iattr *attr)
719789
3. 更新索引节点。
720790
4. 使用 block_truncate_page() 函数,将上一个块中未使用的空间填充为零。
721791
722-
## 地址空间操作
792+
# address_space
723793
724794
**进程的地址空间与文件之间有着密切的联系:程序的执行几乎完全是通过将文件映射到进程的地址空间中进行的。**这种方法非常有效且相当通用,也可以用于常规的系统调用,如 read() 和 write()。
725795
@@ -804,6 +874,10 @@ struct address_space_operations {
804874
};
805875
```
806876

877+
address_space 是以基数树进行组织的文件 Cache。以页为单位,`page->index = 该页在文件中的逻辑偏移 / page_size`
878+
879+
<img src="https://img-blog.csdnimg.cn/direct/1121adafe2cf4ad8808244add141f736.png" alt="image-20241230103801350" style="zoom:30%;" />
880+
807881
例如 minix 文件系统的 minix_aops 结构如下:
808882

809883
```c
@@ -881,7 +955,7 @@ int block_write_full_page(struct page *page, get_block_t *get_block, struct writ
881955
typedef int (get_block_t)(struct inode *inode, sector_t iblock, struct buffer_head *bh_result, int create);
882956
```
883957
884-
# dentry 结构体
958+
# dentry
885959
886960
## struct dentry
887961
@@ -911,6 +985,10 @@ struct dentry
911985
};
912986
```
913987

988+
dentry 也有一个全局哈希表进行组织,与它对应的 inode 互指。
989+
990+
<img src="https://img-blog.csdnimg.cn/direct/05f35b213dce4f42b76e03d96f836f08.png" alt="image-20241230102953331" style="zoom:75%;" />
991+
914992
## dentry 操作
915993

916994
dentry 最常见的操作包括:
@@ -1103,6 +1181,107 @@ if (changed)
11031181
printk(KERN_ALERT "%zu 位已更改\n", idx);
11041182
```
11051183

1184+
# 流程分析
1185+
1186+
## 文件系统整体运行流程
1187+
1188+
1. 加载文件系统的内核模块,在 init 中,注册需要的 file_system_type。格式化过程在内核代码没有任何体现。
1189+
1190+
2. 调用 mount(),挂载文件系统,通过 file_system_type 的 mount() 回调加载对应的 super_block。
1191+
1192+
3. 通过 super_block 的 `s_op->alloc_inode()` 分配一个 inode。
1193+
1194+
4. 分配 root 的 dentry,调用 `dentry->d_op` 初始化 dentry。inode_operation 和 dentry_operation 都被记录在 super_block 中,inode 和 dentry 各自在初始化时拷贝了该指针。
1195+
1196+
5. 设置对应的挂载点,mount 过程完成。
1197+
1198+
6. 应用程序 open 文件,从指定路径一级一级向下读取对应的 dentry,直到找到需要的文件的 dentry。查找时优先从 dentry 的全局唯一哈希表上查。如果哈希表没有数据,则调用 `inode->i_op->lookup()` 查找。如果最后发现没有这样的文件,则可能调用 `inode->i_op->atomic_open()``inode->i_op->create()`。在确保有文件的情况下,调用 `file->f_op->open()` 来打开文件。file 的 address_space 和 f_op 由 inode 赋予。
1199+
1200+
7. read 文件。如果文件加了范围锁,则需判断是否有冲突,然后调用 `file->f_op->read()` 或者 `file->f_op->read_iter()`
1201+
1202+
8. write 文件。如果文件加了范围锁,则需判断是否有冲突,然后调用 `file->f_op->write()` 或者 `file->f_op->write_iter()`
1203+
1204+
9. close 文件,先调用 `file->f_op->flush()` 刷数据,然后进行异步关闭操作。
1205+
1206+
> 如果当前进程不在中断上下文且不是 kthread 线程,将该文件的 close() 操作注册到 current->task_works 中。否则,将该文件的 close() 操作注册到全局的 delayed_fput_work 中。最终,两个异步线程会调到相同的回收代码中来。如果文件设置了 FASYNC 标志,调用 `file->f_op->fasync()` 函数,否则调用 `file->f_op->release()` 函数。
1207+
1208+
10. 调用 umount(),卸载文件系统,触发 `super_block->s_op->kill_sb()` 回调。
1209+
1210+
## path lookup 过程
1211+
1212+
path lookup 是通过用户传递的一个绝对或相对路径,来找到对应文件的 inode 的过程。典型的应用如 open() 和 mount() 的查找。
1213+
1214+
**path lookup 总共可分为 ref-walk 和 rcu-walk 两种模式。**
1215+
1216+
RCU 模式对锁的争用更少,并发更好,但不适合所有场景(因为 RCU 可能会导致进程睡眠)。ref 模式是传统的 path lookup 方式,不容易失败。
1217+
1218+
RCU 模式为了检测 dentry 的修改(rename)带来的查询失败,每次查询都会记录 dentry->d_seq,在查询结束后会检测当前 dentry 和父目录 dentry 的 d_seq 是否改变。针对 ref 模式,每次都会锁住当前 dentry 的 d_lock,在成功查询到需要的 dentry 后,会将其引用计数加一。
1219+
1220+
当从当前目录跳转到下一层目录时,RCU 模式会丢弃掉原来的父目录的 d_seq 记录(因为不用关心祖父目录的引用计数),而 ref 模式则会丢弃对当前目录的引用。
1221+
1222+
## mount 过程
1223+
1224+
mount 系统调用定义如下。更多细节参考博客 [https://blog.csdn.net/bingyu880101/article/details/50481507](https://blog.csdn.net/bingyu880101/article/details/50481507)
1225+
1226+
```c
1227+
#include <sys/mount.h>
1228+
1229+
// source:要挂上的文件系统的名字,通常是一个设备名。
1230+
// target:文件系统要挂在的目标目录。
1231+
// filesystemtype:挂载的文件系统类型,如 "ext4"、"btrfs"、"msdos"、"proc"、"nfs"、"iso9660"、"vfat" 等。
1232+
// mountflags:指定文件系统的读写访问标志。
1233+
// data:文件系统持有的参数。
1234+
int mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *_Nullable data);
1235+
```
1236+
1237+
mount 的过程具体如下:
1238+
1239+
1. 根据 dir_name,进行 path lookup。
1240+
1241+
2. 根据 type,查找对应的 file_system_type。
1242+
1243+
3. 拿到 file_system_type,调 mount() 回调,将 dev_name 对应的块设备和 data 传递给它,mount() 回调将建立好对应的 root 的 dentry,super_block 和 root 的 inode。
1244+
1245+
4. 新建 mount 结构体,将 dentry 与 mount 结构体绑定。
1246+
1247+
5. 在 mount_hashtable 中不断查找。如果找到匹配的 mount 结构体,说明该挂载点已被使用,需要继续查找。直到找不到对应的 mount 结构体,说明当前挂载点尚未被占用,系统可以在此挂载新的文件系统。此时系统最后得到有效的 mount 结构体就作为当前 mount 结构体的父挂载点,得到的 dentry 作为当前 mount 结构体的 mountpoint。
1248+
1249+
6. 建立父 mount 与当前 mount 的联系,建立 mountpoint 与当前 mount 的联系。
1250+
1251+
## open 过程
1252+
1253+
open 主要的流程如下:
1254+
1255+
1. 分析 open 传进来的 flags。
1256+
1257+
2. 分配 fd。
1258+
1259+
3. 对文件执行 open、create 等操作(视具体情况而定)。
1260+
1261+
4. 通知监控文件打开的回调。
1262+
1263+
5. 将打开得到的 file 结构体放到 fdtable 的 fd 数组中。
1264+
1265+
open 具体查找文件 inode 的过程,即是 path lookup 的过程。
1266+
1267+
`file->f_op` 有一个 atomic_open() 回调,允许文件系统以与原子的方式查找某个文件。如果该文件不存在,则文件系统尝试创建该文件(当设置了 O_CREAT 标志时)。故在尝试查找和创建文件时,VFS 优先使用 atomic_open(),当文件系统不支持该操作时,才回归到先 lookup,查找失败再 create 的模式。
1268+
1269+
## 通用 read 流程
1270+
1271+
这里不考虑文件的异步读写,也就是 aio 系列的 read 和 write。
1272+
1273+
所谓通用,是指某些文件系统不单独写 read() 或 read_iter() 回调,而是调 VFS 实现的默认 read 函数 generic_file_read_iter()。
1274+
1275+
读分为两种,一种是 DIRECT_IO,另一种是走 address_space。
1276+
1277+
### DIRECT_IO
1278+
1279+
如果这个 read 操作不能陷入等待(NO_WAIT),且要读取的文件范围内有缓存,则返回 -EAGAIN。否则,先将缓存的数据刷下去,再调用 `mapping->a_ops->direct_io` 读取数据。
1280+
1281+
### address_space
1282+
1283+
TODO
1284+
11061285
# 参考文章
11071286
11081287
1. [Linux Kernel Teaching — The Linux Kernel documentation](https://linux-kernel-labs.github.io/refs/heads/master/)

0 commit comments

Comments
 (0)