Appearance
log.c
#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
TIP
简单的日志记录,允许多个文件系统系统调用并发执行。 日志系统的核心思想: 1. 事务(Transaction): 一个事务由一个或多个文件系统操作(如创建文件、写入数据)组成。 例如,创建一个文件可能需要修改目录的数据块和 inode 块。这两个修改就属于同一个事务。 2. 预写式日志(Write-ahead Logging): 在将数据块实际写入其在文件系统中的最终位置之前, 首先将所有修改记录在一个专门的日志区域(on-disk log)。 3. 提交(Commit): 当一个事务的所有日志记录都成功写入磁盘后,该事务被认为是“已提交”的。 通过在日志的头部记录一个特殊的提交记录来标记。 4. 安装(Install): 提交后,再将日志中的数据块复制到它们在文件系统中的最终位置。 5. 恢复(Recovery): 如果在安装过程中系统崩溃,重启后,系统会检查日志。 如果日志中有已提交但未完全安装的事务,系统会重新执行安装过程,保证数据的一致性。 xv6 的日志系统简化了这一过程: - 原子性单元: 每个系统调用被视为一个原子操作单元。 - 批量提交: 日志系统会等待所有当前正在执行的文件系统调用都结束后,才进行一次性的提交。 这避免了处理部分完成的系统调用的复杂性。 - 磁盘布局: 日志在磁盘上由一个头块(header block)和多个数据块组成。 - 头块:记录了当前事务修改了哪些磁盘块(通过块号)。 - 数据块:按顺序存放着被修改块的新内容。 函数调用流程: a. 文件系统操作开始: begin_op() b. 修改数据块: log_write() (替换 bwrite()) c. 文件系统操作结束: end_op() -> commit() -> write_log() -> write_head() -> install_trans() -> write_head()
TIP
logheader 结构定义了日志头部的格式。 它保存在磁盘日志区的第一个块中。 同时,在内存中的 log.lh 字段也使用这个结构来缓存当前事务的信息。
struct logheader {
int n;
int block[LOGSIZE];
};
TIP
log 结构定义了整个日志系统的状态。
struct log {
struct spinlock lock;
int start;
int size;
int outstanding;
int committing;
int dev;
struct logheader lh;
};
struct log log;
TIP
内部函数声明
static void recover_from_log(void);
static void commit();
TIP
初始化日志系统。 在 fsinit() 中被调用。
void
initlog(int dev, struct superblock *sb)
{
if (sizeof(struct logheader) >= BSIZE)
panic("initlog: logheader struct too large for a block");
initlock(&log.lock, "log");
log.start = sb->logstart;
log.size = sb->nlog;
log.dev = dev;TIP
从日志中恢复,以处理上次系统崩溃时可能存在的未完成的事务。
recover_from_log();
}
TIP
将已提交事务的块从日志区复制到它们在文件系统中的最终位置。 这个过程被称为“安装事务”。
static void
install_trans(int recovering)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {TIP
从日志区读取一个数据块
struct buf *lbuf = bread(log.dev, log.start + tail + 1);TIP
读取该数据块对应的在文件系统中的目标块
struct buf *dbuf = bread(log.dev, log.lh.block[tail]);TIP
将日志中的内容复制到目标块的缓冲区
memmove(dbuf->data, lbuf->data, BSIZE);TIP
将目标块写回磁盘,完成安装
bwrite(dbuf);TIP
如果不是在恢复期间,需要 unpin 目标缓冲区。 在恢复期间,没有其他进程会使用这些块,所以不需要 unpin。
if(recovering == 0)
bunpin(dbuf);
brelse(lbuf);
brelse(dbuf);
}
}
TIP
从磁盘读取日志头到内存中的 log.lh。
static void
read_head(void)
{TIP
读取日志区的第一个块,即头块
struct buf *buf = bread(log.dev, log.start);
struct logheader *lh = (struct logheader *) (buf->data);
int i;
log.lh.n = lh->n;TIP
复制所有块号
for (i = 0; i < log.lh.n; i++) {
log.lh.block[i] = lh->block[i];
}
brelse(buf);
}
TIP
将内存中的 log.lh 写回磁盘上的日志头块。 这是事务提交的关键步骤。一旦头块被成功写入磁盘, 即使系统崩溃,重启后也能通过 recover_from_log 恢复事务。
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);
}
TIP
从日志中恢复。 在 initlog 时调用,用于处理上次运行可能未完成的事务。
static void
recover_from_log(void)
{TIP
读取上次的日志头
read_head();TIP
如果上次有已提交的事务 (log.lh.n > 0),则安装它
install_trans(1);TIP
清空日志头,表示没有待处理的事务了
log.lh.n = 0;TIP
将清空后的日志头写回磁盘
write_head();
}
TIP
每个文件系统系统调用的开始时必须调用此函数。
void
begin_op(void)
{
acquire(&log.lock);
while(1){TIP
如果日志系统正在提交,当前操作必须等待。
if(log.committing){
sleep(&log, &log.lock);
}TIP
检查日志空间是否足够。 这是一个保守的估计:假设当前操作和所有其他正在进行的操作 都会达到它们的最大块写入数 (MAXOPBLOCKS)。
else if(log.lh.n + (log.outstanding + 1) * MAXOPBLOCKS > LOGSIZE){TIP
日志空间可能不足,等待下一次提交以释放空间。
sleep(&log, &log.lock);
} else {TIP
空间足够,增加正在进行的操作计数,然后返回。
log.outstanding += 1;
release(&log.lock);
break;
}
}
}
TIP
每个文件系统系统调用的结束时必须调用此函数。
void
end_op(void)
{
int do_commit = 0;
acquire(&log.lock);TIP
减少正在进行的操作计数
log.outstanding -= 1;
if(log.committing)
panic("end_op: log is committing");TIP
如果这是最后一个正在进行的操作,则触发提交。
if(log.outstanding == 0){
do_commit = 1;
log.committing = 1;
} else {TIP
如果还有其他操作正在进行,唤醒可能在 begin_op() 中等待的进程。 因为当前操作结束,释放了一些预留的日志空间。
wakeup(&log);
}
release(&log.lock);
if(do_commit){TIP
调用 commit() 时不能持有锁,因为它内部会调用 sleep。
commit();TIP
提交完成后,重置提交标志并唤醒所有等待的进程。
acquire(&log.lock);
log.committing = 0;
wakeup(&log);
release(&log.lock);
}
}
TIP
将缓存中已修改的块的内容写入到磁盘上的日志区。
static void
write_log(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {TIP
to: 日志区中的目标块
struct buf *to = bread(log.dev, log.start + tail + 1);TIP
from: 内存缓冲区中被修改的块
struct buf *from = bread(log.dev, log.lh.block[tail]);TIP
将数据从内存缓冲区复制到日志块
memmove(to->data, from->data, BSIZE);
bwrite(to);
brelse(from);
brelse(to);
}
}
TIP
执行事务提交。
static void
commit()
{
if (log.lh.n > 0) {TIP
步骤 1: 将所有修改过的块从缓冲区写入日志区 (write-ahead)
write_log();TIP
步骤 2: 将日志头写入磁盘。这是“提交点”。 一旦这步完成,即使系统崩溃,事务也可以被恢复。
write_head();TIP
步骤 3: 将日志中的块安装到它们在文件系统中的最终位置。
install_trans(0);TIP
步骤 4: 清空内存中的日志头,为下一个事务做准备。
log.lh.n = 0;TIP
步骤 5: 将清空的日志头写入磁盘,表示当前没有待处理的事务。 这样可以防止系统在此时崩溃后,重启时错误地重放同一个事务。
write_head();
}
}
TIP
当调用者修改了一个缓冲区并希望将其写入磁盘时,应调用此函数。 log_write 会记录被修改块的块号,并将其固定在缓冲区缓存中, 以防止它被驱逐,直到事务提交。 log_write() 替代了 bwrite()。典型用法如下: bp = bread(...) // 读取块 (修改 bp->data) // 修改数据 log_write(bp) // 将修改记录到日志 brelse(bp) // 释放缓冲区
void
log_write(struct buf *b)
{
int i;
acquire(&log.lock);TIP
检查日志是否已满。
if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
panic("log_write: too big a transaction");TIP
必须在 begin_op/end_op 对之间调用 log_write。
if (log.outstanding < 1)
panic("log_write: outside of transaction");
TIP
"日志吸收" (Log Absorption): 如果这个块已经被记录在当前事务中,我们不需要再次添加它。 只需确保最新的修改被写入即可。
for (i = 0; i < log.lh.n; i++) {
if (log.lh.block[i] == b->blockno) {
break;
}
}TIP
记录块号。
log.lh.block[i] = b->blockno;TIP
如果这是一个新的块(之前未被记录在本次事务中), 则增加日志中的块计数,并钉住(pin)该缓冲区。
if (i == log.lh.n) {
bpin(b);
log.lh.n++;
}
release(&log.lock);
}