ucore作業系統學習(三) ucore lab3虛擬記憶體管理分析

小熊餐館發表於2020-10-22

1. ucore lab3介紹

虛擬記憶體介紹

  在目前的硬體體系結構中,程式要想在計算機中執行,必須先載入至物理主存中。在支援多道程式執行的系統上,我們想要讓包括作業系統核心在內的各種程式能併發的執行,而物理主存的總量通常是極為有限的,這限制了併發程式的發展。受制於成本問題,擁有足夠大容量主存的個人計算機是普通人承受不起的。因此電腦科學家們另闢蹊徑,想到了利用區域性性原理來解決既要能併發執行大量程式又要使計算機足夠低成本這一矛盾問題。

  區域性性原理告訴我們,大多數程式通常都在執行迴圈邏輯,訪問資料時訪問最頻繁的也是陣列等連續結構。一個程式在某一時刻所執行的程式碼和所訪問的資料通常都聚集在一個很小的範圍內。

  虛擬記憶體機制的核心思想是將物理主存擴充套件到磁碟外存這一容量更大,單位儲存成本更低的儲存介質上,令主存在某種程度上作為了磁碟的快取。因為即使程式所需要的記憶體很大,某一時刻所訪問的記憶體都聚集在一個很小的頁面集合中(工作集),作業系統可以將那些暫時不會被訪問到的記憶體頁置換到磁碟上,等到需要訪問時再從磁碟中讀出置換回主存。

  作業系統提供的這一層抽象,使得應用程式可以申請使用的記憶體遠大於實際可用物理主存的大小,但記憶體資料可能並不是都存放在物理主存中,而是在磁碟上,這也是虛擬記憶體這一名稱的由來。由於區域性性的存在,通過高效的置換演算法進行排程,其訪問速度相比完全使用主存而言速度並未受到太大影響。

lab3相比lab2的改進

  lab3在lab2的基礎上,主要新增了以下功能:

  1. kern_init總控函式中新增了vmm_init、ide_init、swap_init函式入口,分別完成了虛擬記憶體管理器、ide硬碟互動以及虛擬記憶體磁碟置換器的初始化。

  2. 在trap.c的中斷處理分發函式trap_dispatch中新增了對14號中斷(頁訪問異常)的處理邏輯do_pgfault。

2. ucore lab3實驗細節分析

  lab3是建立在之前實驗的基礎之上的。在虛擬記憶體功能的實現中,ucore需要藉助lab1中建立的中斷機制來處理缺頁異常,同時也依賴lab2中實現的實體記憶體管理功能。所以必須先理解之前的實驗內容後才能順利的理解lab3的內容: ucore lab1學習筆記ucore lab2學習筆記

2.1 ucore虛擬記憶體管理框架介紹

  80386是32位的cpu,其支援4GB的定址範圍。而在ucore虛擬記憶體的功能建立後,應用程式能夠擁有最大4GB的虛擬地址空間。但多數程式並不會真的申請完整的4GB虛擬地址空間,而是隻需要部分空間,則未申請的虛擬地址空間會被視為非法的虛擬地址空間,在程式對應的頁表中將不存在非法虛擬地址的對映關係,訪問將會出錯。這也是為什麼在平常編寫的應用程式中,在訪問一個野指標時,有時候會得到一個莫名其妙的值,有時候程式會直接奔潰,導致奔潰的一個原因就是因為野指標指向了一個非法的虛擬地址。

     ucore在lab3中新增了vma_struct結構(kern/mm/vmm.h)來描述合法的連續虛擬記憶體空間塊,一個程式合法的虛擬地址空間段將以vma集合的方式表示。

  在ucore中,以vma_struct虛地址空間的大小順序可以組成一個雙向迴圈連結串列,與lab2類似,vma_struct反向包裹list_link連結串列節點屬性,利用le2vma巨集可以使用page_link節點找到所關聯的vma_struct。

vma_struct結構:

// the virtual continuous memory area(vma)
// 連續虛擬記憶體區域
struct vma_struct {
    // 關聯的上層記憶體管理器
    struct mm_struct *vm_mm; // the set of vma using the same PDT 
    // 描述的虛擬記憶體的起始地址
    uintptr_t vm_start;      //    start addr of vma    
    // 描述的虛擬記憶體的截止地址
    uintptr_t vm_end;        // end addr of vma
    // 當前虛擬記憶體塊的屬性flags
    // bit0 VM_READ標識是否可讀; bit1 VM_WRITE標識是否可寫; bit2 VM_EXEC標識是否可執行
    uint32_t vm_flags;       // flags of vma
    // 連續虛擬記憶體塊連結串列節點 (mm_struct->mmap_list)
    list_entry_t list_link;  // linear list link which sorted by start addr of vma
};

#define le2vma(le, member)                  \
    to_struct((le), struct vma_struct, member)

#define VM_READ                 0x00000001
#define VM_WRITE                0x00000002
#define VM_EXEC                 0x00000004

  ucore提供了mm_struct結構(kern/mm/vmm.h)作為一個總的記憶體管理器,統一的管理一個程式的虛擬記憶體以及實體記憶體。

  其中,mm_struct的mmap_list用來儲存上面提到的用於表示程式合法虛擬地址空間集合的vma雙向迴圈連結串列。

mm_struct結構:

// the control struct for a set of vma using the same PDT
struct mm_struct {
    // 連續虛擬記憶體塊連結串列 (內部節點虛擬記憶體塊的起始、截止地址必須全域性有序,且不能出現重疊)
    list_entry_t mmap_list;        // linear list link which sorted by start addr of vma
    // 當前訪問的mmap_list連結串列中的vma塊(由於區域性性原理,之前訪問過的vma有更大可能會在後續繼續訪問,該快取可以減少從mmap_list中進行遍歷查詢的次數,提高效率)
    struct vma_struct *mmap_cache; // current accessed vma, used for speed purpose
    // 當前mm_struct關聯的一級頁表的指標
    pde_t *pgdir;                  // the PDT of these vma
    // 當前mm_struct->mmap_list中vma塊的數量
    int map_count;                 // the count of these vma
    // 用於虛擬記憶體置換演算法的屬性,使用void*指標做到通用 (lab中預設的swap_fifo替換演算法中,將其做為了一個先進先出連結串列佇列)
    void *sm_priv;                   // the private data for swap manager
};

關係結構圖:

 2.2 虛擬記憶體關聯物理頁換入換出分析

  下面分析ucore lab3中,虛擬記憶體關聯物理頁的換入、換出的細節。

什麼時候進行換出?

  虛擬記憶體頁的換出分為主動與被動兩種換出策略,在lab3中,ucore只實現了基於被動的換出策略。在ucore中,當申請分配物理頁面時如果發現可用的實體記憶體不足時,會進行可置換物理頁的換出。通過某種置換演算法將選中的物理頁暫時置換到磁碟的交換分割槽中,以騰出空閒的物理頁以供分配。

  分配實體記憶體的程式碼位於pmm.c中的alloc_pages函式,alloc_pages是在lab2中已有的,在lab3中為了支援虛擬記憶體的實現進行了一定的改動。

alloc_pages函式:

//alloc_pages - call pmm->alloc_pages to allocate a continuous n*PAGESIZE memory 
struct Page *
alloc_pages(size_t n) {
    struct Page *page=NULL;
    bool intr_flag;
    
    while (1)
    {
        // 關閉中斷,避免分配記憶體時,實體記憶體管理器內部的資料結構變動時被中斷打斷,導致資料錯誤
        local_intr_save(intr_flag);
        {
            // 分配n個物理頁
            page = pmm_manager->alloc_pages(n);
        }
        // 恢復中斷控制位
        local_intr_restore(intr_flag);

        // 滿足下面之中的一個條件,就跳出while迴圈
        // page != null 表示分配成功
        // 如果n > 1 說明不是發生缺頁異常來申請的(否則n=1)
        // 如果swap_init_ok == 0 說明沒有開啟分頁模式
        if (page != NULL || n > 1 || swap_init_ok == 0) break;
         
        extern struct mm_struct *check_mm_struct;
        //cprintf("page %x, call swap_out in alloc_pages %d\n",page, n);
        // 嘗試著將某一物理頁置換到swap磁碟交換扇區中,以騰出一個新的物理頁來
        // 如果交換成功,則理論上下一次迴圈,pmm_manager->alloc_pages(1)將有機會分配空閒物理頁成功
        swap_out(check_mm_struct, n, 0);
    }
    //cprintf("n %d,get page %x, No %d in alloc_pages\n",n,page,(page-pages));
    return page;
}

什麼時候進行換入?

  當CPU進行記憶體訪問時,發現所得到的線性地址對應的二級頁表項的P位為0(不存在),便產生頁異常中斷(頁異常還可能在其它情況下出現),ucore會接受異常並進入頁異常中斷服務例程。

  回顧一下lab1,硬體在一些中斷髮生時會將錯誤號壓入棧中,ucore建立的中斷機制可以通過中斷棧幀trap_frame中的tr_err獲取到一個32位的錯誤號。在頁異常中斷中,這個錯誤號的不同bit位標識了發生異常時的各種資訊,同時80386的cr2頁異常地址暫存器中也會保留最後一次頁異常發生時所訪問的32位線性地址。

捕獲頁異常邏輯:

static void
trap_dispatch(struct trapframe *tf) {
    char c;

    int ret;

    switch (tf->tf_trapno) {
    case T_PGFLT:  //page fault
        // T_PGFLT 14號中斷 頁異常處理
        if ((ret = pgfault_handler(tf)) != 0) {
            // 頁異常處理失敗,列印棧幀
            print_trapframe(tf);
            panic("handle pgfault failed. %e\n", ret);
        }
        break;
    
    // 不完全。。。。。。
}

static int
pgfault_handler(struct trapframe *tf) {
    extern struct mm_struct *check_mm_struct;
    print_pgfault(tf);
    if (check_mm_struct != NULL) {
        // 傳入check_mm_struct是為了配合check_pgfault檢查函式的
        // 在未來的實驗中同一程式是共用一個mm_struct記憶體管理器,而截止lab3只存在一個程式:核心程式
        // rcr2頁異常發生時,cr2頁故障線性地址暫存器,儲存最後一次出現頁故障的32位線性地址
        return do_pgfault(check_mm_struct, tf->tf_err, rcr2());
    }
    panic("unhandled page fault.\n");
}

頁訪問異常錯誤碼示意圖:

  真正處理頁異常的核心邏輯位於vmm.c中的do_pgfault函式。do_pgfault除了接受前面提到的32位中斷錯誤號和引起錯誤的線性地址,還接受了當前程式的mm_struct結構,用以訪問當前程式的頁表。do_pgfault對錯誤碼進行了處理,判斷其究竟是否是因為缺頁造成的頁訪問異常;還是因為非法的虛擬地址訪問、特權級的越級記憶體訪問等錯誤引發的頁異常,如果是後者就應該報錯或者讓程式直接奔潰掉。

  如果發現訪問的是一個合法的虛擬地址,則會進一步找到引起異常的線性地址所對應的二級頁表項,判斷其是真的不存在(pte中的每一位都是0)還是之前被暫時交換到了磁碟上(僅僅是P位為0)。

  如果是真的不存在,則需要立即為其分配一個初始化後全新的物理頁,並建立對映虛實關係。

  如果是被暫時交換到了磁碟中,則需要將交換扇區中的資料重新讀出並覆蓋所分配到的物理頁。

  頁異常中斷屬於異常中斷的一種,當中斷服務例程返回後,會重新執行引起頁異常的那條指令,如果do_pafault實現正確,那麼此時將能夠正確的訪問到虛擬地址對應的物理頁,程式能正常的往下繼續執行。

do_pgfault函式:

int do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr) {
    int ret = -E_INVAL;
    //try to find a vma which include addr
    // 試圖從mm關聯的vma連結串列塊中查詢,是否存在當前addr線性地址匹配的vma塊
    struct vma_struct *vma = find_vma(mm, addr);

    // 全域性頁異常處理數自增1
    pgfault_num++;
    //If the addr is in the range of a mm's vma?
    if (vma == NULL || vma->vm_start > addr) {
        // 如果沒有匹配到vma
        cprintf("not valid addr %x, and  can not find it in vma\n", addr);
        goto failed;
    }
    //check the error_code
    // 頁訪問異常錯誤碼有32位。位0為1 表示對應物理頁不存在;位1為1 表示寫異常(比如寫了只讀頁);位2為1 表示訪問許可權異常(比如使用者態程式訪問核心空間的資料)
    // 對3求模,主要判斷bit0、bit1的值
    switch (error_code & 3) {
    default:
            /* error code flag : default is 3 ( W/R=1, P=1): write, present */
        // bit0,bit1都為1,訪問的對映頁表項存在,且發生的是寫異常
        // 說明發生了缺頁異常
    case 2: /* error code flag : (W/R=1, P=0): write, not present */
        // bit0為0,bit1為1,訪問的對映頁表項不存在、且發生的是寫異常
        if (!(vma->vm_flags & VM_WRITE)) {
            // 對應的vma塊對映的虛擬記憶體空間是不可寫的,許可權校驗失敗
            cprintf("do_pgfault failed: error code flag = write AND not present, but the addr's vma cannot write\n");
            // 跳轉failed直接返回
            goto failed;
        }
        // 校驗通過,則說明發生了缺頁異常
        break;
    case 1: /* error code flag : (W/R=0, P=1): read, present */
        // bit0為1,bit1為0,訪問的對映頁表項存在,且發生的是讀異常(可能是訪問許可權異常)
        cprintf("do_pgfault failed: error code flag = read AND present\n");
        // 跳轉failed直接返回
        goto failed;
    case 0: /* error code flag : (W/R=0, P=0): read, not present */
        // bit0為0,bit1為0,訪問的對映頁表項不存在,且發生的是讀異常
        if (!(vma->vm_flags & (VM_READ | VM_EXEC))) {
            // 對應的vma對映的虛擬記憶體空間是不可讀且不可執行的
            cprintf("do_pgfault failed: error code flag = read AND not present, but the addr's vma cannot read or exec\n");
            goto failed;
        }
        // 校驗通過,則說明發生了缺頁異常
    }
    /* IF (write an existed addr ) OR
     *    (write an non_existed addr && addr is writable) OR
     *    (read  an non_existed addr && addr is readable)
     * THEN
     *    continue process
     */

    // 構造需要設定的缺頁頁表項的perm許可權
    uint32_t perm = PTE_U;
    if (vma->vm_flags & VM_WRITE) {
        perm |= PTE_W;
    }
    // 構造需要設定的缺頁頁表項的線性地址(按照PGSIZE向下取整,進行頁面對齊)
    addr = ROUNDDOWN(addr, PGSIZE);

    ret = -E_NO_MEM;

    // 用於對映的頁表項指標(page table entry, pte)
    pte_t *ptep=NULL;
  
    // try to find a pte, if pte's PT(Page Table) isn't existed, then create a PT.
    // (notice the 3th parameter '1')
    // 獲取addr線性地址在mm所關聯頁表中的頁表項
    // 第三個引數=1 表示如果對應頁表項不存在,則需要新建立這個頁表項
    if ((ptep = get_pte(mm->pgdir, addr, 1)) == NULL) {
        cprintf("get_pte in do_pgfault failed\n");
        goto failed;
    }
    
    // 如果對應頁表項的內容每一位都全為0,說明之前並不存在,需要設定對應的資料,進行線性地址與實體地址的對映
    if (*ptep == 0) { // if the phy addr isn't exist, then alloc a page & map the phy addr with logical addr
        // 令pgdir指向的頁表中,la線性地址對應的二級頁表項與一個新分配的物理頁Page進行虛實地址的對映
        if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
            cprintf("pgdir_alloc_page in do_pgfault failed\n");
            goto failed;
        }
    }
    else { // if this pte is a swap entry, then load data from disk to a page with phy addr
           // and call page_insert to map the phy addr with logical addr
        // 如果不是全為0,說明可能是之前被交換到了swap磁碟中
        if(swap_init_ok) {
            // 如果開啟了swap磁碟虛擬記憶體交換機制
            struct Page *page=NULL;
            // 將addr線性地址對應的物理頁資料從磁碟交換到實體記憶體中(令Page指標指向交換成功後的物理頁)
            if ((ret = swap_in(mm, addr, &page)) != 0) {
                // swap_in返回值不為0,表示換入失敗
                cprintf("swap_in in do_pgfault failed\n");
                goto failed;
            }    
            // 將交換進來的page頁與mm->padir頁表中對應addr的二級頁表項建立對映關係(perm標識這個二級頁表的各個許可權位)
            page_insert(mm->pgdir, page, addr, perm);
            // 當前page是為可交換的,將其加入全域性虛擬記憶體交換管理器的管理
            swap_map_swappable(mm, addr, page, 1);
            page->pra_vaddr = addr;
        }
        else {
            // 如果沒有開啟swap磁碟虛擬記憶體交換機制,但是卻執行至此,則出現了問題
            cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
            goto failed;
        }
   }
   // 返回0代表缺頁異常處理成功
   ret = 0;
failed:
    return ret;
}

pgdir_alloc_page 建立對映虛實關係 

// pgdir_alloc_page - call alloc_page & page_insert functions to 
//                  - allocate a page size memory & setup an addr map
//                  - pa<->la with linear address la and the PDT pgdir
// 令pgdir指向的頁表中,la線性地址對應的二級頁表項與一個新分配的物理頁Page進行虛實地址的對映
struct Page *
pgdir_alloc_page(pde_t *pgdir, uintptr_t la, uint32_t perm) {
    // 分配一個新的物理頁用於對映la
    struct Page *page = alloc_page();
    if (page != NULL) { // !=null 分配成功
        // 建立la對應二級頁表項(位於pgdir頁表中)與page物理頁基址的對映關係
        if (page_insert(pgdir, page, la, perm) != 0) {
            // 對映失敗,釋放剛才分配的物理頁
            free_page(page);
            return NULL;
        }
        // 如果啟用了swap交換分割槽功能
        if (swap_init_ok){
            // 將新對映的這一個page物理頁設定為可交換的,並納入全域性swap交換管理器中管理
            swap_map_swappable(check_mm_struct, la, page, 0);
            // 設定這一物理頁關聯的虛擬記憶體
            page->pra_vaddr=la;
            // 校驗這個新分配出來的物理頁page是否引用次數正好為1
            assert(page_ref(page) == 1);
            //cprintf("get No. %d  page: pra_vaddr %x, pra_link.prev %x, pra_link_next %x in pgdir_alloc_page\n", (page-pages), page->pra_vaddr,page->pra_page_link.prev, page->pra_page_link.next);
        }
    }
    return page;
}

 如何在置換時從swap磁碟分割槽中找到二級頁表項對應的物理頁資料?

  要想在換入時準確的找到二級頁表項所對映實體記憶體頁內容在磁碟交換分割槽中的位置,需要建立某種對映關係或是對映表。在ucore中,沒有單獨的另外構建一張新的對映表,而是巧妙地重複利用了二級頁表項。

  在80386CPU中,記憶體定址時線性地址對應二級頁表項的Present存在位是至關重要的。當P位為1時,代表所對映的物理頁存在,訪問正常;而P位為0時,代表不存在,則整個二級頁表項的其它位都沒有意義了。而ucore則利用了這一特性,在進行物理頁虛擬記憶體的置換後,將P位設定為0的同時,還將二級頁表項pte的高24位作為在磁碟交換扇區中的偏移索引,併為此單獨定義了swap_entry_t這一32位結構表示處於交換區的虛擬記憶體對映頁的狀態。

pte在lab3中有了三種不同的狀態:

  1. 全為0,代表未建立對應物理頁的對映

  2. P位為1,代表已建立對應物理頁的對映

  3. P位為0,但高24位不為0。代表所對映的物理頁存在,只是被交換到了磁碟交換區中。

swap_entry_t:

/* *
 * swap_entry_t
 * --------------------------------------------
 * |         offset        |   reserved   | 0 |
 * --------------------------------------------
 *           24 bits            7 bits    1 bit
 * */

typedef pte_t swap_entry_t; //the pte can also be a swap entry

  一個物理頁面的大小為4KB,而一個ide磁碟扇區的大小為512Byte,比例為8:1。

  在ucore lab3中,由於目前只存在一個頁表(核心頁表),為了簡單起見就直接令swap_entry_t中的高24位偏移 * 8 = 對應儲存磁碟交換起始扇區號。

從磁碟換入到主存swap_in:

int
swap_in(struct mm_struct *mm, uintptr_t addr, struct Page **ptr_result)
{
     // 分配一個新的物理頁
     struct Page *result = alloc_page();
     assert(result!=NULL);

     // 獲得線性地址addr對應的二級頁表項指標
     pte_t *ptep = get_pte(mm->pgdir, addr, 0);
     // cprintf("SWAP: load ptep %x swap entry %d to vaddr 0x%08x, page %x, No %d\n", ptep, (*ptep)>>8, addr, result, (result-pages));
    
     int r;
     // 將磁碟中讀入的一整個物理頁資料,寫入result(此時的ptep二級頁表項中存放的是swap_entry_t結構的資料)
     if ((r = swapfs_read((*ptep), result)) != 0)
     {
        assert(r!=0);
     }
     cprintf("swap_in: load disk swap entry %d with swap_page in vadr 0x%x\n", (*ptep)>>8, addr);
     // 令引數ptr_result指向已被換入記憶體中的result Page結構
     *ptr_result=result;
     return 0;
}

int
swapfs_read(swap_entry_t entry, struct Page *page) {
    // swap_offset巨集右移8位,擷取前24位 = swap_entry_t的offset屬性
    // swap_entry_t的offset * PAGE_NSECT(物理頁與磁碟扇區大小比值) = 要讀取的起始扇區號

    // 從裝置號指定的磁碟中,讀取自某一扇區起始的N個連續扇區,並將其寫入指定起始地址的記憶體空間中
    // SWAP_DEV_NO引數指定裝置號,swap_offset(entry) * PAGE_NSECT指定起始扇區號
    // page2kva(page)指定所要寫入的目的頁面虛地址起始空間,PAGE_NSECT指定了需要順序連續讀取的扇區數量
    return ide_read_secs(SWAP_DEV_NO, swap_offset(entry) * PAGE_NSECT, page2kva(page), PAGE_NSECT);
}

從主存換出到磁碟swap_out:

/**
 * 引數mm,指定對應的記憶體管理器
 * 引數n,指定需要換出到swap扇區的物理頁個數
 * 引數in_tick,可以用於發生時鐘中斷時,定時進行主動的換出操作,騰出更多的物理空閒頁
 * */
int
swap_out(struct mm_struct *mm, int n, int in_tick)
{
     int i;
     for (i = 0; i != n; ++ i)
     {
          uintptr_t v;
          //struct Page **ptr_page=NULL;
          struct Page *page;
          // cprintf("i %d, SWAP: call swap_out_victim\n",i);
          // 由swap置換管理器,挑選出需要被犧牲的(被置換到swap磁碟扇區)的page,令page指標變數指向其指標
          int r = sm->swap_out_victim(mm, &page, in_tick);
          if (r != 0) {
              // 挑選失敗
              cprintf("i %d, swap_out: call swap_out_victim failed\n",i);
              break;
          }          
        
          // 獲得挑選出來的物理頁的虛擬地址
          v=page->pra_vaddr; 
          // 獲得page->pra_vaddr線性地址對應的二級頁表項
          pte_t *ptep = get_pte(mm->pgdir, v, 0);
          assert((*ptep & PTE_P) != 0);

          // 將其寫入swap磁碟
          // page->pra_vaddr/PGSIZE = 虛擬地址對應的二級頁表項索引(前20位);
          // (page->pra_vaddr/PGSIZE) + 1 (+1為了在頁表項中區別 0 和 swap 分割槽的對映)
          // ((page->pra_vaddr/PGSIZE) + 1) << 8,為了構成swap_entry_t的高24位
          // 舉個例子:
          // 假設page->pra_vaddr = 0x0000100,則page->pra_vaddr/PGSIZE = 0x00000001
          // page->pra_vaddr/PGSIZE + 1 = 0x00000002
          // 對應的swap_entry_t = 0x00000002 << 8 = 0x00000200,高24位為0x000002
          if (swapfs_write( (page->pra_vaddr/PGSIZE+1)<<8, page) != 0) {
              cprintf("SWAP: failed to save\n");
              // 當前物理頁寫入swap,交換失敗。重新令其加入swap管理器中
              sm->map_swappable(mm, v, page, 0);
              continue;
          }
          else {
              // 交換成功
              cprintf("swap_out: i %d, store page in vaddr 0x%x to disk swap entry %d\n", i, v, page->pra_vaddr/PGSIZE+1);
              // 設定ptep二級頁表項的值
              *ptep = (page->pra_vaddr/PGSIZE+1)<<8;
              // 釋放、歸還page物理頁
              free_page(page);
          }
          // 由於對應二級頁表項出現了變化,重新整理TLB快表
          tlb_invalidate(mm->pgdir, v);
     }
     return i;
}

int
swapfs_write(swap_entry_t entry, struct Page *page) {
    // swap_offset巨集右移8位,擷取前24位 = swap_entry_t的offset屬性
    // swap_entry_t的offset * PAGE_NSECT(物理頁與磁碟扇區大小比值) = 要寫入的起始扇區號

    // 從裝置號指定的磁碟中,從指定起始地址的記憶體空間開始,將資料寫入自某一扇區起始的N個連續扇區內
    // SWAP_DEV_NO引數指定裝置號,swap_offset(entry) * PAGE_NSECT指定起始扇區號
    // page2kva(page)指定所要讀入的源資料頁面虛地址起始空間,PAGE_NSECT指定了需要順序連續寫入的扇區數量
    return ide_write_secs(SWAP_DEV_NO, swap_offset(entry) * PAGE_NSECT, page2kva(page), PAGE_NSECT);
}

2.3 swap_manager 虛擬記憶體頁面置換管理框架

  雖然虛擬記憶體機制在邏輯上增大了應用程式可用的記憶體,但由於磁碟I/O速度相較於主存有巨大的差距,因此一個好的虛擬記憶體置換演算法是至關重要的。為此,電腦科學家不斷的提出各種不同的置換演算法,以獲得更好的虛擬記憶體訪問效率。

  和lab2中構建一個管理物理頁面的介面集合框架,將呼叫介面與具體實現解耦一樣。在lab3中也抽象出了一個用於管理虛擬記憶體頁面置換的框架swap_manager,並提供了一個預設的實現fifo_swap_manager。從名稱上可以知道,fifo_swap_manager採用的是先進先出這一效率不高,但簡單易懂的排程演算法。

  通過構造不同的swap_manager實現,也可以用來完成挑戰練習要求的效率更高,但更復雜的時鐘頁置換演算法。

swap_manager:

struct swap_manager
{
     const char *name;
     /* Global initialization for the swap manager */
     // 初始化全域性虛擬記憶體交換管理器
     int (*init)            (void);
     /* Initialize the priv data inside mm_struct */
     // 初始化設定所關聯的全域性記憶體管理器
     int (*init_mm)         (struct mm_struct *mm);
     /* Called when tick interrupt occured  */
     // 當時鍾中斷時被呼叫,可用於主動的swap交換策略
     int (*tick_event)      (struct mm_struct *mm);
     /* Called when map a swappable page into the mm_struct */
     // 當對映一個可交換Page物理頁加入mm_struct時被呼叫
     int (*map_swappable)   (struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in);
     /* When a page is marked as shared, this routine is called to
      * delete the addr entry from the swap manager */
     // 當一個頁面被標記為共享頁面,該函式例程會被呼叫。
     // 用於將addr對應的虛擬頁,從swap_manager中移除,阻止其被排程置換到磁碟中
     int (*set_unswappable) (struct mm_struct *mm, uintptr_t addr);
     /* Try to swap out a page, return then victim */
     // 當試圖換出一個物理頁時,返回被選中的頁面(被犧牲的頁面)
     int (*swap_out_victim) (struct mm_struct *mm, struct Page **ptr_page, int in_tick);
     /* check the page relpacement algorithm */
     int (*check_swap)(void);     
};

swap_fifo.c主要邏輯部分:

list_entry_t pra_list_head;
/*
 * (2) _fifo_init_mm: init pra_list_head and let  mm->sm_priv point to the addr of pra_list_head.
 *              Now, From the memory control struct mm_struct, we can access FIFO PRA
 */
static int
_fifo_init_mm(struct mm_struct *mm)
{     
     // 初始化先進先出連結串列佇列
     list_init(&pra_list_head);
     mm->sm_priv = &pra_list_head;
     //cprintf(" mm->sm_priv %x in fifo_init_mm\n",mm->sm_priv);
     return 0;
}
/*
 * (3)_fifo_map_swappable: According FIFO PRA, we should link the most recent arrival page at the back of pra_list_head qeueue
 */
static int
_fifo_map_swappable(struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in)
{
    // 獲取到mm_struct關聯的先進先出連結串列佇列
    list_entry_t *head=(list_entry_t*) mm->sm_priv;
    // 獲取引數page結構對應的swap連結串列節點
    list_entry_t *entry=&(page->pra_page_link);
 
    assert(entry != NULL && head != NULL);
    //record the page access situlation
    /*LAB3 EXERCISE 2: YOUR CODE*/ 
    //(1)link the most recent arrival page at the back of the pra_list_head qeueue.
    // 將其加入佇列的頭部(先進先出,最新的page頁被掛載在最頭上)
    list_add(head, entry);
    return 0;
}
/*
 *  (4)_fifo_swap_out_victim: According FIFO PRA, we should unlink the  earliest arrival page in front of pra_list_head qeueue,
 *                            then assign the value of *ptr_page to the addr of this page.
 */
static int
_fifo_swap_out_victim(struct mm_struct *mm, struct Page ** ptr_page, int in_tick)
{
    // 獲取到mm_struct關聯的先進先出連結串列佇列
     list_entry_t *head=(list_entry_t*) mm->sm_priv;
         assert(head != NULL);
     assert(in_tick==0);
     /* Select the victim */
     /*LAB3 EXERCISE 2: YOUR CODE*/ 
     //(1)  unlink the  earliest arrival page in front of pra_list_head qeueue
     //(2)  assign the value of *ptr_page to the addr of this page
     /* Select the tail */
     // 找到頭節點的前一個(雙向迴圈連結串列 head的前一個節點=佇列的最尾部節點)
     list_entry_t *le = head->prev;
     assert(head!=le);
     // 獲得尾節點對應的page結構
     struct Page *p = le2page(le, pra_page_link);
     // 將le節點從先進先出連結串列佇列中刪除
     list_del(le);
     assert(p !=NULL);
     // 令ptr_page指向被挑選出來的page
     *ptr_page = p;
     return 0;
}

struct swap_manager swap_manager_fifo =
{
     .name            = "fifo swap manager",
     .init            = &_fifo_init,
     .init_mm         = &_fifo_init_mm,
     .tick_event      = &_fifo_tick_event,
     .map_swappable   = &_fifo_map_swappable,
     .set_unswappable = &_fifo_set_unswappable,
     .swap_out_victim = &_fifo_swap_out_victim,
     .check_swap      = &_fifo_check_swap,
};

3.總結

  ucore通過lab2、lab3兩個連續的實驗,完成了對計算機記憶體的抽象與管理,為後續的使用者級的多程式/執行緒的實現打下了基礎。lab3對於已經理解了lab1、lab2中各種硬體互動以及C中晦澀巧妙的巨集實現的人來說難度並不算大,整體的學習曲線變得平緩了。

  由於前幾個實驗都是與作業系統核心緊密相關的,還沒有涉及到與使用者程式的互動,顯得有些單調、枯燥,就連所實現的虛擬記憶體管理的相關功能都是通過一段精心設計的模擬記憶體訪問過程的程式碼來校驗其正確性的。但很快ucore就會在後續的實驗中引入多程式/執行緒、使用者態程式以及程式/執行緒同步等更加貼近平常應用開發時接觸到的系統功能,對ucore的學習也會變得更加有趣。

  通過對ucore作業系統的學習,使我們得以開啟作業系統這個黑盒子一窺究竟,更好的理解上層應用程式執行時背後發生的事情,從能夠寫出更高效、健壯的應用程式。

  這篇部落格的完整程式碼註釋在我的github上:https://github.com/1399852153/ucore_os_lab (fork自官方倉庫)中的lab3_answer。

  希望我的部落格能幫助到對作業系統、ucore os感興趣的人。存在許多不足之處,還請多多指教。

相關文章