頁表是一種操作系統用來控制內存空間的機制,它使得xv6能夠把不同進程的地址空間映射到相同的物理內存上,還能夠為不同的進程的內存提供保護。
在x86中,用戶和內核指令計算的都是虛擬地址,頁表硬件通過映射機制把虛擬地址和物理地址
頁表的具體結構就像一棵樹,樹根是page directory,包含了1024個類似PTE的條目,實際上每一條指向一個page table,每個page table包含1024個32位的PTE條目。這32位中,前20位表示了實際的物理頁號,後12位是一些標誌位。
當xv6翻譯虛擬地址時,先根據前10位在page directory中找到對應的條目,然後跳轉到該條目所指向的page table(若相應的page table未放在page directory中,分頁硬件則會拋出錯誤),在根據後10位在該page table中找到相應的PTE,用該PTE中的高20位來替換虛擬地址中的高20位,即得到物理地址。
疑問:為何大部分虛擬地址沒有進行映射?沒有映射的頁表頁又是指什麼?
#define PTE_P 0x001 //present
#define PTE_W 0x002 //writeable
#define PTE_U 0x004 //user
#define PTE_PWT 0x008 //write-through
#define PTE_PCD 0x010 //cache-disable
#define PTE_A 0x020 //Accessed
#define PTE_D 0x040 //dirty
#define PTE_PS 0x080 //page size
#define PTE_MBZ 0x180 //bits must be zero
每個進程都有自己的頁表,不同進程的頁表將其用戶內存映射到不同的物理內存中,當進程改變時,xv6會通知分頁硬件切換頁表。這樣就實現了概要中所說的對不同的進程的內存提供保護。
如上圖所示,進程的用戶內存:0到KERNBASE,且把相應的PTE_U設置為1.每個進程的頁表中都包含內核運行所需的所有映射,且他們都在KERNBASE之上,KERNBASE:KERNBASE+PHYSTOP映射到0:PHYSTOP。這些PTE的PTE_U被置為0,因此只有內核能使用這些頁。
疑問:PHYSTOP是什麼?xv6無法使用超過2GB的物理內存是怎麼計算出來的?
程式流程如圖所示
內核在運行時要為頁表、進程的用戶內存、內核棧和管道緩衝區分配物理內存。
xv6一次會分配出去一整個大小為4096byte的頁面。
它維護空閒頁的方法是形成一個空閒的物理頁組成的鏈表,分配內存就將該頁移出鏈表,釋放內存就將該頁存入鏈表。
疑問:自舉問題?
數據結構:鏈表
鏈表元素:struct run
struct run{
struct run*next;
};
分配器初始化的流程如圖所示
具體看一下kfree的代碼部分
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;把v变成指向struct run的一个指针
r−>next = kmem.freelist;//这一步之后v已经和原空闲链表的表头连接起来了
kmem.freelist = r;//生成新的空闲链表
if(kmem.use_lock)
release(&kmem.lock);
}
從這段代碼中我們可以看到kfree是如何把頁表釋放並加入空閒鏈表中的。
同理,kalloc可以把空閒鏈表的表頭分配出去,並從空閒鏈表中移除病返回新的空閒鏈表的表頭。
疑問:分配器代碼中的類型轉換是什麼意思?
上圖顯示了在xv6中執行中的用戶進程的內存結構。堆在棧的上面,它可以通過sbrk增長自己。棧佔用的是單獨一頁內存,包含exec創建的廚師數據。棧的下方有一個保護頁(guard page),可以確保棧的增長不會超過一頁。
疑問:如何使得該程序可以彷彿剛剛調用了函數main?
sbrk是一個允許進程縮小或擴張其內存的系統調用。主要靠growprc命令實現。
growproc(int n)
{
uint sz;
struct proc *curproc = myproc();
sz = curproc−>sz;
if(n > 0){
if((sz = allocuvm(curproc−>pgdir, sz, sz + n)) == 0)
return -1;
}else if(n<0){
if((sz = deallocuvm(curproc−>pgdir, sz, sz + n)) == 0)
return −1;
}
curproc−>sz = sz;
switchuvm(curproc);
return 0;
}
當參數n為正時,growprc會分配一個或多個物理頁並把他們映射到進程的地址空間的頂部,如果參數n為負,則會從進程的地址空間取消映射並釋放這些物理頁。
exec的運行流程圖:
接下來分配兩個頁面,一個用作保護頁,第二個用作棧,把參數拷貝到棧頂,然後把指向它們的指針保存在ustack中。最後創建新的內存映像,發現錯誤則跳轉到bad處並返回-1,無誤則在建立新的映像成功後裝載新映像並釋放舊映像。整個流程完成後返回0代表執行成功。
疑問:偽造的返回PC是什麼意思?argc和argv指針的意義?
-現實中的操作系統的分頁硬件更加精巧
-現實中不適總是需要4MB大小的“超級頁”
-現實中更精準的系統可以分配不同大小的內存塊,而不用總是分配4096bytes
學習完這一章之後對與頁表是如何運作的,以及操作系統的內存管理系統還是有了一定的概念,但是學習的過程中也同樣產生了許多疑惑,希望在後面的學習中能夠有機會把這些疑問解決。