WangYu::Space

Study, think, create, and grow. Teach yourself and teach others.

MIT 6.828 - Lab - Locks

分类:操作系统标签: 6.828创建时间:2024-02-19 00:00:00

课程主页:https://pdos.csail.mit.edu/6.828/2023/index.html

Memory allocator

xv6 使用了一个空闲链表来管理空闲的物理内存。分配内存时从空闲链表中拿一个,释放内存时将其加入空闲链表,并使用一个自旋锁来保护空闲链表。当多个核同时在执行内存分配和释放时,会出现比较严重的冲突。本实验的目的是重新设计内存分配的数据结构,降低锁冲突。

一种很直观的思路是为每个核创建一个空闲链表:

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem[NCPU];

初始化的时候,给每个核分配一定的内存:

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

free 的时候,将内存放入当前 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;
  int cpu = cpuid();

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

在分配内存时,先尝试从当前 CPU 对应的空闲链表中分配,如果当前 CPU 的空闲链表中没有可用内存,则尝试去其他 CPU 的空闲链表中拿。

void *
alloc(int cpu)
{
  struct run *r;

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

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

void *
kalloc(void)
{
  struct run *r;
  int cpu = cpuid();
  r = alloc(cpu);
  if (r) {
    return r;
  }
  for (int i = 0; i < NCPU; i++) {
    if (i == cpu) {
      continue;
    }
    r = alloc(i);
    if (r) {
      return r;
    }
  }
  return r;
}

Buffer cache

xv6 的实现中,所有的 cache buffer 存储在 bcache.buf 中,使用一个空闲链表来管理,并使用 bcache.lock 来做并发保护。多个核并发地获取 buf 的时候,这里锁冲突会比较严重。

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;

改进的思路依然是把数据分散保存在互不干扰的多个链表中,每个链表使用一个锁来保护:

#define NBUCKET (NCPU + 7)

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 bucket[NBUCKET];
  struct spinlock bucket_lock[NBUCKET];
} bcache;
cache.buf[NBUF]:
[ buf | buf | buf | buf | buf | buf | buf | buf | buf | buf | buf | buf | buf | buf ]



cache.bucket:
[ head ]  <-> [ buf ] <-> [ buf ]
[ head ]  <-> [ buf ] <-> [ buf ] <-> [ buf ] <-> [ buf ]
[ head ]  <-> [ buf ] <-> [ buf ] <-> [ buf ]
[ head ]  <-> [ buf ]
[ head ]  <-> [ buf ] <-> [ buf ]

在初始化的时候,将所有的 buf 均匀地分散到所有的 bucket 中:

void
binit(void)
{
  struct buf *b;

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

  for (int i = 0; i < NBUCKET; i++) {
    initlock(&bcache.bucket_lock[i], "bcache.bucket");
    // Create linked list of buffers
    bcache.bucket[i].prev = &bcache.bucket[i];
    bcache.bucket[i].next = &bcache.bucket[i];
  }

  for (b = bcache.buf; b < bcache.buf+ NBUF; b++) {
    int idx = (b - bcache.buf) % NBUCKET;
    b->next = bcache.bucket[idx].next;
    b->prev = &bcache.bucket[idx];
    initsleeplock(&b->lock, "buffer");
    bcache.bucket[idx].next->prev = b;
    bcache.bucket[idx].next = b;
  }
}

获取的时候,可以使用 blockno 映射到对应的 bucket,并在该 bucket 中查找。如果找不到,则找一个空闲的 buf 返回。

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

  int idx = blockno % NBUCKET;
  acquire(&bcache.bucket_lock[idx]);
  // Is the block already cached?
  for (b = bcache.bucket[idx].next; b != &bcache.bucket[idx]; b = b->next) {
    if (b->dev == dev && b->blockno == blockno) {
      b->refcnt++;
      release(&bcache.bucket_lock[idx]);
      acquiresleep(&b->lock);
      return b;
    }
  }

  // 尝试在当前 bucket 中找一个空闲的 buf
  for (b = bcache.bucket[idx].prev; b != &bcache.bucket[idx]; b = b->prev) {
    if (b->refcnt == 0) {
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&bcache.bucket_lock[idx]);
      acquiresleep(&b->lock);
      return b;
    }
  }

  // 遍历所有的 bucket,寻找空闲的 buf
  for (int i = 0; i < NBUCKET; i++) {
    // 跳过前面已经查找过的 bucket
    if (i == idx) {
      continue;
    }
    acquire(&bcache.bucket_lock[i]);
    // Not cached.
    // Recycle the least recently used (LRU) unused buffer.
    for (b = bcache.bucket[i].prev; b != &bcache.bucket[i]; b = b->prev) {
      if (b->refcnt == 0) {
        b->dev = dev;
        b->blockno = blockno;
        b->valid = 0;
        b->refcnt = 1;

        // 在其他 bucket 中找到了空闲的 buf,此时需要将其移动到 blockno 对应的 bucket 中
        
        // 从原 bucket 中删除
        b->next->prev = b->prev;
        b->prev->next = b->next;

        // 插入到 blockno 对应的 bucket 中
        b->next = bcache.bucket[idx].next;
        b->prev = &bcache.bucket[idx];
        bcache.bucket[idx].next->prev = b;
        bcache.bucket[idx].next = b;

        release(&bcache.bucket_lock[idx]);
        release(&bcache.bucket_lock[i]);
        acquiresleep(&b->lock);
        return b;
      }
    }
    release(&bcache.bucket_lock[i]);
  }
  panic("bget: no buffers");
}

因为分配出去的 buf 一定存储在 b->blockno 对应的 bucket 中,因此在减小 refcount 的时候,可以只对 bucket 加锁。

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

  releasesleep(&b->lock);
  int idx = b->blockno % NBUCKET;
  acquire(&bcache.bucket_lock[idx]);
  b->refcnt--;
  if (b->refcnt == 0) {
    // no one is waiting for it.
    b->next->prev = b->prev;
    b->prev->next = b->next;
    b->next = bcache.bucket[idx].next;
    b->prev = &bcache.bucket[idx];
    bcache.bucket[idx].next->prev = b;
    bcache.bucket[idx].next = b;
  }

  release(&bcache.bucket_lock[idx]);
}

完整变更

diff --git a/kernel/bio.c b/kernel/bio.c
index 60d91a6..7caa213 100644
--- a/kernel/bio.c
+++ b/kernel/bio.c
@@ -23,6 +23,8 @@
 #include "fs.h"
 #include "buf.h"
 
+#define NBUCKET (NCPU + 7)
+
 struct {
   struct spinlock lock;
   struct buf buf[NBUF];
@@ -30,7 +32,8 @@ struct {
   // 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 buf bucket[NBUCKET];
+  struct spinlock bucket_lock[NBUCKET];
 } bcache;
 
 void
@@ -40,15 +43,20 @@ binit(void)
 
   initlock(&bcache.lock, "bcache");
 
-  // Create linked list of buffers
-  bcache.head.prev = &bcache.head;
-  bcache.head.next = &bcache.head;
-  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
-    b->next = bcache.head.next;
-    b->prev = &bcache.head;
+  for (int i = 0; i < NBUCKET; i++) {
+    initlock(&bcache.bucket_lock[i], "bcache.bucket");
+    // Create linked list of buffers
+    bcache.bucket[i].prev = &bcache.bucket[i];
+    bcache.bucket[i].next = &bcache.bucket[i];
+  }
+
+  for (b = bcache.buf; b < bcache.buf+ NBUF; b++) {
+    int idx = (b - bcache.buf) % NBUCKET;
+    b->next = bcache.bucket[idx].next;
+    b->prev = &bcache.bucket[idx];
     initsleeplock(&b->lock, "buffer");
-    bcache.head.next->prev = b;
-    bcache.head.next = b;
+    bcache.bucket[idx].next->prev = b;
+    bcache.bucket[idx].next = b;
   }
 }
 
@@ -60,31 +68,60 @@ bget(uint dev, uint blockno)
 {
   struct buf *b;
 
-  acquire(&bcache.lock);
-
+  int idx = blockno % NBUCKET;
+  acquire(&bcache.bucket_lock[idx]);
   // Is the block already cached?
-  for(b = bcache.head.next; b != &bcache.head; b = b->next){
-    if(b->dev == dev && b->blockno == blockno){
+  for (b = bcache.bucket[idx].next; b != &bcache.bucket[idx]; b = b->next) {
+    if (b->dev == dev && b->blockno == blockno) {
       b->refcnt++;
-      release(&bcache.lock);
+      release(&bcache.bucket_lock[idx]);
       acquiresleep(&b->lock);
       return b;
     }
   }
 
-  // Not cached.
-  // Recycle the least recently used (LRU) unused buffer.
-  for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
-    if(b->refcnt == 0) {
+  for (b = bcache.bucket[idx].prev; b != &bcache.bucket[idx]; b = b->prev) {
+    if (b->refcnt == 0) {
       b->dev = dev;
       b->blockno = blockno;
       b->valid = 0;
       b->refcnt = 1;
-      release(&bcache.lock);
+      release(&bcache.bucket_lock[idx]);
       acquiresleep(&b->lock);
       return b;
     }
   }
+
+  for (int i = 0; i < NBUCKET; i++) {
+    if (i == idx) {
+      continue;
+    }
+    acquire(&bcache.bucket_lock[i]);
+    // Not cached.
+    // Recycle the least recently used (LRU) unused buffer.
+    for (b = bcache.bucket[i].prev; b != &bcache.bucket[i]; b = b->prev) {
+      if (b->refcnt == 0) {
+        b->dev = dev;
+        b->blockno = blockno;
+        b->valid = 0;
+        b->refcnt = 1;
+
+        b->next->prev = b->prev;
+        b->prev->next = b->next;
+
+        b->next = bcache.bucket[idx].next;
+        b->prev = &bcache.bucket[idx];
+        bcache.bucket[idx].next->prev = b;
+        bcache.bucket[idx].next = b;
+
+        release(&bcache.bucket_lock[idx]);
+        release(&bcache.bucket_lock[i]);
+        acquiresleep(&b->lock);
+        return b;
+      }
+    }
+    release(&bcache.bucket_lock[i]);
+  }
   panic("bget: no buffers");
 }
 
@@ -120,20 +157,20 @@ brelse(struct buf *b)
     panic("brelse");
 
   releasesleep(&b->lock);
-
-  acquire(&bcache.lock);
+  int idx = b->blockno % NBUCKET;
+  acquire(&bcache.bucket_lock[idx]);
   b->refcnt--;
   if (b->refcnt == 0) {
     // no one is waiting for it.
     b->next->prev = b->prev;
     b->prev->next = b->next;
-    b->next = bcache.head.next;
-    b->prev = &bcache.head;
-    bcache.head.next->prev = b;
-    bcache.head.next = b;
+    b->next = bcache.bucket[idx].next;
+    b->prev = &bcache.bucket[idx];
+    bcache.bucket[idx].next->prev = b;
+    bcache.bucket[idx].next = b;
   }
-  
-  release(&bcache.lock);
+
+  release(&bcache.bucket_lock[idx]);
 }
 
 void
diff --git a/kernel/kalloc.c b/kernel/kalloc.c
index 0699e7e..f616f3b 100644
--- a/kernel/kalloc.c
+++ b/kernel/kalloc.c
@@ -21,13 +21,18 @@ struct run {
 struct {
   struct spinlock lock;
   struct run *freelist;
-} kmem;
+} kmem[NCPU];
 
 void
 kinit()
 {
-  initlock(&kmem.lock, "kmem");
-  freerange(end, (void*)PHYSTOP);
+  int step = (PHYSTOP - (uint64)end) / NCPU;
+  void *start = end;
+  for (int i = 0; i < NCPU; i++) {
+    initlock(&kmem[i].lock, "kmem");
+    freerange(start, start + step);
+    start += step;
+  }
 }
 
 void
@@ -55,28 +60,50 @@ kfree(void *pa)
   memset(pa, 1, PGSIZE);
 
   r = (struct run*)pa;
+  int cpu = cpuid();
 
-  acquire(&kmem.lock);
-  r->next = kmem.freelist;
-  kmem.freelist = r;
-  release(&kmem.lock);
+  acquire(&kmem[cpu].lock);
+  r->next = kmem[cpu].freelist;
+  kmem[cpu].freelist = r;
+  release(&kmem[cpu].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)
+alloc(int cpu)
 {
   struct run *r;
 
-  acquire(&kmem.lock);
-  r = kmem.freelist;
+  acquire(&kmem[cpu].lock);
+  r = kmem[cpu].freelist;
   if(r)
-    kmem.freelist = r->next;
-  release(&kmem.lock);
+    kmem[cpu].freelist = r->next;
+  release(&kmem[cpu].lock);
 
   if(r)
     memset((char*)r, 5, PGSIZE); // fill with junk
   return (void*)r;
 }
+
+void *
+kalloc(void)
+{
+  struct run *r;
+  int cpu = cpuid();
+  r = alloc(cpu);
+  if (r) {
+    return r;
+  }
+  for (int i = 0; i < NCPU; i++) {
+    if (i == cpu) {
+      continue;
+    }
+    r = alloc(i);
+    if (r) {
+      return r;
+    }
+  }
+  return r;
+}

评论 (评论内容仅博主可见,不会公开显示)