xv6 通过简单的
日志系统
来解决文件操作过程当中崩溃所导致的问题。一个系统调用并不直接导致对磁盘上文件系统的写操作,相反,他会把一个对磁盘写操作的描述包装成一个日志
写在磁盘中。当系统调用把所有的写操作都写入了日志,它就会写一个特殊的提交记录
到磁盘上,代表一次完整的操作。从那时起,系统调用就会把日志中的数据写入磁盘文件系统的数据结构中。在那些写操作都成功完成后,系统调用就会删除磁盘上的日志文件。
为什么日志可以解决文件系统操作中出现的崩溃呢?如果崩溃发生在
操作提交之前
,那么磁盘上的日志文件就不会被标记为已完成,恢复系统的代码就会忽视
它,磁盘的状态正如这个操作从未执行过一样。如果是在操作提交之后
崩溃的,恢复程序会重演所有的写操作
,可能会重复之前已经进行了的对磁盘文件系统的写操作。在任何一种情况下,日志文件都使得磁盘操作对于系统崩溃来说是原子操作
:在恢复之后,要么所有的写操作都完成了,要么一个写操作都没有完成。
取自xv6中文文档
begin_trans
(4277) 会一直等到它独占
了日志的使用权后返回。
log_write
(4325) 像是 bwrite的一个代理;它把块中新的内容记录到日志
中,并且把块的扇区号
记录在内存中。log_write仍将修改后的块留在内存中的缓冲区
中,因此相继的本会话中对这一块的读操作都会返回已修改的内容。log_write能够知道在一次会话中对同一块进行了多次读写,并且覆盖之前同一块的日志。
commit_trans
(4301) 将日志的起始块
写到磁盘上,这样在这个时间点之后的系统崩溃就能够恢复,只需将磁盘中的内容用日志中的内容改写。commit_trans 调用install_trans
(4221) 来从日志中逐块的读并把他们写到文件系统中合适的地方。最后 commit_trans会把日志起始块中的计数改为0
,这样在下次会话之前的系统崩溃就会使得恢复代码忽略日志。
recover_from_log
(4268) 在initlog
(4205) 中被调用,而 initlog 在第一个用户进程开始前的引导过程中被调用。它读取日志的起始块,如果起始块说日志中有一个提交了的会话,它就会仿照 commit_trans的行为执行,从而从错误中恢复。
bread 和 bwrite
;前者从磁盘中取出一块放入缓冲区,后者把缓冲区中的一块写到磁盘上正确的地方。当内核处理完一个缓冲块之后,需要调用 brelse 释放它。
本作业分两部分研究xv6日志。首先,您将人为地创建一个崩溃(crash
),说明为什么需要日志记录(logging
)。其次,您将消除xv6日志系统中的一个低效。
Creating a Problem
xv6日志的目的是使文件系统操作的所有磁盘更新相对于崩溃(crashes)来说都是原子性的
。例如,文件创建包括向目录添加新条目
和将新文件的inode标记为in-use
。如果没有日志记录,则在一个崩溃之后而另一个崩溃之前发生的崩溃会使文件系统在重新启动之后处于不正确的状态
。
以下步骤将以一种保留部分创建文件的方式破坏日志代码。
首先,用以下代码替换log.c中的commit():
#include "mmu.h"
#include "proc.h"
void
commit(void)
{
int pid = myproc()->pid;
if (log.lh.n > 0) {
write_log(); // 把log.lh.block中记录的块号对应块的内容存到磁盘中log数据块中(>=log.start+1)
write_head(); // 把刚才写入log的数据所在块的块号(即log.lh.block)写到磁盘中log header里(log.start)
if(pid > 1) // AAA
log.lh.block[0] = 0; // BBB 改变了块号就无法写入正确位置
install_trans();// 从磁盘中log header内容读到log.lh.block,然后数据库写入log.lh.block记录的对应块中
if(pid > 1) // AAA
panic("commit mimicking crash"); // CCC
log.lh.n = 0;
write_head(); // 因为lh.n=0了,所以这里相当于清空了磁盘中log header
}
}
BBB行导致日志中的第一个块被写入block 0,而不是它应该被写到的那个块的块号。在文件创建期间,日志中的第一个块是将新文件的inode更新为非零type
。行BBB导致将更新文件inode的块的type为0
(永远不会读取它),使磁盘上的inode仍然被标记为未分配。CCC行迫使强行崩溃。AAA行禁止init发生这种错误行为,因为它在shell启动之前创建文件。这里看懂了
其次,用以下代码替换log.c中的recover_from_log():
static void
recover_from_log(void)
{
read_head();
cprintf("recovery: n=%d but ignoring\n", log.lh.n);
// install_trans();
log.lh.n = 0;
// write_head();
}
此修改将抑制日志恢复
(本该修复您更改commit()所造成的损害)。
最后,从Makefile中的QEMUEXTRA定义中删除-snapshot选项,以便磁盘映像可以看到更改。这是什么意思?
Now remove fs.img and run xv6:
% rm fs.img ; make qemu
Tell the xv6 shell to create a file:
$ echo hi > a
您应该看到commit()中的panic。到目前为止,似乎在创建文件的过程中,非日志系统发生了崩溃。
recovery: n=0 but ignoring
init: starting sh
$ echo hi > a
lapicid 0: panic: commit mimicking crash
80102d94 80102dea 80105222 80104997 80105a4d 8010580c 0 0 0 0
现在重新启动xv6,保持相同的fs.img:
% make qemu
并查看文件a:
$ cat a
应该看到 panic: ilock: no type.
recovery: n=2 but ignoring
init: starting sh
$ cat a
lapicid 0: panic: ilock: no type
80101837 80105178 80104997 80105a4d 8010580c 0 0 0 0 0
确保你明白发生了什么。文件创建的哪些修改是在崩溃前写入磁盘的,哪些不是?
答:由于a的inode被更新成type 0
,所以磁盘上的该inode还是未分配
状态,无法读取。磁盘中log的内容以及根据log内容更新磁盘内容都是在崩溃前写入的吧。log.lh.n=0
没有写入,要知道log.lh.n的值如果non-zero
== 已提交到on-disk log,log内容有效
而 zero == 未提交成功,log内容无效
日志存在于
磁盘末端
已知的固定区域。它包含了一个起始块,紧接着一连串的数据块。起始块
包含了一个扇区号的数组
,每一个对应于日志中的数据块,起始块还包含了日志数据块的计数
// Copy modified blocks from cache to log.
static void
write_log(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *to = bread(log.dev, log.start+tail+1); // log block,跳过了起始块
struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block 在log_write中赋值了log.lh.block
memmove(to->data, from->data, BSIZE);
bwrite(to); // write the log
brelse(from);
brelse(to);
}
}
// Write in-memory log header to disk.
// This is the true point at which the
// current transaction commits.
static void
write_head(void)
{
struct buf *buf = bread(log.dev, log.start);
struct logheader *hb = (struct logheader *) (buf->data);
int i;
hb->n = log.lh.n;
for (i = 0; i < log.lh.n; i++) {
hb->block[i] = log.lh.block[i];
}
bwrite(buf);
brelse(buf);
}
// Copy committed blocks from log to their home location
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 ??dst怎么来的??
memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
bwrite(dbuf); // write dst to disk
brelse(lbuf);
brelse(dbuf);
}
}
Solving the Problem
现在修复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();
}
运行xv6(保持相同的fs.img)并再次读取a:
$ cat a
这一次应该不会出现崩溃。确保您理解了文件系统现在工作的原因。
init: starting sh
$ cat a
$
答:因为发生panic在写入log.lh.n=0之前,所以当系统恢复的时候,lh.n不为0,系统就认为log中数据有效
,就会从log中把数据全部重新加载
一遍,这样就能正常运行了。
为什么文件是空的,即使您使用echo hi > a创建它?
现在删除对commit() (if、AAA和BBB行)的修改,以便再次进行日志记录,并删除fs.img。
init: starting sh
$ echo hi > a
$ cat a
hi
$
Streamlining Commit
假设文件系统代码想要更新第33块中的inode。文件系统代码将调用bp=bread(block 33)
并更新缓冲区数据。commit()中的write_log()
将数据复制到磁盘日志中的一个块,例如块3。稍后在commit中,install_trans()
从日志中读取第3块(包含第33块),将其内容复制到第33块的内存缓冲区中,然后将该缓冲区写入磁盘上的第33块。
然而,在install_trans()
中,修改后的块33被保证仍然在缓冲区缓存中,文件系统代码将它留在缓冲区缓存中。确保您理解了为什么在提交之前缓冲区缓存将块33从缓冲区缓存中删除是一个错误。
答:首先得明白一个概念,什么叫提交?
在这里提交应该指对缓冲区的修改
成功提交到了on-disk log
中,这样log.lh.n非0,证明log中数据有效
,后面系统会从log中读取数据到正确块中;如果提交未成功,log.lh.n=0,log中内容无效,则系统不会管log数据。如果提交之前就已将缓冲区内容删除,那么还没写入on-disk log中,所以是错误的。
由于已修改的块33保证已经在缓冲区缓存中,所以install_trans()不需要从日志中读取块33。您的工作:修改log.c
,以便当从commit()调用install_trans()时,install_trans()
不会执行不必要的日志读取
。
这里比较简单,只是把读取日志的操作注释掉就行。
// Copy committed blocks from 磁盘中的log to their home location
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 在这里存的是dst
//memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
bwrite(dbuf); // write dst to disk
//brelse(lbuf);
brelse(dbuf);
}
}
要测试更改,请在xv6中创建一个文件,重新启动,并确保该文件仍然存在。
init: starting sh
$ echo hi > b
$ cat b
hi
$ QEMU: Terminated
mylab@ubuntu:/mnt/hgfs/share/xv6-public$ make qemu
qemu-system-i386 -serial mon:stdio -drive file=fs.img,index=1,media=disk,format=raw -drive file=xv6.img,index=0,media=disk,format=raw -smp 1 -m 512
VNC server running on `127.0.0.1:5900'
xv6...
cpu0: starting 0
sb: size 20000 nblocks 19937 ninodes 200 nlog 30 logstart 2 inodestart 32 bmap start 58
recovery: n=0 but ignoring
init: starting sh
$ cat b
hi
$ QEMU: Terminated
提问
1. 修改磁盘块内容的流程?
答:struct buf是缓冲区
(cache),想要修改磁盘块的内容,就得先读到缓冲区
,对缓冲区修改
完成后,再写入on-disk log
,然后系统再从on-disk log中读到正确的磁盘块
中。
2. struct log与log在磁盘中的起始块多个数据块的关系?
答:struct log是in-memory log
,而磁盘中的log是on-disk log
。
3. log.lh.block有时是cache block,有时又是dst in disk,但是我基本没有看见再哪里赋值的,什么时候变化了?
答:首先得知道,log.lh.block是in-memory log中的logheader,里面保存的是数据块的块号。在write_head()中log.lh.block[]中的内容会被写到on-disk log header,也就是log.start中。所以无论cache header
还是on disk log header
,里面存的都是修改后的数据
应该要被写入的磁盘块的块号
,所以都行。
4. 什么叫提交?
答:在这里提交应该指对缓冲区的修改
成功提交到了on-disk log
中,这样log.lh.n非0,证明log中数据有效
,后面系统会从log中读取数据到正确块中;如果提交未成功,log.lh.n=0,log中内容无效,则系统不会管log数据。