这个作业主要是让你熟悉xv6的文件系统中log部分。该作业共分为两个部分,第一个部分是根据代码回答一些问题,第二个部分是对现有的代码做一点点优化。
xv6 log
xv6 中 log 主要是为了解决崩溃恢复的问题。因为在文件系统中,很多操作涉及到磁盘上很多块的读写,在这些读写中间如果发生系统崩溃,那么磁盘上的文件系统就会陷入一种不一致的中间状态。比如。而log的设计就可以让一连串的读写成为一个原子操作,要么这一连串读写都不成功,要么都成功,而不会出现一部分成功的中间状态。
xv6 文件接口例子
这部分用几个例子来说明xv6 文件系统接口的函数调用过程,这几个例子是依次进行的。弄懂了这几个例子,就能对 xv6的文件系统的接口执行时的函数调用有更深的了解。
1. xv6 创建文件,命令为 echo > a
, 如下:
$ echo > a
write 34 ialloc (from create sysfile.c; mark it non-free)
write 34 iupdate (from create; initialize nlink &c)
write 59 writei (from dirlink fs.c, from create)
call graph:
sys_open sysfile.c
create sysfile.c
ialloc fs.c
iupdate fs.c
dirlink fs.c
writei fs.c
首先是open系统调用,带了一个新建文件的参数,所以在 sys_open
中会去调用 create
函数,因为是新建文件,所以 dirlookup
在该文件夹中没找到名为 a
的文件,返回0。转而去执行 ialloc
, ialloc
会分配一个新的 inode
,修改它的 type
, 并用 log_write
写回,这就是上面的第一次 write
, 所以 34 就是新分配的 inode
所在的块号。 ialloc
返回后,create
修改它的 nlink
等属性,再用 iupdate
进行更新,这就是上面的第二次 write
。最后create
调用 dirlink
将新的 inode
挂在目标文件夹下面。在 dirlink
中,使用 writei
修改文件夹内容,writei
修改完后会用 log_write
写回磁盘,这就是第三次 write
,所以现在可以回答以下问题了。
Q: block 34
和 block 59
里存的是什么?
A: block 34
存的有新分配的文件 a
的 inode
, block 59
存的是文件 a
的上级文件夹的目录,里面有文件 a
的信息。
Q: 为什么有两次对 block 34
的 writei
?
A: 第一次是 create
调用 ialloc
分配 inode
时修改 type
进行保存所产生的。第二次是 create
随后修改 inode
的 nlink
等属性再保存产生的。不过这两次修改在 cache buf
和 磁盘的 log
都只占一份(对于同一个 block
在内存中只能有一个 cache buf
),所以也没啥影响。
Q: 如果并发地调用 ialloc
会怎么样?他们会得到同一个 inode
吗?
A: ialloc
主要调用 bread
来 读取 inode
, 而bread
主要是调用 bget
, 其在返回时会获取 buf
的sleeplock
, 即整个 block
都被锁住。其他人必须等 brelse
释放了这个 block
才可能使用这个 inode
。所以是不可能得到同一个 inode
的。
2. xv6 写入文件,命令为 echo x > a
, 如下:
$ echo x > a
write 58 balloc (from bmap, from writei)
write 508 bzero
write 508 writei (from filewrite file.c)
write 34 iupdate (from writei)
write 508 writei
write 34 iupdate
call graph:
sys_write sysfile.c
filewrite file.c
writei fs.c
bmap
balloc
bzero
iupdate
由于上面已经创建好了文件 a
, 所以 open
不会产生 write
。后面调用 write
系统调用才会产生 write
。sys_write
只是 filewrite
的一层包装。在 filewrite
中,会调用 writei
真正进行写入。writei
会对每个 block
先调用 bmap
进行映射,由于文件目前还是空的,所以 bmap
会调用 balloc
分配一个 block
, balloc
分配时会修改分配的块的 bit map
标志位,并用 log_write
保存。这对应着上面的第一次 write
。所以block 58
为文件 a
第一个block
的 bit map
标志所在的 block
。随后 balloc
调用 bzero
将新分配的块的内容全部置为0, bzero
对块写入后用 log_write
保存,这对应着上面的第二次 write
。所以 block 508
为 文件 a
的第一个块的块号。bmap
执行完返回后,writei
将数据写入到分配的块中,用 log_write
保存。这对应着上面的第三次 write
。接着它更新 文件 a
的 inode
的 size
, 并用 iupdate
保存,这对应着上面的第四次 write
,所以block 34
存的有文件 a
的 inode
。
到这里到这个文件的写入其实就已经结束了,后面的两次 write
其实是因为 echo
进行了两次 write
系统调用,第二次用来输出 newline
,也就是对应着最后的两次 write
。
Q: block 58
和 block 508
里存的是什么?
A: block 34
存的有新分配的文件 a
的 inode
, block 59
存的是文件 a
的上级文件夹的目录,里面有文件 a
的信息。block 508
为 文件 a
的第一个块的块号。
Q: 为什么有两次 writei
和 iupdate
?
A: 因为 echo
进行了两次 write
系统调用,第二次用来输出 newline
,也就是对应着最后的两次 write
。
3. xv6 删除文件,命令为 rm a
, 如下:
$ rm a
write 59 writei (from sys_unlink; directory content)
write 34 iupdate (from sys_unlink; link count of file)
write 58 bfree (from itrunc, from iput)
write 34 iupdate (from itrunc; zeroed length)
write 34 iupdate (from iput; marked free)
call graph:
sys_unlink
writei
iupdate
iunlockput
iput
itrunc
bfree
iupdate
iupdate
rm
命令调用的就是 sys_unlink
, 它会先调用 writei
去修改父文件夹的内容,在writei
中会将修改后的块用log_write
保存,这就是第一次 write
, 所以block 59
存的是文件 a
的上级文件夹的目录,这与上面分析得到的结论保持一致。然后紧接着 sys_unlink
会修改 inode
的 nlink
, 并用 iupdate 保存,这对应着上面的第二次 write
。所以block 34
存的是文件 a
的 inode
, 这也与上面分析得到的结论保持一致。最后就是 iunlockput(ip);
, 先解锁 inode
, 再调用 iput
释放inode
, iput
中调用 itrunc
再调用 bfree
,
bfree
会修改文件 a
的块在bit map
中的标志位,并用 log_write
保存,这对应着上面的第三次 write
。所以 block 58
为文件 a
第一个block
的 bit map
标志所在的 block
。bfree
执行完后,itrunc
修改 inode
的 size
为 0,并用 iupdate
保存。这对应着上面的第四次 write
。itrunc
执行完后,iput
会将 inode
的 type
置为 0, 并用 iupdate
保存,这对应着上面的第五次 write
Q: block 34
, block 58
和 block 59
里存的是什么?
A: block 34
存的有新分配的文件 a
的 inode
, block 59
存的是文件 a
的上级文件夹的目录,里面有文件 a
的信息,block 58
为文件 a
第一个block
的 bit map
标志所在的 block
。
Q: 为什么有3次 iupdate
?
A: 第一次 iupdate
是修改 inode
的 nlink
。第二次是iupdate
是修改 inode
的 size
。第三次是iupdate
是修改 inode
的 type
。
第一部分
这部分让你将部分代码替换成他给的代码,再分析问题。这里先给结论,分析在后面。
结论: 对 commit()
和 对 recover_from_log
的修改使得文件创建过程中对该文件自身 inode
的更新没有写到磁盘对应块上, 即 inode
和未分配一样,而文件创建过程中对文件夹内容的修改写入了磁盘正确的位置。而文件创建结束后,就直接报错 panic: commit mimicking crash
。 所以 hi
也没有被写入文件内容。重启后运行 cat a
的过程中会 ilock
文件的 inode
,在 ilock
中会检查 inode
的 type
, 发现 type
为空, 便产生 panic: ilock: no type.
报错。
整个问题重现过程如下:
- 将
log.c
中的commit*(
替换成以下代码。修改之后的commit()
将 log中的第一个块写入到磁盘的0号块,而不是它原本应该被写入的块。
#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
install_trans();
if(pid > 1) // AAA
panic("commit mimicking crash"); // CCC
log.lh.n = 0;
write_head();
}
}
- 将
log.c
中的recover_from_log()
替换成以下的代码。修改之后的代码不会从磁盘上的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();
}
- 在
Makefile
文件中移除QEMUEXTRA = -snapshot
(hw: big file 中用到过)。同时用命令rm fs.img
删除 xv6的文件系统镜像并用make qemu-nox
重启 - 在 xv6 shell 中输入
echo hi > a
创建一个名为a
的文件,并向其中写入hi
。然后你就会看到来自commit()
的panic
。这样我们就模拟了在文件创建的过程中发生崩溃的过程。
- 重启 xv6。然后在 xv6 shell 中输入
cat a
查看文件a
的内容。这个时候你会发现会报错panic: ilock: no type
然后让你分析这个过程中的问题。
在 echo hi > a
命令中第一步是创建文件(这里假设原本没有a文件),即调用 sys_open
包含一个新建文件的参数。sys_open
代码如下:
int
sys_open(void)
{
char *path;
int fd, omode;
struct file *f;
struct inode *ip;
if(argstr(0, &path) < 0 || argint(1, &omode) < 0)
return -1;
begin_op();
if(omode & O_CREATE){
ip = create(path, T_FILE, 0, 0);
if(ip == 0){
end_op();
return -1;
}
} else {
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op();
return -1;
}
}
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
if(f)
fileclose(f);
iunlockput(ip);
end_op();
return -1;
}
iunlock(ip);
end_op();
f->type = FD_INODE;
f->ip = ip;
f->off = 0;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
return fd;
那么它会开启一个 log transaction
, 在这个 log transaction
中 调用 create
来创建 名为 a
的文件。 create
的执行过程可以参考上面的第一个例子和其写入的块,基本上差不多。到 create
执行完毕返回,其 log
部分包含两个 block
;第一个block
是对新文件 inode
的更新,包括type
, nlinks
等等。第二个 block
是对父文件夹内容的更新,写如新文件的 name
和 inode numebr
。
create
返回后会调用 filealloc
会去分配一个内核管理的打开文件的数据结构 和 fdalloc
去分配一个文件描述符,不过这两部分都是内存里的操作,不涉及 log
和 cache buf
, 所以直接跳过。
随后就是 iunlock(ip);
和 end_op();
, 对 inode
解锁,并结束 log transaction
,那么在整个 log transaction
中,log
部分其实就只有上面的两个 block
。而在 end_op()
会调用我们修改过的 commit()
, 它将 log
中的第一个 blcok
的目标块号改成了0,其他不变。log
中的第一个 blcok
是对新文件的 inode
的修改,那么commit()
会使得对新文件的 inode
的修改无法保存到磁盘,而log
中的二个 block
保存的是对父文件夹内容的更新,可以成功保存到磁盘上。那么 commit()
执行完就会变成这样:父文件夹能看到新创建的文件 a
, 但是这个文件的 inode
却是未分配的。随后 commit()
就会报错 panic: commit mimicking crash
。到此完毕,但要注意到这里其实连 sys_open
都还没执行完并返回,后面要对文件写入 hi
就更不用说了,根本就没写入!
重启 xv6。然后在 xv6 shell 中输入 cat a
查看文件 a
的内容,cat
会调用 sys_open
, 只是不带新建文件的参数。在 sys_open
会调用 namei
去获得文件 a
的 inode
。上面的分析说明了父文件夹能看到新创建的文件 a
,那么此步能顺利找到文件 a
的 inode
,不会报错。详细过程为 namei
调用 namex
再调用 dirlookup
去找这个 inode
,dirlookup
最后找到后会使用 iget
去创建一个内存里的 inode
,且 inode
的 valid
为0, 表示这个inode
的数据在磁盘中,还没读到内存里来,然后返回。namei
执行完毕后下一条就是 ilock(ip);
, ip
为上面返回的 inode
, 由于 valid
为0, 它回去磁盘读取 inode
数据。正如前面所说,对新文件的 inode
的修改根本没保存到磁盘,所以 type
为0, 在随后对 type
的检查中报错 panic: ilock: no type
。
至此,整个过程分析完毕,结论可以参照这部分的最顶部。
第二部分
这部分主要是优化 commit()
函数。一个大概的流程如下:
struct buf *bp = bread(block 33);
// update block 33 here
log_write(bp);
commit(); // actualy it's inside end_op
...
其中 commit()
分为以下四个阶段:
write_log()
将已修改的块的内容从cache中写入到磁盘的log
区域中。中间系统崩溃那么这个log
会消失,相当于从从没发生过。write_head()
将log
区域中各个块对应的写入的块号和总块数写到磁盘的log
区域的头部。执行完后整个log才算真正写进去,执行完后即使系统崩溃也没有关系。重启后会进行恢复处理。install_trans()
将磁盘的log
区域的各个块写入到磁盘的对应位置。这时中间系统崩溃的话重启重启后会进行恢复处理。write_head()
再将总块数设为0后写入磁盘的log
区域的头部。这时中间系统崩溃的话重启重启后会进行恢复处理。
那么有结论:在 commit()
中install_trans()
没有必要去读取磁盘的 log
区域的块内容,因为这些块的内容必定在 cache中。我们可以将其改成从cache中直接写入到磁盘的log
区域, 从而节省下去磁盘读取的时间。
以上面的流程为例,它读取磁盘上块号为 33
的内容,得到一个 cache buf
, 名为 bp
。 在对内容做了一定修改之后,进行 commit
。由于 每个 cache buf
有一个 refcnt
用于统计指向它的指针数,只有当 refcnt
变成 0 才会被清除出 cache
。在这个例子中, 在 bread()
之后, bp
的 refcnt
为 1,在commit
之前一直为1,即一直在 cache
中。而 commit()
的四个阶段都不会影响任何 buf
的 refcnt
,所以在 commit()
内部 bp
的 refcnt
也为 1,即 bp
一直存在于 cache
,除非用户自己调用 brelse(bp)
手动释放。
install_trans()
共在两个地方被调用,一个是上面的 commit()
, 上面说明了可以改成从cache
写入磁盘对应位置。另一个是 recover_from_log()
, 这个负责在 xv6 启动的时候从磁盘的 log
区域中恢复,这个必须从磁盘的 log
区域中读取。
所以将原来的install_trans()
改成两个版本,一个从cache读,一个从磁盘的 log
区域中读。
// Copy committed blocks from log to their home location
static void
install_trans_from_log(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);
}
}
// for hw: xv6 log, when called from commit, there is no need to read the
// block content from log because the content is in the cache.
// Copy committed blocks from cache to their home location
static void
install_trans_from_cache(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
bwrite(dbuf); // write dst to disk
brelse(dbuf);
}
}
并将 commit()
, recover_from_log()
做相应的修改,如下:
static void
commit()
{
if (log.lh.n > 0) {
write_log(); // Write modified blocks from cache to log
write_head(); // Write header to disk -- the real commit
install_trans_from_cache(); // Now install writes to home locations
log.lh.n = 0;
write_head(); // Erase the transaction from the log
}
}
static void
recover_from_log(void)
{
read_head();
install_trans_from_log(); // if committed, copy from log to disk
log.lh.n = 0;
write_head(); // clear the log
}
自此修改完毕,下面测试一下
- 使用
echo hi > a
创建一个 名为a
的文件,再退出 xv6 - 再次运行 xv6, 使用
cat a
查看文件a
的内容,如正确输出hi
则表示没有问题。
总结
这次作业难度还好,主要是理解xv6的文件系统。整个文件系统的架构还是比较好理解的,难点是层之间的关系和各种锁的使用。