文件系统
本文最后更新于 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()流程:- 分配可用文件描述符
- 打开文件,返回
file指针 - 将
file指针关联到进程结构体task_struct中文件描述符所在的数据项
- 路径
Path字符串被/拆分为若干部分,每一部分成为一个组件Component - 解析文件路径流程:
link_path_walk()函数实现字符串路径的解析- 通过逐个组件遍历最终打开文件
- 组件的遍历就是逐渐实例化该组件的
inode和dentry的过程。 - 无缓存时:
- 初始化
dentry,包含 文件/目录名 字符串 - 在某一级目录中:
- 调用该目录的
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 | |
- 路径遍历时,
path不是只保存一个dentry,而是保存一对(vfsmount, dentry);遇到挂载点时,这一对会整体“跳”到被挂载文件系统的根。即本次路径遍历从此不再以挂载点dentry为当前位置,而是切换到新文件系统的root dentry。
mount 的核心不是把一个文件系统“复制”到某个目录,而是在“父文件系统的挂载点 dentry”和“被挂载文件系统的根 dentry”之间建立跳转关系。路径遍历到挂载点时,VFS 自动完成这次跳转。
基于索引的文件数据管理
基于间接块
缺点:元数据与数据有固定的对应关系。即便索引连续也要连续存储若干次
基于Extent
改善了间接块,采用“一段连续数据块”的描述。
1 | |
文件的数据管理
1 | |
快照和克隆
- 快照:可读备份
- 克隆:可写备份
- COW(Copy-On-Write):先把旧内容复制出去,再允许新内容修改
- ROW(Redirect-On-Write):把新内容写到别的新位置
SELinux
传统权限更多是:
1 | |
SELinux 更强调安全策略:
1 | |
因此即使一个进程被攻破,它能访问的资源仍然受到策略限制。
配额(Quota)管理
权限回答:能不能访问?
Quota 回答:最多能用多少空间?
三个概念:
- Soft Limit:软上限
- Hard Limit:硬上限
- Grace Period:宽限期
可以理解成:
- 使用量 < Soft:正常
- Soft < 使用量 < Hard:允许,但警告/进入宽限期
- 使用量 ≥ Hard:禁止继续增长
宽限期则允许暂时超出软上限一段时间。
Ext2 文件系统代码
inode
Ext2 前 11 个 inode
是保留的,其中:inode 2 = 根目录 inode
访问inode路径
1 | |
inode = VFS 通用内存结构
ext2_inode_info = Ext2 内存结构
ext2_inode = Ext2 磁盘结构
目录数据布局
文件名并不存储在 inode 中,在其父目录的数据区
目录文件数据块中保存ext2_dir_entry_2:存储inode ID、目录项长度、名称长度、文件类型、文件名
目录最核心的功能:建立 name → inode 的映射
1 | |
| 特性 | ext2_dir_entry_2 |
dentry (Directory Entry) |
|---|---|---|
| 存在位置 | 磁盘(也可以被读入内存的缓冲区,但本质是磁盘数据布局)。 | 内存(纯粹的内核内存结构,绝不写入磁盘)。 |
| 所属层级 | 具体的文件系统层(Ext2 文件系统)。 | 虚拟文件系统层(VFS),对所有文件系统通用。 |
| 生命周期 | 永久存在,直到文件或目录被删除。 | 动态创建和销毁,由内核的 dentry cache(dcache)管理。 |
| 包含内容 | 仅包含基础信息:inode 号、目录项长度、文件名长度、文件类型和文件名。 | 包含极其丰富的状态信息:子目录/文件列表、父目录指针、引用计数、使用状态、缓存链表等。 |
完整文件查找流程实例
比如:
1 | |
先:
1 | |
然后:
1 | |
读取目录项:
1 | |
再:
1 | |
完整链条就是:
1 | |
实现结构
1 | |
挂载时:
1 | |
概念对应
| 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 的根 |
创建文件
- 申请一个
inode - 在父目录中增加一个目录项
dentry - 如果要写数据,再申请数据块
普通文件
1 | |
直到之后:
1 | |
才开始需要:
1 | |
软硬连接
硬链接 → inode 相同:本质只是再增加一个目录项
与普通文件创建的区别是
inode的新旧主要做:
1
2
3
4
5
6
7
8
9找到源文件 inode
↓
增加 inode link count
↓
ext2_add_link()
↓
父目录增加新的目录项
↓
d_instantiate() // 关联dentry与inode软链接 → 新 inode:新文件
目录文件
1 | |
删除文件
unlink() 首先删除的是目录中的“名字 →
inode”关系,而不是马上把数据块全部清零。
1 | |
判断是否要删数据块:直到最后一个引用释放,inode 才有机会被 evict,然后真正释放数据块。
1 | |
创建删除核心函数
1 | |
写文件
| 模式 | Page Cache | 块设备 I/O | 核心思想 |
|---|---|---|---|
| DAX | 绕过 | 也尽量绕过传统块 I/O | CPU 直接访问持久内存 |
| Direct I/O | 绕过 | 使用 | 用户数据直接走设备 |
| Normal / Buffered I/O | 使用 | 后续异步执行 | 先写 Page Cache |
Buffer Write
1 | |
注意:
此时写成功不等于数据已经进磁盘。
采用
O_SYNC后:才会在写入持久化设备后才返回
脏页最终什么时候写回磁盘
| 触发方式 | 含义 |
|---|---|
| 缓存水线 | 脏页过多 |
| 系统定时 | 周期性后台写回 |
| 用户命令 | 用户主动要求同步 |
| 挂载选项 | 同步方式挂载 |
| 打开文件选项 | 如 O_SYNC |
假设:
1 | |
而写入的位置以前从来没有分配过磁盘空间。
完整逻辑可以理解为:
1 | |
读文件
1 | |
预读Readahead 本质上就是根据访问历史预测未来访问。
读写总结
1 | |
| 概念 | 含义 |
|---|---|
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 最终提交给块设备层 |