Lab8 Locks

Memory allocator

kalloctest会对Xv6的内存分配进行压力测试。目前的版本会产生大量的锁争用,其原因是kalloc()使用一个空闲链表,并由单个锁进行保护。
本实验的任务是重新设计内存分配器:为每个 CPU 维护一个单独的空闲链表,每个链表都有自己的锁。不同 CPU 上的分配和释放操作可以并行进行。主要挑战在于处理以下情况:当一个 CPU 的空闲链表为空,而另一个 CPU 的链表仍有可用内存时,该 CPU 必须“窃取”部分其他 CPU 的空闲链表。

首先修改kmem结构体,设置为NCPU大小的数组。同时为了符合initlock的初始化,为每个锁命名。

//kernel/kalloc.c
struct {
  struct spinlock lock;
  struct run *freelist;
} kmem[NCPU];//每个CPU都有自己的空闲内存链表

//NCPU值为8
char* kmem_lockname[] = {  
  "kmem_cpu0",
  "kmem_cpu1",
  "kmem_cpu2",
  "kmem_cpu3",
  "kmem_cpu4",
  "kmem_cpu5",
  "kmem_cpu6",
  "kmem_cpu7",
};

初始化

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

释放内存

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;

  //先关中断在获取cpuid
  //防止出现cpu不一致的问题
  push_off();
  int cpu = cpuid();
  acquire(&kmem[cpu].lock);
  r->next = kmem[cpu].freelist;
  kmem[cpu].freelist = r;
  release(&kmem[cpu].lock);
  pop_off();//开中断
  
}

分配内存

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

  push_off();
  int cpu = cpuid();

  acquire(&kmem[cpu].lock);
  r = kmem[cpu].freelist;

  if(r == 0){ //当前cpu无freelist
  //这个地方比较坑,最开始用的是
  //for( int newcpu = (cpu + 1) % NCPU; newcpu != cpu; newcpu++)
  //这时Xv6启动时会卡死,后面发现是分配了两个cpu,然后在启动时互相偷页面导致死锁
    for( int newcpu = 0; newcpu < NCPU; newcpu++){
      if(newcpu == cpu)
        continue;

      acquire(&kmem[newcpu].lock);
      r = kmem[newcpu].freelist;
      if(r == 0){
        release(&kmem[newcpu].lock);
        continue;
      }

      kmem[newcpu].freelist = r->next;
      r->next = kmem[cpu].freelist;
      kmem[cpu].freelist = r;
      release(&kmem[newcpu].lock);
      break;
    }
  }
  if(r){
    kmem[cpu].freelist = r->next;
  }
  release(&kmem[cpu].lock);
  pop_off(); 

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

运行kalloctest和usertest,顺利通过。

Buffer cache

如果多个进程密集使用文件系统,它们很可能会争夺保护内核中磁盘块缓存的 bcache.lock 锁(kernel/bio.c)。目的是不允许多个进程同时操作磁盘块缓存。目前块缓存的设计是使用双向链表存储,链表结点按LRU的思想组织。

//kernel/bio.c
struct {
  struct spinlock lock;
  struct buf buf[NBUF];//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;
} bcache;

//kernel/buf.h
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;
  struct buf *prev; // LRU cache list
  struct buf *next;
  uchar data[BSIZE];//BSIZE = 1024
};

先来看看现在获取buf的代码

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

  acquire(&bcache.lock);//锁住整个缓存区

  // Is the block already cached?
  //在缓冲区中根据dev和blockno查找对应的块是否已在缓存区中
  for(b = bcache.head.next; b != &bcache.head; b = b->next){
  //在缓冲区中
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;//引用计数+1
      release(&bcache.lock);//释放缓存区锁
      acquiresleep(&b->lock);//对对应的块加睡眠锁
      return b;//返回
    }
  }

  // Not cached.
  // Recycle the least recently used (LRU) unused buffer.
  //不在
  //从后往前遍历缓存区:即优先选择最近最久未被使用的那些buf
  for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
  //选引用计数为0的buf
    if(b->refcnt == 0) {
	//修改相关信息
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&bcache.lock);
      acquiresleep(&b->lock);
      return b;
    }
  }
  panic("bget: no buffers");
}

可以看到,每次有进程来尝试获取buf都会给整个缓存区上锁,因此多个进程会发生严重的锁竞争。
根据实验提示,采用哈希桶+时间戳的方式进行优化:

  • 根据 dev 和 blockno 进行哈希映射到某一个哈希桶,每个哈希桶中维护独立的buf链表。这样就将一个锁变为了哈希桶个数的锁,降低了锁冲突;
  • 当某一个桶中没有空闲buf时,全局遍历所有桶中的buf,寻找 refcnt为0 且 时间戳最小 的buf使用。将这个buf移除所在的那个桶并加入新桶

在写这部分实验的时候经常出现panic。通过搜索相关博客和问AI最后才解决,参考博客
https://blog.miigon.net/posts/s081-lab8-locks/#buffer-cache-hard

修改数据结构

//kernel/bio.c
#define NBUCKET 13
#define HASH2BUCKET(dev, blockno) ((blockno + dev) % NBUCKET)

struct {
//  struct spinlock lock;
  struct buf buf[NBUF];
  struct spinlock eviction_lock;
  struct buf bufmap[NBUCKET];//13个桶
  struct spinlock bufmap_locks[NBUCKET];
} bcache;

//kernel/buf.h
struct buf {
...
  //struct buf *prev; // LRU cache list
  struct buf *next;
  uint timestamp;//时间戳属性
  uchar data[BSIZE];
};

相关操作

初始化

void 
binit(void)
{
  struct buf *b = 0;
  for(int i = 0; i < NBUCKET; ++i){
    initlock(&bcache.bufmap_locks[i], "bache");
    bcache.bufmap[i].next = 0;
  }
  for(int i = 0; i < NBUF; ++i){
    b = &bcache.buf[i];
    initsleeplock(&b->lock, "buffer");

    //初始时系统中所有buf都在0号桶中
    b->next = bcache.bufmap[0].next;
    bcache.bufmap[0].next = b;
  }
   initlock(&bcache.eviction_lock,"bache_eviction");
}

获取缓冲区块

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

//先查看block_n0的缓冲区是否在缓冲区中
  acquire(&bcache.bufmap_locks[bucket_no]);
  for(b = bcache.bufmap[bucket_no].next; b != 0; b = b->next){
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;
      release(&bcache.bufmap_locks[bucket_no]);
      acquiresleep(&b->lock);
      return b;
    }
  }
  release(&bcache.bufmap_locks[bucket_no]);

//这里的问题在于当线程1查询发现不存在buf释放锁后,另一个线程在这期间可能创建了这个block_no的buf
//这就导致同一个block_no有两个缓存块
//解决方案是:加一个大锁 和 先检查在尝试

  acquire(&bcache.eviction_lock);//加大锁
//先检查没有确保在释放block_no锁和加大锁期间没有创建buf
  for(b = bcache.bufmap[bucket_no].next; b != 0; b = b->next){
    if(b->dev == dev && b->blockno == blockno){
      acquire(&bcache.bufmap_locks[bucket_no]);
      b->refcnt++;
      release(&bcache.bufmap_locks[bucket_no]);
      release(&bcache.eviction_lock);
      acquiresleep(&b->lock);
      return b;
    }
  }
//确定没有创建buf,此时持有bcache.eviction_lock
  struct buf* lru_buf = 0;
  struct buf* pre_lru_buf = 0;
  int lru_buf_bucket = -1;
  uint min_timestamp = __UINT32_MAX__;

  for (uint newbucket = 0; newbucket < NBUCKET; newbucket++){
    acquire(&bcache.bufmap_locks[newbucket]);
    int newflag = 0;
    
    struct buf* prev = &bcache.bufmap[newbucket];
    for(b = prev->next; b != 0; prev = b, b = b->next){
      if(b->refcnt == 0 && b->timestamp < min_timestamp) {
        lru_buf = b;
        pre_lru_buf = prev;
        newflag = 1;
      }
    }
    if( !newflag )
      release(&bcache.bufmap_locks[newbucket]);
    else{
      if( lru_buf_bucket != -1)
        release(&bcache.bufmap_locks[lru_buf_bucket]);
      lru_buf_bucket = newbucket;
    }
  }

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

  if(lru_buf_bucket != bucket_no) {
    //从原桶移除
    pre_lru_buf->next = lru_buf->next;
    release(&bcache.bufmap_locks[lru_buf_bucket]);
    // 添加到目标桶
    acquire(&bcache.bufmap_locks[bucket_no]);
    lru_buf->next = bcache.bufmap[bucket_no].next;
    bcache.bufmap[bucket_no].next = lru_buf;
  }
  
  lru_buf->dev = dev;
  lru_buf->blockno = blockno;
  lru_buf->refcnt = 1;
  lru_buf->valid = 0;
  release(&bcache.bufmap_locks[bucket_no]);
  release(&bcache.eviction_lock);
  acquiresleep(&lru_buf->lock);
  return lru_buf; 
}

释放

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

  releasesleep(&b->lock);

  uint bucket_no = HASH2BUCKET(b->dev, b->blockno);
  acquire(&bcache.bufmap_locks[bucket_no]);
  b->refcnt--;
  if (b->refcnt == 0) {
    b->timestamp = ticks;
  }
  release(&bcache.bufmap_locks[bucket_no]);
}

pin与upin

void
bpin(struct buf *b) {
  uint bucket_no = HASH2BUCKET(b->dev, b->blockno);
  acquire(&bcache.bufmap_locks[bucket_no]);
  b->refcnt++;
  release(&bcache.bufmap_locks[bucket_no]);
}
void
bunpin(struct buf *b) {
  uint bucket_no = HASH2BUCKET(b->dev, b->blockno);
  acquire(&bcache.bufmap_locks[bucket_no]);
  b->refcnt--;
  release(&bcache.bufmap_locks[bucket_no]);
}

已能通过bcachetest和 usertests

$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem_cpu0: #fetch-and-add 0 #acquire() 32959
lock: kmem_cpu1: #fetch-and-add 0 #acquire() 35
lock: kmem_cpu2: #fetch-and-add 0 #acquire() 104
--- top 5 contended locks:
lock: virtio_disk: #fetch-and-add 177766 #acquire() 2345
lock: proc: #fetch-and-add 83195 #acquire() 85478
lock: proc: #fetch-and-add 80024 #acquire() 85478
lock: proc: #fetch-and-add 68162 #acquire() 85500
lock: proc: #fetch-and-add 59295 #acquire() 86196
tot= 0
test0: OK
start test1
test1 OK
posted @ 2025-08-03 14:28  名字好难想zzz  阅读(13)  评论(0)    收藏  举报