Skip to content

Commit 35e2d42

Browse files
committed
learning vfs
1 parent c5285b6 commit 35e2d42

File tree

1 file changed

+364
-5
lines changed

1 file changed

+364
-5
lines changed

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

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

1111
<meta name="referrer" content="no-referrer"/>
1212

13-
参考文章:
13+
笔记摘抄自 [Linux 内核教学 — Linux 系统内核文档](https://linux-kernel-labs-zh.xyz/) 的 VFS 部分,并总结记录。
1414

15-
- [https://linux-kernel-labs.github.io/refs/heads/master/labs/filesystems_part1.html](https://linux-kernel-labs.github.io/refs/heads/master/labs/filesystems_part1.html)
16-
- [https://linux-kernel-labs.github.io/refs/heads/master/labs/filesystems_part2.html](https://linux-kernel-labs.github.io/refs/heads/master/labs/filesystems_part2.html)
15+
# 虚拟文件系统(VFS)
16+
17+
虚拟文件系统(VFS)是内核的组件,处理所有与文件和文件系统相关的系统调用。VFS 是用户与特定文件系统之间的通用接口。这种抽象简化了文件系统的实现,使得各种文件系统更容易集成。各种文件系统通过使用 VFS 提供的 API 来实现文件系统,通用硬件以及 I/O 子系统的通信部分由 VFS 处理。
18+
19+
文件系统按照功能可分为:
20+
21+
1. 磁盘文件系统(ext3、ext4、xfs、fat 以及 ntfs 等)。
22+
2. 网络文件系统(nfs、smbfs/cifs、ncp 等)。
23+
3. 虚拟文件系统(procfs、sysfs、sockfs、pipefs 等)。
1724

1825
<!-- more -->
1926

20-
TODO
27+
Linux 内核使用 VFS 处理目录和文件的层次结构(其实是一棵树)。通过挂载操作,新的文件系统被添加为 VFS 子树。文件系统通常是从其所对应的环境中挂载的(从块类型设备、网络等)。**然而 VFS 可以将普通文件作为虚拟块设备使用,可以将普通文件挂载为磁盘文件系统。**
28+
29+
VFS 的基本思想是提供可以表示任何文件系统文件的单一文件模型。文件系统驱动程序需要遵守公共的基准。这样,内核可以创建包含整个系统的单一目录结构。其中一个文件系统将作为根文件系统,其他文件系统将挂载在其各个目录下。
30+
31+
# 常见的文件系统模型
32+
33+
任何实现的文件系统都需要包含这几种明确定义的类型:superblock、inode、file 和 dentry。这些也是文件系统的元数据。
34+
35+
模型实体间通过某些 VFS 子系统或内核子系统进行交互:dentry cache(目录项缓存)、inode cache(索引节点缓存)和 buffer cache(缓冲区缓存)。每个实体都被视为对象,具有关联的数据结构和指向方法表的指针。通过替换关联的方法来为每个组件引入特定的行为(类似于 C++ 的多态)。
36+
37+
## superblock
38+
39+
**superblock 超级块存储挂载文件系统需要的信息。**具体如下:
40+
41+
1. inode 和块的位置。
42+
2. 文件系统块大小。
43+
3. 最大文件名长度。
44+
4. 最大文件大小。
45+
5. 根 inode 的位置。
46+
47+
磁盘上的 superblock 通常存储在磁盘的第一个块中,即文件系统控制块。
48+
49+
在 VFS 中,superblock 实体都保留在类型为 `struct super_block` 的结构列表中,方法则保留在类型为 `struct super_operations` 的结构中。
50+
51+
## inode
52+
53+
**inode 索引节点存储有关文件的信息。**这里的文件泛指意义上的文件,常规文件、目录、特殊文件(如管道、fifo 等)、块设备、字符设备、链接或可以抽象为文件的任何内容都包括在内。
54+
55+
inode 存储了以下信息:
56+
57+
1. 文件类型。
58+
2. 文件大小。
59+
3. 访问权限。
60+
4. 访问或修改时间。
61+
5. 数据在磁盘上的位置(指向包含数据的磁盘块的指针)。
62+
63+
> inode 通常不包含文件名。文件名由 dentry 存储。一个 inode 可以有多个名称(如多个硬链接文件指向同一个 inode)。
64+
65+
磁盘上的 inode 通常分组存储在一个专用的 inode 区域中,与数据区域分开。
66+
67+
在 VFS 中,inode 实体由 `struct inode` 结构表示,由 `struct inode_operations` 结构定义与之相关的操作。
68+
69+
## file
70+
71+
file 是文件系统模型中距离用户最近的组件。**inode 抽象了磁盘上的文件,file 抽象了进程打开的文件。**与其他结构不同的是,file 结构体在内存中作为 VFS 的实体存在,但没有在磁盘上的物理物对应。
72+
73+
file 存储了以下信息:
74+
75+
1. 文件游标位置。
76+
2. 文件打开权限。
77+
3. 指向关联 inode 的指针(inode 的索引)。
78+
79+
在 VFS 中,file 实体由 `struct file` 结构表示,与之相关的操作由 `struct file_operations` 结构表示。
80+
81+
## dentry
82+
83+
dentry 将 inode 和 文件名关联起来,存储以下信息:
84+
85+
1. 用于标识 inode 的整数。
86+
2. 表示文件名的字符串。
87+
88+
dentry 是目录或文件路径的特定部分。例如,对于路径 `/bin/vi`,为 `/`, `bin``vi` 创建共 3 个 dentry 对象。
89+
90+
dentry 在磁盘上有对应物,但对应关系不是直接的。每个文件系统都有特定的方式维护 dentry。
91+
92+
在 VFS 中,dentry 实体由 `struct dentry` 结构表示,与之相关的操作在 `struct dentry_operations` 结构中定义。
93+
94+
# 注册和注销文件系统
95+
96+
## struct file_system_type
97+
98+
Linux 内核支持很多文件系统,包括 ext2/ext4、reiserfs、xfs、fat、ntfs 等。但在单个系统上,不太可能超过 5/6 个文件系统。文件系统在内核中被实现为内核模块,可以动态的加载和卸载。
99+
100+
描述特定文件系统的结构是 `struct file_system_type`,定义如下:
101+
102+
```c
103+
// linux/fs.h
104+
105+
106+
struct file_system_type
107+
{
108+
// 表示该文件系统的名称(传递给 mount -t 的参数)。
109+
const char *name;
110+
111+
// 指定文件系统必须以哪些标志挂载。例如标志 FS_REQUIRES_DEV,指定 VFS 文件系统需要一个磁盘(而不是虚拟文件系统)
112+
int fs_flags;
113+
114+
// 在加载文件系统时从磁盘中读取超级块到内存中。每种文件系统的函数都是独一无二的。
115+
struct dentry *(*mount)(struct file_system_type *, int, const char *, void *);
116+
117+
// 释放内存中的超级块。
118+
void (*kill_sb)(struct super_block *);
119+
120+
// 如果是内核模块实现,则为 THIS_MODULE。如果直接写在内核中,则为 NULL。
121+
struct module *owner;
122+
123+
struct file_system_type *next;
124+
125+
// 一个列表,包含与该文件系统关联的所有超级块。由于同一文件系统可能会被多次挂载,因此每个挂载点都会有一个单独的超级块。
126+
struct hlist_head fs_supers;
127+
128+
struct lock_class_key s_lock_key;
129+
130+
struct lock_class_key s_umount_key;
131+
132+
...
133+
};
134+
```
135+
136+
在模块加载函数中,将文件系统注册到内核,需要做以下几步:
137+
138+
1. 初始化 `struct file_system_type` 结构体类型的实体,并填充相应的字段以及回调函数。
139+
2. 调用 register_filesystem() 函数。
140+
141+
例如,ramfs 的部分代码如下:
142+
143+
```c
144+
static struct file_system_type ramfs_fs_type = {
145+
.name = "ramfs",
146+
.mount = ramfs_mount,
147+
.kill_sb = ramfs_kill_sb,
148+
.fs_flags = FS_USERNS_MOUNT,
149+
};
150+
151+
static int __init init_ramfs_fs(void)
152+
{
153+
if (test_and_set_bit(0, &once)) return 0;
154+
155+
156+
return register_filesystem(&ramfs_fs_type);
157+
}
158+
```
159+
160+
## mount() 和 kill_sb()
161+
162+
加载文件系统时,内核调用 `struct file_system_type` 结构定义的 mount() 函数。此函数对每个文件系统都是唯一的,进行初始化操作以后返回挂载点的目录 dentry 指针。mount() 函数一般会调用以下函数之一:
163+
164+
1. mount_bdev():挂载存储在块设备上的文件系统。
165+
2. mount_single():挂载一个在所有挂载操作之间是共享实例的文件系统。
166+
3. mount_nodev():挂载不在物理设备上的文件系统。
167+
4. mount_pseudo():用于伪文件系统的辅助函数(如 sockfs、pipefs 等无法被挂载的文件系统)。
168+
169+
这些函数的其中一个参数是指向 fill_super() 函数的指针,该函数在超级块初始化后被调用,借助驱动程序完成超级块的初始化。
170+
171+
卸载文件系统时,内核调用 kill_sb() 函数,执行清理动作。kill_sb() 函数一般会调用以下函数之一:
172+
173+
1. kill_block_super():卸载块设备上的文件系统。
174+
2. kill_anon_super():卸载虚拟文件系统(当请求时生成信息)。
175+
3. kill_litter_super():卸载不在物理设备上的文件系统(信息保存在内存中)。
176+
177+
对没有磁盘支持的文件系统,一个实例是 ramfs 文件系统的 ramfs_mount() 函数:
178+
179+
```c
180+
struct dentry *ramfs_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data)
181+
{
182+
return mount_nodev(fs_type, flags, data, ramfs_fill_super);
183+
}
184+
```
185+
186+
对来自磁盘的文件系统,一个实例是 minix 文件系统的 minix_mount() 函数:
187+
188+
```c
189+
struct dentry *minix_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data)
190+
{
191+
return mount_bdev(fs_type, flags, dev_name, data, minix_fill_super);
192+
}
193+
```
194+
195+
# VFS 中的超级块
196+
197+
## struct super_block
198+
199+
超级块作为物理实体(磁盘上的实体)存在,也作为 VFS 实体(`struct super_block` 结构)存在。超级块仅包含元信息,并用于从磁盘中读取和写入元数据(如 inode、目录项)。超级块及 `struct super_block` 结构包含有关所使用的块设备、inode 列表、文件系统根目录的 inode 指针以及超级块操作的指针的信息。
200+
201+
struct super_block 定义如下:
202+
203+
```c
204+
struct super_block
205+
{
206+
...
207+
208+
dev_t s_dev; // 标识符
209+
unsigned char s_blocksize_bits; // 块大小(以位为单位)
210+
unsigned long s_blocksize; // 块大小(以字节为单位)
211+
unsigned char s_dirt; // 脏标志
212+
loff_t s_maxbytes; // 最大文件大小
213+
struct file_system_type *s_type; // 文件系统类型
214+
struct super_operations *s_op; // 超级块方法
215+
216+
...
217+
unsigned long s_flags; // 挂载标志
218+
unsigned long s_magic; // 文件系统的魔数
219+
struct dentry *s_root; // 目录挂载点
220+
221+
...
222+
223+
char s_id[32]; // 信息标识符
224+
void *s_fs_info; // 文件系统私有信息
225+
};
226+
```
227+
228+
超级块存储了文件系统的全局信息:
229+
230+
1. 所使用的物理设备。
231+
2. 块大小。
232+
3. 文件的最大大小。
233+
4. 文件系统类型。
234+
5. 支持的操作。
235+
6. 魔数(用于标识文件系统)。
236+
7. 根目录的 dentry。
237+
238+
另外,`void *s_fs_info` 可用于存储文件系统的私有数据,具体实现时候可加入自己的数据。类似于 `struct file``void *private_data`
239+
240+
## 超级块操作
241+
242+
超级块操作由 super_block 描述,定义如下:
243+
244+
```c
245+
struct super_operations
246+
{
247+
...
248+
249+
// 写入与 inode 相关的资源。
250+
int (*write_inode)(struct inode *, struct writeback_control *wbc);
251+
252+
// 分配与 inode 相关的资源。
253+
struct inode *(*alloc_inode)(struct super_block *sb);
254+
255+
// 释放与 inode 相关的资源。
256+
void (*destroy_inode)(struct inode *);
257+
258+
// 卸载时调用,释放文件系统私有数据的任何资源(通常是内存)。
259+
void (*put_super)(struct super_block *);
260+
261+
// 在执行 statfs 系统调用时调用(尝试 stat - f 或 df)。此调用必须填充 struct kstatfs 结构的字段,就像在 ext4_statfs() 函数中所做的那样。
262+
int (*statfs)(struct dentry *, struct kstatfs *);
263+
264+
// 在内核检测到重新挂载尝试(挂载标志 MS_REMOUNTM)时调用。大部分情况下,需要检测是否尝试从只读切换到读写或反之。这可以简单地通过访问旧标志(在 sb->s_flags 中)和新标志 (flags 参数) 来完成。data 是由 mount() 发送的表示文件系统特定选项的数据的指针。
265+
int (*remount_fs)(struct super_block *, int *, char *);
266+
267+
...
268+
};
269+
```
270+
271+
# fill_super()
272+
273+
fill_super() 函数用于在文件系统加载时的 mount() 函数中调用,**用于超级块初始化的最后一段,包括填充 struct super_block 字段和根目录的 inode 结构的初始化**
274+
275+
一个实例是 ramfs_fill_super() 函数:
276+
277+
```c
278+
#include <linux/pagemap.h>
279+
280+
281+
#define RAMFS_MAGIC 0x858458f6
282+
283+
284+
static const struct super_operations ramfs_ops = {
285+
.statfs = simple_statfs,
286+
.drop_inode = generic_delete_inode,
287+
.show_options = ramfs_show_options,
288+
};
289+
290+
static int ramfs_fill_super(struct super_block *sb, void *data, int silent)
291+
{
292+
struct ramfs_fs_info *fsi;
293+
struct inode *inode;
294+
int err;
295+
296+
save_mount_options(sb, data);
297+
298+
fsi = kzalloc(sizeof(struct ramfs_fs_info), GFP_KERNEL);
299+
sb->s_fs_info = fsi;
300+
if (!fsi)
301+
return -ENOMEM;
302+
303+
err = ramfs_parse_options(data, &fsi->mount_opts);
304+
if (err)
305+
return err;
306+
307+
sb->s_maxbytes = MAX_LFS_FILESIZE;
308+
sb->s_blocksize = PAGE_SIZE;
309+
sb->s_blocksize_bits = PAGE_SHIFT;
310+
sb->s_magic = RAMFS_MAGIC;
311+
sb->s_op = &ramfs_ops;
312+
sb->s_time_gran = 1;
313+
314+
inode = ramfs_get_inode(sb, NULL, S_IFDIR | fsi->mount_opts.mode, 0);
315+
sb->s_root = d_make_root(inode);
316+
if (!sb->s_root)
317+
return -ENOMEM;
318+
319+
return 0;
320+
}
321+
```
322+
323+
内核提供了一些实现文件系统结构的通用函数,例如上面的 generic_delete_inode() 和 simple_statfs()。
324+
325+
上面的 ramfs_fill_super() 函数填充了超级块中的一些字段,然后读取根 inode 并分配根 dentry。读取根 inode 在 ramfs_get_inode() 函数中完成,包括使用 new_inode() 函数分配新的 inode 并进行初始化。为了释放 inode,使用了 iput(),并使用 d_make_root() 函数分配根 dentry。
326+
327+
VFS 函数通常以超级块、索引节点或包含指向超级块的指针的目录项作为实参,以便能够轻松访问这些私有数据。
328+
329+
# 缓冲区缓存
330+
331+
**缓冲区缓存是处理块设备读写缓存的内核子系统。**磁盘文件系统的功能与虚拟文件系统类似,唯一区别是使用了缓冲区缓存。基本结构体是 `struct buffer_head`,定义如下:
332+
333+
```c
334+
struct buffer_head
335+
{
336+
unsigned long b_state; // 缓冲区的状态。
337+
struct buffer_head *b_this_page; // circular list of page's buffers
338+
struct page *b_page; // the page this bh is mapped to
339+
340+
sector_t b_blocknr; // 已加载或需要保存在磁盘上的设备的块号。
341+
size_t b_size; // 缓冲区大小。
342+
char *b_data; // 指向读取数据或写入数据的内存区域的指针。
343+
344+
struct block_device *b_bdev; // 块设备。
345+
bh_end_io_t *b_end_io; // I/O completion
346+
void *b_private; // reserved for b_end_io
347+
struct list_head b_assoc_buffers; // associated with another mapping
348+
struct address_space *b_assoc_map; // mapping this buffer is associated with
349+
atomic_t b_count; // users using this buffer_head
350+
spinlock_t b_uptodate_lock; // Used by the first bh in a page, to serialise IO completion of other buffers in the page
351+
};
352+
```
353+
354+
以下函数一般会与 `struct buffer_head` 一起使用:
355+
356+
1. `__bread()`:读取具有给定编号和给定大小的块到一个 `struct buffer_head` 中。如果成功,则返回指向 `struct buffer_head` 的指针,否则返回 NULL。
357+
2. sb_bread():与前一个函数相同,但读取的块的大小从超级块中获取,读取的设备也从超级块中获取。
358+
3. mark_buffer_dirty():将缓冲区标记为脏(设置 BH_Dirty 位)。缓冲区将在稍后的时间写入磁盘 (bdflush 内核线程会定期唤醒并将缓冲区写入磁盘)。
359+
4. brelse():在先前将缓冲区写入磁盘(如果需要)后,释放缓冲区使用的内存。
360+
5. map_bh():将 buffer-head 与相应的扇区关联。
361+
362+
# 函数和有用的宏
363+
364+
超级块通常包含以位图(位向量)形式表示的占用块的映射(索引节点、目录条目、数据占用)。为处理这种映射,建议使用以下功能:
365+
366+
1. find_first_zero_bit():用于在内存区域中查找第一个为零的位。size 参数表示搜索区域中的位数。
367+
2. test_and_set_bit():设置位并获取旧值。
368+
3. test_and_clear_bit():删除位并获取旧值。
369+
4. test_and_change_bit():取反位的值并获取旧值。
370+
371+
以下宏定义可用于验证索引节点的类型:
372+
373+
1. S_ISDIR(inode->i_mode):用于检查索引节点是否为目录。
374+
2. S_ISREG(inode->i_mode):用于检查索引节点是否为普通文件(非链接或设备文件)。
375+
376+
# 参考文章
377+
378+
1. [Linux Kernel Teaching — The Linux Kernel documentation](https://linux-kernel-labs.github.io/refs/heads/master/)
379+
2. [Linux 内核教学 — Linux 系统内核文档](https://linux-kernel-labs-zh.xyz/)
21380

0 commit comments

Comments
 (0)