Memory allocator
xv6是使用linked list來管理空余內存塊,我們先看一下kalloc.c究竟是怎么工作的:
首先是2個結構體,匿名結構體kmem就是我們訪問空余內存的憑據了,kmem里面有一個自旋鎖和一個鏈表頭部指針。struct run顯然就是鏈表結構,里面唯一的成員就是指向下一個空余物理頁面的指針。kinit做的操作是初始化自旋鎖,然后把從內核尾部地址到PHYSTOP的物理內存全部kfree()一遍。
kfree()的作用是:傳給它一個指針,它釋放該指針指向的一頁內存pa。釋放方式是:先使用memset把pa里面的內容全部置為1,然后把pa插入freelist的頭部。kalloc()的作用是:分配freelist直接指向的頁。分配方式是:直接返回freelist的值,然后讓freelist指向下一個空閑的頁。這就是kalloc.c管理空閑頁的過程。在系統boot的時候,CPU0會調用kinit()分配所有內存,得到一個串接起內存中所有可以頁的鏈表。
現在我們要優化這一過程。當多個CPU都需要分配內存時,為了防止race condition,它們在申請新頁表的時候都要獲取kmem中的自旋鎖lock,任何時候只能有一個CPU申請內存。然而自旋鎖執行的是busy waiting,非常耗費CPU資源,所以可以為每個CPU設置一個專屬的free list,這樣多個CPU之間就不需要爭奪一個自旋鎖了。
為每個CPU設計一個struct memnode,結構成員仿照之前的kmem,有一個自旋鎖和一個鏈表頭。初始化一個struct memnode類型的數組cpu_mem,CPU可以按照自己的hart id在cpu_mem里找到自己的free list。
struct memnode{
struct spinlock lock;
struct run *freelist;
};
//初始化一個struct memnode類型的數組
struct memnode cpu_mem[NCPU];
現在修改kinit():
void
kinit()
{
//這里應當多次調用,初始化每個CPU的鎖
//TODO
//char name[20];
for(int i=0;i<NCPU;i++)
{
//snprintf(name,15,"kmem");
initlock(&cpu_mem[i].lock, "kmem");
//memset(name,0,20);
}
//initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
初始化所有鎖,按照提示,鎖的名稱就用kmem。取別的名字的話auto grade程序可能識別不了。
然后修改kfree()。理所當然的,使用cpuid()獲取CPU的hart id,獲取現有CPU專屬的鎖,然后把空頁插入此CPU的freelist頭部。
void
kfree(void *pa)
{
struct run *r;
push_off();//記得關中斷
int this_cpu = cpuid();
pop_off();
//printf("cpu %d free %p\n",this_cpu,pa);
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;
acquire(&cpu_mem[this_cpu].lock);
r->next = cpu_mem[this_cpu].freelist;
cpu_mem[this_cpu].freelist = r;
release(&cpu_mem[this_cpu].lock);
}
似乎按照同樣的道理,kalloc()也直接仿照上面kfree(),獲取現有CPU的鎖,然后把freelist指向下一個空頁。直接這么寫,系統boot的時候就會觸發panic。
這里需要注意,在只有CPU0調用了kinit(),因此在初始時,CPU0的freelist擁有所有空閑內存,其他CPU的freelist都是空的!提示已經告訴我們,如果某一個CPU的free list空了,它應該從其他CPU”偷“一個空閑頁。所謂”偷“的過程是:CPU1使用CPU0的freelist來kalloc()一個頁,CPU1會修改CPU0的freelist,然后把得到的指針傳給CPU1上正在運行的線程。稍后,該線程使用kfree()釋放了這頁內存,kfree()把這頁內存加入到CPU1的freelist。
為kalloc()設計偷頁的功能:
void *
kalloc(void)
{
struct run *r;
push_off();//記得關中斷
int this_cpu = cpuid();
pop_off();
acquire(&cpu_mem[this_cpu].lock);
r = cpu_mem[this_cpu].freelist;
if(r)
{
cpu_mem[this_cpu].freelist = r->next;
release(&cpu_mem[this_cpu].lock);
}
else//嘗試從其他的CPU的freeList里取得
{
int j;
int free_cpu;//哪個CPU有空的free list
for(j=0;j<NCPU;j++)
{
free_cpu=(this_cpu+j)%NCPU;
if(cpu_mem[free_cpu].freelist!=0)//如果freelist是有值的
break;
}
release(&cpu_mem[this_cpu].lock);//釋放了現在cpu的鎖
acquire(&cpu_mem[free_cpu].lock);//獲取cpu j的鎖
r = cpu_mem[free_cpu].freelist;
if(r)
cpu_mem[free_cpu].freelist = r->next;
release(&cpu_mem[free_cpu].lock);//釋放cpu j的鎖
}
//printf("cpu %d kalloc %p\n",this_cpu,r);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
如果自己的freelist已經空了,那么從右側開始循環搜索所有CPU,如果發現誰的freelist不為空,就及時釋放現在CPU的鎖。獲取偷竊目標的鎖,把偷竊目標的freelist指向下一頁,然后返回偷到的空頁指針r。
Buffer cache
xv6文件系統里有一個buffer cache layer,它的用途是:
(1) synchronize access to disk blocks to ensure that only one copy of a block is in memory and that only one kernel thread at a time uses that copy;
(2) cache popular blocks so that they don’t need to be re-read from the slow disk.
很明顯就像CPU里面的cache一樣,要利用局部性原理做低速存儲器的緩沖。這個比kalloc要復雜,我們來看一下它做了什么:

bcache就是整個buffer cache了,里面有一個自旋鎖lock和一個雙向鏈表。鏈表頭部是head,其他節點都存儲在buf數組里。(這里可沒有stdlib.h和malloc,必須使用靜態鏈表)每個buf塊里有它對應的dev和block,每個block只能有一個對應的buf塊。每個buf塊里都有一個用來讀寫的睡眠鎖。
首先調用binit初始化鏈表和鎖。
看一下稍后要修改的bget函數:

這個bget函數先獲取了訪問鏈表的自旋鎖bcache.lock,然后遍歷鏈表,根據傳入參數尋找buf塊,如果找到了對應的塊就釋放自旋鎖,調用睡眠鎖准備讀寫。
如果沒找到,就從后往前遍歷鏈表,找到一塊refcnt為0的塊(refcnt為0意味着未被使用),初始化它,然后獲取睡眠鎖來讀寫。

brelse()函數的主要功能就是釋放的buf塊移動到鏈表的頭部。這樣可以實現LRU。鏈表中越往后的buf塊就是使用的越少的塊。
和上面kalloc一樣,多個進程訪問bcache都需要獲取bcache.lock這個自旋鎖。按照提示,可以使用一個hash表來代替buf鏈表來減少沖突。我們可以把blockno映射到這個hash表的bucket中。
struct bucket{
struct spinlock lock;
struct buf bufarr[BUCKETSZ];//每一個bucket 存儲的buf
};
struct bucket bhash[NBUCKETS];
聲明一個hash表bhash,有NBUCKETS個bucket,每個bucket有一個自旋鎖和一組buf節點。如果需要訪問hash表,首先根據blockno計算出它在表中的位置,然后在bufarr里面搜索即可。
這里用的散列函數比較簡單,直接取模:
int hashkey(uint key)
{
return key%NBUCKETS;
}
為buf添加幾項:
struct buf {
int valid; // has data been read from disk?
int disk; // does disk "own" buf?
uint dev;
uint blockno;
struct sleeplock lock;
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
uchar data[BSIZE];
uint timestamp; //時間戳
int bucket;//屬於哪個bucket
};
初始化hash表。
void binit(void)
{
//初始化每個鎖的名稱
uint init_stamp=ticks;
for(int i=0;i<NBUCKETS;i++)
{
//snprintf(name,18,"bcache",i);
initlock(&bhash[i].lock,"bcache");
for(int j=0;j<BUCKETSZ;j++)//初始化時間戳
{
bhash[i].bufarr[j].timestamp=init_stamp;
bhash[i].bufarr[j].bucket=i;//記錄它屬於哪個bucket
}
}
}
實驗要求使用系統時間戳來實現LRU,所以獲取調用binit()時的ticks作為初始時間戳,然后進行初始化。
然后修改bget,首先根據blockno映射到相應的bucket,獲取該bucket的自旋鎖,然后遍歷這個bucket里面的bufarr,找到之后要更新時間戳,釋放自旋鎖調用睡眠鎖。如果沒有找到,就在所有refcnt為0的項里面搜索時間戳最小的,作為替換對象:
static struct buf* bget(uint dev, uint blockno)
{
int key=hashkey(blockno);
acquire(&bhash[key].lock);//hash到對應的bucket,需要獲取bucket上面的鎖
struct buf* b;
for(b=&bhash[key].bufarr[0]; b<&bhash[key].bufarr[0]+BUCKETSZ; b++)
{
if(b->dev == dev && b->blockno == blockno)//如果找到該節點
{
b->refcnt++; //增加引用數
b->timestamp=ticks;//更新時間戳
release(&bhash[key].lock);//釋放bucket鎖。其他進程可以訪問bucket了
acquiresleep(&b->lock);//獲取該節點的睡眠鎖,准備讀寫
return b;
}
}
//沒有找到:找時間戳最小的未使用項
uint minstamp=~0;
struct buf* min_b=0;
for(b=&bhash[key].bufarr[0] ; b<&bhash[key].bufarr[0]+BUCKETSZ; b++)
{
if(b->timestamp<minstamp && b->refcnt==0)
{
minstamp=b->timestamp;
min_b=b;
}
}
if(min_b!=0)
{
min_b->dev = dev;
min_b->blockno = blockno;
min_b->valid = 0;
min_b->refcnt = 1;
min_b->timestamp=ticks; //記得更新時間戳
release(&bhash[key].lock);//釋放bucket鎖。其他進程可以訪問bucket了
acquiresleep(&min_b->lock);//獲取該節點的睡眠鎖,准備讀寫
return min_b;
}
panic("bget: no buffers");
}
這時brelse就很簡單了,首先務必要釋放之前在bget()調用的睡眠鎖,只需要減少refcnt數目就可以了。LRU功能已經由時間戳實現,所以可以直接刪掉后面的鏈表操作:
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
releasesleep(&b->lock);
acquire(&bhash[b->bucket].lock); //獲取它所在bucket的自旋鎖
b->refcnt--;
release(&bhash[b->bucket].lock);
}
順便再修改一下最后兩個函數,因為之前它們直接獲取了bcache.lock:
void
bpin(struct buf *b) {
acquire(&bhash[b->bucket].lock);
b->refcnt++;
release(&bhash[b->bucket].lock);
}
void
bunpin(struct buf *b) {
acquire(&bhash[b->bucket].lock);
b->refcnt--;
release(&bhash[b->bucket].lock);
}