作业地址:Lab: locks (mit.edu)
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 junk
return (void*)r;
}
多进程同时使用文件系统,原本的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数量为NBUF
struct 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,然后进行修改,这样其实就避免了环路的锁,但并不是真正意义上的LRU
b = 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