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 件事:
-
大块(block)+ 碎片(fragment):v7 块固定 512B,太小,每次磁头寻道开销摊不开。FFS 把块加到 4KB 或 8KB,单次 I/O 拿更多数据。但小文件(< 4KB)用整块就浪费——于是把块切成 8 个 fragment(1KB 一片),小文件用 fragment 拼。
-
柱面组(cylinder group):磁盘按柱面(cylinder = 同半径上下所有磁道)切成几十组,每组自带 inode 表 + 空闲块位图 + superblock 副本。元数据本地化——读一个 inode 不必跨整盘。
-
局部性分配策略:同目录的 inode 尽量放同一柱面组;inode 和它的数据块尽量同组;新目录尽量放在 inode 较空的组。基于一个朴素事实——同目录的文件大概率会被同时访问。
-
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:
- 找
/home的 inode → 在组 N /home/jason的 inode 也分配在组 N(局部性策略)a.txt的 inode 也在组 Na.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 文件系统的语义底座。
踩过的坑
-
预留 5-10% 空间:分配器要”挑位置”,磁盘满到 95% 时已经没法挑——FFS 默认对非 root 用户保留 10%。这就是为什么
df显示 90% 时free还有空间。 -
rotational layout 失效:1990 年代后硬盘加 track buffer(盘内缓存)+ zone bit recording(外圈密度更高),FFS 假设的”恒定转速 + 固定 sector”不再成立——重新计算 skip 反而变慢。BSD 后来把这块逻辑关了。
-
SSD 完全无效:SSD 没有”转”,随机读和顺序读延迟一样。柱面组、rotational layout 全是浪费。F2FS、btrfs 改成”按写入时间分段”。
-
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 多年没换核心思想——这是系统软件少有的”一次设计、终身受用”。
学到什么
- 认硬件,别假装它是抽象——FFS 把磁盘的转、寻道、扇区都写进策略,性能 10x。这是一种反抽象,刻意打破”操作系统应该屏蔽硬件细节”的教条
- 元数据本地化:inode 表跟着数据走,比”全集中”快得多。今天数据库索引设计、向量索引切片、KV 存储 page 布局都还在用同一个直觉
- 大块 + 小碎片 是处理”想要大粒度 I/O 但又有很多小文件”的经典折中。LSM tree 的 SST + tombstone、列存的 row group + dictionary 都是变体
- 思想可以活过硬件:rotational layout 死了,但柱面组的局部性思想活到了 SSD 时代——“一份知识在硬件代际之间筛掉哪部分能留”是判断设计含金量的好问题
- 顺手做的事可能比主线还重要:FFS 论文主推性能,但 symlink / 长文件名 / flock 才是今天 Unix 用户每天用的——做基础设施时不要只盯一个 KPI
延伸阅读
- 论文 PDF:A Fast File System for UNIX (1984)(17 页,可读性极好)
- McKusick 自述:The Design and Implementation of the FreeBSD Operating System, Ch.8(FFS 作者亲笔讲设计取舍)
- 对比阅读:Linux ext4 wiki(看 ext4 怎么把 FFS 思想搬进 Linux)
- exokernel-1995 —— 把抽象推到用户态,与 FFS 截然相反的思路
- hydra-1974 —— 早期能力机操作系统
关联
- exokernel-1995 —— 内核做什么 vs 不做什么的反向取舍
- mach-1986 —— 微内核思路下文件系统也外移
- io-uring —— 现代 Linux 的异步 I/O 接口,假设底层是 SSD
- leveldb —— 现代键值存储,自己管 LSM-tree 不靠 FFS 局部性