Appearance
6.1810 2024 第15讲:文件系统性能和快速崩溃恢复
计划
- 问题:xv6的日志是正确的,但速度慢
- 解决方案:linux ext3风格的日志记录
- 只记录元数据,不记录文件数据
- 校验和
- 并发事务
xv6日志回顾
- 所有写入都进入日志
- 提交
- 安装
- 清除日志
- 速度慢,因为所有数据都被写入两次
- 一次写入日志,一次写入最终位置
- 但简单且正确
我们能避免将文件数据写入日志吗?
- 只记录元数据块
- inodes, 位图块
- 那么文件数据块就可以直接写入它们的最终位置
- 这避免了文件数据的双重写入
- 这就是Linux ext3的“有序”模式所做的
可能会出什么问题?
- 假设我们正在向一个文件追加一个块
- 系统调用需要:
- 将新的文件数据写入一个空闲的数据块 (D)
- 更新文件的inode以指向D
- 更新块空闲位图以将D标记为已使用
- 假设计划是:
- 将D写入其最终位置
- 记录inode和位图的写入
- 提交
- 安装inode和位图
- 如果在D被写入后,但在提交前崩溃会怎样?
- D已被写入,但磁盘上的inode和位图未更新
- 所以D不属于任何文件,但也不在空闲列表上
- 一个“泄漏”的块 —— 不是灾难,但也不好
- 如果在提交后,但在安装前崩溃会怎样?
- 恢复将重做inode和位图的写入
- 所以inode将指向D
- 但D的内容可能还没有到达磁盘!
- 所以文件将包含一个垃圾块
- 这是灾难!
问题在于我们破坏了预写规则
- 我们在事务提交之前安装了一个数据块的写入
- 事务“包含”了数据块的写入,因为
- inode将指向它
- 所以我们需要知道数据块的写入已经完成
- 在我们能提交事务之前
解决方案:有序日志记录 (ordered journaling)
- 将数据块D写入磁盘上的最终位置
- 等待写入完成
- 记录inode和位图块
- 提交
- 安装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_opend_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的简单日志记录性能高得多