LEC_14 Linux ext3 崩溃恢复
课程大纲
基于日志的崩溃恢复机制
1.1 xv6: 实时持久化,性能较弱
1.2 ext3: 非实时持久化,性能较好
体会一种在性能和安全之间的权衡
举例说明
- 本例子用于说明崩溃恢复的必要性
- 在向文件追加内容的时候,我们需要做两步操作.
- 首先,在bitmap中标记block为non-free
- 其次,将block-num添加到inode的addrs[]数组中
- 这两步操作必须是原子的,因此我们不能一次文件系统读写只做一步.
日志的必要性
- 为了在系统崩溃时,保证操作的原子性.
- 提供了文件系统快速恢复功能,而不是使用类似fsck的工具来全磁盘扫描修复.
xv6日志系统回顾
- 组成部分: cache, 磁盘log系统, 磁盘文件系统
- 每个系统调用都是一个事务
- 系统调用会修改内存中缓存的block
在系统调用结束时会做以下动作:
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日志系统的缺陷: 缓慢
- 在每次系统调用后,都会立刻commit.
在每次commit之后,都会立刻写入文件系统.
2.1 为什么必须立刻写入文件系统呢?因为log区仅能保存一次事务
在commit时,所有文件系统相关的系统调用都会阻塞,因此并发情况下,性能很差.
所有block内容都要写入磁盘两次,一次写入log区,一次写入真正的文件位置.
对于小文件而言,写入两次还好.但是大文件如果写入两次,性能损耗比较大.
- xv6所有文件写入都是同步的,xv6会等待写入完成,再继续后面的操作.
- 创建一个空文件就需要6次同步磁盘写入,耗时约60ms.
- 每秒钟只能执行大概10~20个文件系统相关的系统调用
Linux's Ext3 背景
- ext3是基于ext2发展的,主要是在ext2的基础上增加了日志功能
- ext3有许多模式,我们主要介绍
journaled data
.在此模式下,日志系统同时包含元数据和文件内容数据
Ext3结构
内存中:
1.1 block缓存
1.2 每个事务信息: 已被日志记录的block编号集合, 尚未完成的系统调用计数器
磁盘上:
2.1 文件系统
2.2 循环的log区
Ext3 log区结构
- log superblock: 保存了日志的偏移以及第一个有效事务的序号等信息
- descriptor blocks: magic number, 事务序号, block-nums
- data blocks
- commit blocks: magic, 序号
|super: offset+seq #| ... |Descriptor 4|...blocks...|Commit 4| |Descriptor 5|...
Ext3性能提升原因
定时提交
不再是每个系统调用提交一次,而是每过一段时间提交一次,所以每次事务都会包含许多系统调用
为什么定时提交能提升性能?
- 每次事务提交都会有些固定消耗,比如陷入内核等,这些开销被分摊了
- 段时间内,许多系统调用会重复修改同一block,这些修改最终被聚合为一次写入
- 提升了并发性能,不必等待前一次文件系统操作commit完成,即可开始下一个文件系统操作.
备注
- Ext3中,系统调用返回到用户空间时,文件系统并不能保证内容安全.
- 这可能会影响到应用层面的一些崩溃恢复.
- 比如: 邮件服务器接收到消息,并将消息保存到磁盘,然后向客户端回复OK.其实邮件内容还是可能丢失的.
Ext3文件系统并发调用
- 磁盘的log区可能已经保持了部分完整commit的事务,正等待install
- 一些事务正在commit
- 一个尚未完成的事务,正在接受新的系统调用.
Ext3 文件系统调用伪码
sys_open() {
h = start()
get(h, block #)
modify the block in the cache
stop(h)
}
start()
- 告知日志系统将之后的文件系统操作视为一个事务,直到
stop()
- 日志系统必须知道哪些系统调用属于同一个事务,在事务完成前,不能把他们提交到日志系统
- 如果需要,
start()
可能阻塞
get()
- 告知日志系统,我们修改了某个缓存的block,日志系统需要记录已修改的block
- 在一次事务完成前,阻止将已修改的block内容写入到磁盘
stop()
stop()
不能触发一次commit()
- 必须等到所有相关的系统调用都调用了
stop()
之后,事务才可以提交
将一个事务提交到磁盘
- 阻塞新的系统调用
- 等待正在处理中的文件系统操作调用
stop()
- 创建一个新的事务,恢复新的系统调用执行
- 将descriptor和block编号列表写入磁盘log区
- 将block内容从缓存写入到磁盘log区
- 等待所有的log写入完成
- 写入事物已commit标记位
- 将已提交的数据,install到真正的磁盘位置
冲突问题
设想这样一种场景:
- 线程A删除文件x
线程B执行
echo > y
,同时y复用了刚刚删除的x的inode 那么有没有可能线程A,B间发生冲突,导致x,y使用同一个节点呢?如果A,B处于同一个事务中,显然不会发生这种情况
- 如果A,B处于前后分开两个事务中,那么也不会发生.
- 只有当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启动.
设想这样一种场景:
- T1修改了block17
- T1 close,开始将缓存的block写入到log区
- T2启动,T2的某个系统调用也在修改block17
- 那么T1会将T2修改后的block17写入到log区么?
- 如果会的话,那么假设此时发生crash,则会导致部分T2的内容被写入到磁盘,破坏了事务的原子性
所以:
- 当T1 close时,Ext3将会给T1一份修改的block的私有拷贝
- T1会将私有拷贝的内容写入到log区
- 写时拷贝技术此时非常适用
- 正是这样的拷贝技术让T2在T1提交时,就可以同步开始.
备注
- 正确性要求事务必须保持原子性
- 在正确的前提下,Ext3还是使尽了浑身解数来提高并发性能
log区复用
Ext3的log区是循环使用的,那么应该是什么时候复用事务T1的log区呢?有两个条件:
- 所有T1之前的事务都可以释放
- T1相关的block都已经写入到log区正确位置
所谓释放T1对应的log区,也就是修改log superblock中的起始位置
log区不够用,怎么办?
- 假设在事务T2中,执行了系统调用A.
- 执行了系统调用A后,发现T2无法写入log区,因为log区容量不够了.
- 此时,我们既不能提交T2,因为系统调用还未完成.
- 另外,我们也无法撤销系统调用A,因为一来我们没有手段用于撤销系统调用,二来其他系统调用可能已经读取了A修改的内容.
解决方法
- 系统调用提前声明所需的log区大小.
- Ext3
start()
将会阻塞系统调用,直到有足够的空闲log区 - 这样所有能够执行的系统调用均能够顺利完成和提交
性能
删除一个目录中的100个文件的消耗时间:
- xv6大概需要10s,因为每次系统调用都需要6次磁盘同步
- ext3大概需要20ms,因为ext3会将大量的修改合并为极少的操作
一次提交需要多长时间?
- 将修改的block写入到log区
- 等待写入完成
- 将commit标志位写入log区
- 将修改的block内容写入到FS
崩溃
- 在将事务写入到磁盘时,可能发生崩溃
- 最坏的情况时,部分子事务已经写入log区,部分没有
崩溃恢复
- 首先读取log superblock,获取第一个事务的偏移和序号
- 寻找log区结束的位置,可能是错误的序号,也可能是错误的magic
- 可以得出结论,系统就是在log区结束的位置,发生了崩溃.
- 我们需要重新执行log区已经正确记录的事务.
如果最后一个有效日志块之后的块看起来像日志描述符,该怎么办
TODO
Ext3顺序模式
之前我们一直讨论的都是日志模式,在日志模式下,所有的文件内容都会被写入磁盘两次,这将会极大的降低性能.
我们能够不将文件内容写入log区么?如果可以,那应该什么时候将文件内容写入FS?另外我们可以在任意时间将文件内容写入FS么?
答案显然是否定的,因为如果文件的元数据先被提交,而文件内容没有提交.如果此时发生crash,显然磁盘就处于错误的状态.
Ext3顺序模式的实现
- 首先,顺序模式不会将文件内容写入到log区.
- 其次,文件内容会先被写入到FS
- 在文件内容写完之后,才会将文件对应的元数据写入到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可以用于存储元数据也可以用于存储文件内容.
规则总结
经典模式规则
- 除非元数据已经被保存到log区,否则不要写入到磁盘FS.
- 必须等到前一个事务的所有系统调用完成,才能开始下一个事务.
- 在一个buffer cache保存到log区之前,不能覆盖它的内容.
- 所有在log区block的内容没有写入到FS前,不能释放log空间.
顺序模式规则:
- 在提交前,将数据block写入到FS
- 在释放block的系统调用提交前,不能复用这些block
- 不要重复执行 撤销的系统调用
另一个特殊情况: 打开文件和取消链接
- 打开一个文件,然后unlink它
- unlink提交
- 因为文件打开,所以unlink会清楚dir中的内容,但是不会释放文件inode及相关block
- 此时发生crash
- log区中,并没有日志需要重新执行.
- inode及相关block再也不会被释放了,且不可达,不可利用.有点内存泄漏的味道.
解决方案
- unlink提交前,将这个inode添加到FS superblock的一个链表中
- 在恢复过程中,检查这个链表,重新执行inode删除
校验和
事务在将block写入到FS前,必须首先保存到log区.ext3需要等待磁盘完成log区写入后,才能开始真正的写入到FS.
风险在于,磁盘硬件为了性能提升,可能会使用写入缓存和写入重排序.
- 有时操作系统是很难关闭这些特性的
- 有时因为缺乏了解,人们往往会打开这些重排序功能.
- 那么就可能存在乱序的情况,即install在commit之前.
解决方案
- commit block包含了数据block的校验和
- 在崩溃恢复时,首先计算所有相关data block的校验和.
- 如果能够匹配,则重新install.
- 如果不能匹配,则忽略.
- Ext4就具有这样的校验和功能.
Ext3和xv6的比较
- Ext3解决了xv6同步写入的问题,但是有了5s的写入窗口,写入实时性降低.
- 从xv6的一点改动就写入,变成了整合到一起写入.如果重复写入同一block,能极大地提升性能.
- xv6文件内容必须install完成后,才能开始下一次文件操作.Ext3在commit后,已经可以开始下一次文件操作.
- 在实践中,Ext3/Ext4是非常成功的.