lab8: locks
作业地址:Lab: locks (mit.edu)
Memory allocator (moderate)
kalloc和kfree的多次调用,多次获取kmem锁,避免race-condition出现,但降低了内存分配的效率,本实验的目的:修改内存分配的程序,提升内存分配的效率。
一个可行的方式是:每个CPU的内存分配和释放都是独立的,只需要给每个CPU分配一把锁,这样,每个CPU的内存分配释放都是独立的,进而提升内存分配的效率。当某个cpu 申请内存但没有空闲内存时,能够从其他CPU的空闲内存中“窃取内存”。
1、首先为每个CPU分配一个全局的内存空闲列表和锁,并完成初始化
struct {struct spinlock lock;struct run *freelist;
} kmem[NCPU];
void
kinit()
{char kmem_name[10];for(int i = 0; i < NCPU; i++) {snprintf(kmem_name, 10, "kmem%d", i);printf("init lock: %s\n", kmem_name);initlock(&kmem[i].lock, kmem_name);}freerange(end, (void*)PHYSTOP);
}
2、修改kfree,获取当前cpu id,对当前cpu的内存空闲链表释放一页
void
kfree(void *pa)
{struct run *r;if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)panic("kfree");push_off(); // 关中断int current_cpu = cpuid();pop_off(); // 开中断// Fill with junk to catch dangling refs.memset(pa, 1, PGSIZE);r = (struct run*)pa;acquire(&kmem[current_cpu].lock);r->next = kmem[current_cpu].freelist;kmem[current_cpu].freelist = r;release(&kmem[current_cpu].lock);
}
3、修改kalloc,获取当前cpu id, 如果当前cpu有空闲内存,则直接分配,如果没有,则尝试从其他cpu的空闲内存中“窃取”。注意及时获取和释放对应cpu的锁
void *
kalloc(void)
{struct run *r;push_off(); // 关中断int current_cpu = cpuid();pop_off(); // 开中断acquire(&kmem[current_cpu].lock);r = kmem[current_cpu].freelist;if(r) // 有空间,就直接给kmem[current_cpu].freelist = r->next;else{ //当前CPU的内存空闲列表没空间了,从其他cpu那里偷,可能会触发竞争for(int i = 0; i < NCPU; i++){if(i == current_cpu) continue; // 不包括自己acquire(&kmem[i].lock); // 获取锁r = kmem[i].freelist; if(r) // 别的cpu有空间{kmem[i].freelist = r->next; //修改别的cpu的空闲列表release(&kmem[i].lock); //释放锁break;}release(&kmem[i].lock); //释放锁}}release(&kmem[current_cpu].lock);if(r)memset((char*)r, 5, PGSIZE); // fill with junkreturn (void*)r;
}
Buffer cache (hard)
多进程同时使用文件系统,原本的bcache.lock会发生严重的锁竞争,bcache.lock用于保护磁盘区块缓存,xc6原本的设计中,多进程不能同时申请、释放磁盘缓存。
原本的设计是使用双向链表存储区块buf,每次尝试begt时,遍历链表,如果目标块已经在缓冲中,则将引用计数加1,并返回该缓存;若不存在,则选择一个最近最久未使用的(LRU),且引用计数为0的Buf块进行替换,并返回。
优化思路:建立一个blockno到buf的hash table,为每个桶单独加锁(降低锁的粒度),当两个进程同时访问的块哈希到同一个桶时,才会发生竞争,当桶中的空闲Buf不足时,从其他桶中获取Buf,并采用时间戳(全局ticks)的优化方式替换原本的双向链表。
1、为struct buf添加字段:uint prev_use_time,struct buf * next
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;uchar data[BSIZE];uint prev_use_time; // 记录上一次使用的时间,时间戳struct buf * next; // 记录下一个节点
};
2、修改全局bcache,分配prime个桶,并为每个桶分配一个锁,同时采用单链表的方式维护哈希表
#define prime 13
#define NBUCKET prime
#define GET_KEY(dev, blockno) ((blockno) % prime)struct {struct buf hash_table[NBUCKET]; // 申请prime个哈希表(通过单链表维护),总共的BUF数量为NBUFstruct spinlock lock_bucket[NBUCKET]; // 为每个桶分配一个锁struct buf buf[NBUF];} bcache;
3、初始化,初始化所有的锁,并将所有的Buf放入第一个桶,便于后续其他桶中没有buf时,进行“窃取”。
void
binit(void)
{// 初始化桶的锁char bucket_lock_name[10];for(int i = 0; i < NBUCKET; i++) {snprintf(bucket_lock_name, 10, "bcache%d", i);initlock(&bcache.lock_bucket[i], bucket_lock_name);bcache.hash_table[i].next = 0;}// 把所有的buf放入第一个桶中,类似上一个实验把所有的空闲内存放在第一个cpu中struct buf * b;for(int i = 0; i < NBUF; i++) {b = &bcache.buf[i];b->prev_use_time = 0;b->refcnt = 0;initsleeplock(&b->lock, "buffer");b->next = bcache.hash_table[0].next;bcache.hash_table[0].next = b;}}
4、 设计bget函数。(这是本实验的核心部分)
首先获取blockno哈希得到的桶的下标key,在这个桶中查找,如果命中了,就直接返回。
没有命中:
1、首先在当前的桶中查找引用计数为0的最近最久没有使用的Buf,如果有,则直接修改对应的buf,返回。
2、从0开始遍历其他桶,在每个桶中查找引用计数为0的最近最久没有使用的Buf,如果有,则先将这个buf从原本的桶中删去,再将这个buf添加到桶key,并修改buf的内容,返回。
这种设计方式,其实并不是真正意义上的LRU,因为并没有遍历全部的buf去寻找引用计数次数为0的最近最久没有使用的BUF。
static struct buf*
bget(uint dev, uint blockno)
{struct buf *b;// Is the block already cached?int key = GET_KEY(dev, blockno); // 找到桶的下标acquire(&bcache.lock_bucket[key]); //获取这个桶的锁// 遍历key对应的桶查询for(b = bcache.hash_table[key].next; b; b = b->next) {if(b->dev == dev && b->blockno == blockno) { // 命中b->refcnt++; release(&bcache.lock_bucket[key]); acquiresleep(&b->lock);return b;}}// 没有命中cache,先从当前的key对应的桶中寻找int mn = ticks, key_replace = -1;struct buf * b_prev_replace = 0;for(b = &bcache.hash_table[key]; b->next; b = b->next) {if(b->next->prev_use_time <= mn && b->next->refcnt == 0) {b_prev_replace = b;mn = b->next->prev_use_time;}}if(b_prev_replace) { // 在这个桶里找到了b = b_prev_replace->next;b->dev = dev;b->blockno = blockno;b->valid = 0;b->refcnt = 1;release(&bcache.lock_bucket[key]);acquiresleep(&b->lock);return b;}for(int i = 0; i < NBUCKET; i++) {if(i == key) continue;acquire(&bcache.lock_bucket[i]);mn = ticks;for(b = &bcache.hash_table[i]; b->next; b = b->next) {if(b->next->prev_use_time <= mn && b->next->refcnt == 0) {mn = b->next->prev_use_time;b_prev_replace = b;key_replace = i;}if(b_prev_replace && b_prev_replace->next && key_replace >= 0) { // 对bucket[i]中的buf寻找最近最少使用的buf,然后进行修改,这样其实就避免了环路的锁,但并不是真正意义上的LRUb = b_prev_replace->next;// 从旧的桶中删去b_prev_replace->next = b->next;// 在新的桶中添加b->next = bcache.hash_table[key].next;bcache.hash_table[key].next = b;b->dev = dev;b->blockno = blockno;b->valid = 0;b->refcnt = 1;release(&bcache.lock_bucket[i]);release(&bcache.lock_bucket[key]);acquiresleep(&b->lock);// printf("new buf :%d\n", blockno);return b;}}release(&bcache.lock_bucket[i]);}printf("no buffers: %d\n", blockno);release(&bcache.lock_bucket[key]);panic("bget: no buffers");
}
4、 修改brelse函数,修改b的引用计数
void
brelse(struct buf *b)
{if(!holdingsleep(&b->lock))panic("brelse");releasesleep(&b->lock);int key = GET_KEY(b->dev, b->blockno);acquire(&bcache.lock_bucket[key]);b->refcnt--;if (b->refcnt == 0) {// no one is waiting for it.// 更新时间戳b->prev_use_time = ticks;}release(&bcache.lock_bucket[key]);}
5、修改bpin和bunpin
void
bpin(struct buf *b) {int key = GET_KEY(b->dev, b->blockno);acquire(&bcache.lock_bucket[key]);b->refcnt++;release(&bcache.lock_bucket[key]);
}void
bunpin(struct buf *b) {int key = GET_KEY(b->dev, b->blockno);acquire(&bcache.lock_bucket[key]);b->refcnt--;release(&bcache.lock_bucket[key]);
}
不足之处:其实本设计有可能出现环路等待的死锁问题:当两个进程在运行前都没有被缓存。
环路等待的死锁问题参考如下:
[mit6.s081] 笔记 Lab8: Locks | 锁优化 | Miigon’s blog
假设块号 b1 的哈希值是 2,块号 b2 的哈希值是 5
并且两个块在运行前都没有被缓存
----------------------------------------
CPU1 CPU2
----------------------------------------
bget(dev, b1) bget(dev,b2)| |V V
获取桶 2 的锁 获取桶 5 的锁| |V V
缓存不存在,遍历所有桶 缓存不存在,遍历所有桶| |V V...... 遍历到桶 2| 尝试获取桶 2 的锁| |V V遍历到桶 5 桶 2 的锁由 CPU1 持有,等待释放
尝试获取桶 5 的锁|V
桶 5 的锁由 CPU2 持有,等待释放!此时 CPU1 等待 CPU2,而 CPU2 在等待 CPU1,陷入死锁!
解决方法可以参考上述这篇blog,比较复杂,后续有时间再解决。
实验对应的测试样例并没有出现这种隐晦的死锁情况,还是通过了。
== Test running kalloctest ==
$ make qemu-gdb
(68.1s)
== Test kalloctest: test1 == kalloctest: test1: OK
== Test kalloctest: test2 == kalloctest: test2: OK
== Test kalloctest: sbrkmuch ==
$ make qemu-gdb
kalloctest: sbrkmuch: OK (7.7s)
== Test running bcachetest ==
$ make qemu-gdb
(6.4s)
== Test bcachetest: test0 == bcachetest: test0: OK
== Test bcachetest: test1 == bcachetest: test1: OK
== Test usertests ==
$ make qemu-gdb
usertests: OK (102.8s) (Old xv6.out.usertests failure log removed)
== Test time ==
time: OK
Score: 70/70