XV6學習筆記(2) :記憶體管理

周小倫發表於2021-08-18

XV6學習筆記(2) :記憶體管理

在學習筆記1中,完成了對於pc啟動和載入的過程。目前已經可以開始在c語言程式碼中執行了,而當前已經開啟了分頁模式,不過是兩個4mb的大的記憶體頁,而沒有開啟小的記憶體頁。接下來就可以從main.c的init函式開始

這裡會和JOS做一個對比

首先看一下在執行main.c之前的實體記憶體分佈

0x0000-0x7c00     載入程式的棧
0x7c00-0x7d00     載入程式的程式碼(512位元組)
0x10000-0x11000   核心ELF檔案頭(4096位元組)
0xA0000-0x100000  裝置區
0x100000-0x400000 Xv6作業系統(未用滿)

1. Kinit1函式

1.1 xv6中的kinit1函式

int
main(void)
{
  kinit1(end, P2V(4*1024*1024)); // phys page allocator
  kvmalloc();      // kernel page table
  //....
}

這是main函式的開始。所以我們先從kinit1開始

這裡的end地址就是kernel從0x80100000開始。然後是核心的程式碼段 + 只讀資料段+ stab段+ stabstr + 資料段 + .bss段這些之後的起始地址。如下圖所示。

image-20210817213129000
void
kinit1(void *vstart, void *vend)
{
  initlock(&kmem.lock, "kmem");
  kmem.use_lock = 0;
  freerange(vstart, vend);
}

  1. 這裡的vstart就是end的地址而vend是KERNBASE + 4MB = 0x80400000
  2. 這裡就是把[vstart, 0x80400000]的記憶體按頁(4kb大小)進行free
  3. kree這裡會把他插入到freelist中
void
freerange(void *vstart, void *vend)
{
  char *p;
  p = (char*)PGROUNDUP((uint)vstart);
  for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
    kfree(p);
}
//PAGEBREAK: 21
// 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(char *v)
{
  struct run *r;

  if((uint)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  memset(v, 1, PGSIZE);

  if(kmem.use_lock)
    acquire(&kmem.lock);
  r = (struct run*)v;
  r->next = kmem.freelist;
  kmem.freelist = r;
  if(kmem.use_lock)
    release(&kmem.lock);
}

好了這裡就可以完成整個free操作了

image-20210817213005747

1.2 jos的boot_alloc函式

和上面的是非常類似的

  1. 當第一次執行的時候nextfree是空這裡會進行第一次分配。這裡的起始地址也是end的地址
  2. 然後返回這一段分配的地址,並更新nextfree
static void *
boot_alloc(uint32_t n)
{
	static char *nextfree;	// virtual address of next byte of free memory
	char *result = NULL;

	// Initialize nextfree if this is the first time.
	// 'end' is a magic symbol automatically generated by the linker,
	// which points to the end of the kernel's bss segment:
	// the first virtual address that the linker did *not* assign
	// to any kernel code or global variables.
	if (!nextfree) {
		extern char end[];
		nextfree = ROUNDUP((char *) end + 1, PGSIZE);
	}

	// Allocate a chunk large enough to hold 'n' bytes, then update
	// nextfree.  Make sure nextfree is kept aligned
	// to a multiple of PGSIZE.
	//
	// LAB 2: Your code here.
	if (n > 0) {
		result = nextfree;
		nextfree = ROUNDUP(nextfree + n, PGSIZE);
	} else if (n == 0) {
		result = ROUNDUP(nextfree, PGSIZE);
	} else {
		panic("boot_alloc(n): n < 0\n");
	}

	cprintf("boot_alloc(): nextfree=%08x\n", nextfree);

	if ((uintptr_t) nextfree >= KERNBASE + PTSIZE) {
		panic("boot_alloc(): out of memory\n");
	}

	return result;
}

2. kvmalloc

void
kvmalloc(void)
{
  kpgdir = setupkvm();
  switchkvm();
}

這裡我們先看一下setupkvm

// Set up kernel part of a page table.
pde_t*
setupkvm(void)
{
  pde_t *pgdir;
  struct kmap *k;

  if((pgdir = (pde_t*)kalloc()) == 0) // 分配pgdir
    return 0;
  memset(pgdir, 0, PGSIZE);
  if (P2V(PHYSTOP) > (void*)DEVSPACE)
    panic("PHYSTOP too high");
  for(k = kmap; k < &kmap[NELEM(kmap)]; k++) //遍歷kmap進行對映
    if(mappages(pgdir, k->virt, k->phys_end - k->phys_start,
                (uint)k->phys_start, k->perm) < 0) {// 如果對映失敗則 free掉
      freevm(pgdir); 
      return 0;
    }
  return pgdir;

當然這裡引出了很多函式

1. kalloc函式

從我們的空閒佇列中獲得一個指標。返回

char*
kalloc(void)
{
  struct run *r;

  if(kmem.use_lock)
    acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  if(kmem.use_lock)
    release(&kmem.lock);
  return (char*)r;
}

2. kmap指明瞭核心中需要對映的區域:

// This table defines the kernel’s
// every process’s page table.
static struct kmap {
   void *virt;
   uint phys_start;
   uint phys_end;
   int perm;  //許可權
} kmap[] = {
   { (void*)KERNBASE, 0, EXTMEM, PTE_W }, // I/O space
   { (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0 }, // kern text+rodata
   { (void*)data,V2P(data), PHYSTOP, PTE_W }, // kern data+memory
   { (void*)DEVSPACE, DEVSPACE, 0, PTE_W }, // more devices
};

3. mappages函式

  1. 給定虛擬地址va和實體地址pa
  2. 把[va , va + size]和 [pa, pa + size]進行對映。並且以perm位
static int
mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm)
{
  char *a, *last;
  pte_t *pte;

  a = (char*)PGROUNDDOWN((uint)va);
  last = (char*)PGROUNDDOWN(((uint)va) + size - 1);
  for(;;){
    if((pte = walkpgdir(pgdir, a, 1)) == 0)
      return -1;
    if(*pte & PTE_P)
      panic("remap");
    *pte = pa | perm | PTE_P;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

4.walkpgdir函式

這裡關於頁的操作要補充一些關於頁表操作的知識

首先頁表目錄結構如下。4gb記憶體32地址的話

//
// +--------10------+-------10-------+---------12----------+
// | Page Directory |   Page Table   | Offset within Page  |
// |      Index     |      Index     |                     |
// +----------------+----------------+---------------------+
//  \--- PDX(va) --/ \--- PTX(va) --/

// page directory index
#define PDX(va)         (((uint)(va) >> PDXSHIFT) & 0x3FF)

// page table index
#define PTX(va)         (((uint)(va) >> PTXSHIFT) & 0x3FF)

// construct virtual address from indexes and offset
#define PGADDR(d, t, o) ((uint)((d) << PDXSHIFT | (t) << PTXSHIFT | (o)))
#define PTXSHIFT        12      // offset of PTX in a linear address
#define PDXSHIFT        22      // offset of PDX in a linear address
// Address in page table or page directory entry
#define PTE_ADDR(pte)   ((uint)(pte) & ~0xFFF)
  1. 這裡的操作用到了PTE_ADDR函式

    0xFFF = 1111 | 1111 | 1111
    ~0xFFF = 0000 | 0000 | 0000
    #其實就是把最後12位設定為0
    
  2. 這裡就是獲取指定虛擬地址的pte條目

static pte_t *
walkpgdir(pde_t *pgdir, const void *va, int alloc)
{
  pde_t *pde;
  pte_t *pgtab;

  pde = &pgdir[PDX(va)];
  if(*pde & PTE_P){
    pgtab = (pte_t*)P2V(PTE_ADDR(*pde));
  } else {
    if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)
      return 0;
    // Make sure all those PTE_P bits are zero.
    memset(pgtab, 0, PGSIZE);
    // The permissions here are overly generous, but they can
    // be further restricted by the permissions in the page table
    // entries, if necessary.
    *pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U;
  }
  return &pgtab[PTX(va)];
}

整個對映完的圖如下

img

而在xv6的二級頁表條目管理如下

img

5. switchkvm函式

這個函式就是把kernel的pagedir傳輸到cr3暫存器中

kenel的pagedir由上面的操作獲得

// Switch h/w page table register to the kernel-only page table,
// for when no process is running.
void
switchkvm(void)
{
  lcr3(V2P(kpgdir));   // switch to the kernel page table
}

3. kinit2函式

在main函式進入這個函式之前還有好多別的函式。但是這個進入第一個使用者程式之前的最後一個函式,

kinit2()將[0x400000, 0xE00000]範圍內的實體地址納入到記憶體頁管理之中。至此,Xv6的記憶體頁管理系統和核心頁表已經全部建立完畢。需要注意的是,這個核心頁表(kpgdir變數)只會在排程器執行時被使用。對於每一個使用者程式,都會擁有自己獨自的完整頁表,其中也包含了一份一模一樣的核心頁表。

void
kinit2(void *vstart, void *vend)
{
  freerange(vstart, vend);
  kmem.use_lock = 1;
}

此時的虛擬記憶體和實體記憶體的對映關係如下

虛擬地址 對映到實體地址 內容
[0x80000000, 0x80100000] [0, 0x100000] I/O裝置
[0x80100000, 0x80000000+data] [0x100000, data] 核心程式碼和只讀資料
[0x80000000+data, 0x80E00000] [data, 0xE00000] 核心資料+可用實體記憶體
[0xFE000000, 0] [0xFE000000, 0] 其他通過記憶體對映的I/O裝置

參考1
參考2

相關文章