0%

Lab Lock

Lab Lock

前置知识

一、 哈希表(略),双链表(略)

二、 buffer cache的LRU实现

buffer cache是一个双链表。每次新加入一个块,就会把它插到头指针后面,每次需要替换掉一个块,就选择一个头指针前面(也就是最后一个元素)进行替换。每一个块都有一个引用数,代表当前是否有进程在使用它。

Task: Memory allocator

这个task要求我们给每个cpu都分配一个freelist,来减少多个cpu对一个freelist修改的阻塞问题,还是比较简单的。

  1. 我们需要把整个freelist分给所有的cpu。

    1
    2
    3
    4
    5
    // kalloc.c
    struct {
    struct spinlock lock;
    struct run *freelist;
    } kmem[NCPU];
  2. kfree中,释放的时当前cpu的freepage。注意中断的开闭。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ...
    r = (struct run*)pa;
    push_off();

    int cpunum = cpuid();
    acquire(&kmem[cpunum].lock);
    r->next = kmem[cpunum].freelist;
    kmem[cpunum].freelist = r;
    release(&kmem[cpunum].lock);

    pop_off();
    ...
  3. 在kalloc中,我们需要在当前cpu的freelist中来分配。如果当前freelist用光了,那么就需要去别的cpu偷取。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void *
    kalloc(void)
    {
    struct run *r;
    push_off();
    int cpunum = cpuid();
    acquire(&kmem[cpunum].lock);
    r = kmem[cpunum].freelist;
    if(r)
    kmem[cpunum].freelist = r->next;
    release(&kmem[cpunum].lock);

    if (!r) {
    r = ksteal(cpunum); // steal page
    }
    pop_off();
    if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
    return (void*)r;
    }
  4. 偷取我封装成了一个函数。具体实现便是循环所有的cpu,如果找到可用的freelist那就返回它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct run* ksteal(int cpunum)
    {
    struct run* r;
    for (int i = 1; i < NCPU; i++)
    {
    //if (i == cpunum) continue;
    int crcpu = (cpunum + i) % NCPU;

    acquire(&kmem[crcpu].lock);
    r = kmem[crcpu].freelist;
    if (r) kmem[crcpu].freelist = r->next; // if succeed
    release(&kmem[crcpu].lock);

    if (r) break;
    }
    return r;
    }

Task Buffer cache

这个task就很难了。它需要我们像上一个task一样优化cache,但是不是根据cpu,而是实现一个hash。思路不复杂,但有非常多的地方容易出bug。在这个task里,有两个可行的方法来实现LRU(least-recently-used)算法,分别是时间戳和头插法。我都进行了实现并通过了make grade,但因为时间戳比较简单,所以只在最后给出核心代码供参考,这里主要是实现了头插法。(因为按理说头插法跑得要稍微快一点点)

  1. 首先定义并声明一个bcache对象。注意,这里面有一个大锁和一些小锁。至于为何需要大锁,在后面会有说明。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct hashbuf {
    struct buf head;
    struct spinlock lock;
    };

    struct {
    //struct spinlock lock;
    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 hashbuf buckets[NBUCKET]; // 需要去param.h把这个常量定义为13,当然也可以是什么别的质数
    //struct buf head;
    struct spinlock hashlock;
    } bcache;
  2. 把锁和所有双链表初始化一下。同时,将所有的cache在一开始都挂载到0号链表上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    void
    binit(void)
    {
    struct buf *b;
    char lockname[16];

    //initlock(&bcache.lock, "bcache");
    initlock(&bcache.hashlock, "hashlock");
    for (int i = 0; i < NBUCKET; i++)
    {
    snprintf(lockname, sizeof(lockname), "bcache-%d", i);
    initlock(&bcache.buckets[i].lock, lockname);
    // Create linked list of buffers
    bcache.buckets[i].head.prev = &bcache.buckets[i].head;
    bcache.buckets[i].head.next = &bcache.buckets[i].head;
    }

    //bcache.head.prev = &bcache.head;
    //bcache.head.next = &bcache.head;
    for(b = bcache.buf; b < bcache.buf + NBUF; b++){
    b->next = bcache.buckets[0].head.next;
    b->prev = &bcache.buckets[0].head;
    initsleeplock(&b->lock, "buffer"); // doubly linked list init
    bcache.buckets[0].head.next->prev = b;
    bcache.buckets[0].head.next = b;
    }
    //printf("binit ok\n");
    }
  3. 写一个小辅助函数,并修改一下bpin和bunpin。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int hash(int x)
    {
    return x % NBUCKET;
    }
    ...
    void
    bpin(struct buf *b) {
    int bid = hash(b->blockno);
    acquire(&bcache.buckets[bid].lock);
    b->refcnt++;
    release(&bcache.buckets[bid].lock);
    }

    void
    bunpin(struct buf *b) {
    int bid = hash(b->blockno);
    acquire(&bcache.buckets[bid].lock);
    b->refcnt--;
    release(&bcache.buckets[bid].lock);
    }
  4. 修改breles,运用头插法将新使用过的块插到最前面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    void
    brelse(struct buf *b)
    {
    if(!holdingsleep(&b->lock))
    panic("brelse");

    int bid = hash(b->blockno);

    releasesleep(&b->lock);

    acquire(&bcache.buckets[bid].lock);
    //acquire(&bcache.lock);
    b->refcnt--;

    //acquire(&tickslock);
    //b->tstamp = ticks;
    //release(&tickslock);

    if (b->refcnt == 0) {
    // no one is waiting for it.
    b->next->prev = b->prev;
    b->prev->next = b->next;
    b->next = bcache.buckets[bid].head.next;
    b->prev = &bcache.buckets[bid].head;
    bcache.buckets[bid].head.next->prev = b;
    bcache.buckets[bid].head.next = b;
    }

    release(&bcache.buckets[bid].lock);
    }
  5. 修改bget,重点注意not cashed的时候,需要去其他桶里面偷取。同样,steal封装成了函数。因为我们用头插法实现,所以寻找是否cashed的时候只需要从后往前找。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    static struct buf*
    bget(uint dev, uint blockno)
    {
    struct buf *b;
    int bid = hash(blockno);

    acquire(&bcache.buckets[bid].lock);

    // Is the block already cached?
    for(b = bcache.buckets[bid].head.next; b != &bcache.buckets[bid].head; b = b->next){
    if(b->dev == dev && b->blockno == blockno){ // cashed
    b->refcnt++;

    release(&bcache.buckets[bid].lock);
    acquiresleep(&b->lock);
    return b;
    }
    }

    // Not cached.
    // Recycle the least recently used (LRU) unused buffer.
    for (int j = 0; j < NBUCKET; j++)
    {
    int i = (bid + j) % NBUCKET;
    // 一号位置
    for(b = bcache.buckets[i].head.prev; b != &bcache.buckets[i].head; b = b->prev){
    if(b->refcnt == 0) {
    if (i != bid)
    bsteal(b, i, bid);

    b->dev = dev;
    b->blockno = blockno;
    b->valid = 0;
    b->refcnt = 1;
    release(&bcache.buckets[bid].lock);
    acquiresleep(&b->lock);
    return b;
    }
    }
    }
    panic("bget: no buffers");
    }
    ...
    void bsteal(struct buf *b, int i, int bid) // 把它放到bget前面
    {
    acquire(&bcache.hashlock);
    b->next->prev = b->prev;
    b->prev->next = b->next;
    release(&bcache.hashlock);
    //printf("buckets %d released p6\n", i);
    //release(&bcache.buckets[i].lock);

    b->next = bcache.buckets[bid].head.next;
    b->prev = &bcache.buckets[bid].head;
    bcache.buckets[bid].head.next->prev = b;
    bcache.buckets[bid].head.next = b;
    return;
    //printf("bsteal ok\n");
    }

    首先,解释一下为何需要大锁。若一个cpu持有当前桶的锁,想要steal的时候,它必须申请steal桶的锁,假如此时恰好另一个cpu持有steal桶的锁,想要到当前桶来steal,那就会造成deadlock。所以我们如果想要steal,必须拿到大锁和小锁后才能执行。一开始,我只实现了小锁,并学习了网友的特殊做法来避免死锁,通过了所有test,但是这种方法有较大的风险。比如,当你从想要偷取的时候设置的是从0号桶开始寻找,就非常容易出问题。代码如下,供大家思考:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 这段程序放置在1号位置
    if (i != bid) {
    if (!holding(&bcache.buckets[i].lock)) {
    acquire(&bcache.buckets[i].lock);
    //printf("buckets %d acquired p3\n", i);
    //printf("%d\n", bid);
    }
    else
    continue; // avoid deadlock, but little bug
    }

    其次,关于如果没有cashed,为何偷取时从bid + 1开始寻找而不是0开始寻找。这是一个尚未解决的问题。我发现当从0开始寻找的时候,单独跑bcachetest会过,但是make grade时bcachetest会爆test1。怀疑有两点,一点是一开始的挂载问题,第二点是从固定桶开始寻找可能出现分配问题。希望有高人能够解决这个问题。

  6. 最后,挂上时间戳实现的核心代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    static struct buf*
    bget(uint dev, uint blockno)
    {
    struct buf *b;
    int bid = hash(blockno);

    acquire(&bcache.buckets[bid].lock);
    //printf("buckets %d acquired p1\n", bid);

    // Is the block already cached?
    for(b = bcache.buckets[bid].head.next; b != &bcache.buckets[bid].head; b = b->next){
    if(b->dev == dev && b->blockno == blockno){ // cashed
    b->refcnt++;

    acquire(&tickslock);
    b->tstamp = ticks;
    release(&tickslock);

    //printf("buckets %d released p2\n", bid);
    release(&bcache.buckets[bid].lock);

    acquiresleep(&b->lock);
    return b;
    }
    }

    b = 0;
    struct buf* temp;
    // Not cached.
    // Recycle the least recently used (LRU) unused buffer.
    for (int j = 0; j < NBUCKET; j++)
    {
    int i = (bid + j) % NBUCKET;

    uint mx = 0xffffffff;
    for(temp = bcache.buckets[i].head.prev; temp != &bcache.buckets[i].head; temp = temp->prev)
    {
    if (temp->refcnt == 0 && temp->tstamp < mx) {
    b = temp;
    mx = temp->tstamp;
    }
    }
    if(b) {
    if (i != bid)
    bsteal(b, i, bid); // steal

    b->dev = dev;
    b->blockno = blockno;
    b->valid = 0;
    b->refcnt = 1;

    acquire(&tickslock);
    b->tstamp = ticks;
    release(&tickslock);

    //printf("buckets %d released p4 \n", bid);
    release(&bcache.buckets[bid].lock);
    acquiresleep(&b->lock);
    //printf("i go s1 %d\n", i);
    return b;
    }
    }
    panic("bget: no buffers");
    }

后记

其实在实现第二个task时,如果在cache中找到了需要找的数据,那被找到的这个块也应该移到链表的头部,但是当时没有想到这个问题,后来在研究LRU的时候才发现。虽然这样会大量移动指针,但一定会再次提高效率。