LEC_14 Linux ext3 崩溃恢复

课程大纲

  1. 基于日志的崩溃恢复机制

    1.1 xv6: 实时持久化,性能较弱

    1.2 ext3: 非实时持久化,性能较好

  2. 体会一种在性能和安全之间的权衡

举例说明

  1. 本例子用于说明崩溃恢复的必要性
  2. 在向文件追加内容的时候,我们需要做两步操作.
  3. 首先,在bitmap中标记block为non-free
  4. 其次,将block-num添加到inode的addrs[]数组中
  5. 这两步操作必须是原子的,因此我们不能一次文件系统读写只做一步.

日志的必要性

  1. 为了在系统崩溃时,保证操作的原子性.
  2. 提供了文件系统快速恢复功能,而不是使用类似fsck的工具来全磁盘扫描修复.

xv6日志系统回顾

  1. 组成部分: cache, 磁盘log系统, 磁盘文件系统
  2. 每个系统调用都是一个事务
  3. 系统调用会修改内存中缓存的block
  4. 在系统调用结束时会做以下动作:

    4.1 将修改过的block内容保存到磁盘的block区

    4.2 将修改过的block编号和一个done标记写入到log区

    4.3 将修改过的磁盘内容写入到磁盘真正的文件系统中

    4.4 如果在4.3写入中途发生崩溃,则可以从log区重新读取写入.

    4.5 必须保证在4.1和4.2完成后,才开始4.3的操作,从而保证了操作的原子性

    4.6 当4.3完成后,将log区中的done标记擦除,表示一次完整的事务提交已经完成.

LEC_13 homework

请参见LEC_13/homeworkd.md

xv6日志系统的缺陷: 缓慢

  1. 在每次系统调用后,都会立刻commit.
  2. 在每次commit之后,都会立刻写入文件系统.

    2.1 为什么必须立刻写入文件系统呢?因为log区仅能保存一次事务

  3. 在commit时,所有文件系统相关的系统调用都会阻塞,因此并发情况下,性能很差.

  4. 所有block内容都要写入磁盘两次,一次写入log区,一次写入真正的文件位置.

  5. 对于小文件而言,写入两次还好.但是大文件如果写入两次,性能损耗比较大.

  6. xv6所有文件写入都是同步的,xv6会等待写入完成,再继续后面的操作.
  7. 创建一个空文件就需要6次同步磁盘写入,耗时约60ms.
  8. 每秒钟只能执行大概10~20个文件系统相关的系统调用

Linux's Ext3 背景

  1. ext3是基于ext2发展的,主要是在ext2的基础上增加了日志功能
  2. ext3有许多模式,我们主要介绍journaled data.在此模式下,日志系统同时包含元数据和文件内容数据

Ext3结构

  1. 内存中:

    1.1 block缓存

    1.2 每个事务信息: 已被日志记录的block编号集合, 尚未完成的系统调用计数器

  2. 磁盘上:

    2.1 文件系统

    2.2 循环的log区

Ext3 log区结构

  1. log superblock: 保存了日志的偏移以及第一个有效事务的序号等信息
  2. descriptor blocks: magic number, 事务序号, block-nums
  3. data blocks
  4. commit blocks: magic, 序号
  5. |super: offset+seq #| ... |Descriptor 4|...blocks...|Commit 4| |Descriptor 5|...

Ext3性能提升原因

定时提交

不再是每个系统调用提交一次,而是每过一段时间提交一次,所以每次事务都会包含许多系统调用

为什么定时提交能提升性能?

  1. 每次事务提交都会有些固定消耗,比如陷入内核等,这些开销被分摊了
  2. 段时间内,许多系统调用会重复修改同一block,这些修改最终被聚合为一次写入
  3. 提升了并发性能,不必等待前一次文件系统操作commit完成,即可开始下一个文件系统操作.

备注

  1. Ext3中,系统调用返回到用户空间时,文件系统并不能保证内容安全.
  2. 这可能会影响到应用层面的一些崩溃恢复.
  3. 比如: 邮件服务器接收到消息,并将消息保存到磁盘,然后向客户端回复OK.其实邮件内容还是可能丢失的.

Ext3文件系统并发调用

  1. 磁盘的log区可能已经保持了部分完整commit的事务,正等待install
  2. 一些事务正在commit
  3. 一个尚未完成的事务,正在接受新的系统调用.

Ext3 文件系统调用伪码

  sys_open() {
    h = start()
    get(h, block #)
    modify the block in the cache
    stop(h)
  }

start()

  1. 告知日志系统将之后的文件系统操作视为一个事务,直到stop()
  2. 日志系统必须知道哪些系统调用属于同一个事务,在事务完成前,不能把他们提交到日志系统
  3. 如果需要,start()可能阻塞

get()

  1. 告知日志系统,我们修改了某个缓存的block,日志系统需要记录已修改的block
  2. 在一次事务完成前,阻止将已修改的block内容写入到磁盘

stop()

  1. stop()不能触发一次commit()
  2. 必须等到所有相关的系统调用都调用了stop()之后,事务才可以提交

将一个事务提交到磁盘

  1. 阻塞新的系统调用
  2. 等待正在处理中的文件系统操作调用stop()
  3. 创建一个新的事务,恢复新的系统调用执行
  4. 将descriptor和block编号列表写入磁盘log区
  5. 将block内容从缓存写入到磁盘log区
  6. 等待所有的log写入完成
  7. 写入事物已commit标记位
  8. 将已提交的数据,install到真正的磁盘位置

冲突问题

设想这样一种场景:

  1. 线程A删除文件x
  2. 线程B执行echo > y,同时y复用了刚刚删除的x的inode 那么有没有可能线程A,B间发生冲突,导致x,y使用同一个节点呢?

  3. 如果A,B处于同一个事务中,显然不会发生这种情况

  4. 如果A,B处于前后分开两个事务中,那么也不会发生.
  5. 只有当A,B分别同时处于两个不同事务中才可能发生.
     in T1: |--B--|
     in T2:    |--A--|
    

如上图所示,A已经将inode->type修改,但是目录dentry尚未修改.同时B已经复用刚刚A释放的inode.并且B完成提交,然后Crash.即A的修改并没有提交.

事实上,这种情况是不存在的,因为对于Ext3而言,在前一个事务结束前是不会开始下一个事务的.即事务是有序的.

重复修改

    T1: |-syscalls-|-commitWrites-|
    T2:            |-syscalls-|-commitWrites-|

如上图所示,Ext3允许在T1完成commitWrites前,T2启动.

设想这样一种场景:

  1. T1修改了block17
  2. T1 close,开始将缓存的block写入到log区
  3. T2启动,T2的某个系统调用也在修改block17
  4. 那么T1会将T2修改后的block17写入到log区么?
  5. 如果会的话,那么假设此时发生crash,则会导致部分T2的内容被写入到磁盘,破坏了事务的原子性

所以:

  1. 当T1 close时,Ext3将会给T1一份修改的block的私有拷贝
  2. T1会将私有拷贝的内容写入到log区
  3. 写时拷贝技术此时非常适用
  4. 正是这样的拷贝技术让T2在T1提交时,就可以同步开始.

备注

  1. 正确性要求事务必须保持原子性
  2. 在正确的前提下,Ext3还是使尽了浑身解数来提高并发性能

log区复用

Ext3的log区是循环使用的,那么应该是什么时候复用事务T1的log区呢?有两个条件:

  1. 所有T1之前的事务都可以释放
  2. T1相关的block都已经写入到log区正确位置

所谓释放T1对应的log区,也就是修改log superblock中的起始位置

log区不够用,怎么办?

  1. 假设在事务T2中,执行了系统调用A.
  2. 执行了系统调用A后,发现T2无法写入log区,因为log区容量不够了.
  3. 此时,我们既不能提交T2,因为系统调用还未完成.
  4. 另外,我们也无法撤销系统调用A,因为一来我们没有手段用于撤销系统调用,二来其他系统调用可能已经读取了A修改的内容.

解决方法

  1. 系统调用提前声明所需的log区大小.
  2. Ext3 start()将会阻塞系统调用,直到有足够的空闲log区
  3. 这样所有能够执行的系统调用均能够顺利完成和提交

性能

删除一个目录中的100个文件的消耗时间:

  1. xv6大概需要10s,因为每次系统调用都需要6次磁盘同步
  2. ext3大概需要20ms,因为ext3会将大量的修改合并为极少的操作

一次提交需要多长时间?

  1. 将修改的block写入到log区
  2. 等待写入完成
  3. 将commit标志位写入log区
  4. 将修改的block内容写入到FS

崩溃

  1. 在将事务写入到磁盘时,可能发生崩溃
  2. 最坏的情况时,部分子事务已经写入log区,部分没有

崩溃恢复

  1. 首先读取log superblock,获取第一个事务的偏移和序号
  2. 寻找log区结束的位置,可能是错误的序号,也可能是错误的magic
  3. 可以得出结论,系统就是在log区结束的位置,发生了崩溃.
  4. 我们需要重新执行log区已经正确记录的事务.

如果最后一个有效日志块之后的块看起来像日志描述符,该怎么办

TODO

Ext3顺序模式

之前我们一直讨论的都是日志模式,在日志模式下,所有的文件内容都会被写入磁盘两次,这将会极大的降低性能.

我们能够不将文件内容写入log区么?如果可以,那应该什么时候将文件内容写入FS?另外我们可以在任意时间将文件内容写入FS么?

答案显然是否定的,因为如果文件的元数据先被提交,而文件内容没有提交.如果此时发生crash,显然磁盘就处于错误的状态.

Ext3顺序模式的实现

  1. 首先,顺序模式不会将文件内容写入到log区.
  2. 其次,文件内容会先被写入到FS
  3. 在文件内容写完之后,才会将文件对应的元数据写入到FS.

修改一个文件可能同时修改inode, bitmap和data block三块内容.提交时会首先将data block写入FS. 如果此时发生崩溃,那么data block已经包含了新的内容.但是因为inode和bitmap尚未修改因此文件内容是不可见的.

当然此时inode和bitmap的修改依然必须是原子的.对于大部分用户而言,显然使用Ext3的顺序模式能够极大的提升性能.

Ext3顺序模式的正确性挑战

对于Ext3顺序模式而言,存在一些额外的挑战

删除某个目录,然后复用了block来写入部分文件的内容.如果在删除目录或者写入操作提交前,发生crash.那么在crash恢复后,删除目录的动作好像没有发生过.但是此时目录block的内容已经被修改了.解决方法在于释放block的操作在没有提交前,释放的block不能复用.

还有另外一种情况:TODO

以上情况都是由于block的类型改变所导致的.block可以用于存储元数据也可以用于存储文件内容.

规则总结

经典模式规则

  1. 除非元数据已经被保存到log区,否则不要写入到磁盘FS.
  2. 必须等到前一个事务的所有系统调用完成,才能开始下一个事务.
  3. 在一个buffer cache保存到log区之前,不能覆盖它的内容.
  4. 所有在log区block的内容没有写入到FS前,不能释放log空间.

顺序模式规则:

  1. 在提交前,将数据block写入到FS
  2. 在释放block的系统调用提交前,不能复用这些block
  3. 不要重复执行 撤销的系统调用

另一个特殊情况: 打开文件和取消链接

  1. 打开一个文件,然后unlink它
  2. unlink提交
  3. 因为文件打开,所以unlink会清楚dir中的内容,但是不会释放文件inode及相关block
  4. 此时发生crash
  5. log区中,并没有日志需要重新执行.
  6. inode及相关block再也不会被释放了,且不可达,不可利用.有点内存泄漏的味道.

解决方案

  1. unlink提交前,将这个inode添加到FS superblock的一个链表中
  2. 在恢复过程中,检查这个链表,重新执行inode删除

校验和

事务在将block写入到FS前,必须首先保存到log区.ext3需要等待磁盘完成log区写入后,才能开始真正的写入到FS.

风险在于,磁盘硬件为了性能提升,可能会使用写入缓存和写入重排序.

  1. 有时操作系统是很难关闭这些特性的
  2. 有时因为缺乏了解,人们往往会打开这些重排序功能.
  3. 那么就可能存在乱序的情况,即install在commit之前.

解决方案

  1. commit block包含了数据block的校验和
  2. 在崩溃恢复时,首先计算所有相关data block的校验和.
  3. 如果能够匹配,则重新install.
  4. 如果不能匹配,则忽略.
  5. Ext4就具有这样的校验和功能.

Ext3和xv6的比较

  1. Ext3解决了xv6同步写入的问题,但是有了5s的写入窗口,写入实时性降低.
  2. 从xv6的一点改动就写入,变成了整合到一起写入.如果重复写入同一block,能极大地提升性能.
  3. xv6文件内容必须install完成后,才能开始下一次文件操作.Ext3在commit后,已经可以开始下一次文件操作.
  4. 在实践中,Ext3/Ext4是非常成功的.

results matching ""

    No results matching ""