lab8 lock
lock
实验结果

实验
Memory allocator
就如hints上面提示的,一个cpu一个单向链表。
一个cpu上没有可以malloc的空间,那么用一个for循环从别的cpu链表上找可用的page。
唯一的缺点是,for循环时,你得到前面那一个链表没有用的空间时,由于你释放掉,然后获取别的cpu锁,你认为前面的cpu链表上的page是没有malloc的,但其实在那一刻是有的,下面我Buffer cache的代码也有类似问题
// Physical memory allocator, for user processes,
// kernel stacks, page-table pages,
// and pipe buffers. Allocates whole 4096-byte pages.
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "riscv.h"
#include "defs.h"
void freerange(void *pa_start, void *pa_end);
extern char end[]; // first address after kernel.
// defined by kernel.ld.
struct run {
struct run *next;
};
struct _kmem{
struct spinlock lock;
struct run *freelist;
};
struct _kmem kmem[NCPU] = {0};
void
kinit()
{
for (int i = 0; i < NCPU; i++)
{
initlock(&(kmem[i].lock), "kmem");
}
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
int id;
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();
id = cpuid();
acquire(&(kmem[id].lock));
r->next = kmem[id].freelist;
kmem[id].freelist = r;
release(&(kmem[id].lock));
pop_off();
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;
int id;
/*
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
*/
push_off();
id = cpuid();
acquire(&(kmem[id].lock));
r = kmem[id].freelist;
if (r)
{
kmem[id].freelist = r->next;
}
else
{
for(int i = 0; i < NCPU; i++)
{
if (i != id)
{
acquire(&(kmem[i].lock));
r = kmem[i].freelist;
if (r)
{
kmem[i].freelist = r->next;
release(&(kmem[i].lock));
break;
}
release(&(kmem[i].lock));
}
}
}
release(&(kmem[id].lock));
pop_off();
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
Buffer cache
思路
可以把问题拆分成4个主要问题+一些限制条件+一些编程技巧。
4个问题分别是怎么初始化,bget()里怎么查找匹配的cache block,如果不满足那么要怎么替换,怎么释放。
限制条件是,应该要满足局部性原理,即最近访问的cache block被替换的可能性最低。
具体思路。
对于dev,blockno,采用的哈希函数是key = (((dev % 13)^2 % 13)+blockno) % 13,具体细节可以查看GetCacheKey()函数,对于可能出现的散列冲突问题,我是采用每个hash都代表一个单链表的方式,这样就不会出现一个hash值只能对应一个元素的情况。
每个hash bucket都有一个锁,涉及这个bucket不变时,都调用这个自旋锁。然后整体上一个自旋锁,对于一个bucket没有可用块时,要去别的块偷可用块时,要使用这个全局锁。
为了维护局部性,以及方便。每个bucket中的block,离head越远,就代表越近时使用。所以替换时,就从bucket的头来寻找,离头越近,就越远时使用,从一定角度上,维护了局部性。当然从别的块偷时,也是先从头开始偷。对于题目中用trap.c中的tick,发现tick是uint型,即32位,tick越大,就是越近时使用,但是32为的时候呢?1其实是最近的,但反而比其他小很多,而且假设1ms一个tick,只能支持49天左右,系统运行49天后就出现了bug?还是别用了,基于以上考虑,我就没用tick。
寻找就是获得hansh key,然后对着那个hash bucket的单链表上比较dev和blockno,找到就返回。找不到就执行替换策略。
替换时,首先搜索bucket的链表。如果没有,那么再考虑从别的块上偷。
具体的策略是,上bucket锁,然后查找是否已经有块,有的话释放锁然后返回,没有的话,查看这个bucket上是否有空闲的block,有的话用这个然后释放锁,没有的话,也要释放锁。然后获取全局锁,再获取bucket锁(没找到是要先释放bucket锁,然后获取全局锁,再获取bucket锁,否则先获取bucket锁,再获取大锁,由于下面别的进程还可能用到bucekt锁,会造成死锁),然后重复查找是不是有对应的block,然后再看是不是这个bucket有ref为0的block(因为你中间释放掉bucket锁,造成条件可能改变,如不这样,会造成freeing free block),然后在从别的bucket获取锁,偷,然后释放锁。具体可以看代码
相关代码
// Buffer cache.
//
// The buffer cache is a linked list of buf structures holding
// cached copies of disk block contents. Caching disk blocks
// in memory reduces the number of disk reads and also provides
// a synchronization point for disk blocks used by multiple processes.
//
// Interface:
// * To get a buffer for a particular disk block, call bread.
// * After changing buffer data, call bwrite to write it to disk.
// * When done with the buffer, call brelse.
// * Do not use the buffer after calling brelse.
// * Only one process at a time can use a buffer,
// so do not keep them longer than necessary.
#include "types.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "riscv.h"
#include "defs.h"
#include "fs.h"
#include "buf.h"
#define CACHEBUCKET_NUM (13U)
struct __Bucket
{
struct buf head;
struct spinlock lock;
};
struct {
struct spinlock lock;
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 __Bucket Bucket[CACHEBUCKET_NUM];
} bcache;
static uint64 GetCacheKey(uint dev, uint blockno);
static int InsertEnd(struct buf* ListHead, struct buf* Buf);
static uint64 RemovePointFromList(struct buf* ListHead, struct buf* Buf);
static int InsertNewEnd(struct buf* ListHead, struct buf* Buf);
void
binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
for(int i = 0; i < CACHEBUCKET_NUM; i++)
{
bcache.Bucket[i].head.next = 0;
initlock(&(bcache.Bucket[i].lock), "bcache");
initsleeplock(&(bcache.Bucket[i].head.lock), "buffer");
}
b = &(bcache.Bucket[0].head);
for(int i = 0; i < NBUF; i++)
{
b->next = &(bcache.buf[i]);
b = b->next;
}
b->next = 0;
return;
}
// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
uint64 HashValue = 0;
HashValue = GetCacheKey(dev, blockno);
acquire(&(bcache.Bucket[HashValue].lock));
for(b = bcache.Bucket[HashValue].head.next; b != 0; b = b->next)
{
if ((b->dev == dev) && (b->blockno == blockno))
{
b->refcnt++;
release(&(bcache.Bucket[HashValue].lock));
acquiresleep(&b->lock);
return b;
}
}
for(b = bcache.Bucket[HashValue].head.next; b != 0; b = b->next)
{
if (b->refcnt == 0)
{
b->dev = dev;
b->blockno = blockno;
b->valid = 0;
b->refcnt = 1;
release(&(bcache.Bucket[HashValue].lock));
acquiresleep(&b->lock);
return b;
}
}
release(&(bcache.Bucket[HashValue].lock));//没有在这个bucket找到对应的block
acquire(&(bcache.lock));
acquire(&(bcache.Bucket[HashValue].lock));
/*
会出现一个cpu运行完第一步没找到,卡在第二步的bucket锁,另一个cpu运行完第二步,为同样的dev和blockno再分配一块不行
会导致一个block有两个 cache block
*/
for(b = bcache.Bucket[HashValue].head.next; b != 0; b = b->next)
{
if ((b->dev == dev) && (b->blockno == blockno))
{
b->refcnt++;
release(&(bcache.Bucket[HashValue].lock));
release(&(bcache.lock));
acquiresleep(&b->lock);
return b;
}
}
for(b = bcache.Bucket[HashValue].head.next; b != 0; b = b->next)
{
if (b->refcnt == 0)
{
b->dev = dev;
b->blockno = blockno;
b->valid = 0;
b->refcnt = 1;
release(&(bcache.Bucket[HashValue].lock));
release(&(bcache.lock));
acquiresleep(&b->lock);
return b;
}
}
//如果找不到空闲的,去别的链表偷
//使用时间越远的block距离head越近。
for(uint64 i = 0; i < CACHEBUCKET_NUM; i++)
{
if (i != HashValue)//自旋锁非递归锁
{
acquire(&(bcache.Bucket[i].lock));
for(b = bcache.Bucket[i].head.next; b != 0; b = b->next)
{
if (b->refcnt == 0)
{
b->dev = dev;
b->blockno = blockno;
b->valid = 0;
b->refcnt = 1;
if (RemovePointFromList(&(bcache.Bucket[i].head), b) < 0)
{
panic("bget unkown error");
}
if (InsertNewEnd(&(bcache.Bucket[HashValue].head), b) < 0)
{
panic("bget unkown error");
}
release(&(bcache.Bucket[i].lock));
release(&(bcache.Bucket[HashValue].lock));
release(&(bcache.lock));
acquiresleep(&b->lock);
return b;
}
}
release(&(bcache.Bucket[i].lock));
}
}
release(&(bcache.Bucket[HashValue].lock));
release(&(bcache.lock));
//发生系统严重错误,找不到可以ref的
panic("bget: no buffers");
}
// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{
struct buf *b;
b = bget(dev, blockno);
if(!b->valid) {
virtio_disk_rw(b, 0);
b->valid = 1;
}
return b;
}
// Write b's contents to disk. Must be locked.
void
bwrite(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("bwrite");
virtio_disk_rw(b, 1);
}
// Release a locked buffer.
// Move to the head of the most-recently-used list.
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
uint64 HashValue = 0;
releasesleep(&b->lock);
HashValue = GetCacheKey(b->dev, b->blockno);
acquire(&(bcache.Bucket[HashValue].lock));
b->refcnt = b->refcnt - 1;
if (b->refcnt == 0)
{
if (InsertEnd(&(bcache.Bucket[HashValue].head), b) < 0)
{
release(&(bcache.Bucket[HashValue].lock));
panic("cache insert error");
}
}
release(&(bcache.Bucket[HashValue].lock));
}
void
bpin(struct buf *b)
{
uint64 HashValue = GetCacheKey(b->dev, b->blockno);
acquire(&(bcache.Bucket[HashValue].lock));
b->refcnt = b->refcnt + 1;
release(&(bcache.Bucket[HashValue].lock));
}
void
bunpin(struct buf *b)
{
uint64 HashValue = GetCacheKey(b->dev, b->blockno);
acquire(&(bcache.Bucket[HashValue].lock));
b->refcnt = b->refcnt - 1;
release(&(bcache.Bucket[HashValue].lock));
}
/**
* @哈希函数 key = ((a % 13)^2 + b)% 13
*/
static uint64 GetCacheKey(uint dev, uint blockno)
{
uint64 Result;
Result = dev % CACHEBUCKET_NUM;
Result = Result * Result;
Result = (Result + blockno) % CACHEBUCKET_NUM;
return Result;
}
/**
* @brief 保证Buf一定在这条链路上
*/
static int InsertEnd(struct buf* ListHead, struct buf* Buf)
{
if ((ListHead == 0) || (Buf == 0))
{
return -1;
}
struct buf* Temp0 = ListHead;
if (RemovePointFromList(ListHead, Buf) < 0)
{
panic("illegal use InsertEnd");
}
Temp0 = ListHead;
while(Temp0->next != 0)
{
Temp0 = Temp0->next;
}
Temp0->next = Buf;
Buf->next = 0;
return 0;
}
static uint64 RemovePointFromList(struct buf* ListHead, struct buf* Buf)
{
if ((ListHead == 0) || (Buf == 0))
{
return -1;
}
struct buf* Temp0 = ListHead;
struct buf* Temp1 = 0;
while((Temp0->next != Buf) && (Temp0 != 0))
{
Temp0 = Temp0->next;
}
if (Temp0 == 0)//上层非法调用该函数,Buf不在这条链表里
{
return -1;
}
Temp1 = Buf->next;
Temp0->next = Temp1;
Buf->next = 0;
return 0;
}
static int InsertNewEnd(struct buf* ListHead, struct buf* Buf)
{
if ((ListHead == 0) || (Buf == 0))
{
return -1;
}
struct buf* Temp0 = ListHead;
while(Temp0->next != 0)
{
Temp0 = Temp0->next;
}
Temp0->next = Buf;
Buf->next = 0;
return 0;
}
相关问题
balloc:out of blocks
问题如下

从该网址获得解决方案图片3解决方案
即增大FSSIZE就可以解决了。
问题2:出现panic: freeing free block


可以观察到该错误应该是重复释放可以disk引起的,由于我们只修改cache层的代码,则应该是一个disk对应多个cache block引起的。所以获取全局锁,bucket锁后要,然后重复查找是不是有对应的block,然后再看是不是这个bucket有ref为0的block(因为你中间释放掉bucket锁,造成条件可能改变,如不这样,会造成freeing free block),具体思路看上面。
相关知识
锁
定义
无论在哪种操作系统上,锁都是对某种资源同一时间只有一个拥有者的抽象,即某种资源被某个任务获取后,任务不释放该锁,其他任务无法拥有该资源。
为啥需要锁
从数电的角度,数字电路是有竞争冒险的。从计算机组成原理的角度,多个cpu同时访问一个内存会出现问题,多个任务(线程,进程,中断服务程序)同时访问一个资源时会出现问题。锁是为了解决该类问题而提出的抽象。
锁的分类
从是否等待的角度
自旋锁 睡眠锁
是否可以递归
递归锁 非递归锁
锁与中断
睡眠锁是不能在中断中使用,睡眠锁修改一些变量时需要临界区,也可能通过自旋锁或者禁用中断完成。
自旋锁可以在中断中使用,但自旋锁要禁止本cpu的抢占,如果涉及到中断上下文的访问,spin lock需要和禁止本 CPU 上的中断联合使用。如果中断时在bottom中使用自旋锁,那么在使用是要禁用disable bottom half。
锁的应用
rtos中一个上层api要调用某个硬件,下层函数也要调用硬件,那么需要递归锁。
使用原则
1:如果一个变量被一个cpu读写时,可以被另一个cpu读写,那么需要锁来保护这个变量。
2:一个需要被锁保护的变量可能可以存储在多个地方,每个地方都需要锁来保护它。
3:如果有几个任务可能在同一时间拥有几把锁,那么为了避免死锁,这几个任务获取锁的顺序要一致。
4:如果大范围锁后,可能要用小范围的锁,大范围的锁要先获取,如本题目中的cache。
原子操作
原子操作是CPU提供的一种指令,它保证原子性,即该指令不会被本(其他)CPU打断,保证该指令的原子性。
rtos中原子操作和linux中原子操作的区别
rtos一般是单核的,所以原子操作只要保证指令不被中断打断即可实现。rtos中复杂的原子操作可能要参考linux的自旋锁。
linux中可能存在多个cpu,所以可能出现多个cpu同时访问读写一个内存的情况,所以linux中的原子操作不仅要保正cpu的指令不被中断打断,还要借助硬件总线上的帮助,锁住总线,让同一时间只有一个cpu可以访问读写这个内存位置。
其他
如果有人读的话,祝大家新年快乐。
少喝葡萄酒+啤酒,昨晚喝到2点多,今天人都还有晕,幸好昨天就写了一大半部分文章。

浙公网安备 33010602011771号