• MIT-6.S081-2020实验(xv6-riscv64)八:lock


    实验文档

    概述

    这次实验主要涉及锁在内核的应用,没有用到什么特别的理论知识,但是编程的时候陷阱重重,要么资源竞争,要么死锁,和实验三差不多,非常考验耐心和细心。

    内容

    Memory allocator

    这个任务要求给物理内存分配程序重新设计锁,使得等待锁时的阻塞尽量少。可以按CPU的数量将空闲内存分组,分配内存的时候优先从当前所用CPU所管理的空闲内存中分配,如果没有则从其他CPU的空闲内存中获取,这样就可以把原来的锁拆开,每个CPU各自处理自己的空闲内存时只要锁上自己的锁就行了:

    void
    kinit()
    {
      for (int i = 0; i < NCPU; i++) initlock(&kmem[i].lock, "kmem");
      freerange(end, (void*)PHYSTOP);
    }
    
    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_id; push_off(); cpu_id = cpuid(); pop_off();
      acquire(&kmem[cpu_id].lock);
      r->next = kmem[cpu_id].freelist;
      kmem[cpu_id].freelist = r;
      release(&kmem[cpu_id].lock);
    }
    

    由于kinit这个函数不是并行的,所以一开始会将所有空闲内存都交给一个CPU管理。

    void *
    kalloc(void)
    {
      struct run *r;
    
      int cpu_id; push_off(); cpu_id = cpuid(); pop_off();
      acquire(&kmem[cpu_id].lock);
      r = kmem[cpu_id].freelist;
      if(r) {
          kmem[cpu_id].freelist = r->next;
          release(&kmem[cpu_id].lock);
      } else {
          release(&kmem[cpu_id].lock);
          for (int i = 0; i < NCPU; i++) if (i != cpu_id) {
              acquire(&kmem[i].lock);
              r = kmem[i].freelist;
              if (r) {
                  kmem[i].freelist = r->next;
                  release(&kmem[i].lock);
                  break;
              } else release(&kmem[i].lock);
          }
      }
      ......
    

    Buffer cache

    这个任务要求给硬盘缓存分配程序重新设计锁,使得等待锁时的阻塞尽量少。但是,因为硬盘缓存包含遍历查找操作,即查找当前硬盘块是否已被缓存,显然这时就不能把缓存也按CPU进行分配,加上这个任务的操作也比较复杂,因此比上个任务多了很多问题。

    实验文档给出的分配方式是对硬盘块号进行取模哈希,数据结构如下:

    struct {
      struct spinlock lock;
      struct spinlock block[BNUM];
      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[BNUM];
    } bcache;
    

    这里保留了原来的锁是因为在不要求修改的bpin、bunpin函数中使用了,但要小心的是,既然用到了新定义的锁,那么在整个实验中所有相关的代码都必须用新定义的锁,不能用原来的锁。

    void
    binit(void)                                                                     {
      struct buf *b;
    
      initlock(&bcache.lock, "bcache");
    
      // Create linked list of buffers
      for (int i = 0; i < BNUM; i++) {
          initlock(bcache.block + i, "bcache");
          bcache.head[i].prev = &bcache.head[i];
          bcache.head[i].next = &bcache.head[i];
      }
      for(b = bcache.buf; b < bcache.buf+NBUF; b++){
        b->next = bcache.head[0].next;
        b->prev = &bcache.head[0];
        initsleeplock(&b->lock, "buffer");
        bcache.head[0].next->prev = b;
        bcache.head[0].next = b;
      }
    }
    

    因为一开始所有的缓存对应硬盘块号都是0,所以把它们都放到0号桶里。

    void
    brelse(struct buf *b)
    {
      if(!holdingsleep(&b->lock))
        panic("brelse");
    
      releasesleep(&b->lock);
    
      int entry = b->blockno % BNUM;
      acquire(bcache.block + entry);
      b->refcnt--;
      if (b->refcnt == 0) b->ticks = ticks;
      release(bcache.block + entry);
    }
    

    释放缓存时只要获取块对应的桶对应的锁即可,由于原来的代码在释放缓存时会将缓存插在head的下一个节点,按照原来程序的思路head的下一个节点是目前最近使用的缓存,所以把查找最久未使用缓存的方式改成了查找最前时间戳后,这里也应该更新时间戳。

    最麻烦的是bget,这里拆成两部分,第一部分是待查找磁盘块已被缓存的情况:

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

    也是只要获取待查找块对应的桶对应的锁即可,但要注意一个问题,这个锁必须一直持有到整个函数运行结束,不能在中间释放了再重新获取,也就是说:

    获得锁
    查找缓存但没找到
    释放锁
    可以并行的其他操作
    获得锁
    更新一个可用的缓存
    释放锁
    

    不等价于

    获得锁
    查找缓存但没找到
    可以并行的其他操作
    更新一个可用的缓存
    释放锁
    

    而且前者是错的,后者是错的。理由是如果中间释放了锁,当前进程可能会让出控制权执行别的进程,那么就会出现一个问题,比如A进程查找1号缓存,查不到,释放了锁,程序转到B进程,它也查找1号缓存,查不到,释放了锁而刚好B立刻又获得了锁,继续往下更新了一个可用的缓存,释放锁,这是A获得锁,继续往下更新了一个可用的缓存,释放锁。现在缓存中就有两个编号完全相同且引用数都为1的缓存了,这个是不允许的行为,可能会出现各种问题,而且这些问题的发生全靠运气,有时不出错,有时这个panic,有时那个panic,非常难调。

    第二部分是待查找磁盘块未被缓存的情况:

      for (int i = (entry + 1) % BNUM; i != entry; i = (i + 1) % BNUM) {
          uint minticks = 0x3fffffff; struct buf *minbuf = 0;
          acquire(bcache.block + i);
          for(b = bcache.head[i].prev; b != &bcache.head[i]; b = b->prev)
              if (b->refcnt == 0 && b->ticks < minticks) {
                  minticks = b->ticks; minbuf = b;
              }
          if (minbuf != 0) {
              minbuf->dev = dev;
              minbuf->blockno = blockno;
              minbuf->valid = 0;
              minbuf->refcnt = 1;
              minbuf->next->prev = minbuf->prev;
              minbuf->prev->next = minbuf->next;
              minbuf->next = bcache.head[entry].next;
              minbuf->prev = &bcache.head[entry];
              bcache.head[entry].next->prev = minbuf;
              bcache.head[entry].next = minbuf;
              release(bcache.block + i);
              release(bcache.block + entry);
              acquiresleep(&minbuf->lock);
              return minbuf;
          }
           release(bcache.block + i);
      }
      panic("bget: no buffers");
    }
    

    一开始我寻找可更新缓存的办法是直接遍历整个数组,为了防止竞争,需要在遍历前把所有的桶锁起来,然而这样会发生死锁,即假设处理0号桶的进程运行到这里,把0号桶锁了,准备获取1号桶的锁,与此同时处理1号桶的进程运行到这里,把1号桶锁了,准备获取0号桶的锁,这样就死锁了。仔细分析原因,发现只要锁桶的顺序是乱序的,都可能发生死锁,这里的解决方法是使用“资源有序分配法”,就如上面的循环,从当前桶的下一个桶往上遍历到当前桶的前一个桶(循环遍历),保证了顺序,就不会死锁了。另外一个需要小心的是链表的操作,双向链表确实很容易写错,需要谨慎。


    总结一下,这个实验和上一个实验相比,感觉更考验并行思维,主要体现在锁的应用,上个实验的后两个任务和这个实验比真的是小巫见大巫了,可能设计实验的老师主要还是想让学生熟悉一下pthread才弄那两个任务,毕竟pthread太常用了。目前觉得系统编程最难的就是四个问题:缺页错误(这里指的是编程逻辑的错误导致的错误)、内存泄漏、资源竞争、死锁,都是极为难以检查难以调试的。

  • 相关阅读:
    java线程简要
    Unable to find explicit activity class
    用NetBeans生成jar文件
    Linux下三个可以修改环境变量的地方
    linux定时执行shell脚本
    sql server 性能调优之 SQL语句阻塞查询
    sql server 性能调优之 死锁排查
    IObit Advanced SystemCare 系统清理优化工具
    IDEA配置Maven
    maven的生命周期及常用命令的使用
  • 原文地址:https://www.cnblogs.com/YuanZiming/p/14251191.html
Copyright © 2020-2023  润新知