xv6-lab8-lock

Lab:lock

前置知识

  • xv6 book 章节6、3.5、8.1
  • 对于hash的基本认知
  • 对于锁的基本认知以及死锁的处理方法

Memory allocator

实验目标

在未修改前,所有内存块由一个锁管理,若有多个进程并发地获取内存,则会造成非常多的锁等待,kalloctest则统计了锁冲突的次数,如下

$ kalloctest
start test1
test1 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 83375 #acquire() 433015
lock: bcache: #fetch-and-add 0 #acquire() 1260
--- top 5 contended locks:
lock: kmem: #fetch-and-add 83375 #acquire() 433015
lock: proc: #fetch-and-add 23737 #acquire() 130718
lock: virtio_disk: #fetch-and-add 11159 #acquire() 114
lock: proc: #fetch-and-add 5937 #acquire() 130786
lock: proc: #fetch-and-add 4080 #acquire() 130786
tot= 83375
test1 FAIL

你的目标是修改内存块策略,以减少锁冲突,修改后的锁冲突应该大幅减少,如下

$ kalloctest
start test1
test1 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 0 #acquire() 42843
lock: kmem: #fetch-and-add 0 #acquire() 198674
lock: kmem: #fetch-and-add 0 #acquire() 191534
lock: bcache: #fetch-and-add 0 #acquire() 1242
--- top 5 contended locks:
lock: proc: #fetch-and-add 43861 #acquire() 117281
lock: virtio_disk: #fetch-and-add 5347 #acquire() 114
lock: proc: #fetch-and-add 4856 #acquire() 117312
lock: proc: #fetch-and-add 4168 #acquire() 117316
lock: proc: #fetch-and-add 2797 #acquire() 117266
tot= 0
test1 OK
start test2
total free number of pages: 32499 (out of 32768)
.....
test2 OK

实验实现

下面我们先看提示

  • 你可以使用kernel/param.h 中的NCPU (NCPU表示XV6使用了几个虚拟处理器,而xv6对于每个进程只有一个线程,该提示表示我们可以基于CPU数量进行修改)
  • freerange 将所有空闲内存块给予 正在运行 freerange 的CPU(如果所有空闲内存都给了一个CPU,那么其他CPU怎么办?–从有空闲内存块的CPU拿吗?)
  • 函数 cpuid() 返回当前的 cpu 编号, 不过需要关闭中断才能保证该函数被安全地使用。中断开关可使用 push_off()pop_off()
  • kmem 命名你的锁 (提示我们需要创建额外的锁)

根据以上的提示解读以及我们要解决的主要矛盾—单一内存块管理池与多进程争用内存块之间的矛盾。我们就可以得到大致方案

  • NCPU 为基准,给每一个CPU创建一个内存池
  • 使用锁管理每一个内存池
  • 当某个内存池为空时,从其他内存池偷取内存块

代码实现

/*	kernel/kalloc.c	*/

//首先是多内存池创建和锁管理
//该部分我们只需复用单一内存池,并在初始化时初始化所有锁即可
struct kmem{
  struct spinlock lock;
  struct run *freelist;
};

//用cpuid来分配内存池
struct kmem kmem_sum[NCPU];

void
kinit()
{
  int i;
  //初始化所有锁
  for(i = 0; i < NCPU; ++i)
  {
    initlock(&(kmem_sum[i].lock), "kmem");
  }
  //这里不用修改,默认将内存分配给运行这一函数的cpu
  freerange(end, (void*)PHYSTOP);
}


//kfree本来是将内存块释放到单一内存块中,修改后我们需要放回该cpu对应的内存池
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;

  push_off();
  int id = cpuid();
  //获取锁以保证内存池的使用安全
  acquire(&(kmem_sum[id].lock));
  r->next = kmem_sum[id].freelist;
  kmem_sum[id].freelist = r;
  release(&(kmem_sum[id].lock));
  pop_off();
}


//内存块获取
void *
kalloc(void)
{
  struct run *r;

  push_off();

  int id = cpuid();

  acquire(&(kmem_sum[id].lock));
    
  r = kmem_sum[id].freelist;
    
  //先从自己的内存池中寻找可用的内存块
  if(r)
  {
    kmem_sum[id].freelist = r->next;
  }
  else	//没有空闲块则从其他内存池中寻找
  {
    int i;
    for(i = 0; i < NCPU; ++i)
    {
      if(i == id) continue;
	  
      //寻找前记得上锁
      acquire(&(kmem_sum[i].lock));

      r = kmem_sum[i].freelist;
      if(r)
      {
        kmem_sum[i].freelist = r->next;
        release(&(kmem_sum[i].lock));
        break;
      }

      release(&(kmem_sum[i].lock));

    }
  }
  release(&(kmem_sum[id].lock));
  pop_off();

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

Buffer cache

实验目标

学过计组的我们(bushi)都知道,读磁盘是非常慢的,为了提高磁盘使用效率,系统会将最近使用的磁盘块内容读进cache。在读磁盘块是会先到cache中查找块是否已被缓存,若已缓存则可以直接拿来使用。

在本实验中,cache也面临着同上一个实验一样的问题,单一的cache池与锁管理导致了非常多的锁冲突,如下

$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 0 #acquire() 33035
lock: bcache: #fetch-and-add 16142 #acquire() 65978
--- top 5 contended locks:
lock: virtio_disk: #fetch-and-add 162870 #acquire() 1188
lock: proc: #fetch-and-add 51936 #acquire() 73732
lock: bcache: #fetch-and-add 16142 #acquire() 65978
lock: uart: #fetch-and-add 7505 #acquire() 117
lock: proc: #fetch-and-add 6937 #acquire() 73420
tot= 16142
test0: FAIL
start test1
test1 OK

你的目标是,修改cache管理策略,降低锁冲突,如下

$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 0 #acquire() 32954
lock: kmem: #fetch-and-add 0 #acquire() 75
lock: kmem: #fetch-and-add 0 #acquire() 73
lock: bcache: #fetch-and-add 0 #acquire() 85
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4159
lock: bcache.bucket: #fetch-and-add 0 #acquire() 2118
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4274
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4326
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6334
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6321
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6704
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6696
lock: bcache.bucket: #fetch-and-add 0 #acquire() 7757
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6199
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4136
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4136
lock: bcache.bucket: #fetch-and-add 0 #acquire() 2123
--- top 5 contended locks:
lock: virtio_disk: #fetch-and-add 158235 #acquire() 1193
lock: proc: #fetch-and-add 117563 #acquire() 3708493
lock: proc: #fetch-and-add 65921 #acquire() 3710254
lock: proc: #fetch-and-add 44090 #acquire() 3708607
lock: proc: #fetch-and-add 43252 #acquire() 3708521
tot= 128
test0: OK
start test1
test1 OK

实验实现

接下来依旧先看提示

  • 阅读xv6 book 8.1-8.3 ,了解block cache
  • 可以使用固定大小的bucket,使用素数(如13)来减少哈希冲突 (明示我们使用13个cache池来管理cache)
  • 在哈希表(cache池)中寻找cache并找不到cache时,分配cache必须是原子的(使用锁)
  • 删除所有的cache列表(应该指原来的头插法),使用时间戳替代它来达到LRU算法。因此brelse不需要获取锁(这里没懂)
  • 允许在 bget 中寻找并从其他池子偷窃cache块
  • 你的解决方法可能需要同时持有两个锁(和上一个实验一样的),请确保不会出现死锁
  • 当cache未命中时,你需要寻找一个新的块放到本cache池,请确保不会出现死锁。(这里是指在自己的池子里找到可用的cache,不要二次上锁)
  • 一些debug tips

根据提示以及主要矛盾我们得出一个大概方案

  • 创建一个大小为十三的哈希表,表中元素为cache池。每个池拥有一个锁,保证操作的原子性。

  • cache块根据 blockno 哈希到不同池中。

  • 读取新的磁盘块时,先到对应的cache池中寻找是否已缓存;若无,则先在自己的池中寻找空闲块;若无,则到其他池子寻找空闲块,找到后将该块转移到本池。

  • 为cache块打上时间戳,每次使用时更新,寻找空闲块时,优先选择最久未被使用的空闲块。

  • 块被释放时并不做另外的操作,留在原地。

代码实现

/*	kernel/buf.h	*/
struct buf {
  int valid;   // has data been read from disk?
  ...
  // struct buf *prev; // LRU cache list
  struct buf *next;
  uchar data[BSIZE];
  uint time;
};


/*	kernel/bio.c	*/

/*
	创建哈希表,锁,和hash函数
*/
struct {
  struct spinlock lock[NBUCKET];
  struct buf buf[NBUF];
  struct buf head[NBUCKET];
} bcache;

int hash (int n) {
  return n % NBUCKET;
}

/*
	接下来先解决一堆没那么多操作的函数与
*/

//跟上一实验一样的做法,初始化锁并将所有块挂在第一个池子里
void
binit(void)
{
  struct buf *b;
  
  for (int i = 0; i < NBUCKET; i++) {
    initlock(&(bcache.lock[i]), "bcache.hash");
  }

  bcache.head[0].next = &bcache.buf[0];
  for (b = bcache.buf; b < bcache.buf+NBUF-1; b++) {
    b->next = b+1;
    initsleeplock(&b->lock, "buffer");
  }
  initsleeplock(&b->lock, "buffer");
}

//获取锁,然后将块引用计数-1,并不明白提示所说的不用锁的方法是什么
void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);

  int id = hash(b->blockno);
  acquire(&bcache.lock[id]);
  b->refcnt--;  
  release(&bcache.lock[id]);
}

//获取锁,引用计数+1
void
bpin(struct buf *b) {
  int id = hash(b->blockno);
  acquire(&(bcache.lock[id]));
  b->refcnt++;
  release(&(bcache.lock[id]));
}

//获取锁,引用计数-1
void
bunpin(struct buf *b) {
  int id = hash(b->blockno);
  acquire(&(bcache.lock[id]));
  b->refcnt--;
  release(&(bcache.lock[id]));
}


/*
	接下来是比较冗长的一个函数
*/
//辅助函数,用于写cache
void 
write_cache(struct buf *take_buf, uint dev, uint blockno)
{
  take_buf->dev = dev;
  take_buf->blockno = blockno;
  take_buf->valid = 0;
  take_buf->refcnt = 1;
  take_buf->time = ticks;
}

//获取cache
static struct buf*
bget_fir(uint dev, uint blockno)
{

  struct buf *b, *last;
  struct buf *take_buf = 0;
  int id = hash(blockno);
  acquire(&(bcache.lock[id]));

   // 在本池子中寻找是否已缓存,同时寻找空闲块,并记录链表最后一个节点便于待会插入新节点使用
  // Is the block already cached?
  for(b = bcache.head[id].next, last = &(bcache.head[id]); b; b = b->next, last = last->next)
  {

    if(b->dev == dev && b->blockno == blockno)
    {
      b->time = ticks;
      b->refcnt++;
      release(&(bcache.lock[id]));
      acquiresleep(&b->lock);
      return b;
    }
    if(b->refcnt == 0)
    {
      take_buf = b;
    }
  }

  //如果没缓存并且在本池子有空闲块,则使用它
  if(take_buf)
  { 
    write_cache(take_buf, dev, blockno);
    release(&(bcache.lock[id]));
    acquiresleep(&(take_buf->lock));
    return take_buf;
  }

  
 
  // 到其他池子寻找最久未使用的空闲块
  int lock_num = -1;
  
  uint time = __UINT32_MAX__;
  struct buf *tmp;
  struct buf *last_take = 0;
  for(int i = 0; i < NBUCKET; ++i)
  {
    
    if(i == id) continue;
	//获取寻找池子的锁
    acquire(&(bcache.lock[i]));

    for(b = bcache.head[i].next, tmp = &(bcache.head[i]); b; b = b->next,tmp = tmp->next)
    {
      if(b->refcnt == 0)
      {
        //找到符合要求的块
        if(b->time < time)
        {
          
          time = b->time;
          last_take = tmp;
          take_buf = b;
          //如果上一个空闲块不在本轮池子中,则释放那个空闲块的锁		
          if(lock_num != -1 && lock_num != i && holding(&(bcache.lock[lock_num])))
            release(&(bcache.lock[lock_num]));
          lock_num = i;
        }
      }
    }
	//没有用到本轮池子的块,则释放锁
    if(lock_num != i)
      release(&(bcache.lock[i]));
  }
  
  if (!take_buf) 
    panic("bget: no buffers");
  
  //将选中块从其他池子中拿出
  last_take->next = take_buf->next;
  take_buf->next = 0;
  release(&(bcache.lock[lock_num]));
  //将选中块放入本池子中,并写cache
  b = last;
  b->next = take_buf;
  write_cache(take_buf, dev, blockno);


  release(&(bcache.lock[id]));
  acquiresleep(&(take_buf->lock));
  
  return take_buf;
}

该实验到此就结束了



等等!

看着 bget 中频繁地获取锁,最多的时候甚至会同时持有三把锁,是否感觉会有死锁

确实是这样的, bget中获取锁的顺序是不安全的,会出现循环等待

但实验指导中提到

There are some circumstances in which it’s OK if your solution has lock conflicts:

  • When two processes concurrently use the same block number. bcachetest test0 doesn’t ever do this.
  • When two processes concurrently miss in the cache, and need to find an unused block to replace. bcachetest test0 doesn’t ever do this.
  • When two processes concurrently use blocks that conflict in whatever scheme you use to partition the blocks and locks; for example, if two processes use blocks whose block numbers hash to the same slot in a hash table. bcachetest test0 might do this, depending on your design, but you should try to adjust your scheme’s details to avoid conflicts (e.g., change the size of your hash table).

因此上面的代码可以通过测试

下面的代码对获取锁的条件做了限制,试图减少出现死锁的概率( just for fun

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


  // Is the block already cached?
  struct buf *b;
  struct buf *last_b;
  struct buf *take_buf = 0;
  int id = hash(blockno);
  acquire(&bcache.lock[id]);

  for(b = bcache.head[id].next, last_b = &(bcache.head[id]); b; b = b->next, last_b = last_b->next)
  {
    if(b->dev == dev && b->blockno == blockno)
    {
      b->refcnt++;
      release(&bcache.lock[id]);
      acquiresleep(&b->lock);
      return b;
    }
    if(b->refcnt == 0)
    {
      take_buf = b;
    }
  }

  if(take_buf)
  {
    write_cache(take_buf, dev, blockno);
    release(&bcache.lock[id]);
    acquiresleep(&take_buf->lock);
    return take_buf;
  }

  int lock_num = -1;
  uint time = __UINT32_MAX__;
  struct buf *last_take = 0;
  struct buf *tmp;
  
  
  for(int i = 1; i <= NBUCKET/2; ++i)
  {
    int j = id - i >=0 ? id - i : id + (NBUCKET - i);
    // int j = (id + i)%NBUCKET;

    acquire(&bcache.lock[j]);


    for(b = bcache.head[j].next, tmp = &(bcache.head[j]); b; b = b->next, tmp = tmp->next)
    {
      if (b->refcnt == 0) 
      {
        if (b->time < time) 
        {
          time = b->time;
          last_take = tmp;
          take_buf = b;

          if (lock_num != -1 && lock_num != j && holding(&bcache.lock[lock_num])) 
            release(&bcache.lock[lock_num]);

          lock_num = j;
        }   
      }
    }

    if (j!=id && j!=lock_num && holding(&bcache.lock[j])) 
      release(&bcache.lock[j]);
  }

  if (!take_buf) 
    panic("bget: no buffers");

  last_take->next = take_buf->next;
  take_buf->next = 0;
  release(&bcache.lock[lock_num]);


  last_b->next = take_buf;
  take_buf->next = 0;
  write_cache(take_buf, dev, blockno);

  
  release(&bcache.lock[id]);
  acquiresleep(&take_buf->lock);

  return take_buf;


}


该方案在不改变NBUF的前提下无法通过 make grade / Test usertests / test manywrites

逐步增加到 NBUF=MAXOPBLOCKS*12 才能通过

实验结果

请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值