xv6 文件系统(下)

〇、前言

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

我们讨论文件的状态是否正确,是指文件系统对于这个文件是否运行正常,比如 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 inodememset(dip, 0, sizeof(*dip));dip->type = type;log_write(bp);   // mark it allocated on the diskbrelse(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 absorptionbreak;}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 logwrite_head();    // Write header to disk -- the real commitinstall_trans(0); // Now install writes to home locationslog.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 blockstruct buf *from = bread(log.dev, log.lh.block[tail]); // cache blockmemmove(to->data, from->data, BSIZE);bwrite(to);  // write the logbrelse(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 blockstruct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dstmemmove(dbuf->data, lbuf->data, BSIZE);  // copy block to dstbwrite(dbuf);  // write dst to diskif(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 logwrite_head();    // Write header to disk -- the real commitinstall_trans(0); // Now install writes to home locationslog.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 disklog.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 bootinghart 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: 29	bwrite: 3
10	bwrite: 4
11	bwrite: 5
12	bwrite: 2
13	bwrite: 45
14	bwrite: 908
15	bwrite: 33
16	bwrite: 217	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 logwrite_head();    // Write header to disk -- the real commitinstall_trans(0); // Now install writes to home locationslog.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.mzph.cn/news/231311.shtml

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

相关文章

基于VUE3+Layui从头搭建通用后台管理系统(前端篇)十五:基础数据模块相关功能实现

一、本章内容 本章使用已实现的公共组件实现系统管理中的基础数据中的验证码管理、消息管理等功能。 1. 详细课程地址: 待发布 2. 源码下载地址: 待发布 二、界面预览 三、开发视频 3.1 B站视频地址: 基于VUE3+Layui从头搭建通用后台管理系统合集-验证码功能实现 3.2 西瓜…

社科院与新加坡新跃社科联合培养博士—我想我的人生变得精彩

既然人生的幕布已拉开&#xff0c;就一定要积极的演出&#xff0c;既然脚步已经跨出&#xff0c;风吹坎坷也不能退步&#xff0c;既然我已经把希望播在这里&#xff0c;就一定要坚持到胜利的谢幕&#xff0c;人生没有什么是为了别人做的&#xff0c;工作不是为了老板&#xff0…

79-C语言-小球降落和反弹问题

简介&#xff1a;一个球从100m高度处落下&#xff0c;每次落地后反弹回原高度一半&#xff0c;再落下&#xff0c;再反弹。问&#xff1a;它在第十次落地&#xff0c;共运动了多少米&#xff0c;第十次反弹又多高呢&#xff1f; 看代码注释即可 代码如下&#xff1a; #inclu…

Web前端-HTML(常用标签)

文章目录 1. HTML常用标签1.1 排版标签1&#xff09;标题标签h (熟记)2&#xff09;段落标签p ( 熟记)3&#xff09;水平线标签hr(认识)4&#xff09;换行标签br (熟记)5&#xff09;div 和 span标签(重点)6&#xff09;排版标签总结 1.2 标签属性1.3 图像标签img (重点)1.4 链…

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

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

2024年软件测试入坑指南,新人必看系列

本科非计算机专业&#xff0c;在深圳做了四年软件测试工作&#xff0c;从之前的一脸懵的点点点&#xff0c;到现在会点自动化测试&#xff0c;说一点点非计算机专业人员从事软件测试的心得体会&#xff0c;仅供参考交流。 如果你是非计算机专业&#xff0c;毕业不久&#xff0…

<JavaEE> 文件IO -- File类和文件操作

目录 一、文件的概念 二、文件系统 三、文件类型 四、使用 File 类进行文件操作 4.1 File 类中的 pathSeparator 属性 4.2 File 类构造方法 4.3 File 类常用方法 一、文件的概念 什么是文件&#xff1f; 广义上的“文件”是指抽象化的操作系统中的硬件设备和软件资源&a…

第十三章 SpringCloud Alibaba 实现 Seata--分布式事务

分布式事务基础 事务 事务指的就是一个操作单元&#xff0c;在这个操作单元中的所有操作最终要保持一致的行为&#xff0c;要么所有操作 都成功&#xff0c;要么所有的操作都被撤销。简单地说&#xff0c;事务提供一种“要么什么都不做&#xff0c;要么做全套”机制。 本地事…

RAG检索增强技术在知识库智能检索场景下的应用实践

如果你对这篇文章感兴趣&#xff0c;而且你想要了解更多关于AI领域的实战技巧&#xff0c;可以关注「技术狂潮AI」公众号。在这里&#xff0c;你可以看到最新最热的AIGC领域的干货文章和案例实战教程。 一、知识检索增强的基本概述 1.1、知识检索增强技术提出的背景 1.1.1、L…

Spring Boot学习随笔- JSP小项目-员工管理系统(验证码生成、增删改查)

学习视频&#xff1a;【编程不良人】2021年SpringBoot最新最全教程 第十章、项目开发 实现一个登录注册&#xff0c;增删改查功能的系统 10.1 项目开发流程 需求分析 分析用户主要需求 提取项目核心功能&#xff0c;根据核心功能构建页面原型 库表设计&#xff1a; 分析系统…

基于FPGA的视频接口之高速IO(CML)

简介 本章节是对于高速IO接口应用的一个扩展&#xff0c;目前扩展为CML。 CML&#xff08;电流模式逻辑-即Current Mode Logic&#xff09;。电路主要靠电流驱动&#xff0c;也是所有高速数据接口形式中最简单的一种&#xff0c;它的输入与输出的匹配集成在芯片内部&#xff0c…

mysql innodb知识记录

官方文档 官网架构图 innodb 特性 内存 buffer pool 采用优化后的LRU算法&#xff0c; 3/8 of the buffer pool is devoted to the old sublist.The midpoint of the list is the boundary where the tail of the new sublist meets the head of the old sublist.When In…

逆向登录(js逆向)

链接: aHR0cHM6Ly9zc28ubHlua2NvLmNvbS9jdXN0b21lci9sb2dpbj9jbGllbnRfaWQ9OGMxNWE2NGY2Nzk0NDY5YjhhNTlmMTBiODNjZWYzYzEmcmVkaXJlY3RfdXJpPWh0dHBzOi8vaDUubHlua2NvLmNuOjQ0My9hdXRoL21hbGwvaDUvbG9naW4mcmVzcG9uc2VfdHlwZT1jb2RlJnNjb3BlPW9wZW5pZCZzdGF0ZT0zNGQ4NGYxNmIwY…

MySQL安装——备赛笔记——2024全国职业院校技能大赛“大数据应用开发”赛项——任务2:离线数据处理

MySQLhttps://www.mysql.com/ 将下发的ds_db01.sql数据库文件放置mysql中 12、编写Scala代码&#xff0c;使用Spark将MySQL的ds_db01库中表user_info的全量数据抽取到Hive的ods库中表user_info。字段名称、类型不变&#xff0c;同时添加静态分区&#xff0c;分区字段为etl_da…

Win10电脑退出安全模式的两种方法

在Win10电脑中&#xff0c;大家可以点击进入系统安全模式&#xff0c;完成相对应的系统设置。但是&#xff0c;很多用户进入安全模式完成设置后&#xff0c;不知道怎么操作才能成功退出安全模式&#xff1f;接下来小编给大家分享两种简单的方法&#xff0c;帮助大家成功退出Win…

强大的数学软件 GeoGebra 多平台适用

GeoGebra 是一款教育数学软件&#xff0c;可以帮助学生和教师探索、学习和教授各种数学概念和科学领域的知识。GeoGebra 以其灵活性和强大的功能而闻名&#xff0c;它融合了几何、代数、微积分、概率、统计和其他数学领域的工具&#xff0c;以及绘图和计算功能。 功能 GeoGeb…

Java魔法解密:HashMap底层机制大揭秘

文章目录 一、 源码深度解析1.1 窥探Java集合框架中的设计思想1.2 逐行解读HashMap的源代码1.2.1 类信息1.2.2 常量属性1.2.3 变量属性1.2.4 节点信息1.2.5 构造方法1.2.6 put方法1.2.6.1 putVal方法1.2.6.2 putTreeVal方法1.2.6.3 tieBreakOrder方法1.2.6.4 treeifyBin方法1.2…

lua语法

lua语法 1.lua数据类型 lua 脚本输出乱码&#xff0c;将lua脚本改为UTF-8编码&#xff0c;并且需要DOS下修改代码页&#xff1a;CHCP 65001 即可。 基本语法 注释 print("script lua win")-- 单行注释--[[多行注释]]--标识符 类似于&#xff1a;java当中 变量、…

新建vue3项目

三种方法 一. 第一种方式 1、操作步骤&#xff1a; 创建项目目录 vue create 项目名称选择配置方式 ? Please pick a preset: #选择一个配置 Default &#xff08;[Vue 3] babel, eslint&#xff09;Default &#xff08;[Vue 2] babel, eslint&#xff09;Manually select …

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

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