計算機組成原理Ⅱ課程設計 PA3.2
計算機組成原理Ⅱ課程設計 PA3.2
目錄
目錄思考題
-
一些問題
-
高20位是基地址,低12位是頁內偏移量,加在一起一共32位
-
實體地址是必須的。在虛擬地址轉化為虛擬地址時,需要根據
CR3
暫存器得到頁目錄表基址,若CR3
為虛擬地址,則會現如死迴圈 -
(1)使用多級頁表可以使得頁表在記憶體中離散儲存。多級頁表實際上是增加了索引,有了索引就可以定位到具體的項。舉個例子:比如虛擬地址空間大小為4G,每個頁大小依然為4K,如果使用一級頁表的話,共有2^20個頁表項,如果每一個頁表項佔4B,那麼存放所有頁表項需要4M,為了能夠隨機訪問,那麼就需要連續4M的記憶體空間來存放所有的頁表項。隨著虛擬地址空間的增大,存放頁表所需要的連續空間也會增大,在作業系統記憶體緊張或者記憶體碎片較多時,這無疑會帶來額外的開銷。但是如果使用多級頁表,我們可以使用一頁來存放頁目錄項,頁表項存放在記憶體中的其他位置,不用保證頁目錄項和頁表項連續。
(2)使用多級頁表可以節省頁表記憶體。使用一級頁表,需要連續的記憶體空間來存放所有的頁表項。多級頁表通過只為程序實際使用的那些虛擬地址記憶體區請求頁表來減少記憶體使用量(出自《深入理解Linux核心》第三版51頁)。舉個例子:一個程序的虛擬地址空間是4GB,假如程序只使用4MB記憶體空間。對於一級頁表,我們需要4M空間來存放這4GB虛擬地址空間對應的頁表,然後可以找到程序真正使用的4M記憶體空間。也就是說,雖然程序實際上只使用了4MB的記憶體空間,但是為了訪問它們我們需要為所有的虛擬地址空間建立頁表。但是如果使用二級頁表的話,一個頁目錄項可以定位4M記憶體空間,存放一個頁目錄項佔4K,還需要一頁用於存放程序使用的4M(4M=1024*4K,也就是用1024個頁表項可以對映4M記憶體空間)記憶體空間對應的頁表,總共需要4K(頁表)+4K(頁目錄)=8K來存放程序使用的這4M記憶體空間對應頁表和頁目錄項,這比使用一級頁表節省了很多記憶體空間。當然,在這種情況下,使用多級頁表確實是可以節省記憶體的。但是,我們需要注意另一種情況,如果程序的虛擬地址空間是4GB,而程序真正使用的記憶體也是4GB,如果是使用一級頁表,則只需要4MB連續的記憶體空間存放頁表,我們就可以定址這4GB記憶體空間。而如果使用的是二級頁表的話,我們需要4MB記憶體存放頁表,還需要4KB記憶體來存放頁目錄項,此時多級頁表反倒是多佔用了記憶體空間。注意在大多數情況都是程序的4GB虛擬地址空間都是沒有使用的,實際使用的都是小於4GB的,所以我們說多級頁表可以節省頁表記憶體。
-
-
空指標真的是'空'的嗎?
空指標應該也表示一個虛擬地址,只不過在虛擬地址轉換為實體地址後,對應的實體地址會觸發空指標解引用的錯誤。
-
理解
_map
函式void _map(_Protect *p, void *va, void *pa) { PDE *pt = (PDE*)p->ptr; PDE *pde = &pt[PDX(va)]; if (!(*pde & PTE_P)) { *pde = PTE_P | PTE_W | PTE_U | (uint32_t)palloc_f(); } PTE *pte = &((PTE*)PTE_ADDR(*pde))[PTX(va)]; if (!(*pte & PTE_P)) { *pte = PTE_P | PTE_W | PTE_U | (uint32_t)pa; } }
通過
p->ptr
可以獲取頁目錄的基地址,然後根據傳入的虛擬地址和基地址得到頁目錄項。判斷是否需要申請新的頁表,若需要,則可以通過回撥函式palloc_f()
向 Nanos-lite 獲取一頁空閒的物理頁,把剛剛申請的物理頁地址與其他標誌位一併存入一個頁目錄裡。使用這個頁目錄再申請一個頁表項,最後把實體地址連同標誌位一起放入這個頁表項。 -
核心對映的作⽤
pd_val.present
報錯。因為沒有進行核心對映拷貝,所以沒有頁表來存放對應的核心區的虛擬地址,就會出現這種報錯。 -
git log 和 遠端倉庫截圖
實驗內容
1 新增分頁控制相關暫存器
-
CR0, CR3 暫存器的定義
引入標頭檔案
#include "memory/mmu.h"
在
CPU_state
中新增兩個暫存器CR0 cr0; CR3 cr3;
-
初始化 CR0, CR3 暫存器
在
monitor.c
中的restart()
中新增cpu.cr0.val=0x60000011;
2. 修改訪存函式
按照講義的樣子修改即可。
要判斷是否出現數據跨越頁的邊界的情況,可以計算本資料地址加上讀取資料長度是否比一頁大即可。我們知道一頁的大小為4KB,即4096=0x1000。我們可以用addr對4096取餘來得到頁的起始位置,然後將這個起始位置與len相加再與0x1000比較就可以知道是否跨頁了。(addr%4096 <=> addr&0xFFF)
-
vaddr_read()
uint32_t vaddr_read(vaddr_t addr, int len) { if(cpu.cr0.paging) { if ((addr&0xFFF)+len>0x1000) { /* this is a special case, you can handle it later. */ assert(0); } else { paddr_t paddr = page_translate(addr,false); return paddr_read(paddr, len); } } else return paddr_read(addr,len); }
-
vaddr_write()
void vaddr_write(vaddr_t addr, int len, uint32_t data) { if(cpu.cr0.paging) { if ((addr&0xFFF)+len>0x1000) { /* this is a special case, you can handle it later. */ assert(0); } else { paddr_t paddr = page_translate(addr,true); return paddr_write(paddr, len, data); } } else return paddr_write(addr, len, data); }
3. page_translate()
按照講義要求:
-
該函式用於地址轉換,傳入虛擬地址作為引數,函式返回值為實體地址;
-
該函式的實現過程即為我們理論課學到的頁級轉換過程(先找頁目錄項,然後取出);
-
注意使用
assert
來驗證present
位,否則會造成除錯困難; -
PDE
和PTE
的資料結構框架已幫我們定義好,在mmu.h
中; -
注意每個頁目錄想和每個頁表項儲存在記憶體中的地址均為實體地址,使用
paddr_read
去讀取,如果使用vaddr_read
去讀取會造成死遞迴(為什麼?); -
此外,還需要實現訪問位和髒位的功能;
-
需要在
page_translate
中插入Log
並截圖表示實現成功(截圖後可去除 Log 以免影響效能); -
如何編寫這個函式?
-
根據
CR3
暫存器得到頁目錄表基址(是個實體地址); -
用這個基址和從虛擬地址中隱含的
頁目錄
欄位項結合計算出所需頁目錄項地址(是個實體地址);
- 請思考一下這裡所謂的“結合”需要經過哪些處理才能得到正確地地址呢?
-
從記憶體中讀出這個頁目錄項,並對有效位進行檢驗;
-
將取出的
PDE
和虛擬地址的頁表
欄位相組合,得到所需頁表項地址(是個實體地址); -
從記憶體中讀出這個頁表項,並對有效位進行檢驗;
-
檢驗
PDE
的accessed
位,如果為0
則需變為1
,並寫回到頁目錄項所在地址; -
檢驗
PTE
的accessed
位如果為0
,或者PTE
的髒位為0
且現在正在做寫記憶體操作,滿足這兩個條件之一時需要將accessed
位,然後更新dirty
位,最後並寫回到頁表項所在地址; -
頁級地址轉換結束,返回轉換結果(是個實體地址).
並結合視訊裡王助教的小例子,很容易就實現
paddr_t page_translate(vaddr_t vaddr,bool isWrite) {
Log("addr:0x%x\n",vaddr);
//cr3高20位
vaddr_t CR3 = cpu.cr3.page_directory_base<<12;
Log("CR3:0x%x\n",CR3);
//vaddr高10位
vaddr_t dir = (vaddr>>22)*4;
Log("dir:0x%x\n",dir);
//取出cr3的高20位與vaddr的高8位結合
paddr_t pdAddr = CR3 + dir;
Log("pdAddr:0x%x\n",pdAddr);
//讀取
PDE pd_val;
pd_val.val = paddr_read(pdAddr,4);
Log("pdAddr:0x%x pd_val0x%x\n",pdAddr,pd_val.val);
assert(pd_val.present);
//獲取高20位
vaddr_t t1 = pd_val.page_frame<<12;
//獲取第二個十位page
vaddr_t t2 = ((vaddr>>12)&0x3FF)*4;
paddr_t ptAddr = t1 + t2;
Log("pt_addr:0x%x\n",ptAddr);
PTE pt_val;
pt_val.val = paddr_read(ptAddr,4);
assert(pt_val.present);
Log("pt_addr:0x%x pt_val:0x%x\n",ptAddr,pt_val.val);
//獲取高20位
t1 = pt_val.page_frame<<12;
//獲取最後12位
t2 = vaddr&0xFFF;
paddr_t paddr = t1 + t2;
Log("paddr:0x%x\n",paddr);
pd_val.accessed = 1;
paddr_write(pdAddr,4,pd_val.val);
if ((pt_val.accessed == 0) || (pt_val.dirty ==0 && isWrite)){
pt_val.accessed=1;
pt_val.dirty=1;
}
paddr_write(ptAddr,4,pt_val.val);
return paddr;
}
發現有指令沒有實現
mov_r2cr() mov_cr2r()
填表
/* 0x20 */ IDEX(mov_G2E,mov_cr2r), EMPTY, IDEX(mov_E2G,mov_r2cr), EMPTY,
由於我們只用到了cr0
cr3
,因此只需要寫這兩個就行
make_EHelper(mov_r2cr) {//給cr暫存器賦值
switch (id_dest->reg)
{
case 0:
cpu.cr0.val = id_src->val;
break;
case 3:
cpu.cr3.val = id_src->val;
break;
default:
assert(0);
break;
}
print_asm("movl %%%s,%%cr%d", reg_name(id_src->reg, 4), id_dest->reg);
}
make_EHelper(mov_cr2r) {//給暫存器賦值
switch (id_dest->reg)
{
case 0:
id_src->val = cpu.cr0.val;
break;
case 3:
id_src->val = cpu.cr3.val;
break;
default:
assert(0);
break;
}
print_asm("movl %%cr%d,%%%s", id_src->reg, reg_name(id_dest->reg, 4));
#ifdef DIFF_TEST
diff_test_skip_qemu();
#endif
}
4. 修改 loader()
把navy-apps/Makefile.compile
中的連結地址 -Ttext
引數改為 0x8048000
並重新編譯
把nanos-lite/src/loader.c
中的中的 DEFAULT_ENTRY
也需要作相應的修改
#define DEFAULT_ENTRY ((void *)0x8048000)
在nanos-lite/src/main.c
中進行如下操作,別忘了宣告函式load_prog()
- uintptr_t entry = loader(NULL, "/bin/pal");- ((void (*)(void))entry)();+ load_prog("/bin/dummy");
按要求修改loader()
:
- 開啟待裝入的檔案後,還需要獲取檔案大小;
- 需要迴圈判斷是否已建立足夠的頁來裝入程式;
- 對於程式需要的每一頁,做三個事情,即4,5,6步:
- 使用
Nanos-lite
的MM
提供的new_page()
函式獲取一個空閒物理頁 - 使用對映函式
_map()
將本虛擬空間內當前正在處理的這個頁和上一步申請到的空閒物理頁建立對映 - 讀一頁內容,寫到這個物理頁上
- 每一頁都處理完畢後,關閉檔案,並返回程式入口點地址(虛擬地址)
uintptr_t loader(_Protect *as, const char *filename) {
//ramdisk_read(DEFAULT_ENTRY,0,get_ramdisk_size());
//讀取檔案位置
int index=fs_open(filename,0,0);
//讀取長度
size_t length=fs_filesz(index);
//讀取內容
//fs_read(index,DEFAULT_ENTRY,length);
void *va;
void *pa;
int page_count = length/4096 + 1;//獲取頁數量
for (int i=0;i<page_count;i++){
va = DEFAULT_ENTRY + 4096*i;
pa = new_page();
Log("Map va to pa: 0x%08x to 0x%08x",va,pa);
_map(as,va,pa);
fs_read(index,pa,4096);
}
//關閉檔案
fs_close(index);
return (uintptr_t)DEFAULT_ENTRY;
}
重點注意一頁的大小是4K,所以計算頁數時要除4096
執行後發現vaddr_read()
報錯了,推測是出現數據跨越虛擬頁邊界的情況
根據視訊所講,我們可以分別讀出兩頁的內容,再按小端方式組合即可。注意要使用page_translate()
將虛擬地址轉化為實體地址再去讀取,最後小端方式組合資料時,就是把第二頁的資料放在高位。(這裡的affr&0xFFF相當於addr對4096取餘,即獲取本頁開始位置)
uint32_t vaddr_read(vaddr_t addr, int len) {
if(cpu.cr0.paging) {
if ((addr&0xFFF)+len>0x1000) {
/* this is a special case, you can handle it later. */
int len1,len2;
len1 = 0x1000-(addr&0xfff);//獲取前一頁的佔用空間
len2 = len - len1;//獲取後一頁的佔用空間
paddr_t addr1 = page_translate(addr,false);//虛擬地址轉換為實體地址
uint32_t data1 = paddr_read(addr1,len1);//讀取內容
paddr_t addr2 = page_translate(addr+len1,false);//虛擬地址轉換為實體地址
uint32_t data2 = paddr_read(addr2,len2);//讀取內容
//len1<<3表示獲取data1的位數
uint32_t data = (data2<<(len1<<3))+data1;//把data2的資料移到高位,組合讀取到的內容。
return data;
}
else {
paddr_t paddr = page_translate(addr,false);
return paddr_read(paddr, len);
}
}
else
return paddr_read(addr,len);
}
void vaddr_write(vaddr_t addr, int len, uint32_t data) {
if(cpu.cr0.paging) {
if ((addr&0xFFF)+len>0x1000) {
/* this is a special case, you can handle it later. */
int len1,len2;
len1 = 0x1000-(addr&0xfff);//獲取前一頁的佔用空間
len2 = len - len1;//獲取後一頁的佔用空間
paddr_t addr1 = page_translate(addr,true);//虛擬地址轉換為實體地址
paddr_write(addr1,len1,data);//寫入內容
data2 = data >> (len1<<3);
paddr_t addr2 = page_translate(addr+len1,true);
paddr_write(addr2,len2,data2);
}
else {
paddr_t paddr = page_translate(addr,true);
return paddr_write(paddr, len, data);
}
}
else
return paddr_write(addr, len, data);
}
成功執行
5. 在分頁上執行仙劍奇俠傳
mm_brk()
框架已經幫忙實現
之後把原來系統呼叫sys_brk()
換成mm_brk()
就行
case SYS_brk: SYSCALL_ARG1(r)=mm_brk(SYSCALL_ARG2(r)); break;
遇到的問題及解決辦法
-
遇到問題:不知道如何判斷跨頁的情況
解決方案:讀了好幾遍講義,發現講義裡有介紹頁面大小為4K,那麼就可以通過取餘獲取起使位置。後來,我又通過檢視
i386
,瞭解到表項的後12位是頁內偏移量,則可以&0xFFF來獲得起始位置。
實驗心得
本次實驗內容不多,主要了解分頁機制,把虛擬地址轉換為實體地址即可。主要是看了王助教的視訊,有了那幾個例子的講解,看講義以及實現就輕鬆多了。
其他備註
助教真帥