Exercise1 源代碼閱讀
1.內存管理部分: kalloc.c vm.c 以及相關其他文件代碼
- kalloc.c:char * kalloc(void)負責在需要的時候為用戶空間、內核棧、頁表頁以及緩沖區分配物理內存,將物理地址轉為虛擬地址返回,物理頁大小為4k。void kfree(char * v)接收一個虛擬地址,找對對應的物理地址進行釋放。xv6使用空閑內存的前部分作為指針域來指向下一頁空閑內存,物理內存管理是以頁(4K)為單位進行分配的。物理內存空間上空閑的每一頁,都有一個指針域(虛擬地址)指向下一個空閑頁,最后一個空閑頁為NULL ,通過這種方式,kmem只需要保存着虛擬地址空間上的freelist地址即可;
// kalloc.c
// Physical memory allocator, intended to allocate
// memory for user processes, kernel stacks, page table pages,
// and pipe buffers. Allocates 4096-byte pages.
void freerange(void *vstart, void *vend);
extern char end[]; // first address after kernel loaded from ELF file
struct run {
struct run *next;
};
struct {
struct spinlock lock;
int use_lock;
struct run *freelist;
} kmem;
- xv6讓每個進程都有獨立的頁表結構,在切換進程時總是需要切換頁表,switchkvm設置cr3寄存器的值為kpgdir首地址,kpgdir僅僅在scheduler內核線程中使用。頁表和內核棧都是每個進程獨有的,xv6使用結構體proc將它們統一起來,在進程切換的時候,他們也往往隨着進程切換而切換,內核中模擬出了一個內核線程,它獨占內核棧和內核頁表kpgdir,它是所有進程調度的基礎。switchuvm通過傳入的proc結構負責切換相關的進程獨有的數據結構,其中包括TSS相關的操作,然后將進程特有的頁表載入cr3寄存器,完成設置進程相關的虛擬地址空間環境;
// vm.c
……
// 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
}
// Switch TSS and h/w page table to correspond to process p.
void
switchuvm(struct proc *p)
{
pushcli();
cpu->gdt[SEG_TSS] = SEG16(STS_T32A, &cpu->ts, sizeof(cpu->ts)-1, 0);
cpu->gdt[SEG_TSS].s = 0;
cpu->ts.ss0 = SEG_KDATA << 3;
cpu->ts.esp0 = (uint)proc->kstack + KSTACKSIZE;
ltr(SEG_TSS << 3);
if(p->pgdir == 0)
panic("switchuvm: no pgdir");
lcr3(v2p(p->pgdir)); // switch to new address space
popcli();
}
- 進程的頁表在使用前往往需要初始化,其中必須包含內核代碼的映射,這樣進程在進入內核時便不需要再次切換頁表,進程使用虛擬地址空間的低地址部分,高地址部分留給內核,主要接口:
- pde_t * setupkvm(void)通過kalloc分配一頁內存作為頁目錄,然后將按照kmap數據結構映射內核虛擬地址空間到物理地址空間,期間調用了工具函數mappages;
- int allocuvm(pde_t * pgdir, uint oldsz, uint newsz)在設置頁表的同時分配虛擬地址oldsz到newsz的以頁為單位的內存;
- int deallocuvm(pde_t * pgdir, uint oldsz, uint newsz)則將newsz到oldsz對應的虛擬地址空間內存置為空閑;
- int loaduvm(pde_t * pgdir, char * addr, struct inode * ip, uint offset, uint sz)將文件系統上的i節點內容讀取載入到相應的地址上,通過allocuvm接口為用戶進程分配內存和設置頁表,然后調用loaduvm接口將文件系統上的程序載入到內存,便能夠為exec系統調用提供接口,為用戶進程的正式運行做准備;
- 當進程銷毀需要回收內存時,調用void freevm(pde_t * pgdir)清除用戶進程相關的內存環境,其首先調用將0到KERNBASE的虛擬地址空間回收,然后銷毀整個進程的頁表;
- pde_t * copyuvm(pde_t * pgdir, uint sz)負責復制一個新的頁表並分配新的內存,新的內存布局和舊的完全一樣,xv6使用這個函數作為fork()底層實現。
Exercise2 帶着問題閱讀
1.XV6初始化之后到執行main.c時,內存布局是怎樣的(其中已有哪些內容)?
-
內核代碼存在於物理地址低地址的0x100000處,頁表為main.c文件中的entrypgdir數組,其中虛擬地址低4M映射物理地址低4M,虛擬地址 [KERNBASE, KERNBASE+4MB) 映射到 物理地址[0, 4MB);
-
緊接着調用kinit1初始化內核末尾到物理內存4M的物理內存空間為未使用,然后調用kinit2初始化剩余內核空間到PHYSTOP為未使用。kinit1調用前使用的還是最初的頁表(也就是是上面的內存布局),所以只能初始化4M,同時由於后期再構建新頁表時也要使用頁表轉換機制來找到實際存放頁表的物理內存空間,這就構成了自舉問題,xv6通過在main函數最開始處釋放內核末尾到4Mb的空間來分配頁表,由於在最開始時多核CPU還未啟動,所以沒有設置鎖機制。kinit2在內核構建了新頁表后,能夠完全訪問內核的虛擬地址空間,所以在這里初始化所有物理內存,並開始了鎖機制保護空閑內存鏈表;
-
然后main函數通過調用void kvmalloc(void)函數來實現內核新頁表的初始化;
-
最后內存布局和地址空間如下:內核末尾物理地址到物理地址PHYSTOP的內存空間未使用,虛擬地址空間KERNBASE以上部分映射到物理內存低地址相應位置。
// kalloc.c
// Initialization happens in two phases.
// 1. main() calls kinit1() while still using entrypgdir to place just
// the pages mapped by entrypgdir on free list.
// 2. main() calls kinit2() with the rest of the physical pages
// after installing a full page table that maps them on all cores.
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;
}
// kmap.c
……
// 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
};
……
2.XV6 的動態內存管理是如何完成的? 有一個kmem(鏈表),用於管理可分配的物理內存頁。(vend=0x00400000,也就是可分配的內存頁最大為4Mb)
詳見“Exercise 1 源代碼閱讀”部分,已經作出完整解答。
3.XV6的虛擬內存是如何初始化的? 畫出XV6的虛擬內存布局圖,請說出每一部分對應的內容是什么。見memlayout.h和vm.c的kmap上的注釋?
- main函數通過調用void kinit1(void * vstart, void * vend), void kinit2(void * vstart, void * vend), void kvmalloc(void)函數來實現內核新頁表的初始化。虛擬地址與物理地址的轉換接口:
// memlayout.h
// Memory layout
#define EXTMEM 0x100000 // Start of extended memory
#define PHYSTOP 0xE000000 // Top physical memory
#define DEVSPACE 0xFE000000 // Other devices are at high addresses
// Key addresses for address space layout (see kmap in vm.c for layout)
#define KERNBASE 0x80000000 // First kernel virtual address
#define KERNLINK (KERNBASE+EXTMEM) // Address where kernel is linked
#ifndef __ASSEMBLER__
static inline uint v2p(void *a) { return ((uint) (a)) - KERNBASE; }
static inline void *p2v(uint a) { return (void *) ((a) + KERNBASE); }
#endif
#define V2P(a) (((uint) (a)) - KERNBASE)
#define P2V(a) (((void *) (a)) + KERNBASE)
#define V2P_WO(x) ((x) - KERNBASE) // same as V2P, but without casts
#define P2V_WO(x) ((x) + KERNBASE) // same as V2P, but without casts
- 內存布局:
4.關於XV6 的內存頁式管理。發生中斷時,用哪個頁表? 一個內存頁是多大? 頁目錄有多少項? 頁表有多少項? 最大支持多大的內存? 畫出從虛擬地址到物理地址的轉換圖。在XV6中,是如何將虛擬地址與物理地址映射的(調用了哪些函數實現了哪些功能)?
- 發生中斷時,將換入cpu的進程的頁表首地址存入cr3寄存器;一個內存頁為4k;XV6頁表采用的二級目錄,一級目錄有\(2^{10}\)條,二級目錄有\(2^{10} * 2^{10}\)條;頁表項為\(2^2\)Bytes,故頁表有\(2^{12} / 2^2 = 2^{10} = 1024\)項;最大支持4G內存;
- 物理內存頁的申請與釋放,虛擬地址與物理地址如何映射等在“Exercise 1 源代碼閱讀”都已經詳述了,在此主要說下mappages接口,虛擬地址 * va與物理地址 * pa映射size個字節,同時賦予該頁的權限perm,如下:
// vm.c
……
// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned.
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;
}
……
參考文獻
[1] xv6虛擬內存-博客園
[2] xv6 virtual memory-hexo
[3] xv6內存管理-簡書
[4] xv6內存管理-CSDN