logging layer
logging layer的存在主要是为了解决崩溃恢复的问题
因为很多文件系统调用包含了对磁盘的多次写入,所以如果在写入部分完成时发生崩溃就会导致磁盘上的数据处于不一致的状态:
例如,如果崩溃发生在truncate(设置文件大小为0,释放他的内容块)中,那么可能导致如下两种状态:
- 一个inode引用了一个磁盘块,但是该块被标记为free
- 一个块标记为allocated,但是没有inode引用它
update 2020年11月8日:
补充对truncate的解释:
这个操作发生在itrunc里(这个函数其实需要在lab fs第一部分中修改,但是那一篇中没有加进去,TODO):
抛开inode组织数据的逻辑,itrunc做的工作是,将inode的data数组置空(不考虑二级,三级的话),并且在置空之前用bfree释放掉该块
(注意这里bfree中的b指的是block,磁盘块,不是buffer)
回顾一下bfree是怎样“释放一个磁盘块的”:
// Free a disk block. static void bfree(int dev, uint b) { struct buf *bp; int bi, m; bp = bread(dev, BBLOCK(b, sb)); // b是块号,BPB是一个块的字节数乘以8,即一个位图块能保存多少个包的使用状态 // b/BPB,可以得到具体是哪个位图块保存了b这个块的信息; // b%BPB,则可以得到是该位图块的哪个bit保存了这个块的信息 bi = b % BPB; // bi就是该bit在该位图块中的index // bi/8,可以得到是该位图块的哪个字节保存了这个块的信息 // bi%8,则可以得到是该字节的哪个bit保存了这个块的信息,然后造出m这个mask m = 1 << (bi % 8); // 从下面看,位图块的组织方式和inode不一样?他的data部分保存的就是bit map bit?TODO // 然后直接看位图块对应的字节,如果&mask后为0,说明本来就是空闲,那么属于refree // 否则,就是用mask进行释放 if((bp->data[bi/8] & m) == 0) panic("freeing free block"); bp->data[bi/8] &= ~m; log_write(bp); brelse(bp); }
这一部分的补充主要是为了说明上面两种状态分别是怎么造成的:
1.inode引用了磁盘块,是指inode的data数组没有对应置空,标记为free,是指调用了bfree,对应bit map bit为0
2.与上面刚好相反,bit map bit为1,然后没有inode的data数组是直接/间接指向他的
后者导致的后果比较温和,但前者会导致严重的后果:
重启之后,该块(标记为free)可能会被分配给其他文件,那么我们现在有两个inode引用同一个块,如果这是在多用户下,那么两者对同一个块的读写会引起严重的后果:旧用户可以读写新用户的文件
xv6采取了一种简单的日志形式来解决上述问题:
xv6系统调用不直接写入磁盘上的文件块,而是将其希望执行的磁盘写入的描述记录到磁盘上的日志中。一旦它记录完了所有的写入,他就向磁盘上写一个特殊的commit记录,表示这个日志包含了一个完整的操作。此时系统调用再将这些写入到磁盘上的文件系统数据结构。当全部的写入完成后,系统调用再清除掉日志。
系统发生崩溃并重启后,再运行其他进程之前,文件系统会这样恢复:如果日志被标记为complete,那么将其描述的写入到磁盘上,如果没有,那么忽视该日志。恢复代码结束时会删除日志文件
为什么这种方式能够解决上面提到的崩溃恢复问题?如果崩溃发生在commit之前(写入记录没有记录完全),那么恢复的代码将忽略这些日志,就好像什么操作都没有进行一样。