MIT-6.S081 | locks(2021)

133 阅读9分钟

Lab: locks

LRU

​ LRU全称是Least Recently Used,即最近最久未使用的意思。

​ LRU算法的设计原则是:*如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小*。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

LRU实现

  1. 用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。
  2. 利用一个链表来实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃。
  3. 利用链表和hashmap。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部如果不存在,则新建一个节点,放到链表头部若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

​ 对于第一种方法, 需要不停地维护数据项的访问时间戳,另外,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。对于第二种方法,链表在定位数据的时候时间复杂度为O(n)。所以在一般使用第三种方式来是实现LRU算法。

结构体

{前的是类型名(type name),是拿来定义的,}后的是变量名(variable name),是拿来使用的。

lab

bcache本身是kernel virtual space中的一个数据结构,本身是一串虚拟地址,kernel程序创建的时候就会将其创建,存在kerne virtual space里,直接映射到物理内存空间。我们没有必要要知道它的具体物理空间,我们只是用来指向disk block,作用就像是缓存了一样

Memory allocator

​ 实现内存分配器,减少竞争,主要实现思路是原先只有一个freelist,一个lock给八个cpu竞争使用,而我们需要重新设计为八个freelist、八个lock给八个cpu使用。

​ 中途会有一个问题,就是当某个cpu对应的freelist没有空闲页面了,该怎么办?实验介绍给出的方案是从其它cpu的freelist(steal)过来。

// Physical memory allocator, for user processes,
// kernel stacks, page-table pages,
// and pipe buffers. Allocates whole 4096-byte pages.

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "riscv.h"
#include "defs.h"

void freerange(void *pa_start, void *pa_end);

extern char end[]; // first address after kernel.
                   // defined by kernel.ld.
//结构体  /前的是类型名  /后是变量名
struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;


struct Kmem
{
  struct spinlock lock;
  struct run *freelist;
};

struct Kmem kmems[NCPU];

void
kinit()
{
  // initlock(&kmem.lock, "kmem");
  // freerange(end, (void*)PHYSTOP);
  for(int i=0; i<NCPU;i++)
  {
    initlock(&kmems[i].lock, "kmem");
    if(i==0)
    {
      freerange(end, (void*)PHYSTOP);
    }
  }
}

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;
  //当前cpu id
  push_off();
  int cpu_id = cpuid();
  pop_off();

  //acquire(&kmem.lock);
  acquire(&kmems[cpu_id].lock);
  r->next = kmems[cpu_id].freelist;
  kmems[cpu_id].freelist = r;
  release(&kmems[cpu_id].lock);
}

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
  struct run *r;

  // 当前cpu id
  push_off();
  int cpu_id = cpuid();
  pop_off();

  //acquire(&kmem.lock);
  acquire(&kmems[cpu_id].lock);
  r = kmems[cpu_id].freelist;
  //当前页的freelist有freepage
  if(r)
  {
    kmems[cpu_id].freelist = r->next;
    release(&kmems[cpu_id].lock);
    memset((char *)r, 5, PGSIZE); // fill with junk
    return (void *)r;
  }
  else
  {
    release(&kmems[cpu_id].lock);
    for(int i=0;i<NCPU;i++)
    {
      if(i != cpu_id)  //避免和当前CPU竞争
      {
        acquire(&kmems[i].lock);
        // 到最后一个也找不到有空闲内存的,返回0
        if (i == NCPU - 1 && kmems[i].freelist == 0)
        {  
          release(&kmems[i].lock);
          return (void*)0;
        }
        //能窃取到
        if(kmems[i].freelist)
        {
          struct run *to_alloc = kmems[i].freelist;  //窃取
          kmems[i].freelist = to_alloc->next;  //占用
          release(&kmems[i].lock);
          memset((char *)to_alloc, 5, PGSIZE); // fill with junk
          return (void *)to_alloc;
        }
        // 没有匹配的
        release(&kmems[i].lock);
      }
    }
  }
  return (void *)0;
}

Buffer cache

​ 实现buffer cache的并发,保证一个不变性:即每次只有一个buffer被cpu或者process使用,这也是所谓的原子性。同时不要让其它process或cpu都等待一个cache lock。

​ 实现思路主要是将30个buf(NBUF == 30)划分为13个哈希桶(bucket),每个bucket都有一个对应的自旋锁即bcache.bucket在重复使用或者淘汰(eviction)一个buf时,对这个buf对应的bucket上自旋锁,重复使用或者eviction结束之后,才对这个buf对应的bucket解锁,达成这个原则,才能达成不变性的要求。

  • 在初始化时,先将每个buckethead.next置空,再将30个buf全分配给bucket[0]。再根据hash函数算出的buk_id动态地分配,这里还是采用了steal的方式,但找出的空闲块要符合LRU
  • bget()中,buf的「身份认证」也就是devblockno作为参数,经hash函数算出buf对应的buk_id后,会有两种情况。
    • bucket[buk_id]中已经缓存了对应的buf,那直接拿来重复使用就行
    • 没有,那只能eviction,先遍历所有buket,找到符合LRU的buf,从当前buket中steal出来,然后缓存到目标bucket[buk_id]中,修改相关信息就可以拿来用了。
      • 中间会有一个重复缓存的问题导致panic: freeing free block,假设两个进程同时找到了同一个buf,也要缓存到同一个bucket[buk_id]中,这就是重复缓存,从而导致二次释放。
      • 解决方案是在缓存到目标bucket[buk_id]之后,马上做一次检查,看看buf是不是已经被其它进程缓存进去了,如果是就用这个buf就行了,如果不是就要修改相关信息才拿出来用。
    • 为了保证原子性,在「找对应buf」、「找符合LRU的buf并steal出来」、「缓存到目标bucket」的三个过程中必须对各自所查找或使用的bucket保持锁定。
  • 实现了LRU采用的是时间戳(timestamp)的方式
    • 初始化全部timestamp置为0
    • 释放一个buf时,如果引用为0,就更新一下timestamp
    • 每次找符合LRU的空闲块时,找引用为0timestamp最大的即可

访问一个buf的结构时是不需要对sleeplock: buffer进行上锁、解锁操作,但要获得bucket.lock。只有使用这个buf的数据内容才需要对sleeplock: buffer进行上锁、解锁操作。

/* param.h */ ... 
#define FSSIZE 10000 // size of file system in blocks
/* buf.h */
struct buf {
	...
  uchar data[BSIZE];
  uint timestamp;  // the time stamp of current block 
};
// Buffer cache.
//
// The buffer cache is a linked list of buf structures holding
// cached copies of disk block contents.  Caching disk blocks
// in memory reduces the number of disk reads and also provides
// a synchronization point for disk blocks used by multiple processes.
//
// Interface:
// * To get a buffer for a particular disk block, call bread.
// * After changing buffer data, call bwrite to write it to disk.
// * When done with the buffer, call brelse.
// * Do not use the buffer after calling brelse.
// * Only one process at a time can use a buffer,
//     so do not keep them longer than necessary.


#include "types.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "riscv.h"
#include "defs.h"
#include "fs.h"
#include "buf.h"


#define NBUK  13
#define hash(dev, blockno) ((dev * blockno) % NBUK)

struct bucket
{
  struct spinlock lock;
  struct buf head;
};


struct {
  struct spinlock lock; // 主要保护的是连接所有槽位的链表。
  struct buf buf[NBUF]; // 代表我们有30个槽位可用。

  // Linked list of all buffers, through prev/next.
  // Sorted by how recently the buffer was used.
  // head.next is most recent, head.prev is least.
  //struct buf head;
  struct bucket buckets[NBUK]; // the cache has 13 buckets
} bcache;

void
binit(void)
{
  struct buf *b;
  struct buf *prev_b;

  initlock(&bcache.lock, "bcache");
  // 将所有的 buf 都先放到 buckets[0] 中
  for(int i=0; i<NBUK; i++)
  {
    initlock(&bcache.buckets[i].lock, "bcache.bucket");
    bcache.buckets[i].head.next = (void *)0;
    // 我们首先将所有的bufs传给buckets[0]。
    if(i == 0)
    {
      prev_b = &bcache.buckets[i].head;
      for(b = bcache.buf; b < bcache.buf+NBUF; b++)
      {
        if(b == bcache.buf + NBUF - 1)  //buf[29]
        {
          b->next = (void*)0;
        }
        prev_b->next = b;
        b->timestamp = ticks; // 当初始化内核时,ticks == 0
        initsleeplock(&b->lock, "buffer");
        prev_b = b; 
      }
    }
  }
}

// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
// 根据输入的设备号和块号,扫描Buffer Cache中的所有缓存块。
// 如果缓存命中,bget更新引用计数refcnt,释放bcache.lock
// 先判断是否之前已经缓存过了硬盘中的这个块。如果有,那就直接返回对应的缓存,
// 如果没有,会去找到一个最长时间没有使用的缓存,并且把那个缓存分配给当前块。
// 所有的缓存被串到了一个双向链表里。链表的第一个元素是最近使用的,最后一个元素是很久没有使用的。
static struct buf *
bget(uint dev, uint blockno) // 设备号和块号
{
  struct buf *b;
  int buk_id = hash(dev, blockno);

  acquire(&bcache.buckets[buk_id].lock);
  //buckets[nik_id]有buf直接用
  for (b = bcache.buckets[buk_id].head.next; b; b=b->next)
  {
    if(b->dev == dev && b->blockno == blockno)
    {
      b->refcnt++;
      release(&bcache.buckets[buk_id].lock);
      acquiresleep(&b->lock);
      return b;
    }
  }
  release(&bcache.buckets[buk_id].lock);

  // 没有,遍历bucket
  // 从当前的bucket中steal,缓存到目标bucket[buk_id]
  // 释放buf时 cnt=0更新timestamp,
  //找到符合的LRU时, 找cnt=0,timestamp最大的
  int max_timestamp = 0;
  int lru_buk_id = -1;
  int is_better = 0; //是否有更好的lru_buk_id
  struct buf *lru_b = (void*)0;
  struct buf *prev_lru_b = (void*)0;

  // 当refcnt == 0时,找到lru_buk_id,并在每个桶[i]中获得最大的时间戳。
  struct buf *prev_b = (void*)0;
  for(int i=0; i<NBUK; ++i)
  {
    prev_b = &bcache.buckets[i].head;
    acquire(&bcache.buckets[i].lock);
    while(prev_b->next)
    {
      if (prev_b->next->refcnt == 0 && prev_b->next->timestamp >= max_timestamp)
      {
        max_timestamp = prev_b->next->timestamp;
        is_better = 1;
        prev_lru_b = prev_b; // get prev_lru_b
      }
      prev_b = prev_b->next;
    }
    if(is_better)
    {
      if(lru_buk_id != -1)
      {
        release(&bcache.buckets[lru_buk_id].lock);
      }
      lru_buk_id = i;
    }
    else{
      release(&bcache.buckets[i].lock);
    }
    is_better = 0;
  }
  //get lru_b
  lru_b = prev_lru_b->next;
  //steal
  if (lru_b)
  {
    prev_lru_b->next = prev_lru_b->next->next;
    release(&bcache.buckets[lru_buk_id].lock);
  }
  // 缓存lru_b到buckets[buk_id]。
  acquire(&bcache.lock);
  acquire(&bcache.buckets[buk_id].lock);
  if (lru_b)
  {
    lru_b->next = bcache.buckets[buk_id].head.next;
    bcache.buckets[buk_id].head.next = lru_b;
  }
  // 如果两个进程在buckets[lru_buk_id]中使用相同的块(相同的块号)。
  // 一个进程可以检查它,如果已经在这里,我们就可以得到它。
  // 否则,我们将在两个进程中使用相同的块,并进行双倍缓存
  b = bcache.buckets[buk_id].head.next; // buckets[buk_id]中的第一个buf。
  while (b)
  {
    if (b->dev == dev && b->blockno == blockno)
    {
      b->refcnt++;
      release(&bcache.buckets[buk_id].lock);
      release(&bcache.lock);
      acquiresleep(&b->lock);
      return b;
    }
    b = b->next;
  }
  // 在遍历每个桶的时候找不到lru_b
  if (lru_b == 0)
    panic("bget: no buffers");

  lru_b->dev = dev;
  lru_b->blockno = blockno;
  lru_b->valid = 0;
  lru_b->refcnt = 1;
  release(&bcache.buckets[buk_id].lock);
  release(&bcache.lock);
  acquiresleep(&lru_b->lock);
  return lru_b;
}

// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{
  struct buf *b;

  b = bget(dev, blockno); // 通过调用bget来获取指定磁盘块的缓存块
  if(!b->valid) {
    // 如果b->valid=0,说明这个槽位是刚被回收的,还没有缓存任何磁盘块,
    // 因此调用virtio_disk_rw来先从磁盘上读取相应磁盘块的内容,读取完成后更新b->valid。
    virtio_disk_rw(b, 0);
    b->valid = 1;
  }
  return b; // 返回的是上锁的且可用的缓存块。
}

// Write b's contents to disk.  Must be locked.
void
bwrite(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("bwrite");
  virtio_disk_rw(b, 1);
}

// Release a locked buffer.
// Move to the head of the most-recently-used list.
// 调用者结束对一个缓存块的处理(读或写)之后,
// 调用brelse更新bcache的链表,并且释放对应的缓存块的睡眠锁
void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");
  // 先释放缓存块的睡眠锁
  releasesleep(&b->lock);

  int buk_id = hash(b->dev, b->blockno);
  acquire(&bcache.buckets[buk_id].lock);
  b->refcnt--; // 减去引用计数refcnt
  // 最近刚刚更新的块b->refcnt=1
  if (b->refcnt == 0) {
    b->timestamp = ticks;
  }
  release(&bcache.buckets[buk_id].lock);
}

void 
bpin(struct buf *b)
{
  int buk_id = hash(b->dev, b->blockno);
  acquire(&bcache.buckets[buk_id].lock);
  b->refcnt++;
  release(&bcache.buckets[buk_id].lock);
}

void 
bunpin(struct buf *b)
{
  int buk_id = hash(b->dev, b->blockno);
  acquire(&bcache.buckets[buk_id].lock);
  b->refcnt--;
  release(&bcache.buckets[buk_id].lock);
}