MIT 6.S081 lab 8:Lock

1 Memory allocator

Xv6的user/kalloctest 程序关注于内存分配器:三个进程扩大和减小它们的地址空间,导致了很多对于kallockfree的调用,而它们都会获取kmem.lock.kalloctest会打印(像"#fetch-and-add")在acquire中尝试获得一个其他进程已经持有的锁的迭代次数,如kmem、以及一些其他的锁。acquire中的迭代次数是一种粗糙的评估锁竞争次数的方式。在你完成这个lab之前,kalloctest的输出看起来有些类似于以下:

$ kalloctest
start test1
test1 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 83375 #acquire() 433015
lock: bcache: #fetch-and-add 0 #acquire() 1260
--- top 5 contended locks:
lock: kmem: #fetch-and-add 83375 #acquire() 433015
lock: proc: #fetch-and-add 23737 #acquire() 130718
lock: virtio_disk: #fetch-and-add 11159 #acquire() 114
lock: proc: #fetch-and-add 5937 #acquire() 130786
lock: proc: #fetch-and-add 4080 #acquire() 130786
tot= 83375
test1 FAIL

对于每个锁,acquire会维护一个对于这个锁的调用计数,还有acquire中尝试获得锁但是失败的次数。kalloctest调用了系统调用使得内核去打印kmembcache锁的相关计数,还有五个竞争次数最多的锁。如果发生锁竞争的话,acquire产生的迭代次数会是非常大的。这个系统调用会返回kmem和bache 锁的总的迭代次数。

对于这个实验,您必须使用专用的多核卸载机器。如果你使用了一个做其他事情的机器,那么计数将会是无意义的。你可以使用雅典娜机,或你自己的笔记本电脑,但不要使用拨号机。

kalloctest中锁争用的根本原因是kalloc()有一个由一个锁保护的空闲列表。为了移除锁竞争,你需要重新设计一个内存分配器来避免单个锁和列表。基本的想法是为每个核都维护一个空闲列表,每个列表都有一个自己的锁。不同CPU上的分配和释放可以并行,因为它们在不同的空闲列表上操作。主要的挑战会是某个CPU的空闲列表为空,而另一个是空闲的;这种情况下,那个CPU必须从其他CPU那边偷一些过来。偷内存可能需要引入锁竞争,但是这是很不平凡的。

您的工作是实现每个CPU的空闲列表,并在CPU的空闲列表为空时进行窃取。您必须给出所有以“kmem”开头的锁名。也就是说,您应该为每个锁调用initlock,并传递一个以“kmem”开头的名称。运行kalloctest,看看您的实现是否减少了锁争用。要检查它是否仍然可以分配所有的内存,请运行usertests sbrkmuch。您的输出将类似于下面所示,在kmem锁上的争用总数大大减少,尽管具体数字会有所不同。确保用户测试中的所有测试都通过。
$ kalloctest
start test1
test1 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 0 #acquire() 42843
lock: kmem: #fetch-and-add 0 #acquire() 198674
lock: kmem: #fetch-and-add 0 #acquire() 191534
lock: bcache: #fetch-and-add 0 #acquire() 1242
--- top 5 contended locks:
lock: proc: #fetch-and-add 43861 #acquire() 117281
lock: virtio_disk: #fetch-and-add 5347 #acquire() 114
lock: proc: #fetch-and-add 4856 #acquire() 117312
lock: proc: #fetch-and-add 4168 #acquire() 117316
lock: proc: #fetch-and-add 2797 #acquire() 117266
tot= 0
test1 OK
start test2
total free number of pages: 32499 (out of 32768)
.....
test2 OK
$ usertests sbrkmuch
usertests starting
test sbrkmuch: OK
ALL TESTS PASSED
$ usertests
...
ALL TESTS PASSED
$

一些建议:

  • 你可以从kernel/param.h中用常量NCPU
  • 使所有空闲内存都给调用freerange的CPU。(这部分没怎么理解,就按照自己的方式做了。)
  • cpuid会返回一个当前核号,但是这个调用只在关闭中断时才正确。你应当使用push_off()和push_off()来打开或者关闭中断。
  • 看一下snprintf函数,用于字符串格式化。不过,把所有锁都命名为kmem也是可以的。

只要修改kernel/kalloc.c中的内容即可。

为每个CPU都添加一个kmem

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

添加一个全局锁,用于窃取内存时的同步

struct spinlock allLock;

修改freerange()

对于freerange的修改与提示不同,本次实现中把freerange函数用于在初始化时给每个CPU的freelist进行分配内存。窃取内存部分实现在steal函数中。

因为整个空间并不能按CPU数进行等分,所以起始和结束都使用了PGROUNDUP宏,否则会出现某页丢失的情况。

void
freerange(int cpuId,void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
      struct run *r = (struct run*)p;
      r->next = kmem[cpuId].freelist;
      kmem[cpuId].freelist = r;
  }
}

修改kinit()

这边主要就是锁的初始化以及空闲列表的分配。

void
kinit()
{
    initlock(&allLock,"all");
    uint64 partLen = (PHYSTOP - (uint64)end)>>3;
    for(uint64 i = 0;i < NCPU;i++){
        initlock(&kmem[i].lock, "kmem");
        void *st = (void *)PGROUNDUP((uint64) (end) + i*partLen);
        void *ed = (void *)PGROUNDUP((uint64) (end) + (i + 1)*partLen);
        freerange(i,st, ed);
    }
    
}

添加steal()

该函数用于某CPU自身空闲列表为空,向其他所有CPU窃取内存的情况。

需要在开头释放掉本CPU对应的空闲列表的锁,因为之后就是去获取全局锁,且在循环中会去获取其他CPU的锁,如果不是放自身的锁,而有另一个CPU持有了全局锁,且此时申请了本CPU对应的锁,就会产生死锁。需要在释放全局锁之前,重新获得本CPU对应的锁,来完成之后的分配操作。

循环中就是去窃取所有其他的CPU的空闲列表。

void steal(int cpuId){
    release(&kmem[cpuId].lock);
    acquire(&allLock);
    for(int i = 0;i < NCPU;i++){
        if(i == cpuId)
            continue;

        // steal.
        acquire(&kmem[i].lock);

        if(kmem[i].freelist){
            struct run* s = kmem[i].freelist;
            while(s->next)
                s = s->next;
            s->next = kmem[cpuId].freelist;
            kmem[cpuId].freelist = kmem[i].freelist;
            kmem[i].freelist = 0;
        }

        release(&kmem[i].lock);
    }

    acquire(&kmem[cpuId].lock);
    release(&allLock);
}

修改kfree()和kalloc()

kfree()和kalloc()只需要增加一个中断关闭和打开,还有锁就可以。

void
kfree(void *pa)
{
  struct run *r;
  push_off();
  int cpuId = cpuid();

  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;

  acquire(&kmem[cpuId].lock);
  r->next = kmem[cpuId].freelist;
  kmem[cpuId].freelist = r;
  release(&kmem[cpuId].lock);
  pop_off();
}

void *
kalloc(void)
{
    push_off();
  struct run *r;
  int cpuId = cpuid();
  acquire(&kmem[cpuId].lock);
  r = kmem[cpuId].freelist;
  if(!r){
      steal(cpuId);
      r = kmem[cpuId].freelist;
  }
  if(r)
    kmem[cpuId].freelist = r->next;
  release(&kmem[cpuId].lock);
  pop_off();

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

2 Buffer cache

这一半作业是独立于前一半的;不管你是否完成了前一半,你都可以完成这一半(并通过测试)。

如果多个进程集中使用文件系统,它们可能会争夺bcache.Lock,用于保护kernel/bio.c中的磁盘块缓存。Bcachetest创建了几个进程,这些进程重复读取不同的文件,会cache.lock上产生竞争;它的输出看起来像以下这样(在你完成这个实验之前):

$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 0 #acquire() 33035
lock: bcache: #fetch-and-add 16142 #acquire() 65978
--- top 5 contended locks:
lock: virtio_disk: #fetch-and-add 162870 #acquire() 1188
lock: proc: #fetch-and-add 51936 #acquire() 73732
lock: bcache: #fetch-and-add 16142 #acquire() 65978
lock: uart: #fetch-and-add 7505 #acquire() 117
lock: proc: #fetch-and-add 6937 #acquire() 73420
tot= 16142
test0: FAIL
start test1
test1 OK

您可能会看到不同的输出,但bcache锁的获取循环迭代次数会很高。如果你看一下kernel/bio.c中的代码,你会看到bcache.Lock保护缓存的块缓冲区列表,每个块缓冲区中的引用计数(b->refcnt),以及缓存块的标识(b->dev和b->blockno)。

修改块缓存,使bcache中所有锁的获取循环迭代次数在运行bcachetest时接近于零。理想情况下,块缓存中涉及的所有锁的总数应该为零,但如果总数小于500也可以。修改bget和brelse,以便bcache中不同块的并发查找和释放不太可能在锁上发生冲突(例如,不必所有块都要等待bcache.lock)。您必须保持每个块最多只能缓存一个副本。当您完成时,您的输出应该类似于下面所示(尽管不完全相同)。确保用户测试仍然通过。当你完成时,make grade会通过所有测试。
$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 0 #acquire() 32954
lock: kmem: #fetch-and-add 0 #acquire() 75
lock: kmem: #fetch-and-add 0 #acquire() 73
lock: bcache: #fetch-and-add 0 #acquire() 85
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4159
lock: bcache.bucket: #fetch-and-add 0 #acquire() 2118
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4274
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4326
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6334
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6321
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6704
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6696
lock: bcache.bucket: #fetch-and-add 0 #acquire() 7757
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6199
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4136
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4136
lock: bcache.bucket: #fetch-and-add 0 #acquire() 2123
--- top 5 contended locks:
lock: virtio_disk: #fetch-and-add 158235 #acquire() 1193
lock: proc: #fetch-and-add 117563 #acquire() 3708493
lock: proc: #fetch-and-add 65921 #acquire() 3710254
lock: proc: #fetch-and-add 44090 #acquire() 3708607
lock: proc: #fetch-and-add 43252 #acquire() 3708521
tot= 128
test0: OK
start test1
test1 OK
$ usertests
  ...
ALL TESTS PASSED

请输入所有以“bcache”开头的锁名。也就是说,您应该为每个锁调用initlock,并传递一个以“bcache”开头的名称。

减少块缓存中的争用比kalloc更加棘手,因为bcache缓冲区是在进程(以及cpu)之间真正共享的。对于kalloc,可以通过给每个CPU分配自己的分配器来消除大多数争用;这对块缓存不起作用。我们建议您使用每个哈希桶都有一个锁的哈希表在缓存中查找块数。

以下情况产生的冲突是ok的:

  • 两个进程同时使用相同的块号。bcachetest test0不会做这种事情。
  • 两个进程并发地丢失缓存,然后需要寻找未使用的块来替代。bcachetest test0不会做这种事情。
  • 两个进程同时使用哈希值冲突的块。cachetesttest0可能会遇到这件事情,但是你应当去避免发生这件事(比如改变哈希表的大小)。

bcachetest的test1用了比buffer多很多的不同的块,并且测试了很多的文件系统代码路径。

一些提示:

  • 读xv6 book的相关部分( 8.1-8.3)
  • 可以不动态改变桶的数目。用一个素数(比如13)来减少冲突的可能性。
  • 在哈希表里面查找一个buffer以及当找不到buffer时,为buffer分配一个项应当是原子的。
  • 在哈希表上面查找一个buffer以及没有缓存目标buffer时的替换操作必须是源自的。
  • 移除所有buffer的链表,使用最后一次使用的时间戳buffer(比如用 kernel/trap.c中的ticks)。这使得brelse不需要申请bcache锁,并且bget可以通过时间戳来挑选LRU block。
  • bget中的串行化遗弃策略是可以的(当缓存查找丢失时,bget中选择一个重用buffer的部分)。
  • 你的方案可能有时候需要持有两把锁;比如,在替换时可能需要持有bcache lock和一个bucket lock。确保避免死锁。
  • 当替换块时,你可能会把一个struct buf从一个桶移到另一个,因为新的块会hash到另一个桶。你可以使用一个策略:新的块hash到相同的桶。确保避免死锁。
  • 一些debug建议:实现桶锁,但保留全局bcache。在bget的开始/结束处锁定获取/释放,以序列化代码。一旦您确定它是正确的,没有竞争条件,删除全局锁并处理并发问题。您还可以运行make CPUS=1 qemu来使用一个核进行测试。

修改struct buf

实现没有用到链表,所以把指针都删了。

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 ticks;					// for LRU
  int hashBucket;				// indicate the bucket num
};

修改其余简单部分

首先是添加一个哈希表,还有一个glo_tick(一开始用的trap.c的ticks,后面发现这个有bug,可能是会超限的缘故)。

binit中初始化锁,然后把每个buf都预先固定桶。

然后下面几个函数都比较简单,只要给桶加锁即可。

#define NBUCKET 23
struct {
  struct spinlock lock;
  struct buf buf[NBUF];

} bcache;

struct spinlock hashLock[NBUCKET];
uint glo_tick = 0;


void
binit(void)
{
  struct buf *b;

  initlock(&bcache.lock, "bcache");
  for(int i = 0;i < NBUCKET;i++)
      initlock(hashLock+i,"bcache");
  for(int i = 0;i < NBUF;i++){
      bcache.buf[i].hashBucket = i%NBUCKET;
      bcache.buf[i].ticks = getTicks();
  }


  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    initsleeplock(&b->lock, "buffer");
  }
}

void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);

  acquire(&hashLock[b->hashBucket]);
  b->refcnt--;
  b->ticks = glo_tick++;
  release(&hashLock[b->hashBucket]);
}

void
bpin(struct buf *b) {
  acquire(&hashLock[b->hashBucket]);
  b->refcnt++;
  release(&hashLock[b->hashBucket]);
}

void
bunpin(struct buf *b) {
  acquire(&hashLock[b->hashBucket]);
  b->refcnt--;
  release(&hashLock[b->hashBucket]);
}

修改bget

第一部分是查找是否有缓存,主要就是里面有两次的确认,因为第一次比对的时候没有加锁,所以在加锁之前可能会被替换。如果在比对前加锁的话,冲突会比较大。

第二部分是执行替换策略,首先还是会重新查找一遍,因为在获得bcache之前,有可能有其他的进程刚刚替换出了同一个要查找的buf,所以为了维持最多只有一份副本的不变性,必须要先检查一遍,而这里比对只需要一次,因为此时只有本进程在bcache的临界区,拥有替换的能力。

最后是真正的替换,这里面会使用一个while循环,因为在寻找LRU buffer的时候我们并不加桶锁,因为加上去的话逻辑会很复杂。所以用一个while去做循环,如果在上面寻找到的evicP的引用数在后面处理的时候不再是0(某个进程刚好用到了准备要替换的cache buffer),就表示失败,重新查找。

static struct buf*
bget(uint dev, uint blockno)
{
    struct buf *b;

    // Is the block already cached?
    for(int i = 0;i < NBUF;i++){
        b = bcache.buf + i;
        if(b->dev == dev && b->blockno == blockno){
            // The buf may be replaced here.
            acquire(&hashLock[b->hashBucket]);


            // Check it again.
            if(b->dev == dev && b->blockno == blockno){
                b->refcnt++;
                b->ticks = glo_tick++;
                release(&hashLock[b->hashBucket]);
//                release(&bcache.lock);
                acquiresleep(&b->lock);
                return b;
            }
            // If already replaced,go to eviction.
            release(&hashLock[b->hashBucket]);
            break;
        }
    }


    acquire(&bcache.lock);

    // Check again,for some process already put the buf in.
    for(int i = 0;i < NBUF;i++){
        b = bcache.buf + i;
        if(b->dev == dev && b->blockno == blockno){
            // The buf may be replaced here.
            acquire(&hashLock[b->hashBucket]);

            b->refcnt++;
            b->ticks = glo_tick++;
            release(&hashLock[b->hashBucket]);
            release(&bcache.lock);
            acquiresleep(&b->lock);
            return b;
        }
    }

    // choose the LRU buffer.
    while(1){
        uint minTime = -1;
        struct buf *evitP = 0;
        for(int i = 0;i < NBUF;i++){
            b = bcache.buf + i;
            if(b->refcnt == 0 && minTime > b->ticks){
                evitP = b;
                minTime = b->ticks;
            }
        }
        if(evitP){
            if(evitP->refcnt != 0)
                continue;

            acquire(&hashLock[evitP->hashBucket]);
            evitP->dev = dev;
            evitP->blockno = blockno;
            evitP->valid = 0;
            evitP->refcnt = 1;
            evitP->ticks = glo_tick++;

            release(&hashLock[evitP->hashBucket]);
            release(&bcache.lock);
            acquiresleep(&evitP->lock);

            return evitP;
        }
        else
            panic("bget: no buffers");
    }
    
}

本方法实现的效率有点偏低,跟网上其他题解相比的话,主要的差别是没有真正利用哈希表的优秀的查找复杂性,而是用了扫描。

在这里插入图片描述

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值