Lab: Locks

Memory Allocator

解法

总的来说,这个题目的要求是细化自由表的互斥锁粒度:在原版的xv6中,所有CPU共享一个自由表,因此当多个CPU同时运行多个进程时,且这些进程同时调用 kalloc()kfree() 时会被频繁地阻塞,效率十分低下。我们要做的就是为每个CPU创建一个专属的自由表,同时为每个自由表设置专属的互斥锁,以允许不同CPU上的进程并行地调用 kalloc()kfree()。由于地址空间被分散至各个CPU所有,我们还需要设计一个“借用”函数,当某个CPU的自由表用尽时,从其他CPU的自由表处借用空间。

首先更改自由表的定义,将 kmem 改为大小为 NCPU 的数组:

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

而后,为 kmem[] 中的所有自由表分配一个互斥锁并为这些锁命名(可以通过 snprintf 得到格式化字符串,也可以直接为所有锁取相同的名字):

void
kinit()
{
  for (int i = 0; i < NCPU; ++i) {
    char name[6] = {'k', 'm', 'e', 'm', '0' + i, 0};
    initlock(&kmem[i].lock, name);
  }
  // initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

而后,我们先修改内存的回收,因为这个步骤不会涉及到CPU之间的内存借取。这里我们定义了一个辅助函数帮助我们获取CPU的编号,这是因为xv6中 mycpu() 的返回值是不稳定的,必须在每次调用时(通过 push_off()pop_off())关闭中断以确保其正确性,通过辅助函数我们以可以增加代码的可读性。随后,我们在每次调用 kfree() 时获取调用进程所在CPU的编号,获取对应的互斥锁,并将空闲内存回收至相应的自由表即可:

// Safely get the current cpuid.
int
cpuids()
{
  int id;
  push_off();
  id = cpuid();
  pop_off();

  return id;
}

// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
	// ...
  int id = cpuids();
  struct mem *nmem = kmem + id;
  acquire(&nmem->lock);
  r->next = nmem->freelist;
  nmem->freelist = r;
  release(&nmem->lock);
}

接着我们实现因涉及借取而相对复杂一些的 kalloc 函数,我们不妨假设 steal(int id) 可以从其他CPU处借得一页,那么 kalloc() 的修改也十分简单了:将所有涉及自由表的代码修改为使用对应CPU的自由表,当自由表为空时,调用 steal() 以借得内存即可:

void *
kalloc(void)
{
  // ...
  r = kmem[id].freelist;
  if(r)
    kmem[id].freelist = r->next;
  else {
    steal(id);
    r = kmem[id].freelist;
    if (r)
      kmem[id].freelist = r->next;
  }
  release(&kmem[id].lock);

  // ...
}

接着我们实现 steal 函数,这里我们采取比较简单的逻辑,即遍历所有CPU的自由表,从第一个有空闲页面的CPU处借取即可。

void
steal(int id) {
  for (int i = 0; i < NCPU; ++i) {
    if (i == id)
      continue;

    struct run *r;
    acquire(&kmem[i].lock);
    r = kmem[i].freelist;

    if (!r) {
      release(&kmem[i].lock);
      continue;
    }

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

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

一个要注意的点是被我注释掉的互斥锁,由于我们在 kalloc() 中调用 steal() 时已经锁定了 kmem[id].lock(借用者),因此不能再次锁定;此外,无需考虑系统初始化时的内存问题,因为 kinit() 一定会将所有内存分配至该进程所在的CPU的自由表内,而其他CPU总是能向这个CPU借内存的。

Buffer cache

解法

迄今为止遇到的最 HARD 的题目(汗)。做这道题建议你先充分理解题目的要求之后再开始写代码。

如题所示,在原先的xv6中,块缓冲(即buffer cache,下同)在分配时存放于数组(即 bcache.buf[])当中,在访问和管理时是通过一个链表来实现的,而整张链表由一个互斥锁来进行管理,无论是对块缓冲进行存取,抑或是对块缓冲进行管理,如替换等,都需要首先获取这个互斥锁以保证不变量(invariant)。本题的主要目标就是通过对粒度的细化,从而实现对互斥锁的竞争现象减少。测试程序 bcachetest 通过统计进程因等待相应的互斥锁而休眠的次数来量化这一点。