跳转到内容

FFS — 把磁盘几何写进文件系统

是什么

FFS(Fast File System,1984)是 Berkeley 团队为 4.2BSD 重写的 Unix 文件系统,把原 v7 文件系统的磁盘吞吐从 约 2-5% 拉到 约 47%——同样硬件,约 10 倍 提速。日常类比:原文件系统像把家里所有书随手塞抽屉,每次找书都要跑遍整屋;FFS 把书按主题分柜、相关书放一起、目录贴在柜门上。

核心做法只有一句话:让文件系统知道磁盘是个机械转盘——盘片在转、磁头要寻道、相邻的扇区读起来比远处的快。把这件事写进分配算法,就有了 FFS。

这套布局思想一路活到今天——Linux ext2/3/4 的 block group、BSD UFS、XFS、HFS+ 全都是 FFS 的徒孙。

为什么重要

不理解 FFS,你解释不了下面这些事:

  • 为什么 Linux 装机时分区表里有 “block group”——这是 FFS 的 柱面组(cylinder group) 改名而来
  • 为什么删一个大文件后 df 立刻可见——空闲块位图本地化在每个组里
  • 为什么同目录的文件 cp 起来比跨目录快——FFS 把同目录文件分配到同一组里,物理位置相近
  • 为什么 SSD 普及后文件系统设计才大变(F2FS、btrfs)——FFS 假设盘是会转的,SSD 不转

FFS 是系统软件第一次正面承认硬件物理特性的标志性工作。

核心要点

FFS 改了 4 件事:

  1. 大块(block)+ 碎片(fragment):v7 块固定 512B,太小,每次磁头寻道开销摊不开。FFS 把块加到 4KB 或 8KB,单次 I/O 拿更多数据。但小文件(< 4KB)用整块就浪费——于是把块切成 8 个 fragment(1KB 一片),小文件用 fragment 拼。

  2. 柱面组(cylinder group):磁盘按柱面(cylinder = 同半径上下所有磁道)切成几十组,每组自带 inode 表 + 空闲块位图 + superblock 副本。元数据本地化——读一个 inode 不必跨整盘。

  3. 局部性分配策略:同目录的 inode 尽量放同一柱面组;inode 和它的数据块尽量同组;新目录尽量放在 inode 较空的组。基于一个朴素事实——同目录的文件大概率会被同时访问。

  4. rotational layout(转动布局):知道磁盘 RPM 和 CPU 处理速度,把文件的连续块按 “skip N 个 sector” 放——CPU 处理完块 1 时,块 2 刚好转到磁头下,不用等一整圈

四件事加起来就是 FFS。

实践案例

案例 1:v7 的悲剧

v7 文件系统的 inode 表全在磁盘最前面,数据块散在剩下空间。读一个文件:磁头跳到前面读 inode,再跳到中段读数据,再跳回前面读下一个 inode……一次目录遍历可能 跨 100 次磁道。在 1980 年的硬盘上,每次寻道约 30ms,这就是 2-5% 带宽的根因。

案例 2:FFS 的柱面组怎么救场

磁盘 ─┬─ 柱面组 0:[superblock 副本][inode 表][位图][数据块]
├─ 柱面组 1:[superblock 副本][inode 表][位图][数据块]
├─ 柱面组 2:...

/home/jason/a.txt

  1. /home 的 inode → 在组 N
  2. /home/jason 的 inode 也分配在组 N(局部性策略)
  3. a.txt 的 inode 也在组 N
  4. a.txt 的数据块也在组 N

整个过程磁头几乎不离开组 N,寻道时间从 100×30ms 降到几次。

案例 3:fragment 怎么处理小文件

8KB 块 + 1KB fragment:

  • 写 500B 文件:占 1 个 fragment(1KB),浪费 500B
  • 写 3KB 文件:占 3 个 fragment(3KB),浪费 0
  • 写 9KB 文件:占 1 个块(8KB)+ 1 个 fragment(1KB),浪费 0

小文件不再吃整块,又不放弃大块的连续读优势。这一招让 FFS 在大块 + 小浪费之间找到平衡——一份代码同时优化大文件吞吐和小文件密度

案例 4:rotational layout 在硬件上的样子

假设磁盘转速 3600 RPM(60 圈/秒),一圈有 32 个扇区,磁头每秒读 1920 个扇区。如果 CPU 处理一个块需要的时间相当于盘转过 4 个扇区——

  • 朴素布局:连续放块 1, 块 2, 块 3——CPU 处理完块 1 时盘已经转过块 2 起点,要等 几乎一整圈 才能回来读
  • FFS 布局:块 1 之后空 4 扇区再放块 2——CPU 处理完块 1 时盘正好转到块 2 起点,零等待

这种”踩点”放置在 1980 年代 PDP-11 + 老硬盘上能多榨 2-3x 吞吐。

文件名 + 软链接:顺手搞的革命

FFS 论文的篇幅一半给了性能,但它顺手做的几件事影响一样深远

  • 文件名 14 字符 → 255 字符:v7 把名字硬编码进目录项前 14B,FFS 改成变长。这一改让”长描述性文件名”成为可能
  • symlink(符号链接):v7 只有 hardlink,跨文件系统不能链。FFS 引入 symlink,存的是字符串路径——/etc/rc -> /etc/rc.d/init.sh 这种用法就来自这里
  • 原子 rename:v7 重命名要先 unlink 再 link,中间崩了文件就丢。FFS 给 rename 加原子性
  • flock 文件锁:v7 完全没有,FFS 加进来——后续所有数据库(Postgres / SQLite)都依赖

这些”顺手”的改动构成了今天 Unix 文件系统的语义底座。

踩过的坑

  1. 预留 5-10% 空间:分配器要”挑位置”,磁盘满到 95% 时已经没法挑——FFS 默认对非 root 用户保留 10%。这就是为什么 df 显示 90% 时 free 还有空间。

  2. rotational layout 失效:1990 年代后硬盘加 track buffer(盘内缓存)+ zone bit recording(外圈密度更高),FFS 假设的”恒定转速 + 固定 sector”不再成立——重新计算 skip 反而变慢。BSD 后来把这块逻辑关了。

  3. SSD 完全无效:SSD 没有”转”,随机读和顺序读延迟一样。柱面组、rotational layout 全是浪费。F2FS、btrfs 改成”按写入时间分段”。

  4. fragment 写放大:500B 文件不停追加——可能从 1 个 fragment 变 2 个、3 个,每次都得重新分配 + 拷贝。频繁写小文件性能掉。

适用 vs 不适用

适用(思想层面)

  • 任何 机械硬盘 上的文件系统
  • 元数据本地化(inode 和数据靠近)
  • 同目录文件物理近邻
  • ext2/3/4 / UFS / XFS / HFS+

不适用

  • SSD / NVMe(无寻道,rotational layout 全废)→ F2FS、btrfs
  • 网络分布式文件系统(Ceph、HDFS)——硬件抽象在更高层
  • 对象存储(S3)——压根没有”块”和”目录”概念

历史小故事(可跳过)

  • 1971:Ken Thompson 写 Unix v1 文件系统——简单可用,但单一 inode 表
  • 1979:v7 上线,磁盘越来越大,性能瓶颈暴露
  • 1981-1983:Marshall McKusick 在 Berkeley 重写文件系统层,加柱面组
  • 1984:SIGOPS / TOCS 发表 FFS 论文,4.2BSD 正式带 FFS——成为 BSD 王朝核心组件
  • 1993:Linux ext2 上线,block group 直接搬 FFS
  • 2026:ext4 仍在用同一套 block group + 局部性策略,跑在你电脑上

40 多年没换核心思想——这是系统软件少有的”一次设计、终身受用”。

学到什么

  1. 认硬件,别假装它是抽象——FFS 把磁盘的转、寻道、扇区都写进策略,性能 10x。这是一种反抽象,刻意打破”操作系统应该屏蔽硬件细节”的教条
  2. 元数据本地化:inode 表跟着数据走,比”全集中”快得多。今天数据库索引设计、向量索引切片、KV 存储 page 布局都还在用同一个直觉
  3. 大块 + 小碎片 是处理”想要大粒度 I/O 但又有很多小文件”的经典折中。LSM tree 的 SST + tombstone、列存的 row group + dictionary 都是变体
  4. 思想可以活过硬件:rotational layout 死了,但柱面组的局部性思想活到了 SSD 时代——“一份知识在硬件代际之间筛掉哪部分能留”是判断设计含金量的好问题
  5. 顺手做的事可能比主线还重要:FFS 论文主推性能,但 symlink / 长文件名 / flock 才是今天 Unix 用户每天用的——做基础设施时不要只盯一个 KPI

延伸阅读

关联

  • exokernel-1995 —— 内核做什么 vs 不做什么的反向取舍
  • mach-1986 —— 微内核思路下文件系统也外移
  • io-uring —— 现代 Linux 的异步 I/O 接口,假设底层是 SSD
  • leveldb —— 现代键值存储,自己管 LSM-tree 不靠 FFS 局部性