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();               // 结束事务(可能触发提交)

这个设计确保了即使在系统崩溃的情况下,文件系统也能保持一致性,这是现代文件系统的核心技术之一!