6.828(2018) hw: xv6 log

这个作业主要是让你熟悉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 34block 59 里存的是什么?
A: block 34存的有新分配的文件 ainode, block 59存的是文件 a 的上级文件夹的目录,里面有文件 a 的信息。

Q: 为什么有两次对 block 34writei
A: 第一次是 create 调用 ialloc 分配 inode 时修改 type 进行保存所产生的。第二次是 create 随后修改 inodenlink 等属性再保存产生的。不过这两次修改在 cache buf 和 磁盘的 log 都只占一份(对于同一个 block 在内存中只能有一个 cache buf),所以也没啥影响。

Q: 如果并发地调用 ialloc 会怎么样?他们会得到同一个 inode 吗?
A: ialloc 主要调用 bread 来 读取 inode, 而bread 主要是调用 bget, 其在返回时会获取 bufsleeplock, 即整个 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 系统调用才会产生 writesys_write 只是 filewrite 的一层包装。在 filewrite 中,会调用 writei 真正进行写入。writei会对每个 block 先调用 bmap 进行映射,由于文件目前还是空的,所以 bmap 会调用 balloc 分配一个 block, balloc 分配时会修改分配的块的 bit map 标志位,并用 log_write 保存。这对应着上面的第一次 write。所以block 58 为文件 a 第一个blockbit map 标志所在的 block。随后 balloc 调用 bzero 将新分配的块的内容全部置为0, bzero 对块写入后用 log_write 保存,这对应着上面的第二次 write。所以 block 508 为 文件 a的第一个块的块号。bmap 执行完返回后,writei 将数据写入到分配的块中,用 log_write 保存。这对应着上面的第三次 write。接着它更新 文件 ainodesize, 并用 iupdate 保存,这对应着上面的第四次 write,所以block 34存的有文件 ainode

到这里到这个文件的写入其实就已经结束了,后面的两次 write 其实是因为 echo 进行了两次 write 系统调用,第二次用来输出 newline ,也就是对应着最后的两次 write

Q: block 58block 508 里存的是什么?
A: block 34存的有新分配的文件 ainode, block 59存的是文件 a 的上级文件夹的目录,里面有文件 a 的信息。block 508 为 文件 a的第一个块的块号。

Q: 为什么有两次 writeiiupdate
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 会修改 inodenlink, 并用 iupdate 保存,这对应着上面的第二次 write。所以block 34存的是文件 ainode, 这也与上面分析得到的结论保持一致。最后就是 iunlockput(ip);, 先解锁 inode, 再调用 iput 释放inode, iput 中调用 itrunc 再调用 bfree,
bfree 会修改文件 a 的块在bit map 中的标志位,并用 log_write 保存,这对应着上面的第三次 write。所以 block 58 为文件 a 第一个blockbit map 标志所在的 blockbfree 执行完后,itrunc修改 inodesize 为 0,并用 iupdate 保存。这对应着上面的第四次 writeitrunc 执行完后,iput 会将 inodetype 置为 0, 并用 iupdate 保存,这对应着上面的第五次 write

Q: block 34, block 58block 59 里存的是什么?
A: block 34存的有新分配的文件 ainode, block 59存的是文件 a 的上级文件夹的目录,里面有文件 a 的信息,block 58 为文件 a 第一个blockbit map 标志所在的 block

Q: 为什么有3次 iupdate
A: 第一次 iupdate 是修改 inodenlink。第二次是iupdate 是修改 inodesize。第三次是iupdate 是修改 inodetype

第一部分

这部分让你将部分代码替换成他给的代码,再分析问题。这里先给结论,分析在后面。

结论: 对 commit() 和 对 recover_from_log 的修改使得文件创建过程中对该文件自身 inode的更新没有写到磁盘对应块上, 即 inode 和未分配一样,而文件创建过程中对文件夹内容的修改写入了磁盘正确的位置。而文件创建结束后,就直接报错 panic: commit mimicking crash。 所以 hi 也没有被写入文件内容。重启后运行 cat a 的过程中会 ilock 文件的 inode,在 ilock 中会检查 inodetype, 发现 type 为空, 便产生 panic: ilock: no type. 报错。

整个问题重现过程如下:

  1. 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();
  }
}
  1. 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();
}
  1. Makefile 文件中移除 QEMUEXTRA = -snapshot (hw: big file 中用到过)。同时用命令 rm fs.img 删除 xv6的文件系统镜像并用 make qemu-nox 重启
  2. 在 xv6 shell 中输入 echo hi > a 创建一个名为 a 的文件,并向其中写入 hi。然后你就会看到来自 commit()panic。这样我们就模拟了在文件创建的过程中发生崩溃的过程。
    panic
  3. 重启 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是对父文件夹内容的更新,写如新文件的 nameinode numebr

create 返回后会调用 filealloc 会去分配一个内核管理的打开文件的数据结构 和 fdalloc 去分配一个文件描述符,不过这两部分都是内存里的操作,不涉及 logcache 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 去获得文件 ainode。上面的分析说明了父文件夹能看到新创建的文件 a,那么此步能顺利找到文件 ainode,不会报错。详细过程为 namei 调用 namex再调用 dirlookup 去找这个 inodedirlookup最后找到后会使用 iget 去创建一个内存里的 inode,且 inodevalid 为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() 分为以下四个阶段:

  1. write_log() 将已修改的块的内容从cache中写入到磁盘的 log 区域中。中间系统崩溃那么这个 log 会消失,相当于从从没发生过。
  2. write_head()log 区域中各个块对应的写入的块号和总块数写到磁盘的 log 区域的头部。执行完后整个log才算真正写进去,执行完后即使系统崩溃也没有关系。重启后会进行恢复处理。
  3. install_trans() 将磁盘的 log 区域的各个块写入到磁盘的对应位置。这时中间系统崩溃的话重启重启后会进行恢复处理。
  4. write_head() 再将总块数设为0后写入磁盘的 log 区域的头部。这时中间系统崩溃的话重启重启后会进行恢复处理。

那么有结论:在 commit()install_trans() 没有必要去读取磁盘的 log 区域的块内容,因为这些块的内容必定在 cache中。我们可以将其改成从cache中直接写入到磁盘的log区域, 从而节省下去磁盘读取的时间。

以上面的流程为例,它读取磁盘上块号为 33 的内容,得到一个 cache buf, 名为 bp。 在对内容做了一定修改之后,进行 commit 。由于 每个 cache buf 有一个 refcnt 用于统计指向它的指针数,只有当 refcnt 变成 0 才会被清除出 cache。在这个例子中, 在 bread() 之后, bprefcnt 为 1,在commit 之前一直为1,即一直在 cache 中。而 commit() 的四个阶段都不会影响任何 bufrefcnt,所以在 commit() 内部 bprefcnt 也为 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
}

自此修改完毕,下面测试一下

  1. 使用 echo hi > a 创建一个 名为 a 的文件,再退出 xv6
  2. 再次运行 xv6, 使用 cat a 查看文件 a 的内容,如正确输出 hi 则表示没有问题。

在这里插入图片描述

总结

这次作业难度还好,主要是理解xv6的文件系统。整个文件系统的架构还是比较好理解的,难点是层之间的关系和各种锁的使用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值