1. Memory allocator (moderate)
1.1 要求
Your job is to implement per-CPU freelists, and stealing when a CPU's free list is empty. You must give all of your locks names that start with "kmem". That is, you should call initlock for each of your locks, and pass a name that starts with "kmem". Run kalloctest to see if your implementation has reduced lock contention. To check that it can still allocate all of memory, run usertests sbrkmuch. Your output will look similar to that shown below, with much-reduced contention in total on kmem locks, although the specific numbers will differ. Make sure all tests in usertests pass. make grade should say that the kalloctests pass.
閱讀原本的 kalloc.c
代碼,可以看到內存管理的結構是一個鏈表,如下:
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
當多個 cpu 同時申請內存時,只能串行執行,因爲只有一個 spinlock
,因此,我們需要這個 freelist
拆分給多個 cpu,每個 cpu 都獨自擁有一個上述的 kmem
結構,這樣多個 cpu 就能並行申請內存了。但此刻還需要注意如下情況:
當 CPU1 的 freelist
爲空時,表示內存已用完了,這個時候需要去訪問其他 cpu 的 freelist
,將他們的內存塊挪給 CPU1 使用。
1.2 實現
由於要求比較簡單,且 lab 中的 hints 基本提供了大部分信息,因此略過分析,直接上實現。
- 定義與初始化
這裏自定義了一個初始化函數 kfree_specific
將指定 pa
分配給指定 cpu 的 freelist
struct mem{
struct spinlock lock;
struct run *freelist;
};
struct mem kmem[NCPU];
void
kfree_specific(void *pa, int cpuid)
{
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
struct run* r = (struct run*)pa;
struct mem* cpu_mem = &kmem[cpuid];
acquire(&cpu_mem->lock);
r->next = cpu_mem->freelist;
cpu_mem->freelist = r;
release(&cpu_mem->lock);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
push_off();
int hart = cpuid();
pop_off();
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
kfree_specific(p, hart);
}
}
- 分配內存
// 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;
push_off();
int hart = cpuid();
pop_off();
struct mem* cpu_mem = &kmem[hart];
acquire(&cpu_mem->lock);
r = cpu_mem->freelist;
if(r){
cpu_mem->freelist = r->next;
release(&cpu_mem->lock);
}
else // "steal" free memory from other cpu mem list
{
release(&cpu_mem->lock);
for (int i = 0; i < NCPU ; i++) {
cpu_mem = &kmem[i];
acquire(&cpu_mem->lock);
r = cpu_mem->freelist;
if (r){
cpu_mem->freelist = r->next;
release(&cpu_mem->lock);
break;
}
release(&cpu_mem->lock);
}
}
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
- 釋放內存
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);
push_off();
int hart = cpuid();
pop_off();
r = (struct run*)pa;
//int hart = get_pa_cpu_id((uint64)r);
struct mem* cpu_mem = &kmem[hart];
acquire(&cpu_mem->lock);
r->next = cpu_mem->freelist;
cpu_mem->freelist = r;
release(&cpu_mem->lock);
}
2. Buffer cache (hard)
這個 part 的實現思路也比較清晰,但是由於細節沒注意,實現的時候各種 test case 不通過 =-=。
2.1 要求
Modify the block cache so that the number of acquire loop iterations for all locks in the bcache is close to zero when running bcachetest. Ideally the sum of the counts for all locks involved in the block cache should be zero, but it's OK if the sum is less than 500. Modify bget and brelse so that concurrent lookups and releases for different blocks that are in the bcache are unlikely to conflict on locks (e.g., don't all have to wait for bcache.lock). You must maintain the invariant that at most one copy of each block is cached. When you are done, your output should be similar to that shown below (though not identical). Make sure usertests still passes. make grade should pass all tests when you are done.
該 part 的目的也是通過重新設計文件系統的 buffer & cache 的數據結構來降低鎖的競爭,原先的結構如下:
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 buf head;
} bcache;
bcache.head
是一個 LRU 鏈表,head
的 prev
是最少使用,next
是最近使用,通過維護該 LRU 鏈表,根據空間局部性原理,來達到提升效率的目的。
但是由於 bcache
是多個 cpu 通用的,head
鏈表每次訪問都需要加鎖,這樣導致多 cpu 訪問緩存時只能串行工作。因此需要重新設計該結構,在利用到空間局部性原理的同時,減少鎖的競爭。
2.2 分析
根據 hints,要做如下操作:
- 將原來的
bcache.head
調整成 hash table,每個桶都有自己的鎖。從而減少對全局鎖bcache.lock
的競爭。 - 給
struct buf
結構增加時間戳,用於尋找最少使用的 buffer - 當 目標桶 找不到合適的元素時,需要去全局的
bcache.buf
中尋找buf.refcnt == 0
且最不經常使用的塊,然後將該buf
挪到 目標桶 。需要注意如下:- 先將該
buf
從其所屬的 原來的桶 移除,然後再挪到桶 a,需要注意 原來的桶 和 目標桶 不能是同一個桶(同一個則不需要操作移動了),否則可能導致死鎖。 - 遍歷全局的
bcache.buf
時,需要加鎖bcache.lock
- 遍歷找到 目標元素 時,需要注意,目標元素 原來的桶 是沒加鎖的,因此從找到 目標元素 再到準備給 原來的桶 加鎖移除該元素時,這中間存在空檔期,有可能在這期間該塊被引用了,導致
buf.refcnt != 0
,此時不能在用這塊buf
。因此在操作從 原來的桶 刪除時,需要再檢查一下buf.refcnt
,如果不爲 0 ,則需要重新遍歷全局的bcache.buf
,找尋新的 目標元素
- 先將該
2.3 實現
- 定義初始化
binit
將原來的 bcache.buf
所有 buf
平攤到每個桶中。
#define BUCKET_CNT 13
#define NBUF (BUCKET_CNT * 3)
struct bcache_bucket{
struct buf head;
struct spinlock lock;
};
struct {
struct spinlock lock;
struct buf buf[NBUF];
struct bcache_bucket bucket[BUCKET_CNT];
} bcache;
int hash_key(int blockno){
return blockno % BUCKET_CNT;
}
void binit(void)
{
struct buf *b;
char buf[32];
int sz = 32;
initlock(&bcache.lock, "bcache");
for (int i = 0; i < BUCKET_CNT; i++){
snprintf(buf, sz, "bcache.bucket_%d", i);
initlock(&bcache.bucket[i].lock, buf);
}
// Create linked list of buffers
int blockcnt = 0;
struct bcache_bucket* bucket;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
initsleeplock(&b->lock, "buffer");
b->access_time = ticks;
b->blockno = blockcnt++;
bucket = &bcache.bucket[hash_key(b->blockno)];
b->next = bucket->head.next;
bucket->head.next = b;
}
}
- 獲取 buf
獲取 buf 的策略分爲如下幾步:
- 哈希獲取對應的桶
- 獲取該桶的鎖
bcache_bucket.lock
- 檢查該塊是否已緩存
- 未緩存
- 首先遍歷該桶的列表
bcache_bucket.head
- 檢查是否有
buf.refcnt == 0
的buf
,取其中距離上次訪問時間最久的buf
- 沒有,則需要尋找一塊新的
buf
- 遍歷全局
bcache.buf
,此時需要獲取全局鎖bcache.lock
- 找到
buf.refcnt == 0
的buf
,取其中距離上次訪問時間最久的buf
- 哈希
buf.blockno
獲取該buf
原來的桶 - 遍歷該
buf
原來的桶,從bcache_bucket.head
中移除該buf
,此時應持有該桶的鎖bcache_bucket.lock
,釋放buf
完畢時釋放鎖 - 遍歷時需要注意,需要再次檢查
**buf.refcnt == 0**
,因爲前面遍歷全局**bcache.buf**
的時候,沒有持有該桶的鎖,在此期間**buf**
可能被引用了 - 將該
buf
移動到新桶 - 更新
buf
結構體信息
- 遍歷全局
- 有,則不需要移動該
buf
- 更新
buf
結構體信息
- 更新
- 沒有,則需要尋找一塊新的
- 首先遍歷該桶的列表
- 已緩存
- 更新
buf
結構體信息
- 更新
- 未緩存
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b, *lrub;
struct bcache_bucket* bucket = &bcache.bucket[hash_key(blockno)];
acquire(&bucket->lock);
// Is the block already cached?
for(b = &bucket->head; b; b = b->next){
if(b->dev == dev && b->blockno == blockno){
b->refcnt++;
b->access_time = ticks;
release(&bucket->lock);
acquiresleep(&b->lock);
return b;
}
}
// find bucket lru buffer
lrub = 0;
uint min_time = 0x8ffffff;
for(b = &bucket->head; b; b = b->next){
if (b->refcnt == 0 && b->access_time < min_time){
min_time = b->access_time;
lrub = b;
}
}
if (lrub) {
goto setup;
}
// Not cached.
// find in the global array
acquire(&bcache.lock);
findbucket:
lrub = 0;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
if(b->refcnt == 0 && b->access_time < min_time) {
lrub = b;
}
}
if (lrub) {
// step 1 : release from the old bucket
// need to hold the old bucket lock
struct bcache_bucket* old_bucket = &bcache.bucket[hash_key(lrub->blockno)];
acquire(&old_bucket->lock);
if (lrub->refcnt != 0){
release(&old_bucket->lock);
goto findbucket;
}
b = &old_bucket->head;
struct buf* bnext = b->next;
while (bnext != lrub) {
b = bnext;
bnext = bnext->next;
}
b->next = bnext->next;
// we don't need to modify bcache.bucket , so we release the lock
release(&old_bucket->lock);
// step 2 : add to target bucket
lrub->next = bucket->head.next;
bucket->head.next = lrub;
release(&bcache.lock);
setup:
lrub->dev = dev;
lrub->blockno = blockno;
lrub->valid = 0;
lrub->refcnt = 1;
lrub->access_time = ticks;
release(&bucket->lock);
acquiresleep(&lrub->lock);
return lrub;
}
panic("bget: no buffers");
}
- 釋放 buf
這塊內容較爲簡單,將鎖從全局鎖替換爲桶鎖即可
// 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");
releasesleep(&b->lock);
struct bcache_bucket* bucket = &bcache.bucket[hash_key(b->blockno)];
acquire(&bucket->lock);
b->refcnt--;
release(&bucket->lock);
}
bpin
和bunpin
同上,替換鎖即可
void
bpin(struct buf *b) {
struct bcache_bucket* bucket = &bcache.bucket[hash_key(b->blockno)];
acquire(&bucket->lock);
b->refcnt++;
release(&bucket->lock);
}
void
bunpin(struct buf *b) {
struct bcache_bucket* bucket = &bcache.bucket[hash_key(b->blockno)];
acquire(&bucket->lock);
b->refcnt--;
release(&bucket->lock);
}
3. 總結
該 lab 的坑比較多,在測試 usertests
時,出過一些異常錯誤,如 out of blocks
,這是因爲默認設置的 blocks 大小太少了,默認配置爲 1000,需要將該值調大爲 10000。
#define FSSIZE 1000 // size of file system in blocks
此外還有一些錯誤如 freeing free block
,這裏可能因爲在執行 bget
時,從其他桶挪移 buf
到新桶的時候,沒有再次判斷 refcnt
是否爲 0,從而導致覆蓋了已經被引用的塊。