實驗思考題
Thinking 2.1
指針變量存儲的是虛擬地址,MIPS匯編程序中使用的也是虛擬地址。因為實驗使用的R3000 CPU只會發出虛擬地址,然后虛擬地址映射到物理地址,使用物理地址進行訪存。
Thinking 2.2
宏的一個本身的特性就是可重用,跟函數一樣,可以將一段代碼封裝成一條語句。當這段代碼的具體實現需要更改時,只需要改宏這一處就行。宏相比函數也更加輕便,可以用於結構體定義等,由於是字符串的替換,因此不必進行地址的跳轉和棧的保存,但值得注意的是在編寫宏的時候需要着重注意語法是否有漏洞。此外do/while(0)的架構也大大方便了調用這些宏,可以直接將其當做函數看待。
在實驗環境中,只看到了單向鏈表、雙向鏈表、單向隊列、雙向隊列、循環隊列,感覺循環隊列在插入和刪除操作方面和循環鏈表沒太大差異,據此進一步分析。
插入操作:單向鏈表插入操作十分簡單,兩行代碼,雙向鏈表插入操作一般運行四行代碼,需要額外判斷是否next指向了NULL,循環鏈表與雙向鏈表運行代碼量基本相等,需額外判斷是否next指向了頭指針。特別的是,插入到頭結點對三種鏈表而言性能相似,單向鏈表與雙向鏈表插入到尾結點均要遍歷完整個鏈表。
刪除操作:單向鏈表的刪除操作復雜度為O(n),因為需要靠循環才能找到上一個鏈表節點的位置,雙向鏈表及循環鏈表的刪除操作與插入性能相近,也還是需要額外判斷NULL或HEAD。刪除頭結點對三種鏈表而言性能相似,而單向鏈表與雙向鏈表刪除尾結點還是要遍歷。
Thinking 2.3
typedef LIST_ENTRY(Page) Page_LIST_entry_t;
struct Page {
Page_LIST_entry_t pp_link; /* free list link */
// Ref is the count of pointers (usually in page table entries)
// to this page. This only holds for pages allocated using
// page_alloc. Pages allocated at boot time using pmap.c's "alloc"
// do not have valid reference count fields.
u_short pp_ref;
};
#define LIST_HEAD(name, type) \
struct name { \
struct type *lh_first; /* first element */ \
}
#define LIST_ENTRY(type) \
struct { \
struct type *le_next; /* next element */ \
struct type **le_prev; /* address of previous next element */ \
}
答案選C。Page_list中含有的是Page結構體指針頭。每一個Page內存控制塊都有一個pp_ref
用於表示其引用次數(為0時便可remove),還有一個結構體用於存放實現雙向鏈表的指針。
Thinking 2.4
//在boot_map_segment()函數中調用到了boot_pgdir_walk()函數
//以此得到虛擬地址所對應的二級頁表項
pgtable_entry = boot_pgdir_walk(pgdir, va_temp, 1); //create
//在mips_vm_init()函數中調用到了boot_map_segment函數
boot_map_segment(pgdir, UPAGES, n, PADDR(pages), PTE_R);
boot_map_segment(pgdir, UENVS, n, PADDR(envs), PTE_R);
//alloc已經分配好了虛擬地址
//boot_map_segment分別將頁面結構體與進程控制塊結構體的虛擬地址映射成物理地址
Thinking 2.5
ASID的必要性:同一虛擬地址在不同地址空間中通常映射到不同物理地址,ASID可以判斷是在哪個地址空間。例如有多個進程都用到了這個虛擬地址,但若該虛擬地址對應的數據不是共享的,則基本可以表明指向的是不同物理地址,這也是一種對地址空間的保護。
可容納不同地址空間的最大數量:64個,參考原文如下:
Instead, the OS assigns a 6-bit unique code to each task’s distinct address space. Since the ASID is only 6 bits long, OS software does have to lend a hand if there are ever more than 64 address spaces
Thinking 2.6
tlb_invalidate調用tlb_out
調用tlb_invalidate可以將該地址空間的虛擬地址對應的表項清除出去,一般用於這個虛擬空間引用次數為0時釋放tlb空間
LEAF(tlb_out)
//1: j 1b
nop
mfc0 k1,CP0_ENTRYHI //保存ENTRIHI原有值
mtc0 a0,CP0_ENTRYHI //將傳進來的參數放進ENTRYHI中
nop
tlbp// insert tlbp or tlbwi //檢測ENTRYHI中的虛擬地址在tlb中是否有對應項
nop
nop
nop
nop
mfc0 k0,CP0_INDEX //INDEX可以用來判斷是否命中
bltz k0,NOFOUND //若未命中,則跳轉
nop
mtc0 zero,CP0_ENTRYHI //將ENTRYHI清零
mtc0 zero,CP0_ENTRYLO0 //將ENTRYLO清零
nop
tlbwi// insert tlbp or tlbwi //將清零后的兩寄存器值寫入到對應tlb表項中
//相當於刪除原有的tlb表項
NOFOUND:
mtc0 k1,CP0_ENTRYHI //將原來的ENTRYHI恢復
j ra //return address
nop
END(tlb_out)
Thinking 2.7
Thinking 2.8
X86用到三個地址空間的概念:物理地址、線性地址和邏輯地址。而MIPS只有物理地址和虛擬地址兩個概念。相對而言,段機制對大量應用程序分散地使用大內存的支持能力較弱。所以Intel公司又加入了頁機制,每個頁的大小是固定的(一般為4KB),也可完成對內存單元的安全保護,隔離,且可有效支持大量應用程序分散地使用大內存的情況。x86體系中,TLB表項更新能夠由硬件自己主動發起,也能夠有軟件主動更新。
分段機制和分頁機制都啟動:邏輯地址--->段機制處理--->線性地址--->頁機制處理--->物理地址
RISC-V提供三種權限模式(MSU),而MIPS只提供內核態和用戶態兩種權限狀態。RISC-V SV39支持39位虛擬內存空間,每一頁占用4KB,使用三級頁表訪存。
實驗難點展示
填寫代碼的主要難點在於對C語言指針的運用理解,同時也需要了解一些宏的知識,並且要記住不同的宏可以用來做什么事。
Exercise 2.2
編寫代碼如下
/* Exercise 2.2 */
/*
* Insert the element "elm" *after* the element "listelm" which is
* already in the list. The "field" name is the link element
* as above.
*/
#define LIST_INSERT_AFTER(listelm, elm, field) do{ \
LIST_NEXT((elm), field) = LIST_NEXT((listelm), field); \
if (LIST_NEXT((listelm),field) != NULL) \
LIST_NEXT((listelm), field)->field.le_prev = &LIST_NEXT((elm), field); \
LIST_NEXT((listelm), field) = (elm); \
(elm)->field.le_prev = &LIST_NEXT((listelm), field); \
} while(0)
// Note: assign a to b <==> a = b
//Step 1, assign elm.next to listelm.next.
//Step 2: Judge whether listelm.next is NULL, if not, then assign listelm.next.pre to a proper value.
//step 3: Assign listelm.next to a proper value.
//step 4: Assign elm.pre to a proper value.
/*
* Insert the element "elm" at the tail of the list named "head".
* The "field" name is the link element as above. You can refer to LIST_INSERT_HEAD.
* Note: this function has big differences with LIST_INSERT_HEAD !
*/
#define LIST_INSERT_TAIL(head, elm, field) do { \
if (LIST_FIRST((head)) != NULL) { \
LIST_NEXT((elm), field) = LIST_FIRST((head)); \
while (LIST_NEXT(LIST_NEXT((elm), field), field) != NULL) { \
LIST_NEXT((elm), field) = LIST_NEXT(LIST_NEXT((elm), field), field); \
} \
LIST_NEXT(LIST_NEXT((elm), field), field) = (elm); \
(elm)->field.le_prev = &LIST_NEXT(LIST_NEXT((elm), field), field); \
LIST_NEXT((elm), field) = NULL; \
} else \
LIST_INSERT_HEAD((head), (elm), field); \
} while (0)
結構示意圖如上,每一個框其實就是可以清晰地看到后者的le_prev
指針指向的是前者的le_next
地址。這個地址下的值類型是一個指向后者結構體的指針。也即le_prev = &le_next
。在我看來指針的指針在刪除節點時可以少做更快捷,但增加了讀代碼的難度,或許會有點多此一舉。
LIST_NEXT((elm), field)
這個宏實際上就是表示的elm
結構體指向的下一個結構體(elm)->field.le_next
而field
事實上就是包含兩個指針*le_next
和**le_prev
的結構體,感覺也是有點繞。這么繞的這些指令還真就促成了一些易懂的表達式,le_prev = &LIST_NEXT((elm), field)
其實質就是le_prev = &le_next
。
Exercise 2.3
void page_init(void)
//對物理頁面控制塊進行操作
//以下是最重要的兩個部分
struct Page *now;
for (now = pages; page2kva(now) < freemem; now++) {
now -> pp_ref = 1;
}//將已分配好的頁面引用次數置1
for (; page2ppn(now) < npage; now ++) {
now -> pp_ref = 0;
LIST_INSERT_HEAD(&page_free_list, now, pp_link);
}//將未分配的頁面引用次數置0,並加入到空閑列表中
Exercise 2.4
int page_alloc(struct Page **pp)
//用於分配物理頁面
ppage_temp = LIST_FIRST(&page_free_list);
//得到空閑列表頭的一個頁面
LIST_REMOVE(ppage_temp, pp_link);
//因為要分配了,所以在原有空閑列表頭中刪掉
這個list其實就是物理內存的鏈表了,此時建立了內存管理,故可用鏈表進行物理內存的分配,相比於alloc的順序分配不同。
Exercise 2.6
static Pte *boot_pgdir_walk(Pde *pgdir, u_long va, int create)
//用於得到二級頁表的地址
//……
*pgdir_entryp = PADDR(alloc(BY2PG,BY2PG,1)); //allocate one page
//得到一級表項中二級表項的物理地址(PADDR將低12位標志位清除)
*pgdir_entryp = *pgdir_entryp | PTE_V | PTE_R; //set valid and dirty bit
//一級表項中低12位用於設置標志位
//向一級頁表項中填入所在二級頁表物理地址及標志位
Exercise 2.7
void boot_map_segment(Pde *pgdir, u_long va, u_long size, u_long pa, int perm)
//用於將實頁物理地址存到二級頁表項中
//……
for (i = 0, size = ROUND(size, BY2PG); i < size; i += BY2PG) {
//Step 1. use `boot_pgdir_walk` to "walk" the page directory \*/*
pgtable_entry = boot_pgdir_walk(pgdir,va + i, 1);
/* create if entry of page directory not exists yet
* 把二級頁表項的地址找出來 */
//Step 2. fill in the page table
*pgtable_entry = (PTE_ADDR(pa + i)/* III. Physical Frame Address of `pa + i`*/| perm | PTE_V;
//向二級頁表項中填入所在頁物理地址及標志位
}
值得一提的是,boot_pgdir_walk()
在一級頁表項中填入了二級頁表頭的物理地址,並返回了虛擬地址va
的對應二級頁表項虛擬地址,完成頁目錄的初始化。boot_map_segment()
在二級頁表項中填入了實頁的物理地址,完成頁表的初始化。
Exercise 2.8
int pgdir_walk(Pde *pgdir, u_long va, int create, Pte **ppte)
//用於得到二級頁表的地址
*pgdir_entryp = (page2pa(ppage)/* Physical Address of `page` */) | PTE_V | PTE_R;
ppage->pp_ref++; // 因為該頁被分配了,所以引用次數++
Exercise 2.9
int page_insert(Pde *pgdir, struct Page *pp, u_long va, u_int perm)
//用於將實頁物理地址存到二級頁表項中
pgdir_walk(pgdir, va, 0, &pgtable_entry);
//把二級頁表項的地址找出來
tlb_invalidate(pgdir, va);
//update tlb
*pgtable_entry = page2pa(pp) | PERM;
//將實頁物理地址和標志位放進去
pp->pp_ref++;
//該頁被分配,引用次數++
啟動時區間地址映射函數是用返回值返回二級頁表項虛擬地址,而運行時區間地址映射函數是直接用指針作為參數傳遞該地址,取而代之返回了一個是否運行失敗的int值。
體會與感想
感覺好難……就很亂,記不住呀。不太懂tlb是怎么形成的,只會一個tlb_invalidate
使tlb表項無效的一個函數。
Exercise 2.1和2.2屬於准備工作,用宏定義鏈表為后面的代碼重用帶來了巨大的方便,而且宏名字也是很清晰簡潔的。Exercise 2.3—2.5也是為struct Page
的鏈表做准備,書寫了管理物理頁面的鏈表的一個方法。Exercise 2.6和2.7用於初始化兩級頁表,這是在mips_vm_init()
中調用的,分配一級頁表和struct Page
、struct Env
的空間及各自的二級頁表。Exercise 2.8和2.9將物理頁面和虛擬頁面結合起來了,分配物理頁面,可以讓鏈表減少對應的節點,並讓頁表增加對應的表項。
物理存儲Exercise主要是對struct Page