基本思想是为每个CPU维护一个空闲列表,每个列表都有自己的锁。因为每个CPU将在不同的列表上运行,不同CPU上的分配和释放可以并行运行。主要的挑战将是处理一个CPU的空闲列表为空,而另一个CPU的列表有空闲内存的情况;在这种情况下,一个CPU必须“窃取”另一个CPU空闲列表的一部分。窃取可能会引入锁争用,但这种情况希望不会经常发生。
Memory allocator
实验要求:实现一个per CPU freelist,以减小各个进程同时调用kalloc
、kfree
造成的对kmem.lock
锁的竞争。可以调用cpuid()
函数来获取当前进程运行的CPU ID,但是要在调用前加上push_off
以关闭中断。
在kernel/kalloc.c
中,修改kmem
结构体为数组形式
struct {
struct spinlock lock;
struct run *freelist;
} kmem[NCPU];
kinit()
要循环初始化每一个kmem
的锁
void
kinit()
{
for (int i = 0; i < NCPU; i++) {
initlock(&kmem[i].lock, "kmem");
}
freerange(end, (void*)PHYSTOP);
}
kfree
将释放出来的freelist节点返回给调用kfree
的CPU
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
push_off();
int ncpu = cpuid();
acquire(&kmem[ncpu].lock);
r->next = kmem[ncpu].freelist;
kmem[ncpu].freelist = r;
release(&kmem[ncpu].lock);
pop_off();
}
在kalloc
中,当发现freelist已经用完后,需要向其他CPU的freelist借用节点
void *
kalloc(void)
{
struct run *r;
push_off();
int ncpu = cpuid();
acquire(&kmem[ncpu].lock);
r = kmem[ncpu].freelist;
if(r) {
kmem[ncpu].freelist = r->next;
}
release(&kmem[ncpu].lock);
if (!r) {
// steal other cpu's freelist
for (int i = 0; i < NCPU; i++) {
if (i == ncpu) continue;
acquire(&kmem[i].lock);
r = kmem[i].freelist;
if (r) {
kmem[i].freelist = r->next;
release(&kmem[i].lock);
break;
}
release(&kmem[i].lock);
}
}
pop_off();
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
Buffer cache
实验要求:xv6文件系统的buffer cache采用了一个全局的锁bcache.lock
来负责对buffer cache进行读写保护,当xv6执行读写文件强度较大的任务时会产生较大的锁竞争压力,因此需要一个哈希表,将buf entry以buf.blockno
为键哈希映射到这个哈希表的不同的BUCKET中,给每个BUCKET一个锁,NBUCKET
最好选择素数,这里选择13。注意:这个实验不能像上一个一样给每个CPU一个bcache
,因为文件系统在多个CPU之间是真正实现共享的,否则将会造成一个CPU只能访问某些文件的问题。
这里实验让我们可以使用ticks
作为时间戳,来代替原来的双向链表实现LRU的功能。
在kernel/bio.c
中,首先设置NBUCKET
宏定义为13,声明外部变量ticks
,修改bcache
以为每个BUCKET
设置一个链表头,并设置一个锁
#define NBUCKET 13
uint extern ticks;
struct {
struct spinlock lock[NBUCKET];
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 buf head[NBUCKET];
} bcache;
修改kernel/buf.h
中的buf
结构体,删除struct buf *prev
,即将双向链表变为单向链表,添加uint time
这个成员变量作为时间戳。
实现一个简单的哈希函数
int hash (int n) {
int result = n % NBUCKET;
return result;
}
修改binit
函数,为每个bcache.lock
以及b->lock
进行初始化,并将所有buf
先添加到bucket 0哈希表中
void
binit(void)
{
struct buf *b;
for (int i = 0; i < NBUCKET; i++) {
initlock(&bcache.lock[i], "bcache");
}
bcache.head[0].next = &bcache.buf[0];
// for initialization, append all bufs to bucket 0
for (b = bcache.buf; b < bcache.buf+NBUF-1; b++) {
b->next = b+1;
initsleeplock(&b->lock, "buffer");
}
}
修改bget
,先查找当前哈希表中有没有和传入参数dev
、blockno
相同的buf
。先要将blockno
哈希到一个id值,然后获得对应id值的bcache.lock[id]
锁,然后在这个bucket id哈希链表中查找符合对应条件的buf
,如果找到则返回,返回前释放掉bcache.lock[id]
,并对buf
加sleeplock。
int id = hash(blockno);
acquire(&bcache.lock[id]);
b = bcache.head[id].next;
while (b) {
if (b->dev == dev && b->blockno == blockno) {
b->refcnt++;
if (holding(&bcache.lock[id]))
release(&bcache.lock[id]);
acquiresleep(&b->lock);
return b;
}
b = b->next;
}
如果没有找到对应的buf
,需要在整个哈希表中查找LRU(least recently used)buf
,将其替换掉。这里由于总共有NBUCKET
个哈希表,而此时一定是持有bcache.lock[id]
这个哈希表的锁的,因此当查找其他哈希表时,需要获取其他哈希表的锁,这时就会有产生死锁的风险。风险1:查找的哈希表正是自己本身这个哈希表,在已经持有自己哈希表锁的情况下,不能再尝试acquire
一遍自己的锁。风险2:假设有2个进程同时要进行此步骤,进程1已经持有了哈希表A的锁,尝试获取哈希表B的锁,进程2已经持有了哈希表B的锁,尝试获取哈希表A的锁,同样会造成死锁,因此要规定一个规则,是的当持有哈希表A的情况下如果能够获取哈希表B的锁,则当持有哈希表B锁的情况下不能够持有哈希表A的锁。该规则在can_lock
函数中实现。
int can_lock(int id, int j) {
int num = NBUCKET/2;
if (id <= num) {
if (j > id && j <= (id+num))
return 0;
} else {
if ((id < j && j < NBUCKET) || (j <= (id+num)%NBUCKET)) {
return 0;
}
}
return 1;
}
其中id
是已经持有的锁,j
是判断是否能获取该索引哈希表的锁。这个规则实际上规定了在持有某一个锁的情况下,只能再尝试获取NBCUKET/2
个哈希表锁,另一半哈希表锁是不能获取的。
确定了这个规则之后,尝试遍历所有的哈希表,通过b->time
查找LRUbuf
。先判断当前的哈希表索引是否为id
,如果是,则不获取这个锁(已经获取过它了),但是还是要遍历这个哈希表的;同时也要判断当前哈希表索引是否满足can_lock
规则,如果不满足,则不遍历这个哈希表,直接continue
。如果哈希表索引j
既不是id
,也满足can_lock
,则获取这个锁,并进行遍历。当找到了一个当前情况下的b->time
最小值时,如果这个最小值和上一个最小值不在同一个哈希表中,则释放上一个哈希表锁,一直持有拥有当前情况下LRUbuf
这个哈希表的锁,直到找到新的LRUbuf
且不是同一个哈希表为止。找到LRUbuf
后,由于此时还拥有这个哈希表的锁,因此可以直接将这个buf
从该哈希链表中剔除,并将其append到bucketid
哈希表中,修改这个锁的dev
、blockno
、valid
、refcnt
等属性。最后释放所有的锁。
int index = -1;
uint smallest_tick = __UINT32_MAX__;
// find the LRU unused buffer
for (int j = 0; j < NBUCKET; ++j) {
if (j!=id && can_lock(id, j)) {
// if j == id, then lock is already acquired
// can_lock is to maintain an invariant of lock acquisition order
// to avoid dead lock
acquire(&bcache.lock[j]);
} else if (!can_lock(id, j)) {
continue;
}
b = bcache.head[j].next;
while (b) {
if (b->refcnt == 0) {
if (b->time < smallest_tick) {
smallest_tick = b->time;
if (index != -1 && index != j && holding(&bcache.lock[index])) release(&bcache.lock[index]);
index = j;
}
}
b = b->next;
}
if (j!=id && j!=index && holding(&bcache.lock[j])) release(&bcache.lock[j]);
}
if (index == -1) panic("bget: no buffers");
b = &bcache.head[index];
while (b) {
if ((b->next)->refcnt == 0 && (b->next)->time == smallest_tick) {
selected = b->next;
b->next = b->next->next;
break;
}
b = b->next;
}
if (index != id && holding(&bcache.lock[index])) release(&bcache.lock[index]);
b = &bcache.head[id];
while (b->next) {
b = b->next;
}
b->next = selected;
selected->next = 0;
selected->dev = dev;
selected->blockno = blockno;
selected->valid = 0;
selected->refcnt = 1;
if (holding(&bcache.lock[id]))
release(&bcache.lock[id]);
acquiresleep(&selected->lock);
return selected;
}
修改brelse
。当b->refcnt==0
时,说明这个buf
已经被使用完了,可以进行释放,为其加上时间戳
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
releasesleep(&b->lock);
int id = hash(b->blockno);
acquire(&bcache.lock[id]);
b->refcnt--;
if (b->refcnt == 0) {
b->time = ticks;
}
release(&bcache.lock[id]);
}
修改bpin
和bunpin
,将bcache.lock
修改为bcache.lock[id]