Skip to content

6.1810 2024 第15讲:文件系统性能和快速崩溃恢复

计划

  • 问题:xv6的日志是正确的,但速度慢
  • 解决方案:linux ext3风格的日志记录
    • 只记录元数据,不记录文件数据
    • 校验和
    • 并发事务

xv6日志回顾

  • 所有写入都进入日志
  • 提交
  • 安装
  • 清除日志
  • 速度慢,因为所有数据都被写入两次
    • 一次写入日志,一次写入最终位置
  • 但简单且正确

我们能避免将文件数据写入日志吗?

  • 只记录元数据块
    • inodes, 位图块
  • 那么文件数据块就可以直接写入它们的最终位置
  • 这避免了文件数据的双重写入
  • 这就是Linux ext3的“有序”模式所做的

可能会出什么问题?

  • 假设我们正在向一个文件追加一个块
  • 系统调用需要:
    1. 将新的文件数据写入一个空闲的数据块 (D)
    2. 更新文件的inode以指向D
    3. 更新块空闲位图以将D标记为已使用
  • 假设计划是:
    • 将D写入其最终位置
    • 记录inode和位图的写入
    • 提交
    • 安装inode和位图
  • 如果在D被写入后,但在提交前崩溃会怎样?
    • D已被写入,但磁盘上的inode和位图未更新
    • 所以D不属于任何文件,但也不在空闲列表上
    • 一个“泄漏”的块 —— 不是灾难,但也不好
  • 如果在提交后,但在安装前崩溃会怎样?
    • 恢复将重做inode和位图的写入
    • 所以inode将指向D
    • 但D的内容可能还没有到达磁盘!
    • 所以文件将包含一个垃圾块
    • 这是灾难!

问题在于我们破坏了预写规则

  • 我们在事务提交之前安装了一个数据块的写入
  • 事务“包含”了数据块的写入,因为
    • inode将指向它
  • 所以我们需要知道数据块的写入已经完成
    • 在我们能提交事务之前

解决方案:有序日志记录 (ordered journaling)

  1. 将数据块D写入磁盘上的最终位置
  2. 等待写入完成
  3. 记录inode和位图块
  4. 提交
  5. 安装inode和位图块
  • 现在如果在提交后崩溃,我们知道D在磁盘上
  • 所以在恢复后,inode将指向一个有效的数据块

如何等待一次写入完成?

  • 磁盘硬件在写入完成时产生一个中断
  • xv6的 virtio_disk_intr()
  • 但xv6的 log_write 不等待
  • 所以我们需要一个同步写入函数
    • bwrite_sync()
    • 它等待那个特定块的中断

所以对于一个 write() 系统调用的新计划是:

  • 对于要写入的每个数据块:
    • 找到空闲块D
    • bwrite_sync(D)
  • 然后,在一个事务中:
    • 更新inode
    • 更新位图
  • 这很慢 —— 每个数据块一次同步写入
    • 但如果写入很少,也许可以接受

如果我们想为了性能批量处理数据块写入怎么办?

  • write(fd, buf, 100*BSIZE)
  • 我们希望一次性将所有100个块发送到磁盘
  • 然后才等待它们全部完成
  • 然后才记录元数据事务
  • 所以我们需要一种方法来知道一整组写入何时完成
  • xv6没有这个功能,但这是一个合理的扩展
    • 例如,磁盘上的“写屏障”操作

如果我们覆盖一个现有的块,而不是追加一个新块呢?

  • 假设我们覆盖文件中的块D
  • 计划:
    • 将新内容写入D在磁盘上的位置
    • 等待写入完成
    • 记录inode更新(例如新的大小/时间)
    • 提交
  • 如果在新的D被写入后,但在提交前崩溃会怎样?
    • 文件有新数据
    • 但inode有旧的大小/时间
    • 一个不一致,但也许不是灾难
  • 如果我们需要对多块覆盖进行原子性操作怎么办?
    • 例如数据库更新
    • 那么我们必须记录数据块,就像在xv6中一样

如果我们有并发事务怎么办?

  • xv6序列化了写入文件系统的系统调用
    • begin_op / end_op
    • end_op 提交事务
    • 所以一次只有一个事务
  • 为什么?
    • 更简单
    • 日志是块号的单个数组
    • 很难知道哪个块属于哪个事务
  • 但序列化对于多核性能不利
    • 我们想要并发事务

如何支持并发事务?

  • 需要识别每个记录的块属于哪个事务
  • 所以日志条目需要用事务ID (TID)来标记
  • [日志头 | 块,tid | 块,tid | ... ]
  • 一个事务是一组具有相同TID的日志条目
  • 日志中的提交记录需要提到一个TID
    • [提交, tid]
  • 现在日志中可以有多个事务,但只有一些被提交

如果一个块是多个并发事务的一部分怎么办?

  • 例如,两个进程在同一个目录中创建文件
  • 两者都需要写入目录的数据块
  • 假设T1写入目录块,然后T2写入它
  • 然后T1提交
  • 然后T2提交
  • 然后崩溃
  • 恢复时看到T1提交,安装T1的目录块写入
    • 但那个写入已被T2的写入所取代
    • 所以我们安装了过时的数据
  • 这是灾难

解决方案:

  • 当一个事务提交时,我们只能安装
    • 不属于其他活动(未提交)事务的块
  • 所以我们需要跟踪哪个块在哪个事务中
  • 当我们提交T1时,我们查看T1中的每个块
    • 如果一个块也在T2中(T2是活动的),我们不能安装它
    • 我们必须等待T2提交
  • 这很复杂!

另一个问题:如果日志满了怎么办?

  • xv6等待所有当前的FS系统调用完成,
    • 提交,安装,清空日志,然后让新的调用开始
  • 如果我们有并发事务,一些可能是长时间运行的
    • 我们不能等它们
    • 但我们需要为新事务释放日志空间
  • 所以我们需要能够提交一些事务而不是其他事务
    • 并重用已提交事务的日志空间

校验和

  • 如果在写入日志块的过程中电源故障怎么办?
    • 或者一个提交记录?
    • 那么我们可能在恢复时看到一个部分写入的块
    • 并错误地解释它
  • 解决方案:为每个日志块和提交记录添加一个校验和
    • 例如,块中所有字节的简单总和
    • 写入者计算并附加校验和
    • 恢复时在使用日志块之前验证校验和
    • 如果校验和是坏的,恢复程序假定日志块是垃圾
      • 例如,将整个事务视为未提交

ext3风格日志记录总结

  • 记录元数据,而不是数据
  • 有序:写入数据块,等待,然后记录元数据
  • 并发事务,由TID标识
  • 不要安装属于其他活动事务的块
  • 校验和
  • 结果:比xv6的简单日志记录性能高得多