1. 程式人生 > 其它 >北航作業系統課程lab2實驗報告

北航作業系統課程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

三級頁表頁目錄的基地址:

PD1base = (PTbase >> 9) + PTbase

對映到頁目錄自身的頁目錄項:

PD2base = (PD1base >> 9) + PD1base

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主要是對頁表項賦值