一篇围绕文件系统抽象展开的学习笔记。
文件系统对上提供“按名字访问持久数据”,对下依赖“按块访问磁盘”。因此,本篇先讨论文件、目录与卷上的基本磁盘结构; 磁盘硬件细节放到后续硬件篇, 权限、一致性与 RAID 放到保护篇, POSIX 接口放到虚拟文件系统与 API 篇。
文件结构
1. 逻辑记录与物理记录
文件系统之所以需要抽象,是因为应用程序按逻辑单位思考数据,而磁盘按物理块进行 I/O。
| 对象 | 含义 |
|---|---|
| 逻辑记录 | 由应用定义的记录单位,如一行、一条日志、一条索引项 |
| 物理记录 | 实际进行 I/O 的块或块组 |
两者之间通常不是一一对应关系。应用可能要读取某个偏移处的几十个字节,但文件系统必须先把包含这些字节的整个磁盘块读入内存,再返回目标范围的数据。
在Unix系统上,逻辑记录的最小单位是字节,而物理记录大小往往与磁盘有关。
2. 文件
文件是操作系统对外存内容提供的统一逻辑视图。从用户角度看,数据必须先组织成文件,才能被命名、保存、共享和保护。
文件从抽象层看,更重要的是区分“文件内容组织”和“文件类型识别”:
| 维度 | 含义 |
|---|---|
| 文件内容组织 | 可以是字节序列,或具有更严格格式的数据格式 |
| 文件类型识别 | 依赖扩展名提示,或维护专门的类型信息 |
Unix 的典型做法是将普通文件视为 8-bit byte sequence。这意味着内核通常不解释普通文件内部的语义,扩展名更多是给工具链和用户空间程序提供 提示,而不是文件系统必须理解的结构。
3. 文件访问方式
文件访问方式直接影响后续的磁盘分配策略。
| 访问方式 | 含义 | 对存储布局的要求 |
|---|---|---|
| 顺序访问 | 按当前位置依次向后读写 | 更适合连续布局或预读 |
| 直接访问 | 按给定偏移访问任意位置 | 要求能快速完成偏移到块地址的映射 |
前者更接近流式读取;后者则要求文件系统能高效完成定位,否则随机读取会退化成大量寻址与I/O。
4. inode 与文件元数据
目录项中的文件名并不是文件本体。多数 Unix 文件系统会把“文件名”、“数据”和“文件元数据”分开保存,最后者通常由 inode 承担。
inode 可以理解为文件控制块在 Unix 文件系统中的典型实现。它至少包含:
| 信息 | 作用 |
|---|---|
| inode 号 | 作为文件在文件系统内部的唯一标识 |
| 类型与权限 | 说明是普通文件、目录还是链接,以及访问权限 |
| 大小与时间戳 | 描述文件状态 |
| 数据块指针 | 指向文件内容所在的块,或指向索引块 |
文件名可以存储于包含该文件的目录中,作为索引条目的键,inode 才是文件系统真正跟踪的对象。
目录结构
1. 目录是一种特殊文件
在 Unix 里,目录通常也被视为文件,只不过它的内容不是普通用户数据,而是一组“文件名 -> inode”的映射。
| 目录条目 | 含义 |
|---|---|
| 文件名 | 用户可读名称 |
| inode 号或 inode 指针 | 指向对应文件元数据 |
因为目录本身也是文件,所以目录自己也有 inode,也会作为上一级目录中的一个条目出现。这就自然形成了多级目录树。
2. 目录条目与多级目录
使用路径索引多级目录的过程往往是“逐级查表”:
- 从某个起点目录开始,如根目录或当前目录。
- 取路径中的下一个名字。
- 在当前目录中查找该名字对应的目录条目。
- 若路径尚未结束,则继续在该条目指向的目录中查找下一个名字;直到路径结尾。
因此,路径解析本质上是一系列目录查找操作。文件系统之所以重视目录缓存,也是因为路径遍历会频繁访问目录。
3. 目录的组织方式
目录条目表本身也需要一种组织结构。常见选择如下:
| 结构 | 特点 | 优点 | 缺点 |
|---|---|---|---|
| 线性列表 | 使用文件名直接作为键 | 实现简单,插入与遍历直接 | 查找依赖线性搜索,大目录性能差 |
| 哈希表 | 使用文件名哈希作为键 | 按名字更快定位目录条目 | 需要处理冲突,目录实现更复杂 |
线性列表适合小目录,因为实现成本低;一旦目录很大,查找成本就会成为问题。 哈希表使用哈希索引列表,再使用文件名在冲突项目中二次索引,在大目录中往往比线性列表更快。
4. 硬链接与软链接
| 类型 | 目录条目指向 | 删除链接会发生 |
|---|---|---|
| 硬链接 | 直接指向同一个 inode | inode 引用计数减一,归零时回收文件 |
| 软链接 | 是一个特殊文件,该文件内容是目标的路径名 | 只删除这个链接文件,不影响目标 inode |
硬链接是在目录中增加一个新的“文件名 -> inode”条目,因此多个目录条目可以指向同一个 inode。这也是删除文件在 Unix 里常被称为 unlink 的原因:删除的是目录项与 inode 之间的关联。
软链接是一种特殊文件,文件内容是目标路径名。它有自己独立的 inode,因此可以跨文件系统,也可以指向不存在的目标;访问软链接时需要额外做一次路径解析。
磁盘结构
1. 分区、卷与原始磁盘
文件系统管理的直接对象通常不是整块磁盘,而是卷。
| 对象 | 含义 |
|---|---|
| 分区 | 磁盘被划分出的连续区域 |
| 卷 | 被某个文件系统管理的存储范围 |
| 原始磁盘 | 没有文件系统、可被系统或应用直接按块使用的区域 |
两者关系并不固定:
| 关系 | 说明 |
|---|---|
| 一个文件系统管理一个分区 | 最常见情况 |
| 一个文件系统管理同一磁盘上的多个分区 | 多个分区组合为一个卷 |
| 一个文件系统管理多个磁盘上的多个分区 | 常见于更复杂的存储管理或 RAID 之上 |
| 一个分区没有文件系统 | 即原始分区,可直接供交换区或数据库使用 |
原始磁盘上的数据被视为连续的字节序列,由使用者解释,而不是由文件系统解释。
2. 卷上的关键结构
一个卷通常至少包含下面几类关键结构:
| 结构 | 作用 |
|---|---|
| 引导控制块 | 如果该卷可引导,则保存启动操作系统所需信息 |
| 卷控制块 | 描述卷整体信息,如块大小、空闲块、inode 数量等 |
| inode 区域 | 在经典 Unix 文件系统中,用于集中保存 inode |
| 数据区 | 存放目录内容与普通文件数据块 |
不同文件系统命名不同,但概念相近。比如 Unix 中常说的 superblock,就是卷控制块:它不是某个单独文件的元数据,而是整个卷的元数据。
3. 文件块分配方法
文件系统必须回答一个实际问题:文件内容应该落到哪些磁盘块上。不同分配方式的差异,在于逻辑块到物理块的映射如何保存。不同分配方式的性能与文件的访问方式相关。
连续分配
连续分配要求每个文件在磁盘上占据一段连续块区间。若文件从块 b 开始,占用 n 个块,那么第 i 个逻辑块的位置就是 b + i。
| 访问方式 | 表现 |
|---|---|
| 顺序访问 | 只需一次磁盘寻址便可连续读取 |
| 直接访问 | 起始 + 偏移可以获取目标地址 |
连续分配对于顺序访问与直接访问都具有良好的性能,但缺点在于存在外部碎片问题,且对于动态分配不友好。
链接分配
链接分配将文件组织为磁盘块链表,每个数据块保存下一个数据块的指针。文件的块可以分散在卷的不同区域。
| 访问方式 | 表现 |
|---|---|
| 顺序访问 | 数据块分散,直接访问需要多次寻址 |
| 直接访问 | 直接访问需要遍历链表,涉及多次磁盘寻址 |
链接分配解决了连续分配的外部碎片问题,但带来了
- 指针存储导致的额外空间占用
- 指针错误导致后续数据块全部丢失
- 直接访问需要遍历链表
为了解决问题1,成簇的链接分配会将多个连续磁盘块合并为一个链表节点,并指保留一个指针。
为了解决问题2/3,便使用了索引分配。
索引分配
索引分配把数据块指针集中保存在索引块中。访问文件时,先通过索引块找到目标数据块,再进行实际 I/O。
| 访问方式 | 表现 |
|---|---|
| 顺序访问 | 与链接分配相似 |
| 直接访问 | 无需遍历链表,只需先访问索引块 |
索引分配不仅解决了连续分配的外部碎片问题,也解决了链接分配导致的遍历链表问题。 但缺点在于,索引分配需要预先分配索引块空间,在文件大小未知的情况下,索引块中保留的数据指针数目是不确定的,因此产生了多级索引。
| 形式 | 含义 |
|---|---|
| 直接索引 | 索引项直接指向数据块 |
| 一级间接 | 索引项指向保存数据块指针的间接块 |
| 二级间接 | 索引项指向保存一级间接块指针的块 |
| 三级间接 | 索引项指向保存二级间接块指针的块 |
Unix inode 中,前12个数据指针是直接索引,后三个指针分别是一级间接,二级间接与三级间接。
组合方案
磁盘分配方式与访问方式有关,也与文件大小有关。为此,有的操作系统将多种分配方式结合,对于小文件采用连续分配,大文件使用索引分配。 也可以在创建文件时声明访问方式,对于只使用顺序访问的文件,使用连续分配,对于可能使用直接访问的文件,使用索引分配。
4. 空闲空间管理
文件系统不仅需要管理已经分配的空间,也需要管理空闲空间。
最常见的记录空闲空间的数据结构如下:
| 方法 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| 位图 | 用一串位表示块是否空闲 | 查找与统计方便,适合扫描连续空闲块 | 大磁盘上位图本身空间占用上升 |
| 空闲链表 | 将空闲块串联成链表 | 结构简单 | 遍历链表可能导致大量 I/O |
空闲链表常见有两个优化:
| 优化 | 思路 |
|---|---|
| 成簇 | 在第一个空闲块中保存多个空闲块地址,前 n-1 个地址直接给出空闲块位置,第 n 个地址指向下一组地址块 |
| 计数 | 记录“起始块指针 + 连续长度”,而不是单个指针,将一段连续空闲块压缩成一个条目 |
小结
本篇介绍了文件系统如何在“按块访问磁盘”的基础上,提供“按名字访问数据”的抽象。
围绕这一目标,讨论了:文件结构,目录结构与磁盘结构。
这些内容共同构成了 Unix 文件系统的基础模型,也为后续讨论权限保护、一致性与文件系统接口提供了基础。