MIT6.1810 Lab8

Lab: locks

实验链接

实验准备

切换到lock分支

ruby 复制代码
  $ git fetch
  $ git checkout lock
  $ make clean

Memory allocator

需求

程序 user/kalloctest 给xv6的内存分配器上强度:三个进程增加和缩小它们的地址空间,导致对kalloc和kfree的多次调用。Kalloc和kfree获取kmem.lock。对于kmem锁和其他一些锁,Kalloctest打印(作为"#test-and-set")由于试图获取另一个核心已经持有的锁而导致的acquire中的循环迭代次数。acquire中的循环迭代次数是锁争用的粗略度量。

kalloctest中锁争用的根本原因是kalloc()只有一个空闲列表,由一个锁保护。要消除锁争用,必须重新设计内存分配器,以避免使用单个锁和列表。

您的工作是为每个CPU维护一个空闲列表,每个列表都有自己的锁。在一个CPU的空闲列表为空时"窃取"另一个CPU的空闲列表的一部分。您必须为所有锁提供以"kmem"开头的名称。也就是说,您应该为每个锁调用initlock,并传递一个以"kmem"开头的名称。运行kalloctest,看看您的实现是否减少了锁争用。要检查它是否仍然可以分配所有内存,请运行usertests sbrkmuch。确保usertests -q中的所有测试都通过。make grade应该说kallocets通过了。

The solution

首先为每个cpu都配一把锁,并维护一个空闲列表

c 复制代码
struct {
  struct spinlock lock;
  struct run *freelist;
} kmem[NCPU];

在kinit中为所有锁初始化:

c 复制代码
void
kinit()
{
  for(int i=0; i<NCPU; i++)
    initlock(&kmem[i].lock, "kmem");//初始化自旋🔓
  freerange(end, (void*)PHYSTOP);
}

首先是kalloc,我们需要判断当前cpu的空闲列表是否为空,若为空则需窃取其它cpu的空闲页

c 复制代码
void *
kalloc(void)
{
  struct run *r;
  
  push_off();//关闭中断
  int idx =cpuid(); //获取当前cpuid
  acquire(&kmem[idx].lock);
  r = kmem[idx].freelist;
  if(r)//判断当前cpu是否还有空闲页
    kmem[idx].freelist = r->next;
  else{//窃取
    for(int i=0; i<NCPU; i++){
      if(i == idx)continue;
      acquire(&kmem[i].lock);
      if(kmem[i].freelist){
        r = kmem[i].freelist;
        kmem[i].freelist = r->next;
        release(&kmem[i].lock);
        break;
      }
      else release(&kmem[i].lock);
    }
  }
  release(&kmem[idx].lock);
  pop_off();//开启中断
  
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  
  return (void*)r;
}

释放物理内存页的时候只要将被释放页放入当前cpu的空闲列表即可,配合kalloc中的窃取操作我们可以暂时将该物理页交予当前cpu管理,进而减少了资源竞争。

c 复制代码
void
kfree(void *pa)
{
  ...
  push_off();//关闭中断
  int idx =cpuid(); //获取当前cpuid
  acquire(&kmem[idx].lock);
  r->next = kmem[idx].freelist;
  kmem[idx].freelist = r;
  release(&kmem[idx].lock);
  pop_off();//开启中断
}

Buffer cache

需求

如果多个进程对文件系统进行密集使用,它们很可能会争夺 bcache.lock 锁,该锁用于保护 kernel/bio.c 中的磁盘块缓存。bcachetest 创建了多个进程,这些进程反复读取不同的文件,以在 bcache.lock 上产生竞争。

修改块缓存以使得在运行 bcachetest 时,bcache 中所有锁的获取循环迭代次数接近零。理想情况下,所有与块缓存相关的锁的计数之和应为零,但如果总和小于500也是可以接受的。修改 bget 和 brelse,以使在 bcache 中进行的不同块的并发查找和释放操作不太可能在锁上产生冲突(例如,不必都等待bcache.lock)。确保 'usertests -q' 能够通过,make grade 在完成后也应该能通过所有测试。

The solution

在Buffer cache上产生大量竞争的关键原因是它使用的数据结构是一个双向循环链表,该链表被一把大锁保护,每当一个进程使用文件系统的时候都会获取该锁。

为了减少竞争我们需要修改其数据结构,改用哈希桶来存储buf,为了保留LRU功能,桶中元素使用双向循环链表进行链接,为了避免极端情况,我令每个桶中元素个数仍为NBUF

c 复制代码
#define BKSIZE 13
#define NBUFSIZE NBUF * BKSIZE
struct {
  struct buf bucket[BKSIZE];//哈希桶
  struct spinlock lock[BKSIZE];//为每个桶配一把🔒
  struct buf buf[NBUFSIZE];
} bcache;

修改binit对哈希桶进行初始化:

c 复制代码
void
binit(void)
{
  struct buf *b;
    
  for(int i = 0; i < BKSIZE; i++)
    initlock(&bcache.lock[i], "bcache");//初始化自旋锁
    
  for(int i = 0; i < BKSIZE; i++){
    bcache.bucket[i].prev = &bcache.bucket[i];//初始化双向循环链表
    bcache.bucket[i].next = &bcache.bucket[i];
    for(b = bcache.buf+NBUF*i; b < bcache.buf+NBUF*(i+1); b++){//每个桶有NBUF个元素
      b->next = bcache.bucket[i].next;
      b->prev = &bcache.bucket[i];
      initsleeplock(&b->lock, "buffer");
      bcache.bucket[i].next->prev = b;
      bcache.bucket[i].next = b;
    }
  }
}

然后是bget函数,只需多加个哈希函数寻找到指定的桶,其余逻辑与原版无异常: 注:这里使用dev(设备号) + blockno(设备盘块号)作为key

c 复制代码
static struct buf*
bget(uint dev, uint blockno)
{
  int idx = (dev+blockno)%BKSIZE; //hash_function
 
  struct buf *b;

  acquire(&bcache.lock[idx]);

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

  // Not cached.
  // Recycle the least recently used (LRU) unused buffer.
  for(b = bcache.bucket[idx].prev; b != &bcache.bucket[idx]; b = b->prev){
    if(b->refcnt == 0) {
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&bcache.lock[idx]);
      acquiresleep(&b->lock);
      return b;
    }
  }
  panic("bget: no buffers");
}

brelse、bpin、bunpin也是只需通过哈希函数找对对应的桶即可。

c 复制代码
void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);

  int idx = (b->dev+b->blockno)%BKSIZE; //hash_function

  acquire(&bcache.lock[idx]);
  b->refcnt--;
  if (b->refcnt == 0) {
    // no one is waiting for it.
    b->next->prev = b->prev;
    b->prev->next = b->next;
    b->next = bcache.bucket[idx].next;
    b->prev = &bcache.bucket[idx];
    bcache.bucket[idx].next->prev = b;
    bcache.bucket[idx].next = b;
  }
  
  release(&bcache.lock[idx]);
}

void
bpin(struct buf *b) {
  int idx = (b->dev+b->blockno)%BKSIZE; //hash_function
  acquire(&bcache.lock[idx]);
  b->refcnt++;
  release(&bcache.lock[idx]);
}

void
bunpin(struct buf *b) {
  int idx = (b->dev+b->blockno)%BKSIZE; //hash_function
  acquire(&bcache.lock[idx]);
  b->refcnt--;
  release(&bcache.lock[idx]);
}

Error

做完上述工作启动qemu,就会获得一个panic: findslot,我们可以在kernel/spinlock.c中找到该函数:

c 复制代码
static void
findslot(struct spinlock *lk) {
  acquire(&lock_locks);
  int i;
  for (i = 0; i < NLOCK; i++) {
    if(locks[i] == 0) {
      locks[i] = lk;
      release(&lock_locks);
      return;
    }
  }
  panic("findslot");
}

我们可以发现该函数是用来分配锁的,分配的数量上限为NLOCK,在该文件中可以找到其宏定义#define NLOCK 500 可知xv6运行过程中最多可同时存在500把锁。

bcache中有NBUFSIZE个buf,而每个buf都分配有一个sleeplock,结合下述搜集到的宏定义我们可以发现光是buf就占有390把锁,这直接导致系统的锁不够用了。

c 复制代码
#define MAXOPBLOCKS  10  // max # of blocks any FS op writes
#define NBUF         (MAXOPBLOCKS*3)  // size of disk block cache
#define BKSIZE 13
#define NBUFSIZE NBUF * BKSIZE

接下来就有两种解决方式,一种是提高xv6可使用锁的上限NLOCK,或者调低每个桶中buf的个数,即降低NBUF,实测两种方法均可拿下满分。

相关推荐
WZF-Sang9 小时前
Linux—进程学习-01
linux·服务器·数据库·学习·操作系统·vim·进程
Goboy21 小时前
0帧起步:3分钟打造个人博客,让技术成长与职业发展齐头并进
程序员·开源·操作系统
结衣结衣.1 天前
【Linux】Linux管道揭秘:匿名管道如何连接进程世界
linux·运维·c语言·数据库·操作系统
OpenAnolis小助手1 天前
龙蜥副理事长张东:加速推进 AI+OS 深度融合,打造最 AI 的服务器操作系统
ai·开源·操作系统·龙蜥社区·服务器操作系统·anolis os
小蜗的房子2 天前
SQL Server 2022安装要求(硬件、软件、操作系统等)
运维·windows·sql·学习·microsoft·sqlserver·操作系统
邂逅岁月4 天前
【多线程奇妙屋】 Java 的 Thread类必会小技巧,教你如何用多种方式快速创建线程,学并发编程必备(实践篇)
java·开发语言·操作系统·线程·进程·并发编程·javaee
CXDNW5 天前
【系统面试篇】进程和线程类(1)(笔记)——区别、通讯方式、同步、互斥、死锁
笔记·操作系统·线程·进程·互斥·死锁
Anemone_5 天前
MIT 6.S081 Lab3
操作系统
掘了6 天前
持久化内存 | Persistent Memory
c++·架构·操作系统
结衣结衣.7 天前
【Linux】掌握库的艺术:我的动静态库封装之旅
linux·运维·服务器·c语言·操作系统·