本页目录

操作系统 III · 文件系统与崩溃一致性

对标:MIT 6.S081 / OSTEP 持久化篇 | 前置:os-01/02、csapp-02(磁盘在存储金字塔底层) OSTEP 三大主题的最后一个——持久化。内存断电即失,文件系统负责把数据可靠地存进磁盘、并在崩溃后不损坏。这一页讲文件系统怎么把"文件与目录"这个抽象铺在裸盘的扇区上(inode、目录、分配),以及最难的部分——断电发生在写一半时,怎么保证不变成一团乱麻(崩溃一致性:日志与 fsck)。这也直接是 db 线事务恢复的前身。

学习层:断电发生在第几个块,文件系统还能自洽吗?

具体谜题:追加一个数据块为何要改三处?

文件当前为空,要把数据写入块 42。系统必须写数据块、数据位图和 inode 指针/大小;若恰好完成其中 1 或 2 次写入就断电,重启后是“文件丢了”、块泄漏,还是两个文件共享同一块?先按写入顺序做出判断。

先预测,再展开:选择“直接写盘”或“预写日志”,并预测每一个崩溃点恢复后的文件大小、块 42 的位图状态,以及是否存在孤儿块。

最小心智模型:名字、inode、位图不是同一个对象

目录把名字映射到 inode;inode 再把文件映射到数据块;位图记录块的分配状态。一次更新是一个小型状态转换,而不是一条不可分割的“保存文件”指令。日志把这些修改先写入稳定存储,再以提交记录声明它们属于同一事务。

形式机制与不变量

对每个被 inode 指向的块 \(b\),一致性要求 \(\mathrm{pointed}(b)\Rightarrow\mathrm{allocated}(b)\land\mathrm{data}(b)\);反向的 allocated 但未被引用是空间泄漏。WAL 要求日志记录的持久化序号先于数据页落盘:\(\mathrm{LSN}_{log}\le\mathrm{LSN}_{page}\)。恢复时,已提交事务 redo,未提交事务不应用(或 undo),从而把多块修改呈现为原子状态。

反例与失效边界

fsck 可以在事后推测并修补,却需要扫描全盘,且无法恢复用户原本想写的内容;只记录元数据的日志不等于数据日志,仍可能暴露旧数据;fsync 只在正确的文件描述符、目录项和存储设备持久化语义下提供承诺,不能把任意缓存都变成原子写。

迁移任务:从块分配到数据库恢复

先在 L03 malloc 实验中把“已分配但不可达”的块类比为内存泄漏,再在 P01-C 画出 xv6 创建文件的 inode、位图和目录写集合。最后把同一不变量带到 db-03 的 WAL;数据库恢复会复用这里的持久化顺序,但还要增加事务提交与并发控制。

交互实验:逐个崩溃点检查文件系统状态

无 JavaScript 时的静态读法:追加块 42 的直接写入顺序固定为“数据→位图→inode”。崩溃在 0 次写入时旧文件仍一致;在第 1 次后出现“数据已写但不可达”;在第 2 次后出现“位图已占用但 inode 未指向”的孤儿块;在第 3 次后才一致。WAL 版本先写三条日志记录,再写 commit:commit 前恢复旧状态,commit 后 redo 三项,所有崩溃点都不会暴露半个事务。

恢复策略 崩溃点 inode 指针 位图 结果
直接写盘 1/3 空 空 数据不可达
直接写盘 2/3 空 42 已用 块泄漏
直接写盘 3/3 42 42 已用 一致
WAL commit 前 空 空 丢弃未提交日志
WAL commit 后 42 42 已用 redo 后一致

1. 从裸盘到文件抽象

磁盘布局:超级块|inode位图|数据位图|inode表|数据块 + inode 多级间接指针。

图 os-03.3磁盘布局:超级块|inode位图|数据位图|inode表|数据块 + inode 多级间接指针。

磁盘/SSD 只提供"编号的块,可读可写"。文件系统在其上盖起抽象:

磁盘布局:[超级块(全局元数据)| inode 位图 | 数据位图 | inode 表 | 数据块]。理解这个布局,你就能回答"创建一个文件要改磁盘上哪几处"——这正是崩溃一致性的关键。

2. 性能:为什么文件系统要那么多花招

磁盘(尤其机械盘)随机访问极慢(寻道 ~10 ms,是内存的十万倍,🔗 csapp-02)。文件系统的大量设计是在对抗这个:

3. 崩溃一致性:本页的核心难题

崩溃一致性:写日志→提交→应用;崩溃在提交前后的两种恢复。

图 os-03.2崩溃一致性:写日志→提交→应用;崩溃在提交前后的两种恢复。

问题:往文件追加一块数据,要更新三处磁盘:inode(新块指针 + 大小)、数据位图(标记块已用)、数据块本身。磁盘一次只能原子写一个块——如果在写了其中一两处后断电,文件系统就处于不一致状态:

两种解法:

① fsck(崩溃后扫描修复):重启后扫全盘,检查一致性并修补(位图与 inode 对不上就以 inode 为准等)。缺点:要扫整个磁盘,TB 级盘要几小时——不可接受。

② 预写日志(journaling / write-ahead logging)【核心机制】:先把"我要做的所有修改"作为一个事务写进磁盘上的日志区,写完并标记提交(commit)后,再把修改实际应用到文件系统。崩溃后只需重放日志:

关键洞察:通过"先顺序写日志、再更新原地",把'多处修改'变成了'一次原子提交'——代价是每个数据写两遍(日志 + 原地),故有只记元数据的折中(ordered 模式)。这个"预写日志 → 提交 → 应用/重放"的模式,就是 db-03 数据库事务恢复(WAL)的原型——文件系统和数据库在崩溃一致性上是同一套思想,本站在此埋下这条主线。

4. fsync 与持久化的真相

write() 返回不代表数据落盘——它可能还在内存的 page cache 里。要真正持久化必须 fsync()(强制刷盘)。这是无数数据丢失事故的根源:程序以为写成功了,断电后数据没了。数据库、消息队列在提交时必 fsync(并因此慢)。"写入成功 ≠ 持久化"是每个做存储/后端的人必须内化的一课(🔗 db-03、web-02 的数据可靠性)。

5. 练习与要点

路径解析 /a/b/c 逐级查目录 inode。

图 os-03.1路径解析 /a/b/c 逐级查目录 inode。

例 1(路径解析手走) 解析 /home/user/a.txt:根 inode(固定号)→ 读根目录找 home 的 inode → 读 home 目录找 user……一步步走一遍,理解"每级目录都是一次磁盘查找"(也解释了深路径 + 冷缓存为什么慢)。

例 2(崩溃时刻分析) 追加写的三处更新,枚举"在第 1/2 处之后断电"分别导致什么不一致——手推一遍,你就懂了为什么需要日志。

例 3(fsync 实验) 写文件不 fsync 然后立即拔电源(虚拟机里模拟)——数据可能丢;加 fsync 后保证在。"要么慢要么可能丢"的存储权衡亲手体会一次。\(\blacksquare\)


📋 大 Project P01(第三阶段·收官)· xv6 文件系统

P01-C · 文件系统与 mmap(承接 P01-A/B):

  • 学习目标:理解 inode 指针树、路径解析、符号链接、文件页与虚拟内存页之间的关系;把“文件系统”和“虚拟内存”两章接起来。
  • 教师提供:bigfile、symlinktest、mmaptest 公共测试,崩溃一致性讨论题,不提供内核实现补丁。
  • 学生任务:① 给 inode 增加二级间接块,支持更大文件;② 实现 symlink 与递归路径解析,处理循环链接深度上限;③ 实现最小 mmap/munmap:懒加载文件页、脏页写回、进程退出清理映射。
  • 接口约束:mmap 至少支持 MAP_SHARED、读写权限检查、页对齐;不得一次性把整个文件读进内存;符号链接解析必须避免无限递归。
  • 验收测试:大文件读写正确;符号链接跨目录、删除目标、循环链接测试行为合理;mmaptest 全过;usertests 回归全过。
  • 评分重点:inode 扩展 25%,路径/符号链接 20%,mmap 缺页与写回 35%,边界测试与报告 20%。
  • P01 总结报告:要求学生画出 xv6 的系统调用、调度、锁、文件系统、页表五张小图,并说明每阶段改动如何穿过这些边界。

操作系统三页完成。下一页转入网络 I:数据如何跨越世界到达,TCP/IP 分层与可靠传输、拥塞控制。