我见过软语
开过的花
眼酸到说梦话
——黑月光
完整代码见:SnowLegend-star/6.s081 at lock (github.com)
Memory allocator (moderate)
我们把实验说明里面的内容仔细分析下:
kalloctest中锁争用的根本原因是kalloc()有一个空闲列表,由一个锁保护。要消除锁争用,您必须重新设计内存分配器,以避免使用单个锁和列表。基本思想是为每个CPU维护一个空闲列表,每个列表都有自己的锁。因为每个CPU将在不同的列表上运行,不同CPU上的分配和释放可以并行运行。主要的挑战将是处理一个CPU的空闲列表为空,而另一个CPU的列表有空闲内存的情况;在这种情况下,一个CPU必须“窃取”另一个CPU空闲列表的一部分。窃取可能会引入锁争用,但这种情况希望不会经常发生。
您的工作是实现每个CPU的空闲列表,并在CPU的空闲列表为空时进行窃取。所有锁的命名必须以“kmem”开头。也就是说,您应该为每个锁调用initlock,并传递一个以“kmem”开头的名称。运行kalloctest以查看您的实现是否减少了锁争用。要检查它是否仍然可以分配所有内存,请运行usertests sbrkmuch。您的输出将与下面所示的类似,在kmem锁上的争用总数将大大减少,尽管具体的数字会有所不同。确保usertests中的所有测试都通过。评分应该表明考试通过。
感觉这两次的hints十分鸡肋,也权且看看
您可以使用kernel/param.h中的常量NCPU
1、让freerange将所有可用内存分配给运行freerange的CPU。
看起来这样是不妥的,但是引入“steal”机制就合理了:当前是CPU0在运行,则CPU0获取了所有空闲内存。此时系统切换到CPU1上运行,但是已经没有多余的空闲空间分配给CPU1了。什么时候CPU1的freelist会不够呢?调用kalloc()时。但是CPU1可以“steal”CPU0的freelist,那么“steal”的空间大小应该是多少呢?尝试下“一半”。
在kfee()中操作
2、函数cpuid返回当前的核心编号,但只有在中断关闭时调用它并使用其结果才是安全的。您应该使用push_off()和pop_off()来关闭和打开中断。
借鉴myproc()
3、看看kernel/sprintf.c中的snprintf函数,了解字符串如何进行格式化。尽管可以将所有锁命名为“kmem”。
要给每个CPU都分配一个“kmem”锁吗?对,给每个CPU都分配一个锁。
开始我的想法是先把所有的空间都分配给一个CPU,然后别的CPU的freelist为空时就从第一个不空的CPU的freelist截取后半段,同时这个freelist得大于等于两个PAGE。但是这个想法实现时却遇到了各种bug,我遂采用了最简单的一种——即从第一个不空的CPU的freelist中取一个节点就行。这个方案确实是方便不少,但是我感觉不同CPU进行“steal”的操作太多了,可能影响性能。我想到的那种方案则是遍历不同CPU的freelist需要时间,但是“steal”的操作会大大减少。两者之间的取舍就仁者见仁智者见智了。
By the way,通过所有测试后我又回来完成了“二分法”,不愧是我。真乃百折不挠之士。
我还发现了了个有趣的问题,对freelist的每个page进行初始化后
查看每个块的内容如下:
按理说每个PGSIZE的大小是4K,那应该是000 0000 0101,这里怎么会是上述内容呢?而且两个84215045拼起来就可以得到*r,各种门道也是让我百思不得其解。
遇到的最后一个问题也是离谱,我用“二分法”的思路测试时一共只有32498个page,但是naive方案测试却有32499个page。
仔细查看了下代码发现问题出在这里:
这样问题就明了了。就算空闲的CPU的freelist只有一个page,当前的CPU也应该可以把这个page给据为己有。
kinit()
void
kinit()
{
initlock(&kmem.lock, "kmem");
int i;
for(i=0; i<NCPU; i++){
initlock(&kmem[i].lock, "kmem");
kmem[i].sz=0;
}
freerange(end, (void*)PHYSTOP);
}
kfree()
......
push_off();
//初始状态下,当前运行的cpuid一下子获得所有的空闲空间
// int cpuid=cpuid_Modify();
int cpuid_Cur=cpuid();
acquire(&kmem[cpuid_Cur].lock);
r->next = kmem[cpuid_Cur].freelist;
kmem[cpuid_Cur].freelist = r;
kmem[cpuid_Cur].sz++;
release(&kmem[cpuid_Cur].lock);
pop_off();
}
kalloc()
kalloc(void)
{
struct run *r;
struct run *node_steal;
int i;
push_off();
// int cpuid=cpuid_Modify();
int cpuid_Cur=cpuid();
acquire(&kmem.lock);
r = kmem.freelist;
acquire(&kmem[cpuid_Cur].lock);
r = kmem[cpuid_Cur].freelist;
//如果当前CPU的freelist为空
if(!r)
{
for (i = 0; i < NCPU; i++)
{
if (i == cpuid_Cur)
continue;
acquire(&kmem[i].lock);
// if (kmem[i].freelist && kmem[i].freelist->next)
if (kmem[i].freelist)
{
// 从第一个不空的CPU的freelist截取后半段,同时这个freelist得大于等于两个PAGE
struct run *tmp = kmem[i].freelist;
node_steal = kmem[i].freelist;
for (int j = 1; j < kmem[i].sz / 2; j++) //西巴,千算万算没看懂这里混进去了i
{
// tmp=tmp->next;
}
// 已经遍历到了当前freelist的中间部分
kmem[i].freelist=tmp->next;
tmp->next = (void *)0; // 截断当前的freelist
r = node_steal;
kmem[i].sz-=kmem[i].sz/2;
kmem[cpuid_Cur].sz=kmem[i].sz/2;
release(&kmem[i].lock);
break; //找到合适的freelist就直接退出
}
release(&kmem[i].lock);
}
}
// r=node_steal; //可恶,这句话怎么写在if(!r)外面了
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
kmem[cpuid_Cur].freelist = r->next;
kmem[cpuid_Cur].sz--;
release(&kmem[cpuid_Cur].lock);
pop_off();
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
// struct run* xx=r;
// if(xx)
// printf("xx: %p", xx);
return (void*)r;
}
Buffer cache(hard)
不得不说对这个part的实验说明简直是一坨狗屎。这个lab的思路和multithread那个是一样的,都是利用哈希桶来细化锁的颗粒从而达到提高并发的效果。但是越看实验说明越迷糊,简直就是在画蛇添足地解释这解释那的。
我总结下就是两点要求:
1、把NBUF个缓冲块分配给NBUCKET个哈希桶中,通过给每个哈希桶进行加锁去锁的操作来提高并发性。
2、利用ticks来更好地实现LRU。开始我以为原始的代码已经实现了LRU,后来想了下发现这种LRU其实是不完整的——只有brelse后才会进行一次缓冲块释放和插入。即当前bcache.head->next的元素只是最新被释放然后插入进来的,bcache.head->prev则是最早被插入到bcache的元素。但是真正的LRU是最久未被访问的,也就是最久没有被读写的块才被evict。有可能bcache.head->prev这个元素才刚刚被访问,但是由于我们没有记录每个缓冲块的访问时间,就导致这个最近被访问的块直接被evict了。
实现的逻辑也不算复杂,但是这个part却有最为可恶的一点:如果buffer cache的代码有执行问题,没能通过bcachetest的测试,那整个qemu模拟的文件系统都会被损坏。也就是这个问题导致我不断修改bio.c却一直报相同的错误——“panic:init exiting”。
这个bug困扰了我好久好久,我从网上复制了份可以跑通的代码来运行也是这个问题。直到我开始怀疑是自己的内存出了问题,这里重新建立一个新的test文件夹立功了,我在这个文件夹新clone了一份lab,然后用自己的代码跑了遍,虽然还是有bug,但是却可以发现自己的改进确实是由效果的。
然后就是fs.img这个镜像,经过群里老哥的指点发现直接把这个文件删了就行,make qemu会生成一个新的文件系统镜像,就不用反复clone了。(原来就在lecture14就提到了这个操作)
两份一样的代码,一份是“panic:init exiting”,另一份成功执行。
至此,文件系统损坏的bug终于被解决了。
哈希桶的应用
struct Hash_Bucket{
struct spinlock lock;
struct buf head; //哈希桶内部由链表构成
};
struct {
struct spinlock lock;
struct buf buf[NBUF];
struct Hash_Bucket Hash_Bucket[NBUCKET];
// 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;
} bcache;
用到的哈希函数如下
//用blockno进行哈希映射
int Hash(int blockno){
return blockno%NBUCKET;
}
获得当前ticks的方法
//获取当前的ticks
uint get_ticks(){
uint tick_Cur;
acquire(&tickslock);
tick_Cur=ticks;
release(&tickslock);
return tick_Cur;
}
bmap()
void
binit(void)
{
struct buf *b;
// struct buf *tmp;
for(int i=0; i<NBUCKET; i++){
initlock(&bcache.Hash_Bucket[i].lock, "bcache"); //把整个缓冲区的大锁换成每个哈希桶的小锁
bcache.Hash_Bucket[i].head.prev=&bcache.Hash_Bucket[i].head;
bcache.Hash_Bucket[i].head.next=&bcache.Hash_Bucket[i].head;
}
// Create linked list of buffers
for(int i=0; i<NBUF; i++){
b = &bcache.buf[i];
// struct Hash_Bucket bucket = bcache.Hash_Bucket[i%NBUCKET]; //bucket[i%NBUCKET]可以拥有buf[i]这个缓冲区 这句话直接坏事
b->ticks=0;
b->next = bcache.Hash_Bucket[i%NBUCKET].head.next;
b->prev = &bcache.Hash_Bucket[i%NBUCKET].head;
initsleeplock(&b->lock, "buffer");
bcache.Hash_Bucket[i%NBUCKET].head.next->prev=b;
bcache.Hash_Bucket[i%NBUCKET].head.next=b;
// printf("bucket[%d]的内容如下:", i%NBUCKET);
// for(tmp=bcache.Hash_Bucket[i%NBUCKET].head.next; tmp!=&bcache.Hash_Bucket[i%NBUCKET].head; tmp=tmp->next){
// printf("%p ",tmp);
// }
// printf("\n");
}
// printf("\n");
// for(int i=0; i<NBUCKET; i++){
// // struct buf *tmp=&bcache.Hash_Bucket[i].head;
// printf("bucket[%d]的内容如下:", i);
// for(b=bcache.Hash_Bucket[i].head.next; b!=&bcache.Hash_Bucket[i].head; b=b->next){
// printf("%p ",b);
// }
// printf("\n");
// }
}
bget()
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
int index=Hash(blockno); //查找当前块应该在哪个bucket里面
// // Is the block already cached?
acquire(&bcache.Hash_Bucket[index].lock); //先获得这个bucket的自旋锁
//从bucket真正的第一个buf元素开始查找
// struct buf tmp= bcache.Hash_Bucket[index].head; //增加代码可读性
// printf("tmp->next的值是: %p\n", tmp.next);
//查找当前块是不是已经被缓存了
for(b = bcache.Hash_Bucket[index].head.next ; b != &bcache.Hash_Bucket[index].head; b = b->next){
if(b->dev==dev && b->blockno==blockno){
b->refcnt++;
release(&bcache.Hash_Bucket[index].lock);
acquiresleep(&b->lock); //这句话的目的是什么?
return b;
}
}
// printf("卡在“查找当前块是不是已经被缓存了” \n");
// Not cached.
// Recycle the least recently used (LRU) unused buffer.
// tmp= bcache.Hash_Bucket[index].head;
// printf("tmp->next的值是: %p\n", tmp.next);
for(b = bcache.Hash_Bucket[index].head.next ; b != &bcache.Hash_Bucket[index].head; b = b->next){
if(b->refcnt == 0) {
b->dev = dev;
b->blockno = blockno;
b->valid = 0;
b->refcnt = 1;
release(&bcache.Hash_Bucket[index].lock);
acquiresleep(&b->lock);
return b;
}
}
//如果当前bucket没有buf了,从其他bucket里面抢夺一个用用(:
//这里的思路和kalloc的修改差不多
struct buf *buf_available=0;
int buf_from=-1; //记录这块空闲的buf来自于哪个bucket
for(int i=0; i<NBUCKET; i++){
if(i==index)
continue;
acquire(&bcache.Hash_Bucket[i].lock);
for(b = bcache.Hash_Bucket[i].head.next; b != &bcache.Hash_Bucket[i].head; b = b->next){
if(b->refcnt==0){
if(buf_available==0 || buf_available->ticks > b->ticks){ //利用LRU选择一个空闲块,效率是真低 用ticks简直是画蛇添足
buf_available=b;
buf_from=i;
}
}
}
release(&bcache.Hash_Bucket[i].lock);
}
if(buf_available){
buf_available->dev = dev; //原来这里写成b->dev了,可恶啊啊啊啊
buf_available->blockno = blockno;
buf_available->valid = 0;
buf_available->refcnt = 1;
//从原来的bucket中取出这个buf
acquire(&bcache.Hash_Bucket[buf_from].lock);
buf_available->prev->next = buf_available->next;
buf_available->next->prev = buf_available->prev;
release(&bcache.Hash_Bucket[buf_from].lock);
//把这个空闲buf插入到bucket[index]中 头插法
buf_available->next = bcache.Hash_Bucket[index].head.next;
buf_available->prev = &bcache.Hash_Bucket[index].head;
bcache.Hash_Bucket[index].head.next->prev=buf_available;
bcache.Hash_Bucket[index].head.next=buf_available;
release(&bcache.Hash_Bucket[index].lock);
acquiresleep(&buf_available->lock);
return buf_available;
}
panic("bget: no buffers");
}
最后贴一张通过的截图
注:
Spinlock主要是完成互斥作用,相当于mutex=1
Sleeplock更多的是类似于生产者-消费者问题,producer_mutex=n这种。