MIT 6.S081 2021: Lab Lock


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);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM