xv6 文件系统(下)

news2024/12/23 9:15:30

〇、前言

计算机崩溃后如何恢复,是一个很重要的话题。对于内存中的数据无关痛痒,开机后重新载入就能解决问题;但是对于持久化存储设备,当你尝试修改一个文件,突然断电当你重新打开文件后,这个文件的状态是否正确,是一个问题。

我们讨论文件的状态是否正确,是指文件系统对于这个文件是否运行正常,比如 entry 中的 inode 信息是否与用户期望的一致,比如 size 字段是否正确等。

因此,如何从崩溃中恢复文件,是着重研究的问题。事实上,logging 是解决这一类问题的普遍方法,不管是 RedisMySQL,还是其它数据系统,都利用 logging 技术恢复、导入、复制数据等。

在文件系统中,logging 通过记录修改操作的顺序和内容来确保文件系统的数据一致性。通过在事务发生前将修改操作写入日志,然后再将这些修改应用到实际的文件系统结构中,即使在崩溃时,系统也可以根据日志来重新应用这些操作,从而恢复到崩溃之前的状态。

这种技术在许多数据系统中也被广泛应用,比如数据库系统(比如MySQLPostgreSQL等)、缓存系统(比如Redis)等。它们使用日志记录来确保数据库或者缓存在崩溃或者异常情况下的数据完整性和一致性。通过记录操作和事务,系统可以在崩溃后重新应用这些记录,从而确保数据的恢复和一致性。

对于文件系统而言,logging 技术能够确保即使在发生意外情况时,文件系统也能够安全地进行崩溃恢复,防止数据损坏或丢失。

一、文件系统 logging

super block 之后就是 log block,我们今天主要介绍的就是 log blocklog block 之后是 inode block,每个 block 可能包含了多个 inode。之后是 bitmap block,它记录了哪个 data block 是空闲的。最后是 data block,这里包含了文件系统的实际数据。

logging的基本思想就是将磁盘分成两部分:一个是用于记录变更的log区域;另一个是存储实际文件系统的数据区域。这种分区的方式确保了在写入文件系统之前,先将变更记录到log中,从而提供了一种安全机制以应对崩溃或意外关机等情况

这种方式的优点在于,即使在写入文件系统的过程中发生了意外情况,比如断电或者系统崩溃,log中的记录仍然存在,记录了文件系统的变更操作。这样,在系统重启时,可以根据 log 中的内容重新应用这些变更,从而恢复到上一次的一致状态。

文件系统通常会选择定期地或者在关键操作之前将 log 中的变更同步到文件系统数据区域中,以确保文件系统的一致性和持久性。

这种设计对于确保数据的完整性和系统的稳定性非常有帮助,因为它提供了一种机制来处理异常情况下的数据恢复,并且在很大程度上减少了数据损失的风险。

它有一些好的属性:

  • 首先,它可以确保文件系统的系统调用是原子性的。比如你调用 create/write 系统调用,这些系统调用的效果是要么完全出现,要么完全不出现,这样就避免了一个系统调用只有部分写磁盘操作出现在磁盘上。
  • 其次,它支持快速恢复(Fast Recovery)。在重启之后,我们不需要做大量的工作来修复文件系统,只需要非常小的工作量。这里的快速是相比另一个解决方案来说,在另一个解决方案中,你可能需要读取文件系统的所有 block,读取 inodebitmap block,并检查文件系统是否还在一个正确的状态,再来修复。而logging可以有快速恢复的属性
  • 最后,原则上来说,它可以非常的高效。

log write

当需要更新文件系统时,我们并不是更新文件系统本身。假设我们在内存中缓存了 bitmap block,也就是 block45。当需要更新bitmap 时,我们并不是直接写 block45,而是将数据写入到 log 中,并记录这个更新应该写入到 block45。对于所有的写 block 都会有相同的操作,例如更新inode,也会记录一条写 block33log

所以基本上,任何一次写操作都是先写入到 log,我们并不是直接写入到 block 所在的位置,而总是先将写操作写入到 log 中。

commit op

之后在某个时间,当文件系统的操作结束了,并且都存在于 log 中,我们会 commit 文件系统的操作。这意味着我们需要在 log 的某个位置记录属于同一个文件系统的操作的个数,例如 5

install log

当我们在 log 中存储了所有写 block 的内容时,如果我们要真正执行这些操作,只需要将 blocklog分区 移到 文件系统分区。我们知道第一个操作该写入到 block45,我们会直接将数据从 log 写到 block45,第二个操作该写入到 block33,我们会将它写入到 block33,依次类推。

clean log

一旦完成了,就可以 清除log清除log 实际上就是将属于同一个文件系统的操作的个数设置为0

因此 logging 可以分为以下几步:

  • 1)log write;
    1. commit op;
    1. install;
    1. clean up.

我们考虑 crash 的几种可能情况:

  • 在第1步和第2步之间崩溃,会导致数据丢失,因为 log 还未被写入磁盘。这样的情况下,系统重启时不会有任何数据变更,就好像这些系统调用从未发生过一样;
  • 在第2步和第3步之间崩溃,由于 log 已经被成功写入磁盘并有commit记录,所有的文件系统操作已经完成。这种情况下,在重启后系统能够保持一致性;
  • 在install(第3步)过程中和第4步之前崩溃,系统在下次重启时会重新执行 logredo log 会再次将 log block 的数据应用到文件系统。因为 install log 是幂等操作,即使重复多次也不会产生问题,这确保了系统在恢复时能够达到一致状态。

有一个有意思的问题:当正在 commit log 的时候 crash 了会发生什么?比如说你想执行多个写操作,但是只 commit 了一半。

在上面的第2步,执行 commit 操作时,你只会在记录了所有的 write操作之后,才会执行commit操作。所以在执行 commit 时,所有的write操作必然都在log中。而commit操作本身也有个有趣的问题,它究竟会发生什么?如我在前面指出的,commit操作本身只会写一个block。

文件系统通常可以这么假设,单个block或者单个sector的write是原子操作。这里的意思是,如果你执行写操作,要么整个sector都会被写入,要么sector完全不会被修改。所以sector本身永远也不会被部分写入,并且commit的 目标sector 总是包含了有效的数据。

而commit操作本身只是写log的header,如果它成功了只是在commit header中写入log的长度,例如5,这样我们就知道log的长度为5。这时crash 并重启,我们就知道需要重新install 5个block的log。如果 commit header 没能成功写入磁盘,那这里的数值会是0。**我们会认为这一次事务并没有发生过。**这里本质上是 write ahead rule它表示 logging系统在所有的写操作都记录在log中之前,不能install log。

在执行 commit 操作时,系统会确保在将所有的write操作记录到log之后再执行commit。这种写前日志记录(write-ahead logging)确保了事务的持久性。

当执行 commit 时,系统会记录一个 commit headerlog 中。这个 header 包含了当前事务的信息,比如事务的长度等。这个写入commit header的操作本身也是原子的,并且遵循着原子性和持久性的规则。如果这个 commit header 成功写入磁盘,系统就知道了这个事务已经完整地被记录下来了。如果在写commit header时系统崩溃,那么在重启时会认为这个事务并没有发生过,因为这个commit操作本身的信息并没有被成功地记录下来。

这种机制确保了在系统崩溃或故障时能够维持数据的一致性,并且能够通过log中的信息来进行恢复,使得系统能够重新执行那些还未完全记录的操作,从而达到事务的完整性和原子性。

xv6的 log结构如往常一样也是极其的简单。我们在最开始有一个 header block,也就是我们的 commit record,里面包含了:

  • 数字n代表有效的 log block 的数量;
  • 每个 log block 的实际对应的 block 编号。

之后就是 log的数据,也就是每个block的数据,依次为 bn0 对应的 block 的数据,bn1 对应的 block 的数据以此类推。这就是 log 中的内容,并且 log 也不包含其他内容。

在这里插入图片描述

当文件系统在运行时,在内存中也有header block的一份拷贝,拷贝中也包含了 nblock编号 的数组。这里的 block编号 数组就是log数据对应的实际block编号,并且相应的 block 也会缓存在 block cache 中。

二、log_write() 函数

我们 不应该在所有的写操作完成之前写入commit record,这意味着文件系统操作必须表明事务的开始和结束。在 xv6 中,以创建文件的 sys_open 为例(在 sysfile.c 文件中)每个文件系统操作,都有begin_opend_op分别表示事务的开始和结束。

uint64
sys_open(void)
{
  ...

  begin_op(); // 事务开始

  if(omode & O_CREATE){
    ip = create(path, T_FILE, 0, 0);
    if(ip == 0){
      end_op(); // 事务结束
      return -1;
    }
  }
  ...
  return fd;
}

事务中的所有写block操作具备原子性,这意味着这些写block操作要么全写入,要么全不写入。xv6 中的文件系统调用都有这样的结构,最开始是begin_op之后是实现系统调用的代码,最后是end_op。在end_op中会实现commit操作。

begin_opend_op 之间,磁盘上或者内存中的数据结构会更新,但是磁盘中的数据不会有实际的改变。即在 end_op 之前,是不会写入到实际的 block中。在 end_op 时,我们会将数据写入到log中,之后再写入commit record或者log header

可以看到,这个事务看了一件事,就是创建了inode。看看 create()

static struct inode*
create(char *path, short type, short major, short minor)
{
  struct inode *ip, *dp;
  char name[DIRSIZ];

  if((dp = nameiparent(path, name)) == 0)
    return 0;

  ilock(dp);

  if((ip = dirlookup(dp, name, 0)) != 0){
    iunlockput(dp);
    ilock(ip);
    if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
      return ip;
    iunlockput(ip);
    return 0;
  }

  if((ip = ialloc(dp->dev, type)) == 0){
    iunlockput(dp);
    return 0;
  }

  ilock(ip);
  ip->major = major;
  ip->minor = minor;
  ip->nlink = 1;
  iupdate(ip);

  if(type == T_DIR){  // Create . and .. entries.
    // No ip->nlink++ for ".": avoid cyclic ref count.
    if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
      goto fail;
  }

  if(dirlink(dp, name, ip->inum) < 0)
    goto fail;

  if(type == T_DIR){
    // now that success is guaranteed:
    dp->nlink++;  // for ".."
    iupdate(dp);
  }

  iunlockput(dp);

  return ip;

 fail:
  // something went wrong. de-allocate ip.
  ip->nlink = 0;
  iupdate(ip);
  iunlockput(ip);
  iunlockput(dp);
  return 0;
}

这段代码是一个函数 create,其作用是在文件系统中创建一个新的文件或目录,并返回对应的 inode 结构体指针。

  1. 参数

    • path: 要创建文件或目录的路径。
    • type: 文件类型,可以是文件 (T_FILE) 或目录 (T_DIR)。
    • major: 设备文件的主设备号。
    • minor: 设备文件的次设备号。
  2. 主要步骤

    • 函数首先通过 nameiparent 函数获取路径的父目录的 inode 结构体指针 dp,并且获取了要创建的文件或目录的名称 name
    • 然后对父目录 dp 进行加锁 ilock(dp),确保不会在并发情况下出现问题。
    • 接着,通过 dirlookup 函数在父目录中查找要创建的文件或目录。如果找到了同名的文件或目录,且其类型与要创建的类型相同(即均为文件或目录),则直接返回该 inode 指针。
    • 如果没有找到同名文件或目录,则通过 ialloc 函数分配一个新的 inode,表示新文件或目录。如果分配失败,则解锁父目录并返回 0。
    • 为新分配的 inode 设置一些基本属性,如 majorminornlink,然后通过 iupdate 函数将这些属性更新到磁盘上。
    • 如果要创建的是目录 (T_DIR),则创建 ... 两个目录项,分别表示当前目录和父目录。
    • 将新创建的文件或目录项写入父目录中,如果写入失败,则跳转到 fail 标签进行错误处理。
    • 如果创建的是目录 (T_DIR),则在操作成功后更新父目录的 nlink,表示父目录包含了一个子目录。
    • 最后解锁父目录 dp 并返回新创建的 inode 结构体指针。
  3. 错误处理

    • 在出现错误时,函数会跳转到 fail 标签处,将 inodenlink 设为 0,通过 iupdate 将这个修改写入磁盘,然后释放这个 inode 的锁并解锁父目录,最后返回 0,表示创建过程出现了问题。

再看看它是怎么获取 inode 的:

// Allocate an inode on device dev.
// Mark it as allocated by  giving it type type.
// Returns an unlocked but allocated and referenced inode,
// or NULL if there is no free inode.
struct inode*
ialloc(uint dev, short type)
{
  int inum;
  struct buf *bp;
  struct dinode *dip;

  for(inum = 1; inum < sb.ninodes; inum++){
    bp = bread(dev, IBLOCK(inum, sb));
    dip = (struct dinode*)bp->data + inum%IPB;
    if(dip->type == 0){  // a free inode
      memset(dip, 0, sizeof(*dip));
      dip->type = type;
      log_write(bp);   // mark it allocated on the disk

      brelse(bp);
      return iget(dev, inum);
    }
    brelse(bp);
  }
  printf("ialloc: no inodes\n");
  return 0;
}

可以看到,在拿到 inode 之后,并没有直接在磁盘中 block修改这个 inode 中的字段,而是初始化 buffer(内存中) 中的 inode、修改这个 inodetype。它并没有将这些改变写回到磁盘中,而是直接调用 log_write(),写到日志中。

来看看 log_write()

// Caller has modified b->data and is done with the buffer.
// Record the block number and pin in the cache by increasing refcnt.
// commit()/write_log() will do the disk write.
//
// log_write() replaces bwrite(); a typical use is:
//   bp = bread(...)
//   modify bp->data[]
//   log_write(bp)
//   brelse(bp)
void
log_write(struct buf *b)
{
  int i;
  acquire(&log.lock);
  if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
    panic("too big a transaction");
  if (log.outstanding < 1)
    panic("log_write outside of trans");

  for (i = 0; i < log.lh.n; i++) {
    if (log.lh.block[i] == b->blockno)   // log absorption
      break;
  }
  log.lh.block[i] = b->blockno;
  if (i == log.lh.n) {  // Add new block to log?
    bpin(b);
    log.lh.n++;
  }
  release(&log.lock);
}

这段代码是一个日志系统中的 log_write 函数。它的作用是记录被修改的数据块,并在日志中进行相应的标记,以便稍后将这些数据块的更改写入磁盘。

这个函数的主要步骤:

  1. 函数作用

    • 记录被修改的数据块的块号,并将其标记在日志中,以便在后续的事务提交 (commit()) 或日志写入 (write_log()) 过程中执行磁盘写入。
  2. 主要步骤

    • 首先,通过 acquire(&log.lock) 获取了日志的锁,确保在并发情况下不会出现问题。
    • 然后检查当前日志中记录的块号数量是否超出了规定的阈值 (LOGSIZE) 或者超出了日志的最大容量 (log.size - 1),如果超出,则会产生 panic
    • 接着检查 log_write 是否在一个事务 (transaction) 中进行调用,如果不是,则会产生 panic。
    • 然后,通过循环遍历当前日志中已经记录的块号,检查是否存在与要记录的块号相同的块号,如果存在则跳出循环,否则将当前要记录的块号加入到日志中。
    • 如果要记录的块号是新的,即不存在于当前日志中,则增加日志的块号数量 log.lh.n,并通过 bpin(b) 增加对应数据块的引用计数,表示这个数据块被日志所引用。
  3. 释放资源

    • 最后通过 release(&log.lock) 释放了日志的锁,表示记录过程结束。

这个函数是日志系统中的一部分,用于记录被修改的数据块的块号,并在日志中进行标记,以备后续的磁盘写入操作。

可以看到它做的事情不多,就是简单地记录下需要写入的块号,如果是 新block,就把 log.lh.n 字段自增1。log 字段如下:

// 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];
};

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;
};

以上就是 log_write 的全部工作了。任何文件系统调用,如果需要更新block或者说更新block cache中的block,都会将block编号加在这个内存数据中(注,也就是log header在内存中的cache),除非编号已经存在。

三、end_op() 函数

事务在完成之后,立即调用 end_op()

// called at the end of each FS system call.
// commits if this was the last outstanding operation.
void
end_op(void)
{
  int do_commit = 0;

  acquire(&log.lock);
  log.outstanding -= 1;
  if(log.committing)
    panic("log.committing");
  if(log.outstanding == 0){
    do_commit = 1;
    log.committing = 1;
  } else {
    // begin_op() may be waiting for log space,
    // and decrementing log.outstanding has decreased
    // the amount of reserved space.
    wakeup(&log);
  }
  release(&log.lock);

  if(do_commit){
    // call commit w/o holding locks, since not allowed
    // to sleep with locks.
    commit();
    acquire(&log.lock);
    log.committing = 0;
    wakeup(&log);
    release(&log.lock);
  }
}

在简单情况下,没有其他的文件系统操作正在处理中。这部分代码非常简单直观,首先调用了 commit() 函数。让我们看一下commit() 函数的实现:

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(0); // Now install writes to home locations
    log.lh.n = 0;
    write_head();    // Erase the transaction from the log
  }
}

这个 commit() 函数是文件系统中用于提交事务的部分,它负责实际执行事务的提交操作:

  1. 检查日志中是否有待提交的内容

    • 首先,它会检查日志头中的记录数 log.lh.n 是否大于 0,这表示是否存在需要提交到磁盘的修改。
  2. 执行提交的具体步骤

    • 如果 log.lh.n > 0,表示有待提交的内容:
      • write_log(): 将缓存中被修改的块(blocks)写入到日志(log)中。这一步是将对磁盘上数据的修改记录在日志中,以便在需要时恢复数据。
      • write_head(): 将日志头(log header)写入磁盘。这个动作是真正的提交(commit),表示事务的变更已经被记录到了日志中。
      • install_trans(0): 将修改写入到各自的位置,也就是将对文件系统的修改从日志应用到实际的位置上。这一步确保了数据的一致性。
      • log.lh.n = 0: 清零记录在日志头中的日志计数,表示事务已经成功提交并完成。
      • write_head(): 最后一次调用,此次是为了擦除日志中的事务记录,确保之前的事务已经完全提交,日志可以重新开始记录新的事务。

来看看 write_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
    memmove(to->data, from->data, BSIZE);
    bwrite(to);  // write the log
    brelse(from);
    brelse(to);
  }
}

这个 write_log() 函数是用来将缓存中被修改的块(blocks)复制到日志(log)中的过程:

  1. 循环处理需要写入日志的块

    • for 循环遍历日志头中记录的需要写入日志的块数 log.lh.n
  2. 从缓存中读取和写入到日志中

    • struct buf *to = bread(log.dev, log.start+tail+1);: 从日志中读取一个指定位置的日志块(log block)。
    • struct buf *from = bread(log.dev, log.lh.block[tail]);: 从缓存中读取一个需要被写入到日志的块(cache block)。这里需要注意的是,这个块是缓存块,它已经被修改好了,现在准备要将它复制到日志块中,而且还要写到硬盘中。
    • memmove(to->data, from->data, BSIZE);: 将缓存块中的数据复制到日志块中。这一步实际上是将被修改的数据从缓存中拷贝到了日志中,以记录这些修改。
    • bwrite(to);: 将这个日志块写入磁盘,这一步是将缓存块的内容写入到日志中,记录了被修改的数据。
    • brelse(from); brelse(to);: 释放对缓存块和日志块的引用,表示不再需要这些块的数据。

我们来看看 bwrite()

// Write b's contents to disk.  Must be locked.
void
bwrite(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("bwrite");
  virtio_disk_rw(b, 1);
}

之前有提到,bwrite()是真正向硬盘中写东西的函数,所以 write_log() 的作用,就是将缓存中的数据,或者说需要修改的缓存块,写到日志中,而且是硬盘中的 log block 中!

write_head()

// 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);
}

这个函数 write_head() 的功能是将内存中的日志头信息写入硬盘中的日志块中,实现当前事务的真正提交

具体步骤如下:

  1. 读取日志块

    • struct buf *buf = bread(log.dev, log.start);: 从磁盘中读取日志块到内存中的缓冲区 buf 中。
  2. 更新日志头信息

    • struct logheader *hb = (struct logheader *) (buf->data);: 将缓冲区中的数据强制转换为日志头结构体 (C语言的强转机制,就是这么暴力!),以便更新其中的信息。
    • hb->n = log.lh.n;: 将当前事务中记录的块数更新到日志头中。
    • for (i = 0; i < log.lh.n; i++) { hb->block[i] = log.lh.block[i]; }: 将当前事务中记录的每个块的块号(block number)更新到日志头中。
  3. 将更新后的日志头信息写入硬盘

    • bwrite(buf);: 使用 bwrite() 函数将更新后的日志头数据写入硬盘中的日志块,这个过程实际上是将当前事务的信息提交到磁盘中的日志。
  4. 释放缓冲区

    • brelse(buf);: 释放对日志块的引用,表示不再需要这个块的数据。

这个函数的作用就是将当前事务的日志头信息持久化到磁盘中,标志着事务的正式提交。

install_trans(int recovering):

// Copy committed blocks from log to their home location
static void
install_trans(int recovering)
{
  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
    if(recovering == 0)
      bunpin(dbuf);
    brelse(lbuf);
    brelse(dbuf);
  }
}

这个函数 install_trans() 的主要功能是将已经提交的块(blocks)从日志(log)中复制到它们原本的位置(home location)。

具体步骤如下:

  1. 循环处理需要安装的块

    • for (tail = 0; tail < log.lh.n; tail++) {: 使用循环遍历日志头中记录的需要安装的块数 log.lh.n
  2. 从日志和目标位置读取块

    • struct buf *lbuf = bread(log.dev, log.start+tail+1);: 从日志中读取一个指定位置的日志块(log block)。
    • struct buf *dbuf = bread(log.dev, log.lh.block[tail]);: 从磁盘中读取一个目标位置的块(destination block),这个块是在日志中记录的需要被修改的块。
  3. 复制块数据到目标位置

    • memmove(dbuf->data, lbuf->data, BSIZE);: 将日志块中的数据复制到目标位置的块中,实际上是将日志中记录的修改写入到了原本的位置。
  4. 将更新后的块数据写回磁盘

    • bwrite(dbuf);: 将被修改后的块数据写回到磁盘中,这一步是将日志中记录的修改应用到了磁盘的相应位置。
  5. 解除引用和释放资源

    • bunpin(dbuf);: 如果正在恢复过程中,则解除对目标位置块的引用。这可能会是一个引用计数的操作,确保释放相关资源。
    • brelse(lbuf); brelse(dbuf);: 释放对日志块和目标位置块的引用,表示不再需要这些块的数据。

这个函数实现了事务的安装。

回到 commit()函数:

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(0); // Now install writes to home locations
    log.lh.n = 0;
    write_head();    // Erase the transaction from the log
  }
}

commit函数中,install结束之后,会将log header中的n设置为0,再将log header写回到磁盘中。将n设置为0的效果就是清除log。这里,才算是 commit() 真正结束!

四、文件恢复

我们看一下发生在xv6的启动过程中的文件系统的恢复流程。当系统crash并重启了,在xv6启动过程中做的一件事情就是调用initlog() 函数:

void
initlog(int dev, struct superblock *sb)
{
  if (sizeof(struct logheader) >= BSIZE)
    panic("initlog: too big logheader");

  initlock(&log.lock, "log");
  log.start = sb->logstart;
  log.size = sb->nlog;
  log.dev = dev;
  recover_from_log();
}

superblock 中拿到 log 初始化所需的数据后,开始恢复:

static void
recover_from_log(void)
{
  read_head();
  install_trans(1); // if committed, copy from log to disk
  log.lh.n = 0;
  write_head(); // clear the log
}

首先从硬盘中的 log block 中的 log header 数据恢复到内存中的 log header,还恢复了一点 log 信息:

// Read the log header from disk into the in-memory log header
static void
read_head(void)
{
  struct buf *buf = bread(log.dev, log.start);
  struct logheader *lh = (struct logheader *) (buf->data);
  int i;
  log.lh.n = lh->n;
  for (i = 0; i < log.lh.n; i++) {
    log.lh.block[i] = lh->block[i];
  }
  brelse(buf);
}

然后就再一次 install_trans(),将日志中的数据复制到 buffer 中,然后写回硬盘。

这就是恢复的全部流程。如果我们在 install_trans函数中又crash了,也不会有问题。

五、Log 写磁盘流程

为了追踪写磁盘,修改 bwrite() 函数:

// Write b's contents to disk.  Must be locked.
void
bwrite(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("bwrite");

  printf("bwrite: %d\n",b->blockno);
  virtio_disk_rw(b, 1);
}

然后运行以下命令( 文件x 之前存在,因此开始将它删了):

xv6 kernel is booting

hart 1 starting
hart 2 starting
bwrite: 2
init: starting sh
$ rm x
bwrite: 3
bwrite: 4
bwrite: 5
bwrite: 6
bwrite: 2
bwrite: 46
bwrite: 32
bwrite: 33
bwrite: 45
bwrite: 2
$ echo hi > x
1	bwrite: 3
2	bwrite: 4
3	bwrite: 5
4	bwrite: 2
5	bwrite: 33
6	bwrite: 46
7	bwrite: 32
8	bwrite: 2

9	bwrite: 3
10	bwrite: 4
11	bwrite: 5
12	bwrite: 2
13	bwrite: 45
14	bwrite: 908
15	bwrite: 33
16	bwrite: 2

17	bwrite: 3
18	bwrite: 4
19	bwrite: 2
20	bwrite: 908
21	bwrite: 33
22	bwrite: 2

可以看到,系统刚启动,就往磁盘中 log block2中写东西了。根据这些输出信息,我们随意畅谈,只要合理就好。

比如,对于 rm x,很明显,需要修改多个 log block,然后修改根目录的 block,然后还要修改 inode block、bitmap block。这都能与 shell 打印的信息吻合。

我们再看看 echo hi > x

1	bwrite: 3
2	bwrite: 4
3	bwrite: 5

这 3 行都是 log block,记录的 3 个操作,写入到了这里。

4	bwrite: 2

这个是 log header,这条是 commit 记录,因为只有 commit() 中才会执行 write_head() 函数。

5	bwrite: 33
6	bwrite: 46
7	bwrite: 32

这里实际就是将前3行的 log data 写入到实际的文件系统的block位置,这里 install log,还写了entry(创建了文件x),顺便修改了根目录的 inode 字段的 size

4	bwrite: 2
5	bwrite: 33
6	bwrite: 46
7	bwrite: 32
8	bwrite: 2

连起来看,就很清楚,完美对应commit()

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(0); // Now install writes to home locations
    log.lh.n = 0;
    write_head();    // Erase the transaction from the log
  }
}

因此,第 8 行的 bwrite 2 就是在清除log。

9	bwrite: 3
10	bwrite: 4
11	bwrite: 5
12	bwrite: 2
13	bwrite: 45
14	bwrite: 908
15	bwrite: 33
16	bwrite: 2

这里也是,记录 3 组操作之后,然后 commit()。这里修改了位图,之后写了 hi,并修改了 文件xinode 中的 size 等字段(block33),

17	bwrite: 3
18	bwrite: 4
19	bwrite: 2
20	bwrite: 908
21	bwrite: 33
22	bwrite: 2

这里记录了两组操作,也就 commit()了,最后清除 log。这里应该在写最后的换行符,同时修改文件大小。

所以以上就是 xv6 中文件系统的 logging 介绍,即使是这么一个简单的 logging 系统也有一定的复杂度。这里立刻可以想到的一个问题是,通过观察这些记录,这是一个很有效的实现吗?很明显不是的,因为数据被写了两次。如果我写一个大文件,我需要在磁盘中将这个大文件写两次。所以这必然不是一个高性能的实现,为了实现 Crash safety 我们将原本的性能降低了一倍。

全文完,感谢阅读。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1320395.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java基础回顾——面向对象编程

文章目录 面向对象基础方法构造方法默认构造方法多构造方法 方法重载继承多态抽象类接口静态字段和静态方法包作用域内部类 写在最后 https://www.liaoxuefeng.com/wiki/1252599548343744/1255943520012800 面向对象编程Object-Oriented Programming&#xff0c;简称OOP&#…

06. Python模块

目录 1、前言 2、什么是模块 3、Python标准库模块 3.1、os模块 3.2、datetime 模块 3.3、random模块 4、自定义模块 4.1、创建和使用 4.2、模块命名空间 4.3、作用域 5、安装第三方依赖 5.1、使用 pip 安装单个依赖 5.2、从 requirements.txt 安装依赖 5.3、安装指…

Python:(Sentinel-1)如何解析SNAP输出的HDF5文件并输出为GeoTIFF?

博客已同步微信公众号&#xff1a;GIS茄子&#xff1b;若博客出现纰漏或有更多问题交流欢迎关注GIS茄子&#xff0c;或者邮箱联系(推荐-见主页). Python&#xff1a;&#xff08;Sentinel-1&#xff09;如何解析SNAP输出的HDF5文件并输出为GeoTIFF&#xff1f; 01 前言 最近…

【NI-RIO入门】使用LabVIEW进行数据采集测量

于ni kb摘录 选择合适的编程模式 CompactRIO系统具有至少两个用户可选模式。某些CompactRIO型号具有附加的用户可选模式&#xff0c;可以在实时NI-DAQmx中进行编程。请参考本文以判断您的CompactRIO是否能够使用实时NI-DAQmx。将目标添加到项目后&#xff0c;将提示您选择要使…

TestSSLServer4.exe工具使用方法简单介绍(查SSL的加密版本SSL3或是TLS1.2)

一、工具使用方法介绍 工具使用方法参照&#xff1a;http://www.bolet.org/TestSSLServer/ 全篇英文看不懂&#xff0c;翻译了下&#xff0c;能用到的简单介绍如下&#xff1a; 将下载的TestSSLServer4.exe工具放到桌面上&#xff0c;CMD命令行进入到桌面目录&#xff0c;执…

Gitee基础知识

目录 1-gitee 1.1gitee介绍 1.2git与gitee的关系 1.3在国内为什么选择Gitee 2-注册与创建远程仓库 2.1注册 2.2创建远程仓库 2.3配置ssh公钥 2.3.1公钥的生成方法&#xff1a; 2.3.2 在gitee中配置公钥 2.3.4验证公钥 3-添加与推送远程仓库master 3.1基本命令…

78-C语言-完数的判断,以及输出其因子

简介&#xff1a;一个数如果恰好等于它的因子之和&#xff0c;这个数就称为完数&#xff0c;C语言编程找出1000之内的所有完数&#xff0c;并输出其因子。因子可以整除该数字的数&#xff0c; 如6的因子&#xff1a;1 2 3&#xff0c;6%10 6%20 6%30 解释全在注…

20 5G中高速列车通信:设计相关元素以减轻高移动性带来的影响

文章目录 一 、物理层设计1 DMRS2 CSI 和SRS3 PTRS4 多天线配置 二 初始接入三 目前面临困难 解决问题&#xff1a;列车高速移动&#xff0c;会使信道相干时间较短、多普勒频移和多普勒扩展较大等问题。为了在列车高速移动中解决这些问题&#xff0c;这篇文章概括了5G关键技术&…

掌动智能浅谈云网络流量分析的主要过程

在云计算时代&#xff0c;网络流量分析成为确保网络安全和性能的重要环节。随着企业和组织将应用和服务迁移到云平台&#xff0c;对云网络流量进行深入的分析变得至关重要。本文将介绍云网络流量分析的主要过程&#xff0c;以帮助读者更好地理解如何有效地监控和管理云环境中的…

MySQL进阶|MySQL中的事务(一)

文章目录 数据库事务MySQL中的存储引擎InnoDB存储引擎架构什么是事务事务的状态总结 数据库事务 MySQL 事务主要用于处理操作量大&#xff0c;复杂度高的数据。比方我想要删除一个用户&#xff08;销户&#xff09;以及这个用户的个人信息、订单信息以及其他信息&#xff0c;这…

对JVM内存模型的理解

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一份大厂面试资料《史上最全大厂面试题》&#xff0c;Springboot、微服务、算法、数据结构、Zookeeper、Mybatis、Dubbo、linux、Kafka、Elasticsearch、数据库等等 …

MySQL主从复制详解

目录 1. 主从复制的工作原理 1.1. 主从复制的角色 1.2. 主从复制的流程 2. 配置MySQL主从复制 2.1. 确保主服务器开启二进制日志 2.2. 设置从服务器 2.3. 连接主从服务器 2.4. 启动复制 3. 主从复制的优化与注意事项 3.1. 优化复制性能 3.2. 注意复制延迟 3.3. 处理…

干涉光学测试导论

1.用于光学测试的基本干涉仪 2。相移干涉术 3。专业光学测试 4。长波长干涉术 5。非球面试验 6。表面微观结构的测量 7。绝对测量 8。结束语 第1部分-光学测试用基本干涉仪 (1)双光束干涉 (2)菲佐干涉仪和特维曼-格林干涉仪 (3)测试平面和球面的基本技术 (4)球面的基本…

2020年度NPcon-容器与微服务实践峰会 回顾

一&#xff0c;会议基本信息 时间&#xff1a;12月16日14:00-17:00 地点&#xff1a;上海机遇星球&#xff08;上海市黄浦区南京西路389号明天广场裙楼2楼&#xff09; 电梯旁边的指示牌 会场现场 出来的时候&#xff0c;天快黑了 二&#xff0c;内容回顾 由四个讲座和一个…

(5)shell命令以及Linux的权限

写在前面 本章我们将重点讲解 Linux 权限&#xff0c;这是 Linux 基础部分中非常重要的一部分。内容比较干&#xff0c;我会稍稍正经些去讲解。话不多说&#xff0c;我们直接切入正题。 shell 命令及运行原理 严格意义上说的是一个操作系统&#xff0c;我们称之为 —— &…

【AI基础设施】智算场景的资源管理系统与未来展望

高性能计算与智算场景 首先澄清两个概念&#xff0c;高性能计算与智算场景&#xff0c;高性能计算主要是面向天气预测、生物计算、材料计算等场景&#xff0c;而最近几年很火的智算主要是面向AI场景的计算&#xff0c;如语音识别、图像识别、自动驾驶等场景&#xff0c;我们可…

Git 如何撤回已 Push 的代码

在日常的开发过程中&#xff0c;我们广泛利用Git进行版本控制。然而&#xff0c;有时我们可能会误将错误的代码推送到远程仓库&#xff0c;或者希望在本地返回到先前的某个版本以重新进行开发。 就像我个人的经历一样&#xff0c;我曾推送了一些我认为未来会非常有用的优化方案…

14:00面试,14:05就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到12月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40…

SQL进阶理论篇(十一):什么是MVCC?

文章目录 简介什么是MVCC快照读与当前读悲观锁的问题示例参考文献 简介 在MySQL中&#xff0c;默认的隔离级别是可重复读&#xff0c;可以解决脏读和不可重复读的问题&#xff0c;但不能解决幻读问题。如果想要解决幻读问题&#xff0c;就需要采用串行化的方式&#xff0c;通过…

数据结构——【万字文章+配图+代码】带你深入理解二叉树

1. 二叉树的概念 二叉树是一种有限集合&#xff0c;由根和左右子树构成&#xff0c;每个结点最多有两棵子树&#xff0c;且这两棵子树具有顺序关系 2. 二叉树的特殊情况&#xff1a; 2.1 满二叉树&#xff1a; 一个二叉树&#xff0c;如果每次的结点都达到最大值&#xff…