xv6文件系统二:buffer cache层解析


系列文章目录

系列第一篇文章:第一章 xv6磁盘层详解

注释:从底向上理解FS的原因在于磁盘层的操作不依赖于任何其余层,并向上层提供接口,因此先理解磁盘层不会像从上到下解析一样,在遇到某个不属于本层次的接口时暂时跳过。自底向上会在每层的解析中逐渐揭开文件系统的全貌。

Buffer Cache层概述

上篇文章我们解析了磁盘层的操作。本篇文章中我们会直接使用其提供的接口,不再做具体阐述。

xv6文件系统层次结构图:在这里插入图片描述
buffer cache层用于对磁盘中的block块在内存中存储副本,方便高速操作。
buffer cache层提供的接口:

  • 1:struct buf* bread(uint dev, uint blockno)—>为某个特定磁盘块获取一块缓冲
  • 2:void bwrite(struct buf *b)—>对缓冲中数据修改完之后,将它写入硬盘
  • 3:void brelse(struct buf *b)—>对缓冲操作结束,释放缓冲

buffer cache的原则是硬盘中每一个块在buffer cache中只有唯一的缓存。同一块的多个副本会造成文件内容错乱等未知问题。
buffer cache层中的缓冲满时,需要使用LRU算法在所有未被使用的块中选出一块置换。

一、buffer缓冲结构

文件系统中关于bcache的定义如下:

struct buf {
  int valid;   // 记录是否数据从磁盘读取到内存,首次记录目标块时会标记为0
  int disk;    //用于磁盘层驱动和中断之间作为消息
  uint dev;		// 设备号
  uint blockno; //缓冲对应的硬盘块号
  struct sleeplock lock;//睡眠锁
  uint refcnt;		//记录有多少进程在使用该缓冲块
  struct buf *prev; // 双向循环链表,方便LRU算法处理
  struct buf *next;
  uchar data[BSIZE]; //硬盘数据存储位置
};
struct {
	//自旋锁
  struct spinlock lock;
  struct buf buf[NBUF];//共30个缓冲块供上层使用

  // 用于LRU算法
  struct buf head;
} bcache;

bcache结构的缺点:bcache自旋锁使得同一时间只有一个进程可以修改bcache的内容。造成了效率不高的情况。

二、buffer cache层初始化binit函数

void binit(void)
{
  struct buf *b;
	//初始化锁
  initlock(&bcache.lock, "bcache");

  //初始化双向循环链表头尾指向自身
  bcache.head.prev = &bcache.head;
  bcache.head.next = &bcache.head;
  // 头插法初始化好双向循环链表
  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    b->next = bcache.head.next;
    b->prev = &bcache.head;
    initsleeplock(&b->lock, "buffer");
    bcache.head.next->prev = b;
    bcache.head.next = b;
  }
}

由于初次访问硬盘块时必然内存中无缓存,初始化会设置好供LRU算法使用的双链表,同时初始化所有的锁。

三、核心函数bget,从bcache中获取缓冲块

代码如下:仅文件内部可见函数,供bread接口函数直接调用

static struct buf* bget(uint dev, uint blockno)
{
  struct buf *b;
  // 申请bcache全局锁
  acquire(&bcache.lock);

  // 搜索双向循环链表,按最近最多顺序依次访问
  for(b = bcache.head.next; b != &bcache.head; b = b->next){
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;
      release(&bcache.lock);
      acquiresleep(&b->lock);
      return b;
    }
  }

  // 缓冲未命中
  // 按LRU算法置换出最近最少使用的闲置块,无闲置块则直接panic
  for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
    if(b->refcnt == 0) {
      //对目的空闲块记录详细信息,设置尚未从硬盘中读取内容valid标志位等标志位
      //申请该块的操作锁,准备对该缓冲块使用接口进行操作
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&bcache.lock);
      acquiresleep(&b->lock);
      return b;
    }
  }
  panic("bget: no buffers");
}

四、三个对外提供的接口函数

代码如下:

struct buf* bread(uint dev, uint blockno)
{
  struct buf *b;
  //申请空闲块,未缓冲则记录信息
  b = bget(dev, blockno);
  //未命中该块则先从硬盘中读进内存
  if(!b->valid) {
    virtio_disk_rw(b, 0);
    b->valid = 1;
  }
  return b;
}

// 将缓冲内容写入硬盘,必须持有该块的睡眠锁
void
bwrite(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("bwrite");
  //调用硬盘层提供的接口,写入硬盘
  virtio_disk_rw(b, 1);
}

// 释放一个缓冲,必须持有该缓冲的睡眠锁
// 若无进程等待使用该块,将它移动至最近最多使用的头部
void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);

  acquire(&bcache.lock);
  b->refcnt--;
  if (b->refcnt == 0) {
    // 先把b从链表中取出来
    b->next->prev = b->prev;
    b->prev->next = b->next;
    //把b链到bcache.head的next位置
    //next指针按顺序往后为最近最多使用链表
    //prev指针按顺序往前为最近最少使用链表
    b->next = bcache.head.next;
    b->prev = &bcache.head;
    bcache.head.next->prev = b;
    bcache.head.next = b;
  }
  
  release(&bcache.lock);
}

完结撒花。至此源码解析完毕。

注释:文件中剩余的bpinbunpin函数为Logging(日志)层的内容,留到下篇做解析。


Buffer Cache优化(xv6中lock实验内容)

Buffer Cache的缺点在于使用bcache.lock作为本层的全局锁,同一时间只有一个进程可以访问bcache。降低了效率。
一个可行的优化方法:使用hash表,按hash表的桶为基本访问单位
1:哈希表的hash方法为块号对哈希表桶的数量取余,即同一余数所有块号在一个桶中,hash表桶容量尽量为素数,如13,实验中我取用29。
2:哈希表中每一个桶有自己的自旋锁,初始化时把所有的缓冲都挂在哈希表的桶下。同一组对哈希表桶数量取余之后相同的所有块号竞争一个锁。也就是:桶数量越多,性能越好。
3:LRU算法基于桶进行,每次寻找本桶中的最近最少使用的可用块。当没有时,则跨桶寻找,按照桶在hash表中的下标从大到小顺序申请锁防止死锁。

总结

Buffer Cache层本身不困难,理解其功能后理解源码会更加简单。

关于源码详解中注释部分可能观感上不太清楚,如有问题,欢迎私信,必虚心受教。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值