MIT6.S081实验八学习记录

Lab: locks

这个实验主要任务是将锁进行细粒化,提高并行程度。比如说,如果整个系统只有一把锁,那么只要你占有,其他所有的进程都得停滞,对于多核的系统来说更是灾难,完全无法发挥出应该有的作用。再者来说,多块内存如果只由一个锁来管理,那么即使不同线程要访问不同的内存,但是由于锁被占用,本来没有被访问到的内存也没法被使用。 上锁保证了隔离性,但是如果对并行程度影响过大就得不偿失了。

首先,切换到实验八的分支。

$ git fetch
$ git checkout lock
$ make clean

1、Memory allocator (moderate)

第一个实验就是要将kalloc 中kmem 的锁细粒化,要保证给每个CPU都能分配到锁。当当前CPU分配内存时,如果发现自己的freelist都被占用了,就要去“偷”其他CPU的可用freelist为己所用。
首先,给每个CPU都细化一个kmem类。

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

接下来,初始化阶段将所有锁初始化。

void
kinit()
{
  for(int i = 0; i < NCPU; i++){
    char name[9] = {0};
    snprintf(name, 8, "kmem_%d", i); //每个锁的名字不一样
    initlock(&kmem[i].lock,name);
  }
  //initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

接下来,修改分配内存的代码,如果当前freelist可用,就直接分配,如果不可用,那就从其他CPU的freelist 找一个可用的。

void *
kalloc(void)
{
  struct run *r;
  
  //关闭所有中断,找到当前的cpuid
  push_off();
  int i = cpuid();
  pop_off();
  
  //获取当前cpu的freelist
  acquire(&kmem[i].lock);
  r = kmem[i].freelist;
  
  if(r){
    kmem[i].freelist = r->next; //如果可用,就直接用
  }else{ //不可用,找其他的CPU是否有可用的freelist
    for(int j = 0; j < NCPU; j++){
        if(j == i) continue;
        
    	acquire(&kmem[j].lock);  //上锁
    	if(!kmem[j].freelist){   //没有可用的,解锁,继续找
    	  release(&kmem[j].lock);
    	  continue;
    	}
    	
    	r = kmem[j].freelist;  //找到了可用的freelist
    	kmem[j].freelist = r->next;  //将可用的freelist赋给之前需要的CPU
    	release(&kmem[j].lock);  //解锁
    	break;  //直接退出,无需再找
    }
  
  }
  release(&kmem[i].lock);
  
  
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

然后需要修改一下释放内存的函数 kfree ,主要就是把上锁方式修改一下,因为上面已经将锁细粒化了,代码如下:

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;
  
  //关中断,获取当前CPU的id
  push_off();
  int i = cpuid();
  pop_off();
  
  acquire(&kmem[i].lock);
  r->next = kmem[i].freelist;
  kmem[i].freelist = r;
  release(&kmem[i].lock);
}

然后这部分就修改完了,可以在xv6里测试一下,这里usertests太多了,我就测试一下kalloctest和sbrkmuch。

xv6 kernel is booting

hart 2 starting
hart 1 starting
init: starting sh
$ kalloctest
start test1
test1 results:
--- lock kmem/bcache stats
lock: bcache: #fetch-and-add 0 #acquire() 8
lock: bcache: #fetch-and-add 0 #acquire() 2
lock: bcache: #fetch-and-add 0 #acquire() 12
lock: bcache: #fetch-and-add 0 #acquire() 18
lock: bcache: #fetch-and-add 0 #acquire() 6
lock: bcache: #fetch-and-add 0 #acquire() 6
lock: bcache: #fetch-and-add 0 #acquire() 12
lock: bcache: #fetch-and-add 0 #acquire() 10
lock: bcache: #fetch-and-add 0 #acquire() 274
--- top 5 contended locks:
lock: proc: #fetch-and-add 390592 #acquire() 124893
lock: proc: #fetch-and-add 366233 #acquire() 124892
lock: proc: #fetch-and-add 327611 #acquire() 124892
lock: proc: #fetch-and-add 263392 #acquire() 124891
lock: proc: #fetch-and-add 260295 #acquire() 124892
tot= 0
test1 OK
start test2
total free number of pages: 32495 (out of 32768)
.....
test2 OK
$ usertests sbrkmuch
usertests starting
test sbrkmuch: OK
ALL TESTS PASSED

发现都通过了。

2、Buffer cache (hard)

这个实验相较于上一个实验要复杂许多,首先需要理解这个实验的目的。
这个实验是要给文件系统的缓存区上锁,当多个进程频繁适用文件系统时,会高频次访问这个缓存区,因此这里的锁被 accquire的次数会非常多。原始版本的代码只有一个锁来管理,而题目也给出了一些提示,也就是用哈希桶,将整个缓存区分块,对每一块都上锁,这样访问其中一个缓存块就不会把整个缓存区都锁上了,因此可以提高并行度。

这里原本缓存区使用一个双向链表来管理的,其实也就是为了方便使用LRU算法,如果在leetcode写过LRU的话,就应该知道双向链表的作用。

因此,首先需要定义一个哈希桶,将整个缓存区分块,这里也提示,建议哈希桶数量设置为质数来避免哈希冲突,因此就把哈希桶的大小定义为13.

extern uint ticks;
#define NBUCKET      13

struct {
  struct spinlock lock[NBUCKET];  //每个桶都有一个锁
  struct buf buf[NBUF];
  struct buf buckets[NBUCKET];  //每个桶都有一个索引开始的地方,可以看作是原来的head
} bcache;

//定义哈希函数
int hash(uint blockno){
  return blockno % NBUCKET;
 }

接下来就是初始化整个bcache

void
binit(void)
{
  struct buf *b;
  
  //初始化每个桶的锁
  for(int i = 0; i < NBUCKET; i++){
      initlock(&bcache.lock[i], "bcache");
  }

  // 将每个桶内的双向链表初始化
  for(int i = 0; i < NBUCKET;i++){
      bcache.buckets[i].prev = &bcache.buckets[i];
      bcache.buckets[i].next = &bcache.buckets[i];
  }
  
  //将每个双向链表分配好NUBF的内存
  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    b->next = bcache.buckets[0].next;
    b->prev = &bcache.buckets[0];
    initsleeplock(&b->lock, "buffer");
    bcache.buckets[0].next->prev = b;
    bcache.buckets[0].next = b;
  }
}

接下来就是最重要的bget函数,这个函数本来的作用就是遍历整个buffer cache找对应块的缓存,如果找到,直接返回这个缓存块,把引用计数增加就行。没找到的话,就需要先把当前使用最早的缓存块淘汰掉。原版应该是把引用计数为0的块换掉,这里实验多加了一条要求,就是要按照时间来判断,把最早的替换掉,所以也算是实现一个LRU。

如果当前没找打满足要求的缓存块,就需要从其他桶中找一个空闲的块分配给当前块使用,因为这里是链表形式的,所以空间上并不连续,这种实现是完全可行的。

static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;
  
  int id = hash(blockno);
  acquire(&bcache.lock[id]);

  // 寻找对应设备号和块号的缓存块
  for(b = bcache.buckets[id].next; b != &bcache.buckets[id]; b = b->next){
    if(b->dev == dev && b->blockno == blockno){  //命中
      b->refcnt++;    //增加引用数
      release(&bcache.lock[id]);
      acquiresleep(&b->lock);
      return b;
    }
  }
  
  uint least = ticks; //获取当前时间戳
  struct buf *res = 0; //定义一个临时缓存块
  
  //找到最小的时间戳,也就是最早使用的缓存块,赋值给res
  for(b = bcache.buckets[id].next; b != &bcache.buckets[id]; b = b->next){
    if(b->refcnt == 0 && b->lastuse <= least){  //
        least = b->lastuse;
        res = b;
    }
  }
  
  if(res){ //如果这个块可用,那就初始化,将所有信息重置
     res->dev = dev;
     res->blockno = blockno;
     res->valid = 0;
     res->refcnt = 1;
     release(&bcache.lock[id]);
     acquiresleep(&res->lock);
     return res;
  } else { /如果不可用,需要从其他的桶中来“偷”
    for(int i = 0; i < NBUCKET;i++){
  	if(i == id) continue;  //跳过当前id的桶
  	
  	acquire(&bcache.lock[i]);  //给当前桶上锁
  	least = ticks;  //获取当前时间戳
  	//找到这个桶里最早使用的块
  	for(b = bcache.buckets[i].next; b != &bcache.buckets[i]; b = b->next){
          if(b->refcnt==0 && b->lastuse<=least) {
            least = b->lastuse;
            res = b;
          }
        }
          //如果这个块不可用,那就找下一个桶
          if(!res){
            release(&bcache.lock[i]);
            continue;
          }
          //可用就初始化这个快的信息
          res->dev = dev;
          res->blockno = blockno;
          res->valid = 0;
          res->refcnt = 1;
          
          //把这个可用块从当前桶(非id桶)中拿出来
          res->next->prev = res->prev;
          res->prev->next = res->next;
          release(&bcache.lock[i]);
          
          //把这个可用块放到需要的桶中id位置
          res->next = bcache.buckets[id].next;
          bcache.buckets[id].next->prev = res;
          bcache.buckets[id].next = res;
          res->prev = &bcache.buckets[id];
          
          release(&bcache.lock[id]);
          acquiresleep(&res->lock);
          return res; 
    }
  }
  //上述过程也没找到,说明当前调度算法下,没有可用块
  release(&bcache.lock[id]);
  panic("bget: no buffers");  

}

写到这里,基本上就完成了,但是还有一个重要问题,如何获取到当前块的最早使用时间呢?也就是上面的b->lastuse,这里首先在 buf.h中定义这个变量。

struct buf {
  int valid;   // has data been read from disk?
  int disk;    // does disk "own" buf?
  uint dev;
  uint blockno;
  struct sleeplock lock;
  uint refcnt;
  struct buf *prev; // LRU cache list
  struct buf *next;
  uchar data[BSIZE];
  uint lastuse; //上一次使用的时间戳
};

接下来需要在其被LRU调度后,也就是把这个缓存解锁后,放到整个使用链表头部的时候,说明是被刚刚访问的,此时加上时间戳。因此放到 brelse 函数中

// Release a locked buffer.
// Move to the head of the most-recently-used list.
void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");
    
  releasesleep(&b->lock);
  
  int id = hash(b->blockno);
  acquire(&bcache.lock[id]);
  b->refcnt--;
  if (b->refcnt == 0)
    b->lastuse = ticks; //引用计数为0,说明刚初始化,加上时间戳
 
  release(&bcache.lock[id]);
}

最后还需要修改上锁的地方,因为前面已经把锁细粒化了。

void
bpin(struct buf *b) {
  int id = hash(b->blockno);
  acquire(&bcache.lock[id]);
  b->refcnt++;
  release(&bcache.lock[id]);
}

void
bunpin(struct buf *b) {
  int id = hash(b->blockno);
  acquire(&bcache.lock[id]);
  b->refcnt--;
  release(&bcache.lock[id]);
}

到这里整个实验就做完了,然后用脚本测试一下。

jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ ./grade-lab-lock 
make: 'kernel/kernel' is up to date.
== Test running kalloctest == (104.3s) 
== Test   kalloctest: test1 == 
  kalloctest: test1: OK 
== Test   kalloctest: test2 == 
  kalloctest: test2: OK 
== Test kalloctest: sbrkmuch == kalloctest: sbrkmuch: OK (14.5s) 
== Test running bcachetest == (41.1s) 
== Test   bcachetest: test0 == 
  bcachetest: test0: OK 
== Test   bcachetest: test1 == 
  bcachetest: test1: OK 
== Test usertests == usertests: OK (245.1s) 
    (Old xv6.out.usertests failure log removed)
== Test time == 
time: OK 
Score: 70/70

全部通过。

总结

这个实验和上个实验都有用到锁这一关键的计数,只是侧重点不同。上个实验重在体会锁带给整个系统,尤其是线程间的隔离性,而这个实验主要是要能够平衡隔离性,提升并行化程度。

第一个实验就是简单地将kmem按照CPU数目来上锁,并且还要做一个不同CPU间“借”内存的操作。

第二个实验是对文件系统缓存区进行优化,将原来的一个大锁用哈希桶分块,然后对每个块进行上锁解锁,进而可以较好地保证当一个线程访问当前缓存时,不会将整个缓存区上锁,而是对每个缓存块上锁,如果当前线程不需要这个缓存块,那么也不会一直占着不给别的线程用,因此进一步提升运行效率。
[1]. https://pdos.csail.mit.edu/6.S081/2020/labs/lock.html
[2]. https://zhuanlan.zhihu.com/p/609771386
[3]. https://zhuanlan.zhihu.com/p/629372818

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值