duduru

6.S081 Locks

Memory allocator

在这里插入图片描述
本测试用kalloctest例创造了3个进程,不断kalloc()和kfree(),这就意味着会频繁使用锁。然而我们的freelist只有1个,在多核里3个并行运行的进程必定会为了获得锁而自旋。

运行kalloctest可以看到结果。其中669281代表自旋次数,600057代表获得锁(进入acquire()的次数)。
在这里插入图片描述
值得注意的是进行实验前一定要设置qemu为多核,否则单核的自旋次数将永远为0。
在这里插入图片描述
修改核心数(smp为处理器核心数):

在Makefile中修改smp:

qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

或者

make qemu CPUS=3

所以本实验的目的就是减少这种自旋,因为自旋会占用cpu资源。解决办法是为每一个核心都分配一个freelist,分别管理不同的物理地址,这样就不会有不通cpu修改同一物理地址的问题,自旋的问题也解决了。

但是就像实验所说,kalloc()的实现更复杂了,因为它需要在内存不够的时候(freelist为空),去别的核心的freelist中去“偷”内存块,放入自己的freelist里为己所用。所以在这里是会出现自旋的,我们的锁也是在这里应用。

此实验是在COW Fork实验基础上更改

freelist

将共享的一个freelist改为NCPU独享的freelist

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

kfree()

kfree():

// 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)
{
  struct run *r;

  if (((uint64)pa % PGSIZE) != 0 || (char *)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // 获得当前核心号
  // 关闭中断是因为万一在返回了curcpu之后马上进入定时器中断,导致进程切换到别的cpu运行,那么cpu将不对应
  push_off();
  int curcpu = cpuid();
  pop_off();

  acquire(&kmem[curcpu].lock);
  int pn = (uint64)pa / PGSIZE;
  if (refcount[pn] < 1)
  {
    panic("kfree ref");
  }

  int tmp = --refcount[pn];
  release(&kmem[curcpu].lock);

  if (tmp > 0)
    return;
  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run *)pa;

  acquire(&kmem[curcpu].lock);
  r->next = kmem[curcpu].freelist;
  kmem[curcpu].freelist = r;	// 将pa加入freelist中
  release(&kmem[curcpu].lock);
}

kalloc()

kalloc()是本实验的重点,本身逻辑不难,难的是容易出现各种bug。
以下是有问题的kalloc(),因为整个解决问题的过程很有价值,所以附上:

// 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;
  int pn;
  // 获得当前核心号
  push_off();
  int curcpu = cpuid();
  pop_off();

  acquire(&kmem[curcpu].lock);
  r = kmem[curcpu].freelist;
  if (r)
  {
    kmem[curcpu].freelist = r->next;
    pn = (uint64)r / PGSIZE;
    if (refcount[pn] != 0)
      panic("kalloc ref");
    refcount[pn] = 1;
  }
  else
  {
    // 从其他核心的freelist“偷”
    for (int i = 0; i < NCPU; i++)
    {
      if (i == curcpu)
        continue;

      r = kmem[i].freelist;
      acquire(&kmem[i].lock);
      if (r)
      {
        // 将内存块从别的freelist中移出
        kmem[i].freelist = r->next;
        pn = (uint64)r / PGSIZE;
        // printf("cpuid=%d,pn=%d\r\n ", curcpu, pn);
        if (refcount[pn] != 0)
        {
          printf("cpuid=%d,refcount[pn]=%d,pn=%d ", curcpu, refcount[pn], pn);
          panic("kalloc ref");
        }
        refcount[pn] = 1;
        release(&kmem[i].lock);
        break;
      }
      else
		 release(&kmem[i].lock);
    }
  }
  release(&kmem[curcpu].lock);

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

运行结果:

在这里插入图片描述
可以看到,lock kmem/bcache stats的结果符合预期,采用cpu独享freelist的方法确实减少了自旋,在此处直接为0,test0通过;可是test1就没那么幸运了,直接报错,定位到我们的代码:

    // 从其他核心的freelist“偷”
    for (int i = 0; i < NCPU; i++)
    {
      if (i == curcpu)
        continue;

      r = kmem[i].freelist;
      acquire(&kmem[i].lock);
      if (r)
      {
        // 将内存块从别的freelist中移出
        kmem[i].freelist = r->next;
        pn = (uint64)r / PGSIZE;
        if (refcount[pn] != 0)
        {
          printf("cpuid=%d,refcount[pn]=%d,pn=%d ", curcpu, refcount[pn], pn);
          panic("kalloc ref");
        }
        refcount[pn] = 1;
        release(&kmem[i].lock);
        break;
      }
      else
		 release(&kmem[i].lock);

报错出现在我们“偷”内存的地方。也就是说它在refcount[pn]已经被分配的情况下(!=0)的情况下仍想分配这一块内存。

我们再来试一下单核运行kalloctest,以此来排除是否是多核引发的并发错误。

make qemu CPUS=1

运行结果:

在这里插入图片描述
可以看到单核运行时正常,所以问题就出在我们多核“偷”内存那里。

我们的报错refcount[pn]在已经被引用一次的情况下再引用了一次,我们就要想多核可能引发的并发问题。

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

看到这一段代码,多核会出现这样一种情况:两个进程同时运行到r = kmem[i].freelist,其中一个获得锁进入了if( r ) ,而另外一个再获得锁时,也可以进入if( r ) 里面,但此时该页已经被引用过了,所以出现了我们上面看到的bug。

知道原因后,现在对我们的代码进行修改,只用替换一下位置即可:

void *
kalloc(void)
{
  	···
  else
  {
    // 从其他核心的freelist“偷”
    for (int i = 0; i < NCPU; i++)
    {
      if (i == curcpu)
        continue;

      acquire(&kmem[i].lock);	// 注意获得锁的位置,否则会出现bug
      r = kmem[i].freelist;
	···
}

运行结果:
在这里插入图片描述
运行正确。

posted on 2023-12-05 14:58  duduru  阅读(0)  评论(0)    收藏  举报  来源

导航