文件系统

本文最后更新于 2026年9月20日 晚上

文件系统概念

文件系统是什么

  • 对文件系统而言,所有文件都是字节流。文件的格式由具体软件负责。
  • 文件链接
    • 软链接(符号链接):内容指向另外一个文件路径(相对或绝对)。
    • 硬链接:是一个已经存在文件的附加名称,也就是同一个文件的第2个或第N个名称。共享inode ID。
  • 对任意线性空间存储的管理

格式化文件系统与挂载

  • 格式化文件系统相当于在块设备上创建一个文件系统
  • 挂载则是将该文件系统激活(在操作系统目录树呈现)的过程

VFS

  • VFS通过函数指针集合来实现对具体文件系统访问

关键处理流程(Ext2为例)

文件系统的注册

  • Linux中文件系统通常是一个内核模块,在内核模块被加载(初始化)时完成文件系统的注册。

  • 流程:将表示某种类型的文件系统的结构体file_system_type实例添加到一个全局的链表中,该实例主要实现mount()函数。用于用户态执行挂载命令时调用该函数指针。

  • mount()作用:从存储介质读取超级块信息,并创建该文件系统根目录的dentry实例。

  • dentry:(directory entry,即目录项)在VFS中用来连接文件名和inode。

打开文件的流程

  • 所有打开的文件都必须与进程结构体task_struct的成员file结构体关联
  • fdtable 里的 fd 成员,是一个 struct file * 指针数组;而用户态看到的文件描述符 fd,就是这个数组的下标
  • 核心函数do_sys_openat2()流程:
    1. 分配可用文件描述符
    2. 打开文件,返回file指针
    3. 将file指针关联到进程结构体task_struct中文件描述符所在的数据项
  • 路径Path字符串被/拆分为若干部分,每一部分成为一个组件Component
  • 解析文件路径流程:link_path_walk()函数实现字符串路径的解析
    • 通过逐个组件遍历最终打开文件
    • 组件的遍历就是逐渐实例化该组件的inode和dentry的过程。
    • 无缓存时:
      1. 初始化dentry,包含 文件/目录名 字符串
      2. 在某一级目录中:
        • 调用该目录的inode的lookup()函数查找该目录对应子项(目录/文件)
        • 完成子项dentry和inode(由dentry中的一个成员储存)的初始化
    • 有缓存时:无需lookup()直接从缓存中获得dentry和inode

挂载文件系统

  • 用户态发起mount

  • 文件/目录名 <-> dentry <-> inode

  • 对于挂载操作:将dentry中的d_inode成员由原系统的inode替换为新文件系统的inode

  • 关键信息的初始化 在 do_new_mount()函数完成

    • 获取待挂载的文件系统类型数据结构
    • 创建文件系统上下文数据结构体(fc),储存根目录对应的dentry
    • 获取待挂载文件系统的根目录等
  • 完成后续挂载do_new_mount_fc()

    • 会根据fc中的信息创建一个vfsmount的实例(包括文件系统的根目录、超级块数据等信息)
  • 挂载过程中d_set_mounted()函数给dentry增加DCACHE_MOUNTED旗标,标识该子目录挂载了文件系统,打开文件会用到

  • mount结构体包括vfsmount和dentry,父子mount关联->dentry关联(挂载点与待挂载设备)

1
2
3
4
5
6
7
8
9
10
11
12
路径解析
/
↓
mnt
↓
xfs_test ← 发现 mount point
↓
切换文件系统
↓
XFS root dentry
↓
继续解析 XFS 内部路径
  • 路径遍历时,path 不是只保存一个 dentry,而是保存一对 (vfsmount, dentry);遇到挂载点时,这一对会整体“跳”到被挂载文件系统的根。即本次路径遍历从此不再以挂载点dentry为当前位置,而是切换到新文件系统的root dentry。

mount 的核心不是把一个文件系统“复制”到某个目录,而是在“父文件系统的挂载点 dentry”和“被挂载文件系统的根 dentry”之间建立跳转关系。路径遍历到挂载点时,VFS 自动完成这次跳转。

基于索引的文件数据管理

基于间接块

缺点:元数据与数据有固定的对应关系。即便索引连续也要连续存储若干次

基于Extent

改善了间接块,采用“一段连续数据块”的描述。

1
Extent ≈ 起始位置 + 长度

文件的数据管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
连续分配
↓
简单,但扩展困难

链表
↓
扩展方便,但随机访问差

索引
↓
随机访问好,但大文件元数据很多

Extent
↓
用“一段连续区域”压缩索引信息

快照和克隆

  • 快照:可读备份
  • 克隆:可写备份
  • COW(Copy-On-Write):先把旧内容复制出去,再允许新内容修改
  • ROW(Redirect-On-Write):把新内容写到别的新位置

SELinux

传统权限更多是:

1
2
3
用户
↓
能不能访问资源

SELinux 更强调安全策略:

1
2
3
4
5
进程/应用
↓
安全策略数据库
↓
能不能访问资源

因此即使一个进程被攻破,它能访问的资源仍然受到策略限制。

配额(Quota)管理

权限回答:能不能访问?

Quota 回答:最多能用多少空间?

三个概念:

  • Soft Limit:软上限
  • Hard Limit:硬上限
  • Grace Period:宽限期

可以理解成:

  • 使用量 < Soft:正常
  • Soft < 使用量 < Hard:允许,但警告/进入宽限期
  • 使用量 ≥ Hard:禁止继续增长

宽限期则允许暂时超出软上限一段时间。

Ext2 文件系统代码

inode

Ext2 前 11 个 inode 是保留的,其中:inode 2 = 根目录 inode

访问inode路径

1
2
3
4
5
6
7
8
9
10
11
12
13
inode ID
↓
算出属于哪个 Block Group
↓
group descriptor
↓
bg_inode_table
↓
找到本组 inode table
↓
根据 inode ID 计算 table 内 offset
↓
读取 ext2_inode
  • inode = VFS 通用内存结构

  • ext2_inode_info = Ext2 内存结构

  • ext2_inode = Ext2 磁盘结构

目录数据布局

文件名并不存储在 inode 中,在其父目录的数据区

目录文件数据块中保存ext2_dir_entry_2:存储inode ID、目录项长度、名称长度、文件类型、文件名

目录最核心的功能:建立 name → inode 的映射

1
2
3
4
5
6
7
struct ext2_dir_entry_2 {
__le32 inode;
__le16 rec_len;
__u8 name_len;
__u8 file_type;
char name[];
};
特性 ext2_dir_entry_2 dentry (Directory Entry)
存在位置 磁盘(也可以被读入内存的缓冲区,但本质是磁盘数据布局)。 内存(纯粹的内核内存结构,绝不写入磁盘)。
所属层级 具体的文件系统层(Ext2 文件系统)。 虚拟文件系统层(VFS),对所有文件系统通用。
生命周期 永久存在,直到文件或目录被删除。 动态创建和销毁,由内核的 dentry cache(dcache)管理。
包含内容 仅包含基础信息:inode 号、目录项长度、文件名长度、文件类型和文件名。 包含极其丰富的状态信息:子目录/文件列表、父目录指针、引用计数、使用状态、缓存链表等。

完整文件查找流程实例

比如:

1
/f1.txt

先:

1
root inode = 2

然后:

1
2
3
4
5
6
7
8
9
inode 2
↓
inode table
↓
root ext2_inode
↓
i_block[]
↓
根目录 data block

读取目录项:

1
2
3
...
f1.txt → inode 12
...

再:

1
2
3
4
5
6
7
8
9
inode 12
↓
inode table
↓
f1.txt 的 ext2_inode
↓
i_block[]
↓
f1.txt 的 data blocks

完整链条就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"/f1.txt"
↓
root inode 2
↓
root directory data
↓
"name=f1.txt, inode=12"
↓
inode 12
↓
inode table
↓
i_block[]
↓
file data

实现结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
                Ext2 磁盘
│
▼
SuperBlock
“整个文件系统信息”
│
▼
Group Descriptor
“每个 Block Group”
/ \
/ \
▼ ▼
Block Bitmap inode Bitmap
哪些block已使用 哪些inode已使用
│
▼
inode Table
│
inode ID → inode
│
▼
i_block[]
│
▼
Data Blocks
/ \
/ \
普通文件 目录
│
▼
Directory Entry
name → inode ID

挂载时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mount("/dev/xxx")
↓
ext2_mount()
↓
mount_bdev()
↓
ext2_fill_super()
↓
读取 ext2_super_block
↓
初始化 VFS super_block
↓
找到 root inode = 2
↓
构造 struct inode
↓
构造 root dentry
↓
返回给 VFS
↓
接入 Linux 目录树

概念对应

Ext2 概念 含义、作用
SuperBlock 整个文件系统的全局信息
Group Descriptor 某块组的导航信息
Block Bitmap 哪些数据块空闲
inode Bitmap 哪些 inode 空闲
inode Table inode 的磁盘实体
inode number 找 inode 的编号
i_block[] inode → 文件数据
Directory Entry 文件名 → inode number
inode 2 根目录
ext2_mount() 进入块设备通用挂载
ext2_fill_super() 把磁盘文件系统初始化成 VFS 对象
root dentry 把 Ext2 接入 VFS 的根

创建文件

  1. 申请一个 inode
  2. 在父目录中增加一个目录项dentry
  3. 如果要写数据,再申请数据块
普通文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
create("/home/a.txt")
↓
VFS
↓
ext2_create()
↓
┌──────────────────────────────┐
│ 1. ext2_new_inode() │
│ │
│ inode bitmap 找 0 │
│ ↓ │
│ 0 → 1 │
│ ↓ │
│ 得到 inode number │
└──────────────────────────────┘
↓
┌──────────────────────────────┐
│ 2. 初始化 inode │
│ │
│ ext2_set_file_ops() │
│ mark_inode_dirty() │
└──────────────────────────────┘
↓
┌──────────────────────────────┐
│ 3. ext2_add_link() │
│ │
│ 父目录中加入: │
│ "a.txt" → inode number │
└──────────────────────────────┘
↓
┌──────────────────────────────┐
│ 4. d_instantiate_new() │
│ │
│ 关联dentry → inode │
└──────────────────────────────┘

直到之后:

1
write()

才开始需要:

1
2
3
block bitmap
↓
申请 data block
软硬连接
  • 硬链接 → inode 相同:本质只是再增加一个目录项

    与普通文件创建的区别是inode的新旧

    主要做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    找到源文件 inode
    ↓
    增加 inode link count
    ↓
    ext2_add_link()
    ↓
    父目录增加新的目录项
    ↓
    d_instantiate() // 关联dentry与inode
  • 软链接 → 新 inode:新文件

目录文件
1
2
3
4
5
6
7
8
9
10
11
申请新 inode
↓
inode 类型设置为 directory
↓
初始化目录数据
↓
加入 "." 和 ".."
↓
父目录增加目录项
↓
建立 dentry/inode

删除文件

unlink() 首先删除的是目录中的“名字 → inode”关系,而不是马上把数据块全部清零。

1
2
3
4
5
6
7
8
9
10
11
ext2_unlink()
↓
ext2_find_entry()
↓
找到 "a.txt"
↓
ext2_delete_entry()
↓
删除目录关系
↓
inode_dec_link_count()

判断是否要删数据块:直到最后一个引用释放,inode 才有机会被 evict,然后真正释放数据块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ext2_evict_inode()
↓
判断 i_nlink 是否为 0
↓
如果确实需要删除
↓
更新删除时间
↓
写 inode
↓
inode->i_size = 0
↓
ext2_truncate_blocks()
↓
释放文件数据块
↓
删除扩展属性
↓
清理内存 inode
↓
ext2_free_inode()
↓
inode bitmap 1 → 0

创建删除核心函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ext2_new_inode()
= 分配 inode

ext2_add_link()
= 父目录增加 name → inode

ext2_link()
= 创建硬链接

ext2_symlink()
= 创建新 inode,并保存目标路径

ext2_delete_entry()
= 从目录中解除 name → inode

ext2_evict_inode()
= 最后真正释放数据块和 inode

写文件

模式 Page Cache 块设备 I/O 核心思想
DAX 绕过 也尽量绕过传统块 I/O CPU 直接访问持久内存
Direct I/O 绕过 使用 用户数据直接走设备
Normal / Buffered I/O 使用 后续异步执行 先写 Page Cache
Buffer Write
1
2
3
4
5
6
7
8
9
10
11
write()
↓
找到 Page Cache page
↓
需要时分配磁盘 block
↓
用户数据 copy 到 page
↓
page = dirty
↓
write() 可以返回

注意:

此时写成功不等于数据已经进磁盘。

采用O_SYNC后:才会在写入持久化设备后才返回

脏页最终什么时候写回磁盘
触发方式 含义
缓存水线 脏页过多
系统定时 周期性后台写回
用户命令 用户主动要求同步
挂载选项 同步方式挂载
打开文件选项 如 O_SYNC

假设:

1
write(fd, buf, 4096);

而写入的位置以前从来没有分配过磁盘空间。

完整逻辑可以理解为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
用户 write()
↓
VFS
↓
ext2_file_write_iter()
↓
generic_file_write_iter()
↓
generic_perform_write()
↓
ext2_write_begin()
↓
需要处理 logical block N
↓
ext2_get_block()
↓
ext2_get_blocks()
↓
ext2_block_to_path()
│
│ 计算:
│ direct / single / double / triple
↓
ext2_get_branch()
│
│ 查看已有路径
↓
发现物理块不存在
↓
计算缺多少块
↓
ext2_alloc_branch()
↓
Block Bitmap 分配 physical block
↓
建立 logical → physical 映射
↓
准备 Page Cache
↓
用户数据 copy 到 Page Cache
↓
Page 变 Dirty
↓
write() 返回

之后:

Dirty Page
↓
do_writepages()
↓
ext2_writepages()
↓
submit_bio()
↓
磁盘

读文件

page X
1
2
3
4
5
6
7
8
9
10
11
12
13
          要读 page X
│
┌───────┴───────┐
▼ ▼
Page Cache 命中 Page Cache 未命中
│ │
▼ ▼
直接 copy 从磁盘读入 Cache
│ │
└───────┬───────┘
▼
用户空间

预读Readahead 本质上就是根据访问历史预测未来访问。

读写总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
                    inode
│
▼
logical block number
│
▼
┌──── direct / indirect tree ────┐
│ │
▼ ▼
已经有映射 没有映射
│ │
│ ext2_get_blocks()
│ │
│ 分配 physical block
│ │
└───────────────┬────────────────┘
▼
physical block
│
┌─────────┴─────────┐
▼ ▼
READ WRITE
│ │
Page Cache Page Cache
│ │
用户 Dirty Page
│
writeback
│
Disk
概念 含义
i_block[] 文件逻辑块到存储位置的索引
direct / indirect 数据寻址树
iblock 文件逻辑块号,不是磁盘块号
ext2_block_to_path() logical block → 应走哪条索引路径
ext2_get_branch() 当前路径已经存在到哪一级
ext2_get_blocks() 获取/建立 logical → physical 映射
write_begin() 为缓存写准备 page 和底层 block
Page Cache buffered I/O 的核心中间层
Dirty Page 内存已修改,磁盘尚未同步
writepages() 把 Dirty Page 写回设备
Readahead 根据访问模式提前读未来数据
submit_bio() I/O 最终提交给块设备层

文件系统
https://cdro.tech/notes/CS/file-system/
作者
k9Q6CK42
发布于
2026年9月11日
更新于
2026年9月20日
许可协议