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,实测两种方法均可拿下满分。