Lab: locks
LRU
LRU全称是Least Recently Used,即最近最久未使用的意思。
LRU算法的设计原则是:*如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小*。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
LRU实现
- 用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。
- 利用一个链表来实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃。
- 利用链表和hashmap。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
对于第一种方法, 需要不停地维护数据项的访问时间戳,另外,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。对于第二种方法,链表在定位数据的时候时间复杂度为O(n)。所以在一般使用第三种方式来是实现LRU算法。
结构体
在{前的是类型名(type name),是拿来定义的,在}后的是变量名(variable name),是拿来使用的。
lab
bcache本身是kernel virtual space中的一个数据结构,本身是一串虚拟地址,kernel程序创建的时候就会将其创建,存在kerne virtual space里,直接映射到物理内存空间。我们没有必要要知道它的具体物理空间,我们只是用来指向disk block,作用就像是缓存了一样。
Memory allocator
实现内存分配器,减少竞争,主要实现思路是原先只有一个freelist,一个lock给八个cpu竞争使用,而我们需要重新设计为八个freelist、八个lock给八个cpu使用。
中途会有一个问题,就是当某个cpu对应的freelist没有空闲页面了,该怎么办?实验介绍给出的方案是从其它cpu的freelist中偷(steal)过来。
// 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 {
struct spinlock lock;
struct run *freelist;
} kmem;
struct Kmem
{
struct spinlock lock;
struct run *freelist;
};
struct Kmem kmems[NCPU];
void
kinit()
{
// initlock(&kmem.lock, "kmem");
// freerange(end, (void*)PHYSTOP);
for(int i=0; i<NCPU;i++)
{
initlock(&kmems[i].lock, "kmem");
if(i==0)
{
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;
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;
//当前cpu id
push_off();
int cpu_id = cpuid();
pop_off();
//acquire(&kmem.lock);
acquire(&kmems[cpu_id].lock);
r->next = kmems[cpu_id].freelist;
kmems[cpu_id].freelist = r;
release(&kmems[cpu_id].lock);
}
// 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;
// 当前cpu id
push_off();
int cpu_id = cpuid();
pop_off();
//acquire(&kmem.lock);
acquire(&kmems[cpu_id].lock);
r = kmems[cpu_id].freelist;
//当前页的freelist有freepage
if(r)
{
kmems[cpu_id].freelist = r->next;
release(&kmems[cpu_id].lock);
memset((char *)r, 5, PGSIZE); // fill with junk
return (void *)r;
}
else
{
release(&kmems[cpu_id].lock);
for(int i=0;i<NCPU;i++)
{
if(i != cpu_id) //避免和当前CPU竞争
{
acquire(&kmems[i].lock);
// 到最后一个也找不到有空闲内存的,返回0
if (i == NCPU - 1 && kmems[i].freelist == 0)
{
release(&kmems[i].lock);
return (void*)0;
}
//能窃取到
if(kmems[i].freelist)
{
struct run *to_alloc = kmems[i].freelist; //窃取
kmems[i].freelist = to_alloc->next; //占用
release(&kmems[i].lock);
memset((char *)to_alloc, 5, PGSIZE); // fill with junk
return (void *)to_alloc;
}
// 没有匹配的
release(&kmems[i].lock);
}
}
}
return (void *)0;
}
Buffer cache
实现buffer cache的并发,保证一个不变性:即每次只有一个buffer被cpu或者process使用,这也是所谓的原子性。同时不要让其它process或cpu都等待一个cache lock。
实现思路主要是将30个buf(NBUF == 30)划分为13个哈希桶(bucket),每个bucket都有一个对应的自旋锁即bcache.bucket。在重复使用或者淘汰(eviction)一个buf时,对这个buf对应的bucket上自旋锁,重复使用或者eviction结束之后,才对这个buf对应的bucket解锁,达成这个原则,才能达成不变性的要求。
- 在初始化时,先将每个
bucket的head.next置空,再将30个buf全分配给bucket[0]。再根据hash函数算出的buk_id动态地分配,这里还是采用了steal的方式,但找出的空闲块要符合LRU。 - 在
bget()中,buf的「身份认证」也就是dev和blockno作为参数,经hash函数算出buf对应的buk_id后,会有两种情况。bucket[buk_id]中已经缓存了对应的buf,那直接拿来重复使用就行- 没有,那只能eviction,先遍历所有buket,找到符合LRU的buf,从当前buket中steal出来,然后缓存到目标
bucket[buk_id]中,修改相关信息就可以拿来用了。- 中间会有一个重复缓存的问题导致
panic: freeing free block,假设两个进程同时找到了同一个buf,也要缓存到同一个bucket[buk_id]中,这就是重复缓存,从而导致二次释放。 - 解决方案是在缓存到目标
bucket[buk_id]之后,马上做一次检查,看看buf是不是已经被其它进程缓存进去了,如果是就用这个buf就行了,如果不是就要修改相关信息才拿出来用。
- 中间会有一个重复缓存的问题导致
- 为了保证原子性,在「找对应buf」、「找符合LRU的buf并steal出来」、「缓存到目标bucket」的三个过程中必须对各自所查找或使用的bucket保持锁定。
- 实现了LRU采用的是时间戳(timestamp)的方式
- 初始化全部
timestamp置为0 - 释放一个buf时,如果引用为
0,就更新一下timestamp - 每次找符合LRU的空闲块时,找引用为
0,timestamp最大的即可
- 初始化全部
访问一个buf的结构时是不需要对sleeplock: buffer进行上锁、解锁操作,但要获得bucket.lock。只有使用这个buf的数据内容才需要对sleeplock: buffer进行上锁、解锁操作。
/* param.h */ ...
#define FSSIZE 10000 // size of file system in blocks
/* buf.h */
struct buf {
...
uchar data[BSIZE];
uint timestamp; // the time stamp of current block
};
// 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 NBUK 13
#define hash(dev, blockno) ((dev * blockno) % NBUK)
struct bucket
{
struct spinlock lock;
struct buf head;
};
struct {
struct spinlock lock; // 主要保护的是连接所有槽位的链表。
struct buf buf[NBUF]; // 代表我们有30个槽位可用。
// 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;
struct bucket buckets[NBUK]; // the cache has 13 buckets
} bcache;
void
binit(void)
{
struct buf *b;
struct buf *prev_b;
initlock(&bcache.lock, "bcache");
// 将所有的 buf 都先放到 buckets[0] 中
for(int i=0; i<NBUK; i++)
{
initlock(&bcache.buckets[i].lock, "bcache.bucket");
bcache.buckets[i].head.next = (void *)0;
// 我们首先将所有的bufs传给buckets[0]。
if(i == 0)
{
prev_b = &bcache.buckets[i].head;
for(b = bcache.buf; b < bcache.buf+NBUF; b++)
{
if(b == bcache.buf + NBUF - 1) //buf[29]
{
b->next = (void*)0;
}
prev_b->next = b;
b->timestamp = ticks; // 当初始化内核时,ticks == 0
initsleeplock(&b->lock, "buffer");
prev_b = b;
}
}
}
}
// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
// 根据输入的设备号和块号,扫描Buffer Cache中的所有缓存块。
// 如果缓存命中,bget更新引用计数refcnt,释放bcache.lock
// 先判断是否之前已经缓存过了硬盘中的这个块。如果有,那就直接返回对应的缓存,
// 如果没有,会去找到一个最长时间没有使用的缓存,并且把那个缓存分配给当前块。
// 所有的缓存被串到了一个双向链表里。链表的第一个元素是最近使用的,最后一个元素是很久没有使用的。
static struct buf *
bget(uint dev, uint blockno) // 设备号和块号
{
struct buf *b;
int buk_id = hash(dev, blockno);
acquire(&bcache.buckets[buk_id].lock);
//buckets[nik_id]有buf直接用
for (b = bcache.buckets[buk_id].head.next; b; b=b->next)
{
if(b->dev == dev && b->blockno == blockno)
{
b->refcnt++;
release(&bcache.buckets[buk_id].lock);
acquiresleep(&b->lock);
return b;
}
}
release(&bcache.buckets[buk_id].lock);
// 没有,遍历bucket
// 从当前的bucket中steal,缓存到目标bucket[buk_id]
// 释放buf时 cnt=0更新timestamp,
//找到符合的LRU时, 找cnt=0,timestamp最大的
int max_timestamp = 0;
int lru_buk_id = -1;
int is_better = 0; //是否有更好的lru_buk_id
struct buf *lru_b = (void*)0;
struct buf *prev_lru_b = (void*)0;
// 当refcnt == 0时,找到lru_buk_id,并在每个桶[i]中获得最大的时间戳。
struct buf *prev_b = (void*)0;
for(int i=0; i<NBUK; ++i)
{
prev_b = &bcache.buckets[i].head;
acquire(&bcache.buckets[i].lock);
while(prev_b->next)
{
if (prev_b->next->refcnt == 0 && prev_b->next->timestamp >= max_timestamp)
{
max_timestamp = prev_b->next->timestamp;
is_better = 1;
prev_lru_b = prev_b; // get prev_lru_b
}
prev_b = prev_b->next;
}
if(is_better)
{
if(lru_buk_id != -1)
{
release(&bcache.buckets[lru_buk_id].lock);
}
lru_buk_id = i;
}
else{
release(&bcache.buckets[i].lock);
}
is_better = 0;
}
//get lru_b
lru_b = prev_lru_b->next;
//steal
if (lru_b)
{
prev_lru_b->next = prev_lru_b->next->next;
release(&bcache.buckets[lru_buk_id].lock);
}
// 缓存lru_b到buckets[buk_id]。
acquire(&bcache.lock);
acquire(&bcache.buckets[buk_id].lock);
if (lru_b)
{
lru_b->next = bcache.buckets[buk_id].head.next;
bcache.buckets[buk_id].head.next = lru_b;
}
// 如果两个进程在buckets[lru_buk_id]中使用相同的块(相同的块号)。
// 一个进程可以检查它,如果已经在这里,我们就可以得到它。
// 否则,我们将在两个进程中使用相同的块,并进行双倍缓存
b = bcache.buckets[buk_id].head.next; // buckets[buk_id]中的第一个buf。
while (b)
{
if (b->dev == dev && b->blockno == blockno)
{
b->refcnt++;
release(&bcache.buckets[buk_id].lock);
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
b = b->next;
}
// 在遍历每个桶的时候找不到lru_b
if (lru_b == 0)
panic("bget: no buffers");
lru_b->dev = dev;
lru_b->blockno = blockno;
lru_b->valid = 0;
lru_b->refcnt = 1;
release(&bcache.buckets[buk_id].lock);
release(&bcache.lock);
acquiresleep(&lru_b->lock);
return lru_b;
}
// 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); // 通过调用bget来获取指定磁盘块的缓存块
if(!b->valid) {
// 如果b->valid=0,说明这个槽位是刚被回收的,还没有缓存任何磁盘块,
// 因此调用virtio_disk_rw来先从磁盘上读取相应磁盘块的内容,读取完成后更新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.
// 调用者结束对一个缓存块的处理(读或写)之后,
// 调用brelse更新bcache的链表,并且释放对应的缓存块的睡眠锁
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
// 先释放缓存块的睡眠锁
releasesleep(&b->lock);
int buk_id = hash(b->dev, b->blockno);
acquire(&bcache.buckets[buk_id].lock);
b->refcnt--; // 减去引用计数refcnt
// 最近刚刚更新的块b->refcnt=1
if (b->refcnt == 0) {
b->timestamp = ticks;
}
release(&bcache.buckets[buk_id].lock);
}
void
bpin(struct buf *b)
{
int buk_id = hash(b->dev, b->blockno);
acquire(&bcache.buckets[buk_id].lock);
b->refcnt++;
release(&bcache.buckets[buk_id].lock);
}
void
bunpin(struct buf *b)
{
int buk_id = hash(b->dev, b->blockno);
acquire(&bcache.buckets[buk_id].lock);
b->refcnt--;
release(&bcache.buckets[buk_id].lock);
}