First, you’ll artificially create a crash which illustrates why logging is needed. Second, you’ll remove one inefficiency in the xv6 logging system.
xv6日志的要点是使所有可能产生崩溃的文件系统操作都是原子的。 例如,文件创建涉及向目录添加新条目以及将新文件的inode标记为使用中。 如果没有日志,则在重新启动后,在一个之后但在另一个之前发生的崩溃将使文件系统处于不正确的状态。
First, replace commit() in log.c with this code:
#include "mmu.h"
#include "proc.h"
void
commit(void)
{
int pid = myproc()->pid;
if (log.lh.n > 0) {
write_log();
write_head();
if(pid > 1) // AAA
log.lh.block[0] = 0; // BBB
// Copy committed blocks from log to their home location
install_trans();
if(pid > 1) // AAA
panic("commit mimicking crash"); // CCC
log.lh.n = 0;
write_head();
}
}
BBB行导致日志中的第一个块被写入 0,而不是写入应该写入的任何位置。 在文件创建期间,日志中的第一个块是新文件已更新为非零类型的inode。 行BBB使具有更新的inode的块被写入 0(从而永远不会被读取),使得磁盘上的inode仍然标记为未分配。 CCC行强制一次崩溃。 AAA行抑制了init
的这种错误行为,它在shell启动之前创建文件。
log结构体定义。
struct log {
struct spinlock lock;
int start;
int size;
int outstanding; // how many FS sys calls are executing.
int committing; // in commit(), please wait.
int dev;
struct logheader lh;
};
// Contents of the header block, used for both the on-disk header block
// and to keep track in memory of logged block# before commit.
struct logheader {
int n;
int block[LOGSIZE];
};
Second, replace recover_from_log() in log.c with this code:
此修改会抑制日志恢复(这本能修复因更改commit()
而导致的 damage)。
Finally, remove the
-snapshot
option from the definition of QEMUEXTRA in your Makefile so that the disk image will see the changes.
$ echo hi>a
lapicid 0: panic: commit mimicking crash
80102dc3 80105292 80104987 80105b89 8010587c 0 0 0 0 0
$ cat a
lapicid 0: panic: ilock: no type
80101837 801051d8 80104977 80105b79 8010586c 0 0 0 0 0
我们应该明白为什么会出现这种现象。哪部分文件创建的修改在崩溃发生之前被写入了磁盘,哪部分没有。block[0]写入了全零,log.lh.n = 0;
没有写入。此时OS认为该文件还有缓冲区的内容没有写入,同时因为其 type 修改成了零(block[0] = 0),就出现了 no type
的 panic.
fix recover_from_log()
static void
recover_from_log(void)
{
read_head();
cprintf("recovery: n=%d\n", log.lh.n);
install_trans();
log.lh.n = 0;
write_head();
}
此时不会出现 panic, 但尽管执行了echo hi>a
,a文件依旧是一个空文件。因为再次启动OS时, 缓冲区的内容已经丢失了,install_trans
操作并没有实际写入内容。这里也反映出必须保证文件操作的原子性。
怀着好奇心,我进行了又一步骚操作。但是不知道为什么,在我们实验修改了install_trans
(即直接从缓冲区将数据写入物理内存)之后,按照之前的操作,会出现no type
的 panic。讲道理应该是没有影响的啊。
recovery: n=2 but ignoring
init: starting sh
$ cat a
$
这一部分要分清几个概念。commit
在end_op
中调用。
log.lh.block
: 定义在log结构体中,OS直接修改的内容。log.start
: 记录缓冲区内容的变换,log.start 记录了修改内容的地址。假设文件系统代码想要更新块33中的一个 inode。文件系统代码将调用bp = bread(block 33)
并且更新缓冲区中的数据。commit()
中的write_log()
将会把缓冲区的数据拷贝到 log 块,例如块3。install_trans()
读块3,拷贝块3的内容到块33的内存缓冲区,最后将内容写到块33中。
**Since the modified block 33 is guaranteed to already be in the buffer cache, there’s no need for install_trans() to read block 33 from the log. **其实我们可以不必从log中读数据,我们可以直接将缓冲区的数据写到块33就可以了。
修改install_trans
如下。
static void
install_trans(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
// struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
// memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
bwrite(dbuf); // write dst to disk
// brelse(lbuf);
brelse(dbuf);
}
}
到最后,我们可以仔细体会一下这个HW。首先,这个HW让我们体会到了文件操作需要满足原子性,不然十分容易导致文件系统产生错误。log 机制实现了 OS对文件的操作(对 log 缓冲区的操作),最后通过commit 将修改写入物理内存,这避免了频繁地访问磁盘,可以提高文件操作地速度。 最后,我们发现可以直接将缓冲区的数据写入物理内存,可以优化数据存取的速度!