崩溃恢复是存储系统的核心问题
任何涉及多次磁盘写入的操作都可能因崩溃而不一致。
MIT 6.1810 · 第 18 课 · rtm
断电后文件系统还能用吗?本节课探讨崩溃恢复问题及其解决方案——预写日志(Write-Ahead Logging)。
文件系统操作需要多次磁盘写入,崩溃可能发生在部分写入之后,导致文件系统不变量被破坏:
| 操作 | 崩溃风险 |
|---|---|
| 创建文件 | inode 已分配但目录项未写入 → inode 泄漏 |
| 写入数据 | inode 指向未分配的块 → 读取垃圾数据 |
| 删除文件 (unlink) | 目录项指向已释放的 inode → 灾难性后果 |
安全:立即写入磁盘(但磁盘很慢!)
性能:批量写入、写回缓存(但有崩溃风险!)
核心规则:在日志中所有写入完成且标记为已提交之前,不能将任何事务的写入安装到磁盘上的文件系统。
崩溃恢复对于复杂可变数据结构通常很难。日志可以:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 缓冲缓存 │ │ 磁盘 FS 树 │ │ 磁盘日志 │
│ (buffer │ │ (实际文件 │ │ (事务记录 │
│ cache) │ │ 系统) │ │ + 提交点) │
└─────────────┘ └──────────────┘ └─────────────┘
$ echo > x
log_write 34 // ialloc
log_write 34 // iupdate
log_write 47 // writei (目录项)
log_write 33 // iupdate (目录 inode)
log_write 34 // iupdate (itrunc)
// 提交前没有磁盘写入!
bwrite 3 // 写入日志块 (x 的 inode)
bwrite 4 // 写入日志块 (目录块)
bwrite 5 // 写入日志块 (目录 inode)
bwrite 2 // 提交(写入日志头)
bwrite 34 // 安装 inode
bwrite 47 // 安装目录内容
bwrite 33 // 更新目录 inode
bwrite 2 // 标记日志为空
// 同一块的多次写入合并为一次
// 每个块写两次:日志 + 安装
| 崩溃时机 | 后果 |
|---|---|
| 提交前 | 日志头 n=0,恢复时忽略日志,操作未发生 |
| 提交期间 | 提交点是单个扇区写入,原子的;要么提交要么不提交 |
| 安装期间 | 恢复时重放日志,完成安装 |
| 恢复期间 | 再次重放日志(幂等)——只要没有其他活动介入就没问题 |
关键洞察:多次重放日志是安全的(幂等性),只要没有其他活动介入。
sys_open() // sysfile.c
→ begin_op() / end_op() // 标记事务边界
→ create()
→ ialloc() // 分配 inode
→ log_write() // 记录到日志
→ iupdate() // 更新 inode
→ dirlink() // 创建目录项
→ writei()
→ iupdate()
→ itrunc()
→ iupdate()
begin_op() // log.c
→ 检查日志是否正在提交
→ 检查写入是否能容纳在日志中
log_write() // log.c
→ 添加扇区号到内存数组
→ bpin() 固定缓冲区
end_op() // log.c
→ 如果没有未完成操作,提交
commit() // log.c
→ 将更新的块从缓存复制到磁盘日志
→ 在磁盘日志头中记录扇区号和 "done"
→ 安装写入(从磁盘日志复制到磁盘 FS)
→ bunpin() 解除固定
→ 擦除日志中的 "done"
问题:系统调用可以安全地更新缓存块,但在事务提交之前不能写入 FS。
xv6 方案:
解决方案:日志只影响内存中的缓存块,安装只在没有进行中的事务时发生 → 写入吸收 (write absorption)优化
解决方案:ireclaim 在重启时检查孤儿文件
典型方案:sync() 系统调用——仅在所有脏块写入磁盘后返回
| 优点 | 缺点 |
|---|---|
| 预写日志保证正确性 | 每个块写两次(日志 + 安装) |
| 日志自然批量化写入,磁盘吞吐量好 | 即使只修改几个字节也记录整个块 |
| 支持并发 | 同步写入日志块(可以批量化) |
| 不适合操作超出日志容量的情况 |
任何涉及多次磁盘写入的操作都可能因崩溃而不一致。
先记录到日志,再安装到实际位置,崩溃后重放日志。
单个扇区的原子写入(日志头)决定了事务是否提交。
xv6 的日志牺牲了一些性能换取简单性和正确性。
Q1:预写日志的核心规则是什么?
Q2:为什么可以多次重放日志?
Q3:xv6 日志的提交点是什么?