xv6 日志系统 (log.c) 详细讲解
这是 xv6 操作系统的日志文件系统实现,用于保证文件系统操作的原子性和崩溃恢复。让我从设计理念到具体实现为你详细讲解。
🎯 设计目标
问题:如果系统在文件系统操作过程中崩溃,可能导致文件系统处于不一致状态。
解决方案:使用预写日志 (Write-Ahead Logging, WAL) 机制,先将修改记录到日志,再应用到实际位置。
📊 核心数据结构
// 日志头部结构 - 存储在磁盘和内存中
struct logheader {
int n; // 日志中有多少个有效块
int block[LOGSIZE]; // 记录哪些块被修改了
};
// 日志系统的主结构
struct log {
struct spinlock lock; // 保护并发访问
int start; // 日志在磁盘上的起始位置
int size; // 日志区域大小
int outstanding; // 正在进行的文件系统调用数量
int committing; // 是否正在提交事务
int dev; // 设备号
struct logheader lh; // 内存中的日志头
};
🏗️ 磁盘布局
磁盘日志区域布局:
┌─────────────┬─────────────┬─────────────┬─────────────┐
│ Header │ Block A │ Block B │ Block C │
│ Block │ │ │ │
└─────────────┴─────────────┴─────────────┴─────────────┘
↑ ↑ ↑ ↑
记录A,B,C 修改后的 修改后的 修改后的
的块号 A块内容 B块内容 C块内容
🔄 事务生命周期
1. 事务开始 - begin_op()
void begin_op(void) {
acquire(&log.lock);
while(1){
if(log.committing){
// 如果正在提交,等待
sleep(&log, &log.lock);
} else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
// 如果可能超出日志空间,等待
sleep(&log, &log.lock);
} else {
log.outstanding += 1; // 增加活跃操作计数
release(&log.lock);
break;
}
}
}
作用:
- 检查是否有足够的日志空间
- 增加活跃操作计数
- 如果空间不足或正在提交,则等待
2. 记录修改 - log_write()
void log_write(struct buf *b) {
acquire(&log.lock);
// 检查是否已经记录过这个块(日志吸收)
for (i = 0; i < log.lh.n; i++) {
if (log.lh.block[i] == b->blockno)
break;
}
log.lh.block[i] = b->blockno; // 记录块号
if (i == log.lh.n) { // 新块
bpin(b); // 钉住缓存块
log.lh.n++; // 增加日志块计数
}
release(&log.lock);
}
关键特性:
- 日志吸收:同一个块的多次修改只记录一次
- 钉住缓存:防止修改的块被换出内存
3. 事务结束 - end_op()
void end_op(void) {
int do_commit = 0;
acquire(&log.lock);
log.outstanding -= 1; // 减少活跃操作计数
if(log.outstanding == 0){ // 如果是最后一个操作
do_commit = 1;
log.committing = 1; // 标记正在提交
} else {
wakeup(&log); // 唤醒等待的操作
}
release(&log.lock);
if(do_commit){
commit(); // 执行提交
// 提交完成后清理
acquire(&log.lock);
log.committing = 0;
wakeup(&log);
release(&log.lock);
}
}
💾 提交过程 - commit()
提交是一个4步原子操作:
static void commit() {
if (log.lh.n > 0) {
write_log(); // 1. 将修改的块从缓存写入日志
write_head(); // 2. 写入日志头 - 真正的提交点
install_trans(0); // 3. 将日志块安装到目标位置
log.lh.n = 0;
write_head(); // 4. 清除日志头
}
}
详细步骤分析:
步骤1:write_log() - 写日志
static void write_log(void) {
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *to = bread(log.dev, log.start+tail+1); // 日志块
struct buf *from = bread(log.dev, log.lh.block[tail]); // 缓存块
memmove(to->data, from->data, BSIZE); // 复制数据
bwrite(to); // 写入磁盘
brelse(from); brelse(to);
}
}
步骤2:write_head() - 写头部(提交点)
static void write_head(void) {
struct buf *buf = bread(log.dev, log.start);
struct logheader *hb = (struct logheader *) (buf->data);
hb->n = log.lh.n; // 设置有效块数
for (i = 0; i < log.lh.n; i++) {
hb->block[i] = log.lh.block[i]; // 复制块号数组
}
bwrite(buf); // ⚡ 真正的提交点!
brelse(buf);
}
步骤3:install_trans() - 安装事务
static void install_trans(int recovering) {
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *lbuf = bread(log.dev, log.start+tail+1); // 读日志块
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // 读目标块
memmove(dbuf->data, lbuf->data, BSIZE); // 复制到目标位置
bwrite(dbuf); // 写入目标位置
if(recovering == 0) bunpin(dbuf); // 解除钉住
brelse(lbuf); brelse(dbuf);
}
}
🚨 崩溃恢复 - recover_from_log()
系统启动时自动恢复:
static void recover_from_log(void) {
read_head(); // 读取日志头
install_trans(1); // 如果有未完成的事务,重新应用
log.lh.n = 0;
write_head(); // 清除日志
}
恢复逻辑:
- 如果日志头的
n > 0,说明有未完成的事务 - 重新执行
install_trans()完成事务 - 清除日志,准备新的事务
🎯 关键设计特性
1. 原子性保证
只有写入日志头(write_head)成功后,事务才算提交
崩溃恢复时,要么事务完全完成,要么完全不执行
2. 并发控制
多个文件系统调用可以同时执行
但只有当所有调用完成后才会提交
outstanding 计数器跟踪活跃操作
3. 日志吸收
同一个块的多次修改只记录最后状态
减少日志空间占用和I/O操作
4. 批量提交
等待所有活跃操作完成后才提交
提高性能,减少磁盘写入次数
📈 使用示例
典型的文件系统操作流程:
// 用户程序调用文件系统
begin_op(); // 开始事务
bp = bread(dev, blockno);// 读取块
// 修改 bp->data[] // 修改数据
log_write(bp); // 记录到日志
brelse(bp); // 释放缓存块
end_op(); // 结束事务(可能触发提交)
这个设计确保了即使在系统崩溃的情况下,文件系统也能保持一致性,这是现代文件系统的核心技术之一!