一篇围绕文件系统抽象展开的学习笔记。


文件系统对上提供“按名字访问持久数据”,对下依赖“按块访问磁盘”。因此,本篇先讨论文件、目录与卷上的基本磁盘结构; 磁盘硬件细节放到后续硬件篇, 权限、一致性与 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. 目录条目与多级目录

使用路径索引多级目录的过程往往是“逐级查表”:

  1. 从某个起点目录开始,如根目录或当前目录。
  2. 取路径中的下一个名字。
  3. 在当前目录中查找该名字对应的目录条目。
  4. 若路径尚未结束,则继续在该条目指向的目录中查找下一个名字;直到路径结尾。

因此,路径解析本质上是一系列目录查找操作。文件系统之所以重视目录缓存,也是因为路径遍历会频繁访问目录。

3. 目录的组织方式

目录条目表本身也需要一种组织结构。常见选择如下:

结构特点优点缺点
线性列表使用文件名直接作为键实现简单,插入与遍历直接查找依赖线性搜索,大目录性能差
哈希表使用文件名哈希作为键按名字更快定位目录条目需要处理冲突,目录实现更复杂

线性列表适合小目录,因为实现成本低;一旦目录很大,查找成本就会成为问题。 哈希表使用哈希索引列表,再使用文件名在冲突项目中二次索引,在大目录中往往比线性列表更快。

4. 硬链接与软链接

类型目录条目指向删除链接会发生
硬链接直接指向同一个 inodeinode 引用计数减一,归零时回收文件
软链接是一个特殊文件,该文件内容是目标的路径名只删除这个链接文件,不影响目标 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. 直接访问需要遍历链表

为了解决问题1,成簇的链接分配会将多个连续磁盘块合并为一个链表节点,并指保留一个指针。

为了解决问题2/3,便使用了索引分配。

索引分配

索引分配把数据块指针集中保存在索引块中。访问文件时,先通过索引块找到目标数据块,再进行实际 I/O。

访问方式表现
顺序访问与链接分配相似
直接访问无需遍历链表,只需先访问索引块

索引分配不仅解决了连续分配的外部碎片问题,也解决了链接分配导致的遍历链表问题。 但缺点在于,索引分配需要预先分配索引块空间,在文件大小未知的情况下,索引块中保留的数据指针数目是不确定的,因此产生了多级索引。

形式含义
直接索引索引项直接指向数据块
一级间接索引项指向保存数据块指针的间接块
二级间接索引项指向保存一级间接块指针的块
三级间接索引项指向保存二级间接块指针的块

Unix inode 中,前12个数据指针是直接索引,后三个指针分别是一级间接,二级间接与三级间接。

组合方案

磁盘分配方式与访问方式有关,也与文件大小有关。为此,有的操作系统将多种分配方式结合,对于小文件采用连续分配,大文件使用索引分配。 也可以在创建文件时声明访问方式,对于只使用顺序访问的文件,使用连续分配,对于可能使用直接访问的文件,使用索引分配。

4. 空闲空间管理

文件系统不仅需要管理已经分配的空间,也需要管理空闲空间。

最常见的记录空闲空间的数据结构如下:

方法核心思路优点缺点
位图用一串位表示块是否空闲查找与统计方便,适合扫描连续空闲块大磁盘上位图本身空间占用上升
空闲链表将空闲块串联成链表结构简单遍历链表可能导致大量 I/O

空闲链表常见有两个优化:

优化思路
成簇在第一个空闲块中保存多个空闲块地址,前 n-1 个地址直接给出空闲块位置,第 n 个地址指向下一组地址块
计数记录“起始块指针 + 连续长度”,而不是单个指针,将一段连续空闲块压缩成一个条目

小结

本篇介绍了文件系统如何在“按块访问磁盘”的基础上,提供“按名字访问数据”的抽象。

围绕这一目标,讨论了:文件结构,目录结构与磁盘结构。

这些内容共同构成了 Unix 文件系统的基础模型,也为后续讨论权限保护、一致性与文件系统接口提供了基础。