2020 6.s081——Lab8:Locks完成思路

我见过软语

开过的花

眼酸到说梦话

——黑月光

完整代码见:SnowLegend-star/6.s081 at lock (github.com)

Memory allocator (moderate)

我们把实验说明里面的内容仔细分析下:

kalloctest中锁争用的根本原因是kalloc()有一个空闲列表,由一个锁保护。要消除锁争用,您必须重新设计内存分配器,以避免使用单个锁和列表。基本思想是为每个CPU维护一个空闲列表,每个列表都有自己的锁。因为每个CPU将在不同的列表上运行,不同CPU上的分配和释放可以并行运行。主要的挑战将是处理一个CPU的空闲列表为空,而另一个CPU的列表有空闲内存的情况;在这种情况下,一个CPU必须“窃取”另一个CPU空闲列表的一部分。窃取可能会引入锁争用,但这种情况希望不会经常发生。

您的工作是实现每个CPU的空闲列表,并在CPU的空闲列表为空时进行窃取。所有锁的命名必须以“kmem”开头。也就是说,您应该为每个锁调用initlock,并传递一个以“kmem”开头的名称。运行kalloctest以查看您的实现是否减少了锁争用。要检查它是否仍然可以分配所有内存,请运行usertests sbrkmuch。您的输出将与下面所示的类似,在kmem锁上的争用总数将大大减少,尽管具体的数字会有所不同。确保usertests中的所有测试都通过。评分应该表明考试通过。

感觉这两次的hints十分鸡肋,也权且看看

您可以使用kernel/param.h中的常量NCPU

1、让freerange将所有可用内存分配给运行freerange的CPU。

看起来这样是不妥的,但是引入“steal”机制就合理了:当前是CPU0在运行,则CPU0获取了所有空闲内存。此时系统切换到CPU1上运行,但是已经没有多余的空闲空间分配给CPU1了。什么时候CPU1的freelist会不够呢?调用kalloc()时。但是CPU1可以“steal”CPU0的freelist,那么“steal”的空间大小应该是多少呢?尝试下“一半”。

在kfee()中操作

2、函数cpuid返回当前的核心编号,但只有在中断关闭时调用它并使用其结果才是安全的。您应该使用push_off()和pop_off()来关闭和打开中断。

借鉴myproc()

3、看看kernel/sprintf.c中的snprintf函数,了解字符串如何进行格式化。尽管可以将所有锁命名为“kmem”。

要给每个CPU都分配一个“kmem”锁吗?对,给每个CPU都分配一个锁。

开始我的想法是先把所有的空间都分配给一个CPU,然后别的CPU的freelist为空时就从第一个不空的CPUfreelist截取后半段,同时这个freelist得大于等于两个PAGE。但是这个想法实现时却遇到了各种bug,我遂采用了最简单的一种——即从第一个不空的CPU的freelist中取一个节点就行。这个方案确实是方便不少,但是我感觉不同CPU进行“steal”的操作太多了,可能影响性能。我想到的那种方案则是遍历不同CPU的freelist需要时间,但是“steal”的操作会大大减少。两者之间的取舍就仁者见仁智者见智了。

By the way,通过所有测试后我又回来完成了“二分法”,不愧是我。真乃百折不挠之士。

我还发现了了个有趣的问题,对freelist的每个page进行初始化后

查看每个块的内容如下:

按理说每个PGSIZE的大小是4K,那应该是000 0000 0101,这里怎么会是上述内容呢?而且两个84215045拼起来就可以得到*r,各种门道也是让我百思不得其解。

遇到的最后一个问题也是离谱,我用“二分法”的思路测试时一共只有32498个page,但是naive方案测试却有32499个page。

仔细查看了下代码发现问题出在这里:

这样问题就明了了。就算空闲的CPU的freelist只有一个page,当前的CPU也应该可以把这个page给据为己有。

kinit()

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

kfree()

  ......
  push_off();
  //初始状态下,当前运行的cpuid一下子获得所有的空闲空间
  // int cpuid=cpuid_Modify();
  int cpuid_Cur=cpuid();
  acquire(&kmem[cpuid_Cur].lock);
  r->next = kmem[cpuid_Cur].freelist;
  kmem[cpuid_Cur].freelist = r;
  kmem[cpuid_Cur].sz++;
  release(&kmem[cpuid_Cur].lock);
  pop_off();
}

kalloc()

kalloc(void)
{
  struct run *r;
  struct run *node_steal;
  int i;

  push_off();
  // int cpuid=cpuid_Modify();
  int cpuid_Cur=cpuid();

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

  //如果当前CPU的freelist为空
  if(!r)
  {
    for (i = 0; i < NCPU; i++)
    {
      if (i == cpuid_Cur)
        continue;
      acquire(&kmem[i].lock);
      // if (kmem[i].freelist && kmem[i].freelist->next)
      if (kmem[i].freelist)
      {
        // 从第一个不空的CPU的freelist截取后半段,同时这个freelist得大于等于两个PAGE
        struct run *tmp = kmem[i].freelist;
        node_steal = kmem[i].freelist;
        for (int j = 1; j < kmem[i].sz / 2; j++)   //西巴,千算万算没看懂这里混进去了i
        {
          // tmp=tmp->next;
        }
        // 已经遍历到了当前freelist的中间部分
        kmem[i].freelist=tmp->next;
        tmp->next = (void *)0;        // 截断当前的freelist
        r = node_steal;
        kmem[i].sz-=kmem[i].sz/2;
        kmem[cpuid_Cur].sz=kmem[i].sz/2;
        release(&kmem[i].lock);
        break;                        //找到合适的freelist就直接退出
      }
      release(&kmem[i].lock);
    }
  }
  // r=node_steal; //可恶,这句话怎么写在if(!r)外面了
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);
    kmem[cpuid_Cur].freelist = r->next;
  kmem[cpuid_Cur].sz--;
  release(&kmem[cpuid_Cur].lock);
  pop_off();

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

  // struct run* xx=r;
  // if(xx)
  //   printf("xx: %p", xx);
  return (void*)r;
}

Buffer cache(hard)

不得不说对这个part的实验说明简直是一坨狗屎。这个lab的思路和multithread那个是一样的,都是利用哈希桶来细化锁的颗粒从而达到提高并发的效果。但是越看实验说明越迷糊,简直就是在画蛇添足地解释这解释那的。

我总结下就是两点要求:

1、NBUF个缓冲块分配给NBUCKET个哈希桶中,通过给每个哈希桶进行加锁去锁的操作来提高并发性。

2、利用ticks来更好地实现LRU。开始我以为原始的代码已经实现了LRU,后来想了下发现这种LRU其实是不完整的——只有brelse后才会进行一次缓冲块释放和插入。即当前bcache.head->next的元素只是最新被释放然后插入进来的,bcache.head->prev则是最早被插入到bcache的元素。但是真正的LRU是最久未被访问的,也就是最久没有被读写的块才被evict。有可能bcache.head->prev这个元素才刚刚被访问,但是由于我们没有记录每个缓冲块的访问时间,就导致这个最近被访问的块直接被evict了。

实现的逻辑也不算复杂,但是这个part却有最为可恶的一点:如果buffer cache的代码有执行问题,没能通过bcachetest的测试,那整个qemu模拟的文件系统都会被损坏。也就是这个问题导致我不断修改bio.c却一直报相同的错误——“panic:init exiting”。

这个bug困扰了我好久好久,我从网上复制了份可以跑通的代码来运行也是这个问题。直到我开始怀疑是自己的内存出了问题,这里重新建立一个新的test文件夹立功了,我在这个文件夹新clone了一份lab,然后用自己的代码跑了遍,虽然还是有bug,但是却可以发现自己的改进确实是由效果的。

然后就是fs.img这个镜像,经过群里老哥的指点发现直接把这个文件删了就行,make qemu会生成一个新的文件系统镜像,就不用反复clone了。(原来就在lecture14就提到了这个操作)

两份一样的代码,一份是“panic:init exiting”,另一份成功执行。

至此,文件系统损坏的bug终于被解决了。

哈希桶的应用

struct Hash_Bucket{
  struct spinlock lock;
  struct buf head;      //哈希桶内部由链表构成
};

struct {
  struct spinlock lock;
  struct buf buf[NBUF];
  struct Hash_Bucket Hash_Bucket[NBUCKET];
  // 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;

用到的哈希函数如下

//用blockno进行哈希映射
int Hash(int blockno){
  return blockno%NBUCKET;
}

 获得当前ticks的方法

//获取当前的ticks
uint get_ticks(){
  uint tick_Cur;
  acquire(&tickslock);
  tick_Cur=ticks;
  release(&tickslock);
  return tick_Cur;
}

bmap()

void
binit(void)
{
struct buf *b;
// struct buf *tmp;
  
  for(int i=0; i<NBUCKET; i++){
    initlock(&bcache.Hash_Bucket[i].lock, "bcache");             //把整个缓冲区的大锁换成每个哈希桶的小锁
    bcache.Hash_Bucket[i].head.prev=&bcache.Hash_Bucket[i].head;
    bcache.Hash_Bucket[i].head.next=&bcache.Hash_Bucket[i].head;
  }

  // Create linked list of buffers
  for(int i=0; i<NBUF; i++){
    b = &bcache.buf[i];
    // struct Hash_Bucket bucket = bcache.Hash_Bucket[i%NBUCKET];  //bucket[i%NBUCKET]可以拥有buf[i]这个缓冲区  这句话直接坏事
    b->ticks=0;
    b->next = bcache.Hash_Bucket[i%NBUCKET].head.next;
    b->prev = &bcache.Hash_Bucket[i%NBUCKET].head;
    initsleeplock(&b->lock, "buffer");
    bcache.Hash_Bucket[i%NBUCKET].head.next->prev=b;
    bcache.Hash_Bucket[i%NBUCKET].head.next=b;
    
    // printf("bucket[%d]的内容如下:", i%NBUCKET);
    // for(tmp=bcache.Hash_Bucket[i%NBUCKET].head.next; tmp!=&bcache.Hash_Bucket[i%NBUCKET].head; tmp=tmp->next){
    //   printf("%p ",tmp);
    // }
    // printf("\n");
  }

  // printf("\n");
  // for(int i=0; i<NBUCKET; i++){
  //   // struct buf *tmp=&bcache.Hash_Bucket[i].head;
  //   printf("bucket[%d]的内容如下:", i);
  //   for(b=bcache.Hash_Bucket[i].head.next; b!=&bcache.Hash_Bucket[i].head; b=b->next){
  //     printf("%p ",b);
  //   }
  //   printf("\n");
  // }
}

bget()

static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;
  int index=Hash(blockno);    //查找当前块应该在哪个bucket里面

  // // Is the block already cached?
  acquire(&bcache.Hash_Bucket[index].lock);         //先获得这个bucket的自旋锁
  //从bucket真正的第一个buf元素开始查找
  // struct buf tmp= bcache.Hash_Bucket[index].head;   //增加代码可读性
  // printf("tmp->next的值是: %p\n", tmp.next);
  //查找当前块是不是已经被缓存了
  for(b = bcache.Hash_Bucket[index].head.next ; b != &bcache.Hash_Bucket[index].head; b = b->next){
    if(b->dev==dev && b->blockno==blockno){
      b->refcnt++;
      release(&bcache.Hash_Bucket[index].lock);
      acquiresleep(&b->lock);                       //这句话的目的是什么?
      return b;
    }
  }
  // printf("卡在“查找当前块是不是已经被缓存了” \n");

  // Not cached.
  // Recycle the least recently used (LRU) unused buffer.

  // tmp= bcache.Hash_Bucket[index].head;
  // printf("tmp->next的值是: %p\n", tmp.next);
  for(b = bcache.Hash_Bucket[index].head.next ; b != &bcache.Hash_Bucket[index].head; b = b->next){
      if(b->refcnt == 0) {
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&bcache.Hash_Bucket[index].lock);
      acquiresleep(&b->lock);
      return b;
    }
  }

  //如果当前bucket没有buf了,从其他bucket里面抢夺一个用用(:
  //这里的思路和kalloc的修改差不多
  struct buf *buf_available=0;
  int buf_from=-1;             //记录这块空闲的buf来自于哪个bucket
  for(int i=0; i<NBUCKET; i++){
    if(i==index)
      continue;
    acquire(&bcache.Hash_Bucket[i].lock);
    for(b = bcache.Hash_Bucket[i].head.next; b != &bcache.Hash_Bucket[i].head; b = b->next){
      if(b->refcnt==0){
        if(buf_available==0 || buf_available->ticks > b->ticks){  //利用LRU选择一个空闲块,效率是真低    用ticks简直是画蛇添足
          buf_available=b;
          buf_from=i;
        }
      }
    }
    release(&bcache.Hash_Bucket[i].lock);
  }

  if(buf_available){
    buf_available->dev = dev;               //原来这里写成b->dev了,可恶啊啊啊啊
    buf_available->blockno = blockno;
    buf_available->valid = 0;
    buf_available->refcnt = 1;

    //从原来的bucket中取出这个buf
    acquire(&bcache.Hash_Bucket[buf_from].lock);
    buf_available->prev->next = buf_available->next;
    buf_available->next->prev = buf_available->prev;
    release(&bcache.Hash_Bucket[buf_from].lock);

    //把这个空闲buf插入到bucket[index]中   头插法
    buf_available->next = bcache.Hash_Bucket[index].head.next;
    buf_available->prev = &bcache.Hash_Bucket[index].head;
    bcache.Hash_Bucket[index].head.next->prev=buf_available;
    bcache.Hash_Bucket[index].head.next=buf_available;
    release(&bcache.Hash_Bucket[index].lock);

    acquiresleep(&buf_available->lock);
    return buf_available;
  }
  panic("bget: no buffers");
}

最后贴一张通过的截图

注:

Spinlock主要是完成互斥作用,相当于mutex=1

Sleeplock更多的是类似于生产者-消费者问题,producer_mutex=n这种。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值