北航操作系統課程lab2實驗報告


lab2 OS實驗報告

實驗思考題

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 in concurrent use; but it probably won’t happen too often.

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

頁表基地址(page table) 為PTbase

頁中間目錄基地址(page middle directory) PMDbase:

(PTbase << 12) >> 3 + PTbase

頁全局目錄(page global directory) PGDbase:

(PTbase << 21) >> 3 + PMDbase(三級頁表頁目錄的基地址)

頁全局目錄項(page global directory entry)PGDE:

(PTbase << 30) >> 3 + PGDbase(映射到頁目錄自身的頁目錄項)

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_nextfield事實上就是包含兩個指針*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 Pagestruct Env的空間及各自的二級頁表。Exercise 2.8和2.9將物理頁面和虛擬頁面結合起來了,分配物理頁面,可以讓鏈表減少對應的節點,並讓頁表增加對應的表項。

物理存儲Exercise主要是對struct Page進行處理,虛擬存儲Exercise主要是對頁表項賦值


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM