xv6文件系统四:日志Logging层详解

系列文章目录

系列第一篇文章:第一章 xv6文件系统磁盘层详解
系列第二篇文章:第二章 xv6文件系统BufferCache层详解
系列第三篇文章:第三章 xv6文件系统总览


Logging日志层概述

还是先给出xv6文件系统层次结构图:
在这里插入图片描述

由上文得知日志层主要作用在于文件系统崩溃后的恢复,日志层可以使得系统调用的结果要么完全出现,要么完全不出现。同时,日志层还具有快速恢复的优点。本文将从xv6源码角度解析日志层具体实现


一、日志层作用及接口

日志层作用

日志层把一整个系统调用过程中对于磁盘的操作作为一次事务,要么全部写入,要么全部不写入。
日志层的实现机制主要通过磁盘写一个块的过程必然发生或必然不发生的特性实现。
xv6的日志层位于磁盘的2-31块。磁盘第2块也是文件系统日志层的header block,用于存储logheader数据结构。3-31块存储本次事务中待写回的块数据。
logheader数据结构如下:

struct logheader {
  int n;
  int block[LOGSIZE];
};

回顾上篇文章,假设一次系统调用中只对33号块进行了写操作且过程中没有其余进程对文件进行任何操作(无并发情况),则日志层块数据变化如下图:
在这里插入图片描述

日志层接口

void begin_op(void)任何系统调用涉及磁盘操作时必须通过begin_op()表示事务的开始
void end_op(void)任何系统调用涉及磁盘操作时必须通过end_op()表示事务的结束,end_op会提交本次事务。
void log_write(struct buf *b)写磁盘的例程由原始的bread-->操作buffer-->bwrite-->brelse变为bread-->操作buffer-->log_write()-->brelse也就是用log_write替换掉bwrite()

二、日志层面临的挑战

1:在logheader中记录的数据块不可被换出。由buffer cache层解析知,当块被换出时,其必会被写回磁盘位置,则违背日志层原子性。
2:文件系统的操作必须适应日志层的大小,由于日志层起始于2号块,结束于31号块,一块header block,29个数据块,所以整个系统全部进程最多同时操作不大于30个块。
3:考虑不同进程的并发调用,每个进程在调用begin_op时会进行评估本次操作是否可能超出日志层大小,有可能超出大小则会考虑调用sleep等待本次事务提交。

接下来会在分析源码的同时解答xv6的文件系统如何解决上述问题。

日志层源码解析

日志层所用数据结构

struct log为日志层操作中的核心数据结构。

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;// 日志log header
};
struct log log;

日志层初始化

代码如下:各属性分别初始化操作+日志恢复

注:log.lock只有在修改log中的内容时才需要获取

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; // 初始化start属性
  log.size = sb->nlog;     // 初始化日志层块数
  log.dev = dev;		// 初始化设备号
  recover_from_log();// 从log中恢复
}
日志恢复

日志恢复函数用于从日志层读取并初始化内存log.lh结构并提交事务。同时对硬盘中的事务记录进行清零。期间若断电则文件系统重启继续重复该恢复过程。

static void
recover_from_log(void)
{
  read_head();//从磁盘第一块读入内存
  install_trans(1); // 提交日志层中遗留的事务
  log.lh.n = 0;
  write_head(); // 日志层header block清零
}

功能性函数

read_head函数负责从磁盘到内存的logheader数据读取

// 从日志层磁盘第一块读取内容到内存log的lh结构中。
static void
read_head(void)
{
  struct buf *buf = bread(log.dev, log.start);//先把日志层head block读入内存
  struct logheader *lh = (struct logheader *) (buf->data);//设置指针指向该数据
  int i;
  // 复制硬盘内容中的n和后面block数组的n个内容
  log.lh.n = lh->n;
  for (i = 0; i < log.lh.n; i++) {
    log.lh.block[i] = lh->block[i];
  }
  brelse(buf);
}

write_head函数负责从内存到硬盘header block块的存储,也是整个事务提交的节点。

// 写内存log的lh结构内容到日志层磁盘第一块中
static void
write_head(void)
{
  struct buf *buf = bread(log.dev, log.start);//先把日志层head block读入内存
  struct logheader *hb = (struct logheader *) (buf->data);//设置指针指向该数据
  int i;
  hb->n = log.lh.n;//修改n,待写回磁盘块数
  for (i = 0; i < log.lh.n; i++) {//按顺序写好待完成的事务中操作的磁盘块号
    hb->block[i] = log.lh.block[i];
  }
  bwrite(buf); // 事务提交成功与否的关键点,写回head block 
  brelse(buf);
}

事务开始函数

任何系统调用在开始操作磁盘之前必须调用begin_op函数表示本次事务的开始

begin_op函数保证并发的系统调用一起不会超过日志层1块head block+29块数据磁盘块的算法在于:
1:在系统调用中限制每次系统调用最大操作磁盘块数为MAXOPBLOCKS(=10),当读写大文件时会分解为小的事务进行。
2:在日志层中会对新加入的事务做预判,假设它操作的磁盘数为10,则判断是否超过日志层的30块大小。超过则当前进程睡眠等待唤醒。

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 {
      // 当前正在进行事务的进程数加1,释放锁
      log.outstanding += 1;
      release(&log.lock);
      break;
    }
  }
}

事务结束函数

任何系统调用在结束操作磁盘之后必须调用end_op函数表示本次事务的结束

注:事务结束进程可能并未操作磁盘,此时log.lh.n不会变化,而outstanding会减一,可以再多一个并发的进程加入日志

void
end_op(void)
{
  int do_commit = 0;
  //申请锁,当前正在进行事务的进程数减1
  acquire(&log.lock);
  log.outstanding -= 1;
  if(log.committing)
    panic("log.committing");
  if(log.outstanding == 0){//本次事务所有进程操作都完成,可以提交,设置状态
    do_commit = 1;
    log.committing = 1;
  } else {
    // outstanding不为0,此时还可能有别的进程加入事务的可能,唤醒可能在等待的进程
    wakeup(&log);
  }
  release(&log.lock);

  if(do_commit){
    // 提交事务
    commit();
    //获取锁,修改状态,唤醒因为log正在提交而等待的进程
    acquire(&log.lock);
    log.committing = 0;
    wakeup(&log);
    release(&log.lock);
  }
}

事务写回磁盘函数

操作磁盘流程bp=bread(…) + 修改bp->data[] + bwrite(bp) + brelse(bp)中所有bwrite(bp)的操作全部由log_write(bp)替换。

1:log_write函数会在该事务操作某一块新的磁盘块时给该块的refcnt加1,使得该块在本次事务结束之前不会被换出,保证一致性
2:日志层的另一个特点是同一事务多次对同一磁盘块读写操作时,则只记录一次。log_write会使得每次操作都在内存中的同一个缓冲块中,该块不会被换出,故而写入最终结果到磁盘即可

void
log_write(struct buf *b)
{
  int i;
  // 要修改log.lh.n,需要获取锁
  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)   // 日志合并,之前操作过这块磁盘,bread必然会返回该块磁盘的同一缓冲
      break;
  }
  log.lh.block[i] = b->blockno;
  if (i == log.lh.n) {  // 本次事务访问了新块,之前没有该块的记录
    bpin(b);    // 该块refcnt++,不会被换出
    log.lh.n++;  // n++表示本次事务使日志层多了一块待写回块
  }
  release(&log.lock);
}

事务提交函数

commit函数负责事务提交,并将日志层head block待写回磁盘数置零

static void commit()
{
  if (log.lh.n > 0) {
    write_log();     // 写回cache到日志层数据块
    write_head();    // 写head block,事务提交节点
    install_trans(0); // 写日志层数据到它目标块中
    log.lh.n = 0;    //设置待操作块数为0
    write_head();    // 写head block,擦除事务记录
  }
}

write_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); // 日志层第i个数据block
    struct buf *from = bread(log.dev, log.lh.block[tail]); // 返回缓存中日志层记录下来的目标块
    memmove(to->data, from->data, BSIZE);//移动数据
    bwrite(to);  // 写回日志层的数据磁盘块
    brelse(from); 
    brelse(to);
  }
}

write_head负责将内存中的logheader数据写回日志层第一块head block中,也是事务提交的关键。上方已经介绍过。

install_trans负责将日志层的数据块按照其待写回的磁盘块号写回目标磁盘块

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); // 读取日志层第i个数据磁盘块
    struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // 读取该写回的第i个目标块
    memmove(dbuf->data, lbuf->data, BSIZE);  // 拷贝数据到目标块中
    bwrite(dbuf);  // 写回磁盘
    if(recovering == 0)// 如果系统不是刚恢复,则需要对缓冲层的buf的refcnt--,以便换出操作
      bunpin(dbuf);
    brelse(lbuf);
    brelse(dbuf);
  }
}

总结

日志层主要负责文件系统的崩溃恢复。
从Buffer Cache层向上基本是软件层面的内容,内容不会像虚拟磁盘那块一样内容繁杂,多点耐心,总能通透的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值