MIT 6s081 lab8:locks

lab8: locks

作业地址:Lab: locks (mit.edu)

Memory allocator (moderate)

kalloc和kfree的多次调用,多次获取kmem锁,避免race-condition出现,但降低了内存分配的效率,本实验的目的:修改内存分配的程序,提升内存分配的效率。

一个可行的方式是:每个CPU的内存分配和释放都是独立的,只需要给每个CPU分配一把锁,这样,每个CPU的内存分配释放都是独立的,进而提升内存分配的效率。当某个cpu 申请内存但没有空闲内存时,能够从其他CPU的空闲内存中“窃取内存”。

1、首先为每个CPU分配一个全局的内存空闲列表和锁,并完成初始化

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem[NCPU];
void
kinit()
{
  char kmem_name[10];
  for(int i = 0; i < NCPU; i++) {
    snprintf(kmem_name, 10, "kmem%d", i);
    printf("init lock: %s\n", kmem_name);
    initlock(&kmem[i].lock, kmem_name);
  }
  
  freerange(end, (void*)PHYSTOP);
}

2、修改kfree,获取当前cpu id,对当前cpu的内存空闲链表释放一页

void
kfree(void *pa)
{
  struct run *r;

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

  push_off(); // 关中断
  int current_cpu = cpuid();
  pop_off(); // 开中断

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

  r = (struct run*)pa;

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

3、修改kalloc,获取当前cpu id, 如果当前cpu有空闲内存,则直接分配,如果没有,则尝试从其他cpu的空闲内存中“窃取”。注意及时获取和释放对应cpu的锁

void *
kalloc(void)
{
  struct run *r;

  push_off(); // 关中断
  int current_cpu = cpuid();
  pop_off(); // 开中断

  acquire(&kmem[current_cpu].lock);
  r = kmem[current_cpu].freelist;
  if(r) // 有空间,就直接给
    kmem[current_cpu].freelist = r->next;
  else{ //当前CPU的内存空闲列表没空间了,从其他cpu那里偷,可能会触发竞争

    for(int i = 0; i < NCPU; i++){
      if(i == current_cpu) continue; // 不包括自己
      acquire(&kmem[i].lock); // 获取锁

      r = kmem[i].freelist; 
      if(r) // 别的cpu有空间
      {
        kmem[i].freelist = r->next; //修改别的cpu的空闲列表
        release(&kmem[i].lock); //释放锁
        break;
      }

      release(&kmem[i].lock); //释放锁
    }

  }
  release(&kmem[current_cpu].lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

Buffer cache (hard)

多进程同时使用文件系统,原本的bcache.lock会发生严重的锁竞争,bcache.lock用于保护磁盘区块缓存,xc6原本的设计中,多进程不能同时申请、释放磁盘缓存。

原本的设计是使用双向链表存储区块buf,每次尝试begt时,遍历链表,如果目标块已经在缓冲中,则将引用计数加1,并返回该缓存;若不存在,则选择一个最近最久未使用的(LRU),且引用计数为0的Buf块进行替换,并返回。

优化思路:建立一个blockno到buf的hash table,为每个桶单独加锁(降低锁的粒度),当两个进程同时访问的块哈希到同一个桶时,才会发生竞争,当桶中的空闲Buf不足时,从其他桶中获取Buf,并采用时间戳(全局ticks)的优化方式替换原本的双向链表。

1、为struct buf添加字段:uint prev_use_time,struct buf * next

struct buf {
  int valid;   // has data been read from disk?
  int disk;    // does disk "own" buf?
  uint dev;
  uint blockno;
  struct sleeplock lock;
  uint refcnt;
  uchar data[BSIZE];

  uint prev_use_time; // 记录上一次使用的时间,时间戳
  struct buf * next; // 记录下一个节点
};

2、修改全局bcache,分配prime个桶,并为每个桶分配一个锁,同时采用单链表的方式维护哈希表

#define prime     13
#define NBUCKET   prime 
#define GET_KEY(dev, blockno) ((blockno) % prime)

struct {

  struct buf hash_table[NBUCKET]; // 申请prime个哈希表(通过单链表维护),总共的BUF数量为NBUF
  struct spinlock lock_bucket[NBUCKET]; // 为每个桶分配一个锁
  
  struct buf buf[NBUF];

} bcache;

3、初始化,初始化所有的锁,并将所有的Buf放入第一个桶,便于后续其他桶中没有buf时,进行“窃取”。

void
binit(void)
{
  // 初始化桶的锁
  char bucket_lock_name[10];
  for(int i = 0; i < NBUCKET; i++) {
    snprintf(bucket_lock_name, 10, "bcache%d", i);
    initlock(&bcache.lock_bucket[i], bucket_lock_name);
    bcache.hash_table[i].next = 0;
  }

  // 把所有的buf放入第一个桶中,类似上一个实验把所有的空闲内存放在第一个cpu中
  struct buf * b;
  for(int i = 0; i < NBUF; i++) {
    b = &bcache.buf[i];

    b->prev_use_time = 0;
    b->refcnt = 0;

    initsleeplock(&b->lock, "buffer");

    b->next = bcache.hash_table[0].next;
    bcache.hash_table[0].next = b;
  }

}

4、 设计bget函数。(这是本实验的核心部分)

首先获取blockno哈希得到的桶的下标key,在这个桶中查找,如果命中了,就直接返回。

没有命中:

1、首先在当前的桶中查找引用计数为0的最近最久没有使用的Buf,如果有,则直接修改对应的buf,返回。

2、从0开始遍历其他桶,在每个桶中查找引用计数为0的最近最久没有使用的Buf,如果有,则先将这个buf从原本的桶中删去,再将这个buf添加到桶key,并修改buf的内容,返回。

这种设计方式,其实并不是真正意义上的LRU,因为并没有遍历全部的buf去寻找引用计数次数为0的最近最久没有使用的BUF。

static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;

  // Is the block already cached?
  int key = GET_KEY(dev, blockno); // 找到桶的下标
  acquire(&bcache.lock_bucket[key]); //获取这个桶的锁

  // 遍历key对应的桶查询
  for(b = bcache.hash_table[key].next; b; b = b->next) {
    if(b->dev == dev && b->blockno == blockno) { // 命中
      b->refcnt++;      
      release(&bcache.lock_bucket[key]); 
      acquiresleep(&b->lock);
      return b;
    }
  }

  // 没有命中cache,先从当前的key对应的桶中寻找
  int mn = ticks, key_replace = -1;
  struct buf * b_prev_replace = 0;

  for(b = &bcache.hash_table[key]; b->next; b = b->next) {
    if(b->next->prev_use_time <= mn && b->next->refcnt == 0) {
      b_prev_replace = b;
      mn = b->next->prev_use_time;
    }
  }
  if(b_prev_replace) { // 在这个桶里找到了
    b = b_prev_replace->next;
    b->dev = dev;
    b->blockno = blockno;
    b->valid = 0;
    b->refcnt = 1;
    release(&bcache.lock_bucket[key]);
    acquiresleep(&b->lock);
    return b;
  }
  
  for(int i = 0; i < NBUCKET; i++) {

    if(i == key) continue;

    acquire(&bcache.lock_bucket[i]);
    mn = ticks;
    
    for(b = &bcache.hash_table[i]; b->next; b = b->next) {
      if(b->next->prev_use_time <= mn && b->next->refcnt == 0) {
        mn = b->next->prev_use_time;
        b_prev_replace = b;
        key_replace = i;
      }
      if(b_prev_replace && b_prev_replace->next && key_replace >= 0) 
      { // 对bucket[i]中的buf寻找最近最少使用的buf,然后进行修改,这样其实就避免了环路的锁,但并不是真正意义上的LRU
        b = b_prev_replace->next;
        
        // 从旧的桶中删去
        b_prev_replace->next = b->next;
        // 在新的桶中添加
        b->next = bcache.hash_table[key].next;
        bcache.hash_table[key].next = b;
        
        b->dev = dev;
        b->blockno = blockno;
        b->valid = 0;
        b->refcnt = 1;
        
        release(&bcache.lock_bucket[i]);
        
        release(&bcache.lock_bucket[key]);

        acquiresleep(&b->lock);
        // printf("new buf :%d\n", blockno);
        return b;
      }
    }
    
    release(&bcache.lock_bucket[i]);
    
  }
  printf("no buffers: %d\n", blockno);
  release(&bcache.lock_bucket[key]);
  panic("bget: no buffers");
}

4、 修改brelse函数,修改b的引用计数

void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);

  int key = GET_KEY(b->dev, b->blockno);
  acquire(&bcache.lock_bucket[key]);

  b->refcnt--;
  if (b->refcnt == 0) {
    // no one is waiting for it.
    // 更新时间戳
    b->prev_use_time = ticks;
  }

  release(&bcache.lock_bucket[key]);
  
}

5、修改bpin和bunpin

void
bpin(struct buf *b) {
  
  int key = GET_KEY(b->dev, b->blockno);
  acquire(&bcache.lock_bucket[key]);

  b->refcnt++;

  release(&bcache.lock_bucket[key]);
}

void
bunpin(struct buf *b) {

  int key = GET_KEY(b->dev, b->blockno);
  acquire(&bcache.lock_bucket[key]);

  b->refcnt--;

  release(&bcache.lock_bucket[key]);
}

不足之处:其实本设计有可能出现环路等待的死锁问题:当两个进程在运行前都没有被缓存。

环路等待的死锁问题参考如下:

[mit6.s081] 笔记 Lab8: Locks | 锁优化 | Miigon’s blog

假设块号 b1 的哈希值是 2,块号 b2 的哈希值是 5
并且两个块在运行前都没有被缓存
----------------------------------------
CPU1                  CPU2
----------------------------------------
bget(dev, b1)         bget(dev,b2)
    |                     |
    V                     V
获取桶 2 的锁           获取桶 5 的锁
    |                     |
    V                     V
缓存不存在,遍历所有桶    缓存不存在,遍历所有桶
    |                     |
    V                     V
  ......                遍历到桶 2
    |                尝试获取桶 2 的锁
    |                     |
    V                     V
  遍历到桶 5          桶 2 的锁由 CPU1 持有,等待释放
尝试获取桶 5 的锁
    |
    V
桶 5 的锁由 CPU2 持有,等待释放

!此时 CPU1 等待 CPU2,而 CPU2 在等待 CPU1,陷入死锁!

解决方法可以参考上述这篇blog,比较复杂,后续有时间再解决。

实验对应的测试样例并没有出现这种隐晦的死锁情况,还是通过了。

== Test running kalloctest == 
$ make qemu-gdb
(68.1s) 
== Test   kalloctest: test1 == 
  kalloctest: test1: OK 
== Test   kalloctest: test2 == 
  kalloctest: test2: OK 
== Test kalloctest: sbrkmuch == 
$ make qemu-gdb
kalloctest: sbrkmuch: OK (7.7s) 
== Test running bcachetest == 
$ make qemu-gdb
(6.4s) 
== Test   bcachetest: test0 == 
  bcachetest: test0: OK 
== Test   bcachetest: test1 == 
  bcachetest: test1: OK 
== Test usertests == 
$ make qemu-gdb
usertests: OK (102.8s) 
    (Old xv6.out.usertests failure log removed)
== Test time == 
time: OK 
Score: 70/70

你可能感兴趣的:(MIT6s081,c语言,risc-v,linux)