第 23 讲:文件、目录、inode 与 VFS

文件系统解决的不只是“把数据放到磁盘”。它要同时提供持久化、命名、随机/顺序访问、共享、权限、空闲空间分配和崩溃后一致性。对应用它是带名字的字节序列,对 OS 它是一组元数据、目录项和物理块映射。

文件这个抽象包含什么

文件可看成:

  • 文件体:实际字节/记录内容;
  • 文件说明/元数据:类型、大小、所有者、权限、时间、块位置、链接计数等;
  • 文件名:人类使用的名称,通常保存在目录项,未必是文件内部对象的固有字段。

“一切皆文件”的意义是用类似字节流的统一接口表达普通数据、管道和设备,不是说键盘在磁盘上真的有一份普通文本文件。特殊文件的 read/write 最终调用驱动。

逻辑结构、存取方式和物理结构

这三层不要混:

  • 逻辑结构:用户看文件是字节流、固定长记录、可变长记录还是索引记录;
  • 存取方式:顺序读、按偏移直接读、按键索引读;
  • 物理结构:文件的逻辑块如何放到磁盘物理块。

字节流文件可同时由底层索引块实现;“用户看不到记录结构”不等于“磁盘上没有索引”。

文件操作的状态

常见操作包括 create、open、close、read、write、append、seek、truncate、delete、rename 和属性操作。open 的价值是把每次按路径查找和权限检查的结果,转成一个可反复使用的打开对象。

简化的 UNIX 三层关系:

进程 fd 表项
  ↓ 指向(含当前偏移、打开标志)
系统打开文件对象
  ↓ 指向
内存 inode/vnode(文件身份、权限、块映射)

fork 后父子 fd 可指向同一打开对象,因而共享文件偏移;两次独立 open 通常有独立打开对象与偏移,即使最终是同一 inode。

目录:从名字走到文件身份

目录是一种特殊文件,其项将名字映射到 FCB/inode 编号或位置。

  • 单级目录:所有文件同一表,同名冲突且难分类;
  • 两级目录:根下每用户一个目录,解决用户间同名,但用户内仍难层次组织;
  • 树形多级目录:目录项可指向下级目录,形成绝对路径和相对路径。

路径查找不是一次字符串查表

查 /usr/WeiYM/mbox:

  1. 从根 inode 读根目录,找名为 usr 的目录项;
  2. 读 usr 对应 inode/目录数据,找 WeiYM;
  3. 再在 WeiYM 目录里找 mbox;
  4. 每级都要检查当前目录的搜索/执行权限。

从根目录逐级查找目录项和 inode 直到目标文件的过程

目录项和 inode 缓存能避免每次都重读磁盘。目录内可用线性表顺序搜索,也可用哈希/树索引提高大目录查找效率。

文件名长度也受目录项格式约束。早期 MS-DOS 的固定 8.3 名便于目录项定长;FAT 长文件名后来用带特殊属性的附加目录项保存 Unicode 名,并保留一个兼容短名。另一类文件系统直接让目录项变长或把长名放到独立区。长名不是“显示层随便加几个字符”,它会改变目录查找、空间回收和崩溃一致性处理。

FCB 与 inode

FCB(File Control Block)是“OS 为管理一个文件保存的元数据”这一通用概念。UNIX inode 是其典型实现,通常记录:

  • 文件类型与权限模式;
  • 用户/组、链接计数、大小、时间;
  • 数据块的直接与间接指针;
  • 特殊文件对应的设备标识等。

文件名主要在目录项里,所以同一 inode 可被多个名字硬链接;文件改名也可只改目录映射,不搬整个文件数据。

文件物理分配的三种基本方式

连续分配

目录/FCB 记起始块和长度:

physical=start+logical_block.physical=start+logical\_block.
  • 顺序和随机访问都快,元数据少;
  • 需要连续空闲块,形成外部碎片;
  • 文件增长超过预留空间时,可能要整体迁移或用 extent 扩展。

链接分配

每个数据块记下一块号,文件可使用任意分散空闲块。

  • 无外部碎片,增长容易;
  • 找第 kk 块要从头跟链,随机访问慢;
  • 块内指针占空间,一个指针损坏可丢失后续链;
  • 块分散使机械磁盘寻道增加。

FAT 将“下一块号”从每个数据块抽出到集中文件分配表,可在内存中跟链,但大分区的 FAT 会很大,且中心表的一致性极重要。

索引分配

为文件建索引块,第 kk 个索引项直接给逻辑块 kk 的物理块号。

  • 支持直接访问,文件可分散增长;
  • 小文件也可能为索引付整块开销;
  • 单索引块容量有限,需链接索引或多级间接索引支持大文件。

UNIX inode 的直接+一/二/三级间接是一种混合索引:小文件快且少付费,大文件逐级扩展。

空闲空间与目录实现

文件数据如何映射到已分配块,与哪些块仍空闲是两个问题。空闲管理可用位图、空闲 extent 表、空闲链表或成组链接。分配时还应考虑局部性:只“找到一块空闲块”可能把文件打得很散。

目录项可直接包含完整 FCB,也可只保存名字和 inode 号。间接方式使目录项小,文件多名共享 inode 自然;代价是多一步 inode 查找,通常用内存 inode cache 弥补。

硬链接与软链接

硬链接

新目录项的名字直接指向同一 inode:

  • 两个名字地位对等,不存在“原件名”必然更根本;
  • 删除一个名字只减链接计数;链接计数归零且无打开引用时才回收数据;
  • 通常不能跨文件系统,因为 inode 号只在所属文件系统内有意义;
  • 通常限制用户对目录建硬链,否则目录树可形成环。

符号/软链接

软链接是自己的 inode 和文件内容,内容是目标路径。

  • 可跨文件系统、可指目录;
  • 目标删除后可悬空;
  • 解析需再走一次路径,还需检测链接环/过深。

保护、共享与一致性

文件保护可用访问控制矩阵表示主体对对象的 read/write/execute 权限,实现时常拆为:

  • ACL:每个文件列谁有什么权限;
  • capability:每个主体持有哪些对象权利。

同一文件被多进程同时打开时,还需文件锁、记录锁或上层协议保持并发更新一致。读权限只说明允许读,不保证一次读到与其他写者无竞争的业务快照。

课件介绍了备份、定时转储、fsck/scandisk 一致性检查。例如一个块既不在任何文件中又不在空闲表,它就是丢失块;同一块同时出现在两个文件或既分配又标空闲,会造成更危险的重复分配。

块缓存与写入取舍

文件系统把近期磁盘块保留在内存,通常用哈希快速找 (device, block),再用 LRU/变体管理可回收顺序。但元数据与数据的多块更新需要正确的持久顺序:

  • 立即同步写窗口小,但性能差;
  • 延迟写能合并、排序和取消重复写,但崩溃时丢更多尚未落盘修改;
  • 记录元数据日志或写时复制结构可使崩溃后知道哪组更新已完整提交。

日志结构文件系统 LFS

课件的 LFS 不是仅在原位更新前写一份小日志,而是将磁盘看成追加日志:数据块和 inode 修改先在内存聚成大 segment,再连续追加写入。

核心数据结构:

  • segment:成批追加的数据+元数据;
  • inode map:从 inode 号找最新 inode 位置;
  • checkpoint:定期保存一个可恢复的全局起点;
  • segment summary:说明段内各块属于谁,供恢复和清理。

读通过 inode map 找最新位置;写永远在日志尾追加,旧版本块变无效。空闲连续段耗尽后,cleaner 要挑选 segment,把其中仍存活的块搬到新段,回收整段。LFS 用大顺序写换性能,关键代价就是清理和读定位元数据。

FAT 实例

典型 FAT 卷布局包含:

保留/分区引导扇区 | FAT1 | FAT2(镜像) | 根目录区(视版本) | 数据簇区

簇是文件分配单位,由若干扇区组成。FAT 表像整数数组:索引是当前簇号,值是下一簇号、空闲、坏簇或文件结束标记。目录项记文件名、属性、起始簇和大小,从起始簇沿 FAT 跟到结束。

分区首扇区的 DBR/引导扇区包含跳转与引导代码以及 BPB/扩展 BPB;这些字段给出每扇区字节数、每簇扇区数、FAT 数量与大小、根目录/数据区位置等。驱动必须先用它们算出 FAT、目录和数据簇的边界,才能解释后续簇号;MBR 描述“分区在哪里”,DBR/BPB 描述“这个 FAT 卷内部怎么排”。

簇大:FAT 项数少、大文件管理简单,但小文件尾部内部碎片大;簇小则反之。FAT12 的 12 位表项还会跨字节交错打包,课件展示了奇/偶簇号用不同半字节取值;理解目标是“表项宽度不是整字节,所以相邻两项共享 3 字节”,不是死背一段汇编。

Ext2 实例

Ext2 将卷分成多个块组,每组尽量包含本组的:

  • 超块/超块备份与块组描述符;
  • 块位图和 inode 位图;
  • inode 表;
  • 数据块。

块组让 inode、目录和文件数据尽量就近,减少机械寻道;同时局部位图与元数据使管理更可分割。超块保存文件系统整体尺寸、块大小、空闲数、状态和魔数(课件给 Ext2 魔数 0xEF53);块组描述符则定位各组位图和 inode 表。

VFS:为什么不同文件系统都能 open

VFS 在具体 FAT/Ext2/NFS 与系统调用之间定义通用对象与操作:

  • superblock:一个已挂载文件系统实例;
  • inode/vnode:文件对象的通用元数据和操作集;
  • dentry:路径名称与 inode 关系的缓存对象;
  • file:一次打开实例,含偏移和打开标志;
  • file_system_type:注册具体文件系统类型和挂载/读超块方法。

挂载时,VFS 根据类型找到实现,从块设备读取具体超块,建立 VFS superblock 和根 dentry,再把该根连到挂载点。之后一条路径越过挂载点时,VFS 无缝切换到另一文件系统实例。

通用 read 最终通过操作函数指针进入当前文件系统的实现,所以应用不需要为 Ext2、FAT、NFS 各写一套系统调用。

文件系统计算题的路线

已知字节偏移找逻辑块

块大小 BB,文件偏移 xx:

logical_block=⌊xB⌋,in_block=x mod B.logical\_block=\left\lfloor\frac{x}{B}\right\rfloor, \qquad in\_block=x\bmod B.

再判断该逻辑块落在直接、一级还是多级间接范围,按层查块号。不要把“第 12 块”与从 0 编号的逻辑块 12 混用。

计算最大文件

若每索引块可放 K=B/pointer_sizeK=B/pointer\_size 个指针,dd 个直接指针,各一个一/二/三级间接:

max_size=B(d+K+K2+K3).max\_size=B(d+K+K^2+K^3).

这只是索引结构上限;块号位宽、文件大小字段位宽、卷总块数可能更早构成上限。

连起这一讲

路径将人类名字逐级映射到 inode,inode 将文件逻辑块映射到物理块,块设备驱动把块请求交给控制器。VFS 让前半段接口对具体文件系统统一,缓存和日志让后半段既不必每次同步等磁盘,又能在崩溃后找回一致边界。

所以从 open("/a/b") 到真正读出字节,不是一次查表,而是名称、身份、逻辑块、物理块和设备请求的连续翻译。

评论