Xv6源碼分析——內存管理

        內存管理主要分爲兩大部分,第一部分是內核的物理內存分配程序,以便內核可以分配內存並稍後釋放它。 分配器將以4096字節爲單位進行操作,稱爲頁面。內核會維護記錄哪些物理頁面是空閒的和哪些已分配的數據結構,以及每個頁面的進程數量,以及如何分配和釋放內存頁面。內存管理的第二個組成部分是虛擬內存,它將內核和用戶軟件使用的虛擬地址映射到物理內存中的地址。 當指令使用內存時,x86硬件的內存管理單元(MMU)執行映射,查詢一組頁表。

預備知識

頁表

        頁表是爲了便於在內存中找到進程的每個頁面所對應的物理塊,系統爲每個進程建立了一張頁表,記錄頁面在內存中對應的物理塊號。XV6主要使用頁表來複用地址空間並保護內存。

地址轉換硬件

        x86指令(包括用戶和內核)直接使用的是虛擬地址,而物理內存使用物理地址進行索引,從虛擬地址到物理地址的轉換是由硬件完成的。x86頁表由一級的頁目錄和二級的頁表項組成,每個頁目錄項有1024個連續的頁表項(每個頁表項4B,剛好佔用4kb的空間,也就是一頁),頁目錄項也是連續的,一共有1024個頁目錄項。CR3是頁目錄基地址寄存器,保存頁目錄表的物理地址,因爲頁目錄表是頁對齊的,所以CR3只有高20位是有效的。地址轉換如下圖所示:

 內存初始化

        boot將內核代碼放到物理地址低地址的0x100000處,爲了使內核運行,entry建立了一個頁表將虛擬地址0x80000000(KERNBASE)映射到從0x0開始的物理地址。頁表爲main.c文件中的enterpgdir數組,其中虛擬地址低4M映射物理地址低4M,(因爲啓動多處理器的時候還需要從低地址啓動)虛擬地址[KERNBASE,KERNBASE+4MB)映射到物理地址[0,4MB)

__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
  // Map VA's [0, 4MB) to PA's [0, 4MB)
  [0] = (0) | PTE_P | PTE_W | PTE_PS,
  // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
  [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};


};

PTE_P:表示此頁已經在內存中,PTE_W:可寫,PTE_PS:頁面大小?

// Page table/directory entry flags.
#define PTE_P           0x001   // Present
#define PTE_W           0x002   // Writeable
#define PTE_PS          0x080   // Page Size

 

來看entry的代碼:

 

entry:
  # Turn on page size extension for 4Mbyte pages
  #設置cr4,使用4M頁,這樣創建的頁表比較簡單
  movl    %cr4, %eax
  orl     $(CR4_PSE), %eax
  movl    %eax, %cr4
  # Set page directory 將 entrypgdir 的物理地址載入到控制寄存器 %cr3 中
  movl    $(V2P_WO(entrypgdir)), %eax
  movl    %eax, %cr3
  # Turn on paging. 開啓分頁
  movl    %cr0, %eax
  orl     $(CR0_PG|CR0_WP), %eax
  movl    %eax, %cr0

  # Set up the stack pointer.創建CPU棧
  movl $(stack + KSTACKSIZE), %esp

  # Jump to main(), and switch to executing at
  # high addresses. The indirect call is needed because
  # the assembler produces a PC-relative instruction
  # for a direct jump.
  mov $main, %eax
  jmp *%eax
#開闢stack區域,大小爲KSTACKSIZE
.comm stack, KSTACKSIZE

cr3寄存器中的數必須是物理地址,因爲現在還沒有頁表,不能進行地址轉換。通過宏定義V2P_W0來得到物理地址

#define V2P_WO(x) ((x) - KERNBASE)    // same as V2P, but without casts

 PG  分頁(CR0的31位)置1啓用分頁,置0不啓用分頁。當禁用分頁時,所有的線性地址都可以當作物理地址對待。

 WP 寫保護(CR0的位16)置1時禁止管理級的過程往用戶級只讀頁中寫,置0時允許管理級的過程往用戶級只讀頁中寫。
        它將棧指針 %esp 指向被用作棧的一段內存。所有的符號包括 stack 都在高地址,所以當低地址的映射被移除時,棧仍然是可用的。最後 entry 跳轉到高地址的 main 代碼中。 必須使用間接跳轉,否則彙編器會生成 PC 相關的直接跳轉(PC-relative direct jump),而該跳轉會運行在內存低地址處的 main。 main 不會返回,因爲棧上並沒有返回 PC 值。之後內核就運行在高地址處的函數 main中了。

物理內存初始化以及管理:

        main函數通過調用kinit1和kinit2來初始化物理內存,區別是kinit1調用時候用的還是之前的頁表,只能初始化4m的空間,這時候多核cpu還沒有啓動所以沒有設置鎖機制。在建立了完整的頁表之後用kinit2初始化剩下的物理內存。

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

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

        xv6通過freelist數據結構來記錄哪些物理頁面是可以被分配的,kinit1和kinit2通過調用freerang來把空閒頁加到freelist,PTE只能引用頁對齊的物理地址,所以freerange通過PGROUNDUP來確保只釋放頁對齊的物理地址。PGROUNDUP(sz)的功能就是當sz不是頁的倍數時進一位使其爲頁的倍數

#define PGROUNDUP(sz)  (((sz)+PGSIZE-1) & ~(PGSIZE-1))

kfree開始把被釋放的內存填滿字節1,目的是使在釋放後再有代碼使用這塊內存(通過野指針非法使用內存)時讀到的是垃圾數據,從而使這段代碼儘快終止。然後kfree將v轉換爲指向struct run的指針,在r-> next中記錄空閒列表的原來的頭,並將空閒列表頭設置爲r。 kalloc刪除並返回空閒列表中的第一個元素。

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

 

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
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;
}

進程地址空間

           entry建立的頁表已經足夠內核的C代碼開始運行了,然而main函數直接通過kvmalloc建立了新頁表,每個進程都有一張獨立的頁表,xv6通過頁表硬件在進程切換時切換頁表。switchkvm將頁表切換成內核頁表。

// Allocate one page table for the machine for the kernel address
// space for scheduler processes.
void
kvmalloc(void)
{
  kpgdir = setupkvm();
  switchkvm();
}

在setupkvm中,先通過kalloc分配一物理塊作爲頁目錄,然後調用mappages來按照kmap將內核虛擬地址空間映射到物理地址空間。

// This table defines the kernel's mappings, which are present in
// 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
};
  


// Return the address of the PTE in page table pgdir
// that corresponds to virtual address va.  If alloc!=0,
// create any required page table pages.
static pte_t *
walkpgdir(pde_t *pgdir, const void *va, int alloc)
{
  pde_t *pde;
  pte_t *pgtab;

  pde = &pgdir[PDX(va)];//前10項 找到在頁目錄中的位置
  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)];
}
 
#define PGSHIFT         12      // log2(PGSIZE)
#define PTXSHIFT        12      // offset of PTX in a linear address
#define PDXSHIFT        22      // offset of PDX in a linear address

 

switchkvm將kpgdir設置爲cr3寄存器的值,這個頁表僅僅在 scheduler內核線程中使用。

// 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
}

頁表和內核棧都是每個進程獨有的,xv6使用結構體proc將它們統一起來,在進程切換的時候,他們也往往隨着進程切換而切換,內核中模擬出了一個內核線程,它獨佔內核棧和內核頁表kpgdir,它是所有進程調度的基礎。

switchuvm通過傳入的proc結構負責切換相關的進程獨有的數據結構,其中包括TSS相關的操作,然後將進程特有的頁表載入cr3寄存器,完成設置進程相關的虛擬地址空間環境。

// Switch TSS and h/w page table to correspond to process p.
void
switchuvm(struct proc *p)
{
  if(p == 0)
    panic("switchuvm: no process");
  if(p->kstack == 0)
    panic("switchuvm: no kstack");
  if(p->pgdir == 0)
    panic("switchuvm: no pgdir");

  pushcli();
  mycpu()->gdt[SEG_TSS] = SEG16(STS_T32A, &mycpu()->ts,
                                sizeof(mycpu()->ts)-1, 0);
  mycpu()->gdt[SEG_TSS].s = 0;
  mycpu()->ts.ss0 = SEG_KDATA << 3;
  mycpu()->ts.esp0 = (uint)p->kstack + KSTACKSIZE;
  // setting IOPL=0 in eflags *and* iomb beyond the tss segment limit
  // forbids I/O instructions (e.g., inb and outb) from user space
  mycpu()->ts.iomb = (ushort) 0xFFFF;
  ltr(SEG_TSS << 3);
  lcr3(V2P(p->pgdir));  // switch to process's address space
  popcli();
}

進程的頁表在使用前往往需要初始化,其中必須包含內核代碼的映射,這樣進程在進入內核時便不需要再次切換頁表,進程使用虛擬地址空間的低地址部分,高地址部分留給內核,設置頁表時通過調用setupkvm、allocuvm、deallocuvm接口完成相關操作。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章