8. Lab: locks

https://pdos.csail.mit.edu/6.S081/2021/labs/lock.html

1. Memory allocator (moderate)

1.1 要求

Your job is to implement per-CPU freelists, and stealing when a CPU's free list is empty. You must give all of your locks names that start with "kmem". That is, you should call initlock for each of your locks, and pass a name that starts with "kmem". Run kalloctest to see if your implementation has reduced lock contention. To check that it can still allocate all of memory, run usertests sbrkmuch. Your output will look similar to that shown below, with much-reduced contention in total on kmem locks, although the specific numbers will differ. Make sure all tests in usertests pass. make grade should say that the kalloctests pass.

閱讀原本的 kalloc.c 代碼,可以看到內存管理的結構是一個鏈表,如下:

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

當多個 cpu 同時申請內存時,只能串行執行,因爲只有一個 spinlock ,因此,我們需要這個 freelist 拆分給多個 cpu,每個 cpu 都獨自擁有一個上述的 kmem 結構,這樣多個 cpu 就能並行申請內存了。但此刻還需要注意如下情況:
當 CPU1 的 freelist 爲空時,表示內存已用完了,這個時候需要去訪問其他 cpu 的 freelist,將他們的內存塊挪給 CPU1 使用。

1.2 實現

由於要求比較簡單,且 lab 中的 hints 基本提供了大部分信息,因此略過分析,直接上實現。

  • 定義與初始化

這裏自定義了一個初始化函數 kfree_specific 將指定 pa 分配給指定 cpu 的 freelist

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

struct mem kmem[NCPU];

void 
kfree_specific(void *pa, int cpuid)
{
  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  struct run* r = (struct run*)pa;
  struct mem* cpu_mem = &kmem[cpuid];
  acquire(&cpu_mem->lock);
  r->next = cpu_mem->freelist;
  cpu_mem->freelist = r;
  release(&cpu_mem->lock);
}

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  push_off();
  int hart = cpuid();
  pop_off();
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
    kfree_specific(p, hart);
  }
}
  • 分配內存
// 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;
  push_off();
  int hart = cpuid();
  pop_off();
  struct mem* cpu_mem = &kmem[hart];
  acquire(&cpu_mem->lock);
  r = cpu_mem->freelist;
  if(r){
    cpu_mem->freelist = r->next;
    release(&cpu_mem->lock);
  }
  else // "steal" free memory from other cpu mem list
  {
    release(&cpu_mem->lock);
    for (int i = 0; i < NCPU ; i++) {
      cpu_mem = &kmem[i];
      acquire(&cpu_mem->lock);
      r = cpu_mem->freelist;
      if (r){
        cpu_mem->freelist = r->next;
        release(&cpu_mem->lock);
        break;
      }
      release(&cpu_mem->lock);
    }
  }

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}
  • 釋放內存
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);

  push_off();
  int hart = cpuid();
  pop_off();

  r = (struct run*)pa;
  //int hart = get_pa_cpu_id((uint64)r);
  struct mem* cpu_mem = &kmem[hart];
  acquire(&cpu_mem->lock);
  r->next = cpu_mem->freelist;
  cpu_mem->freelist = r;
  release(&cpu_mem->lock);
}

2. Buffer cache (hard)

這個 part 的實現思路也比較清晰,但是由於細節沒注意,實現的時候各種 test case 不通過 =-=。

2.1 要求

Modify the block cache so that the number of acquire loop iterations for all locks in the bcache is close to zero when running bcachetest. Ideally the sum of the counts for all locks involved in the block cache should be zero, but it's OK if the sum is less than 500. Modify bget and brelse so that concurrent lookups and releases for different blocks that are in the bcache are unlikely to conflict on locks (e.g., don't all have to wait for bcache.lock). You must maintain the invariant that at most one copy of each block is cached. When you are done, your output should be similar to that shown below (though not identical). Make sure usertests still passes. make grade should pass all tests when you are done.

該 part 的目的也是通過重新設計文件系統的 buffer & cache 的數據結構來降低鎖的競爭,原先的結構如下:

struct {
  struct spinlock lock;
  struct buf buf[NBUF];

  // 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;

bcache.head 是一個 LRU 鏈表,headprev 是最少使用,next 是最近使用,通過維護該 LRU 鏈表,根據空間局部性原理,來達到提升效率的目的。
但是由於 bcache 是多個 cpu 通用的,head 鏈表每次訪問都需要加鎖,這樣導致多 cpu 訪問緩存時只能串行工作。因此需要重新設計該結構,在利用到空間局部性原理的同時,減少鎖的競爭。

2.2 分析

根據 hints,要做如下操作:

  • 將原來的 bcache.head 調整成 hash table,每個桶都有自己的鎖。從而減少對全局鎖 bcache.lock 的競爭。
  • struct buf 結構增加時間戳,用於尋找最少使用的 buffer
  • 當 目標桶 找不到合適的元素時,需要去全局的 bcache.buf 中尋找 buf.refcnt == 0 且最不經常使用的塊,然後將該 buf 挪到 目標桶 。需要注意如下:
    • 先將該 buf 從其所屬的 原來的桶 移除,然後再挪到桶 a,需要注意 原來的桶 和 目標桶 不能是同一個桶(同一個則不需要操作移動了),否則可能導致死鎖。
    • 遍歷全局的 bcache.buf 時,需要加鎖 bcache.lock
    • 遍歷找到 目標元素 時,需要注意,目標元素 原來的桶 是沒加鎖的,因此從找到 目標元素 再到準備給 原來的桶 加鎖移除該元素時,這中間存在空檔期,有可能在這期間該塊被引用了,導致 buf.refcnt != 0 ,此時不能在用這塊 buf。因此在操作從 原來的桶 刪除時,需要再檢查一下 buf.refcnt ,如果不爲 0 ,則需要重新遍歷全局的 bcache.buf,找尋新的 目標元素

2.3 實現

  • 定義初始化

binit 將原來的 bcache.buf 所有 buf 平攤到每個桶中。

#define BUCKET_CNT 13
#define NBUF (BUCKET_CNT * 3)

struct bcache_bucket{
  struct buf head;
  struct spinlock lock;
};
struct {
  struct spinlock lock;
  struct buf buf[NBUF];
  struct bcache_bucket bucket[BUCKET_CNT];
} bcache;

int hash_key(int blockno){
  return blockno % BUCKET_CNT;
}

void binit(void)
{
  struct buf *b;
  char buf[32];
  int sz = 32;

  initlock(&bcache.lock, "bcache");

  for (int i = 0; i < BUCKET_CNT; i++){
    snprintf(buf, sz, "bcache.bucket_%d", i);
    initlock(&bcache.bucket[i].lock, buf);
  }

  // Create linked list of buffers
  int blockcnt = 0;
  struct bcache_bucket* bucket;
  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    initsleeplock(&b->lock, "buffer");
    b->access_time = ticks;
    b->blockno = blockcnt++;
    bucket = &bcache.bucket[hash_key(b->blockno)];
    b->next = bucket->head.next;
    bucket->head.next = b;
  }
}
  • 獲取 buf

獲取 buf 的策略分爲如下幾步:

  • 哈希獲取對應的桶
  • 獲取該桶的鎖 bcache_bucket.lock
  • 檢查該塊是否已緩存
    • 未緩存
      • 首先遍歷該桶的列表 bcache_bucket.head
      • 檢查是否有 buf.refcnt == 0buf,取其中距離上次訪問時間最久的 buf
        • 沒有,則需要尋找一塊新的 buf
          • 遍歷全局 bcache.buf ,此時需要獲取全局鎖 bcache.lock
          • 找到 buf.refcnt == 0buf,取其中距離上次訪問時間最久的 buf
          • 哈希 buf.blockno 獲取該 buf 原來的桶
          • 遍歷該 buf 原來的桶,從 bcache_bucket.head 中移除該 buf ,此時應持有該桶的鎖 bcache_bucket.lock ,釋放 buf 完畢時釋放鎖
          • 遍歷時需要注意,需要再次檢查 **buf.refcnt == 0**,因爲前面遍歷全局 **bcache.buf** 的時候,沒有持有該桶的鎖,在此期間 **buf** 可能被引用了
          • 將該 buf 移動到新桶
          • 更新 buf 結構體信息
        • 有,則不需要移動該 buf
          • 更新 buf 結構體信息
    • 已緩存
      • 更新 buf 結構體信息
static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b, *lrub;
  struct bcache_bucket* bucket = &bcache.bucket[hash_key(blockno)];

  acquire(&bucket->lock);
  // Is the block already cached?
  for(b = &bucket->head; b; b = b->next){
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;
      b->access_time = ticks;
      release(&bucket->lock);
      acquiresleep(&b->lock);
      return b;
    }
  }
  
  // find bucket lru buffer
  lrub = 0;
  uint min_time = 0x8ffffff;
  for(b = &bucket->head; b; b = b->next){
    if (b->refcnt == 0 && b->access_time < min_time){
      min_time = b->access_time;
      lrub = b;
    }
  }
  if (lrub) {
    goto setup;
  }

  // Not cached.
  // find in the global array
  acquire(&bcache.lock);

findbucket:
  lrub = 0;
  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    if(b->refcnt == 0 && b->access_time < min_time) {
      lrub = b;
    }
  }

  if (lrub) {
    // step 1 : release from the old bucket
    // need to hold the old bucket lock
    struct bcache_bucket* old_bucket = &bcache.bucket[hash_key(lrub->blockno)];
    acquire(&old_bucket->lock);
      
    
    if (lrub->refcnt != 0){
      release(&old_bucket->lock);
      goto findbucket;
    }

    b = &old_bucket->head;
    struct buf* bnext = b->next;
    while (bnext != lrub) {
      b = bnext;
      bnext = bnext->next;
    }
    b->next = bnext->next;

    // we don't need to modify bcache.bucket , so we release the lock
    release(&old_bucket->lock);
    // step 2 : add to target bucket 
    lrub->next = bucket->head.next;
    bucket->head.next = lrub;
    release(&bcache.lock);

setup:
    lrub->dev = dev;
    lrub->blockno = blockno;
    lrub->valid = 0;
    lrub->refcnt = 1;
    lrub->access_time = ticks;
    release(&bucket->lock);
    acquiresleep(&lrub->lock);
    return lrub;
  }
  panic("bget: no buffers");
}
  • 釋放 buf

這塊內容較爲簡單,將鎖從全局鎖替換爲桶鎖即可

// Release a locked buffer.
// Move to the head of the most-recently-used list.
void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);

  struct bcache_bucket* bucket = &bcache.bucket[hash_key(b->blockno)];
  acquire(&bucket->lock);
  b->refcnt--;
  release(&bucket->lock);
}
  • bpinbunpin

同上,替換鎖即可

void
bpin(struct buf *b) {
  struct bcache_bucket* bucket = &bcache.bucket[hash_key(b->blockno)];
  acquire(&bucket->lock);
  b->refcnt++;
  release(&bucket->lock);
}

void
bunpin(struct buf *b) {
  struct bcache_bucket* bucket = &bcache.bucket[hash_key(b->blockno)];
  acquire(&bucket->lock);
  b->refcnt--;
  release(&bucket->lock);
}

3. 總結

該 lab 的坑比較多,在測試 usertests 時,出過一些異常錯誤,如 out of blocks ,這是因爲默認設置的 blocks 大小太少了,默認配置爲 1000,需要將該值調大爲 10000。

#define FSSIZE       1000  // size of file system in blocks

此外還有一些錯誤如 freeing free block,這裏可能因爲在執行 bget 時,從其他桶挪移 buf 到新桶的時候,沒有再次判斷 refcnt 是否爲 0,從而導致覆蓋了已經被引用的塊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章