MIT 6.1810 · 第 19 课 · rtm

日志与文件系统性能

以 Linux ext3 文件系统为案例,深入研究日志在真实系统中的高性能实现。

授课教师:rtm (Robert T. Morris)

年份:2025 秋季

难度:中等

视频:Lecture 19 Notes

复习:为什么需要日志?

日志的目标

  1. 保护文件系统自身的不变量不被崩溃破坏
  2. 提供高性能
  3. 注意:日志不提供应用级事务!

两个关键规则

规则含义
预写规则 在所有写入提交到日志之前,不能开始写入 FS 的实际位置
释放规则 在所有实际位置写入完成之前,不能擦除/覆盖日志

ext3 vs xv6:性能对比

方面xv6ext3
提交频率 每个系统调用通常提交一次 每几秒提交一次(批量化)
系统调用等待 等待日志写入和提交 通常不需要等待(异步)
数据模式 日志记录所有块(包括文件内容) ordered data 模式:只记录元数据
并发 有限 多个事务在不同阶段并行

ext3 数据结构

内存中

磁盘上

日志格式

|SB: offset+seq#| ... |D4|...blocks...|C4|D5|...
 ↑                 ↑      ↑         ↑
 日志超级块       描述符  数据块    提交记录

日志超级块:日志偏移和最早有效事务的序列号
描述符块:magic、seq、块号列表
提交块:magic、seq

ext3 可以容纳多个事务

|SB| T4 | T5 | T2 | T3 |
     ↑
   SB 指向最早的有效事务

复合事务 (Compound Transaction)

ext3 将多个系统调用的更新批量化到一个事务中:

  1. 分摊固定事务成本(描述符块、提交块、寻道/旋转、系统调用屏障)
  2. 写入吸收 (write absorption):多个系统调用修改同一块 → 一次磁盘写入

两种并发性来源

  1. 多个系统调用可以向当前事务添加写入
  2. 多个事务可以处于不同阶段:
    • 一个"开放"事务接受新系统调用
    • 提交中的事务执行日志写入
    • 已提交的事务写入 FS 实际位置

系统调用是"异步的"

应用层注意:数据库、邮件系统、编辑器需要调用 fsync() 确保写入顺序。

内核实现

系统调用模式

sys_unlink() {
    h = start()           // 通知日志系统有新系统调用
    get(h, block #) ...   // 告诉日志系统要修改的块
    在缓存中修改块
    stop(h)               // 结束系统调用(不触发提交)
}

start():  // 可能关闭当前事务,开始新事务
get():    // 将块添加到当前事务的日志列表中
stop():   // 只有当所有包含的 system call 都 stop() 后,事务才能提交

提交事务到磁盘(后台进行)

  1. 阻止任何新系统调用开始
  2. 等待进行中的系统调用 stop()
  3. 打开新事务,解除对新系统调用的阻止
  4. 写入描述符到磁盘日志(块号列表)
  5. 将修改的块从缓存写入磁盘日志
  6. 等待描述符和数据日志写入完成
  7. 写入提交记录到磁盘日志
  8. 等待提交写入完成
  9. 现在允许修改的块写入 FS 实际位置(但不强制)

关键设计细节

为什么 ext3 要延迟 T2 的系统调用直到 T1 的系统调用完成?

示例问题:

T1: |-create(x)-|
T2:        |-unlink(y)-|
                  X 崩溃

如果允许 T2 的 unlink 在 T1 的 create 执行期间发生:

解决方案:屏障——不开始 T2 的系统调用直到 T1 的系统调用完成

为什么提交需要从缓存块的快照写入磁盘?

T1: |--create(d/f)--|   ... T1 的日志写入 ...
T2:                   |--create(d/g)--|
两个 create() 修改 d 的目录内容块
如果 T1 在 T2 的 create() 之后写入日志
→ T1 可能包含 T2 的部分写入
→ 崩溃+恢复可能重放 T2 的部分操作

解决方案:给 T1 一个缓存的私有副本(写时复制),T1 从这个快照提交

ext3 "Ordered Data" 模式

问题:日志记录文件内容很慢

每个数据块写两次到磁盘。数据通常比元数据大得多。

解决方案

  1. 将文件内容块写入磁盘 FS
  2. 等待内容写入完成
  3. 提交 i-node(带块号和新大小)和块空闲位图

文件内容不写入日志。

为什么这很安全?

Ordered Data 模式的正确性挑战

挑战 1:rmdir 释放目录内容块 #27,同一事务中的文件写入分配块 #27,写入 #27,崩溃在 rmdir 提交之前。恢复后 rmdir 未发生,但 #27 已被覆盖!

修复:在释放系统调用提交之前,不重用已释放的块

挑战 2:释放目录块后重用,但日志重放覆盖了文件数据

修复:释放目录块时添加 "revoke" 日志记录,阻止先前的日志记录写入该块

崩溃恢复

日志能帮助什么类型的崩溃?

恢复过程

SB T8 xT5 T6 T7
    ↑
  SB 指向 T6(T8 覆盖了 T5 的开始)

1. 在日志超级块中查找开始位置
2. 扫描有效事务找到日志末尾
3. 将所有块写入实际位置(按日志顺序)
4. 开始正常文件系统操作

为什么按日志顺序写入?

确保较新的块版本覆盖较旧的版本。

校验和 (Checksums)

风险:磁盘通常有写缓存并重排序写入以提高性能。磁盘可能"撒谎"说写入已完成。

解决方案:提交块包含整个事务的校验和。恢复时计算校验和,匹配则安装,不匹配则不安装。

注意:ext4 有日志校验和,ext3 没有。

要点总结

批量化是性能的关键

ext3 将多个系统调用批量化到一个事务中,分摊固定成本并启用写入吸收。

异步系统调用提高响应性

系统调用只写入缓存并返回,不等待日志写入或提交。

Ordered Data 模式避免双重写入

文件内容先写入 FS,元数据后提交,避免数据块的双重写入。

屏障保证正确性

系统调用之间的屏障防止事务间的相互干扰。

测验

Q1:ext3 的批量化如何提高性能?

Q2:ext3 ordered data 模式的核心思想是什么?

Q3:为什么 ext3 需要在系统调用之间设置屏障?

延伸阅读