ucore作業系統學習筆記(二) ucore lab2實體記憶體管理分析

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

一、lab2實體記憶體管理介紹

  作業系統的一個主要職責是管理硬體資源,並嚮應用程式提供具有良好抽象的介面來使用這些資源。

  而記憶體作為重要的計算機硬體資源,也必然需要被作業系統統一的管理。最初沒有作業系統的情況下,不同的程式通常直接編寫實體地址相關的指令。在多道併發程式的執行環境下,這會造成不同程式間由於實體地址的訪問衝突,造成資料的相互覆蓋,進而出錯、崩潰。

  現代的作業系統在管理記憶體時,希望達到兩個基本目標:地址保護地址獨立

  地址保護指的是一個程式不能隨意的訪問另一個程式的空間,而地址獨立指的是程式指令給出的記憶體定址命令是與最終的實體地址無關的。在實現這兩個目標後,便能夠為多道併發程式執行時的實體記憶體訪問的隔離提供支援。每個程式在編譯、連結後產生的最終機器程式碼都可以使用完整的地址空間(虛擬地址),而不需要考慮其它的程式的存在。ucore通過兩個連續的實驗迭代,lab2和lab3分別實現了實體記憶體管理和虛擬記憶體管理(利用磁碟快取非工作集記憶體,擴充套件邏輯上的記憶體空間)。

  ucore的每個實驗都是建立在前一個實驗迭代的基礎上的,要想更好的理解lab2,最好先理解之前lab1中的內容(lab1學習筆記)。

  lab2在lab1平坦模型段機制的基礎上,開啟了80386的分頁機制,並建立了核心頁表;同時通過硬體中斷探測出了當前記憶體硬體的佈局,並以此為依據根據可用的記憶體建立了一個實體記憶體管理框架,通過指定某種分配演算法,負責處理所有的實體記憶體頁分配與釋放的請求。

  lab2的程式碼結構和執行流程與lab1差別不大,其主要新增了以下功能:

  1. bootmain.S中的實體記憶體探測

  2. 在新增的entry.S核心入口程式中開啟了80386頁機制

  3. kern_init核心總控函式中通過pmm_init函式進行整個實體記憶體管理器的構建初始化

二、lab2實驗細節分析

2.1 實體記憶體佈局探測

  為了進行實體記憶體的管理,作業系統必須先探測出當前硬體環境下記憶體的佈局,瞭解具體哪些實體記憶體空間是可用的。

  ucore在實驗中是通過e820這一BIOS中斷來探測記憶體佈局的,由於BIOS中斷必須在80386的真實模式下才能正常工作,因此是在bootloader引導進入保護模式前進行的,程式碼位於/boot/bootasm,S中。在引導的彙編程式碼中收集到的資料,通過C中定義的e820map結構體進行對映。

e820map結構:

struct e820map {
    int nr_map;
    struct {
        uint64_t addr;
        uint64_t size;
        uint32_t type;
    } __attribute__((packed)) map[E820MAX];
};

bootasm.S記憶體佈局探測:

# 在真實模式下,通過BIOS的e820中斷探測當前記憶體的硬體資訊
probe_memory:
    # 0x8000處開始存放探測出的記憶體佈局結構(e820map)
    movl $0, 0x8000
    xorl %ebx, %ebx
    # 0x8004處開始存放e820map中的map欄位,存放每一個entry
    movw $0x8004, %di
start_probe:
    # 在eax、ecx、edx中設定int 15h中斷引數
    movl $0xE820, %eax
    movl $20, %ecx
    movl $SMAP, %edx
    int $0x15
    # 如果eflags的CF位為0,說明探測成功,跳轉至cont段執行
    jnc cont
    # e820h中斷失敗,直接結束探測
    movw $12345, 0x8000
    jmp finish_probe
cont:
    # 設定存放下一個探測出的記憶體佈局entry的地址(因為e820map中的entry陣列每一項是8+8+4=20位元組的)
    addw $20, %di
    # e820map中的nr_map自增1
    incl 0x8000
    # 0與中斷響應後的ebx比較(如果是第一次呼叫或記憶體區域掃描完畢,則ebx為0 如果不是,則ebx存放上次呼叫之後的計數值)
    cmpl $0, %ebx
    # 是否還存在新的記憶體段需要探測
    jnz start_probe
finish_probe:
# 結束探測

2.2 啟用分頁機制

  ucore在lab2中開啟了80386的分頁機制,實現了基於平坦段模型的段頁式記憶體管理,為後續虛擬記憶體的實現做好了準備。

  如果對80386分頁機制原理不太熟悉的話,可以參考一下我之前的部落格:80386分頁機制與虛擬記憶體

虛擬地址的概念

  需要注意的是,在80386分頁機制工作原理的許多資料中,開啟了頁機制後由指令(段選擇子+段內偏移)所構成的地址被稱為邏輯地址;而邏輯地址通過GDT或LDT等段錶轉換之後得到的地址被稱為線性地址;如果開啟了頁機制,得到線性地址後還需要查詢頁表來得到最終的實體地址

  整個的轉換過程大致為:邏輯地址->線性地址->實體地址但虛擬地址這一概念並沒有得到統一,在實驗指導書中,虛擬地址指的是程式指令給出的邏輯地址,而在有的資料中,則將線性地址稱作虛擬地址。查閱有關資料時一定要注意虛擬地址這一概念在上下文中的確切含義,避免產生混淆。

ucore開啟分頁機制的細節

  lab2以及往後的實驗中,在ucore的虛擬空間設計中,開啟了頁機制後的核心是位於高位地址空間的,而低位記憶體空間則讓出來交給使用者應用程式使用。

  ucore核心被bootloader指定載入的實體地址基址相對lab1而言是不變的。但在開啟分頁機制的前後,CPU翻譯邏輯地址的方式也立即發生了變化。開啟分頁機制前,核心程式的指令指標是指向低位記憶體的,而開啟了頁機制後,我們希望能夠正確、無損的令核心的指令指標指向高位地址空間,但保證其最終訪問的實體地址不變,依然能夠正確的執行。在實驗指導書中有專門的一節提到:系統執行中地址對映的三個階段

  在這裡補充一下第二個階段開啟分頁模式時的細節:在開啟頁機制的瞬間是如何巧妙的保證後續指令正確訪問的。

  根據git倉庫上的提交記錄,發現ucore開啟分頁機制的實現細節在2018年初進行了很大的改動。網上許多發表較早的ucore學習部落格其內容部分已經過時,在參考時需要注意。(實驗指導書的該節標題也有錯誤:應該是系統執行中地址對映的三個階段,而不是之前的四個階段了)。

entry.S

#include <mmu.h>
#include <memlayout.h>

#define REALLOC(x) (x - KERNBASE)

.text
.globl kern_entry
kern_entry:
    # REALLOC是因為核心在構建時被設定在了高位(kernel.ld中設定了核心起始虛地址0xC0100000,使得虛地址整體增加了KERNBASE)
    # 因此需要REALLOC來對核心全域性變數進行重定位,在開啟分頁模式前保證程式訪問的實體地址的正確性

    # load pa of boot pgdir
    # 此時還沒有開啟頁機制,__boot_pgdir(entry.S中的符號)需要通過REALLOC轉換成正確的實體地址
    movl $REALLOC(__boot_pgdir), %eax
    # 設定eax的值到頁表基址暫存器cr3中
    movl %eax, %cr3

    # enable paging 開啟頁模式
    movl %cr0, %eax
    # 通過or運算,修改cr0中的值
    orl $(CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP), %eax
    andl $~(CR0_TS | CR0_EM), %eax
    # 將cr0修改完成後的值,重新送至cr0中(此時第0位PE位已經為1,頁機制已經開啟,當前頁表地址為剛剛構造的__boot_pgdir)
    movl %eax, %cr0

    # update eip
    # now, eip = 0x1..... next是處於高位地址空間的
    leal next, %eax
    # set eip = KERNBASE + 0x1.....
    # 通過jmp至next處,使得核心的指令指標指向了高位。但由於巧妙的設計了高位對映的核心頁表,使得依然能準確訪問之前低位虛空間下的所有內容
    jmp *%eax
next:

    # unmap va 0 ~ 4M, it is temporary mapping
    xorl %eax, %eax
    # 將__boot_pgdir的第一個頁目錄項清零,取消0~4M虛地址的對映
    movl %eax, __boot_pgdir

    # 設定C的核心棧
    # set ebp, esp
    movl $0x0, %ebp
    # the kernel stack region is from bootstack -- bootstacktop,
    # the kernel stack size is KSTACKSIZE (8KB)defined in memlayout.h
    movl $bootstacktop, %esp
    # now kernel stack is ready , call the first C function
    # 呼叫init.c中的kern_init總控函式
    call kern_init

# should never get here
# 自旋死迴圈(如果核心實現正確,kern_init函式將永遠不會返回並執行至此。因為作業系統核心本身就是通過自旋迴圈常駐記憶體的)
spin:
    jmp spin

.data
.align PGSIZE
    .globl bootstack
bootstack:
    .space KSTACKSIZE
    .globl bootstacktop
bootstacktop:

# kernel builtin pgdir
# an initial page directory (Page Directory Table, PDT)
# These page directory table and page table can be reused!
.section .data.pgdir
.align PGSIZE
__boot_pgdir:
.globl __boot_pgdir
    # map va 0 ~ 4M to pa 0 ~ 4M (temporary)
    # 80386的每一個一級頁表項能夠對映4MB連續的虛擬記憶體至實體記憶體的關係
    # 第一個有效頁表項,當訪問0~4M虛擬記憶體時,虛擬地址的高10位為0,即找到該一級頁表項(頁目錄項),進而可以找到二級頁表__boot_pt1
    # 進而可以進行虛擬地址的0~4M -> 實體地址 0~4M的等價對映
    .long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
    # space用於將指定範圍大小內的空間全部設定為0(等價於P位為0,即不存在的、無效的頁表項)
    # KERNBASE/一個物理頁的大小(PGSHIFT 4KB即偏移12位)/一個二級頁表內的頁表項(2^10個) * 4(一個頁表項32位,即4byte)
    # 偏移的距離 - (. - __boot_pgdir) 是為了對齊
    .space (KERNBASE >> PGSHIFT >> 10 << 2) - (. - __boot_pgdir) # pad to PDE of KERNBASE
    # map va KERNBASE + (0 ~ 4M) to pa 0 ~ 4M
    # 第二個有效頁表項,前面通過.space偏移跳過特定的距離,當虛擬地址為KERNBASE~KERNBASE+4M時,能夠查詢到該項
    # 其對應的二級頁表同樣是__boot_pt1,而其中對映的實體地址為按照下標順序排列的0~4M,
    # 因此其最終的效果便能將KERNBASE~KERNBASE+4M的虛擬記憶體空間對映至實體記憶體空間的0~4M
    .long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
    .space PGSIZE - (. - __boot_pgdir) # pad to PGSIZE

.set i, 0
# __boot_pt1是一個存在1024個32位long資料的陣列,當將其作為頁表時其中每一項都代表著一個實體地址對映項
# i為下標,每個頁表項的內容為i*1024作為對映的物理頁面基址並加上一些低位的屬性位(PTE_P代表存在,PTE_W代表可寫)
__boot_pt1:
.rept 1024
    .long i * PGSIZE + (PTE_P | PTE_W)
    .set i, i + 1
.endr

頁表對映關係圖:

2.3 ucore是如何實現實體記憶體管理功能的

   開啟了分頁機制後,下面介紹lab2中的重點:ucore是如何實現實體記憶體管理功能的。初始化實體記憶體管理器的入口位於總控函式的pmm_init函式。

pmm_init函式:

//pmm_init - setup a pmm to manage physical memory, build PDT&PT to setup paging mechanism 
//         - check the correctness of pmm & paging mechanism, print PDT&PT
void
pmm_init(void) {
    // We've already enabled paging
    // 此時已經開啟了頁機制,由於boot_pgdir是核心頁表地址的虛擬地址。通過PADDR巨集轉化為boot_cr3實體地址,供後續使用
    boot_cr3 = PADDR(boot_pgdir);

    //We need to alloc/free the physical memory (granularity is 4KB or other size). 
    //So a framework of physical memory manager (struct pmm_manager)is defined in pmm.h
    //First we should init a physical memory manager(pmm) based on the framework.
    //Then pmm can alloc/free the physical memory. 
    //Now the first_fit/best_fit/worst_fit/buddy_system pmm are available.

    // 初始化實體記憶體管理器
    init_pmm_manager();

    // detect physical memory space, reserve already used memory,
    // then use pmm->init_memmap to create free page list

    // 探測實體記憶體空間,初始化可用的實體記憶體
    page_init();

    //use pmm->check to verify the correctness of the alloc/free function in a pmm
    check_alloc_page();

    check_pgdir();

    static_assert(KERNBASE % PTSIZE == 0 && KERNTOP % PTSIZE == 0);

    // recursively insert boot_pgdir in itself
    // to form a virtual page table at virtual address VPT
    // 將當前核心頁表的實體地址設定進對應的頁目錄項中(核心頁表的自對映)
    boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;

    // map all physical memory to linear memory with base linear addr KERNBASE
    // linear_addr KERNBASE ~ KERNBASE + KMEMSIZE = phy_addr 0 ~ KMEMSIZE
    // 將核心所佔用的實體記憶體,進行頁表<->物理頁的對映
    // 令處於高位虛擬記憶體空間的核心,正確的對映到低位的實體記憶體空間
    // (對映關係(虛實對映): 核心起始虛擬地址(KERNBASE)~核心截止虛擬地址(KERNBASE+KMEMSIZE) =  核心起始實體地址(0)~核心截止實體地址(KMEMSIZE))
    boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W);

    // Since we are using bootloader's GDT,
    // we should reload gdt (second time, the last time) to get user segments and the TSS
    // map virtual_addr 0 ~ 4G = linear_addr 0 ~ 4G
    // then set kernel stack (ss:esp) in TSS, setup TSS in gdt, load TSS
    // 重新設定GDT
    gdt_init();

    //now the basic virtual memory map(see memalyout.h) is established.
    //check the correctness of the basic virtual memory map.
    check_boot_pgdir();

    print_pgdir();
}

實體記憶體管理器pmm_manager初始化

  pmm_init在得到了核心頁目錄表的實體地址後(boot_cr3),便通過init_pmm_manager函式初始化了實體記憶體管理器框架。該框架(全域性變數pmm_manager)是一個被抽象出來的,用於表達實體記憶體管理行為的函式指標集合,核心啟動時會對這一函式指標集合進行賦值。

  有了這一層函式指標集合的抽象層後,呼叫方就可以與提供服務的邏輯解耦了,在不修改任何呼叫方邏輯的情況下,簡單的修改函式指標集合的實現便能進行不同實體記憶體管理器的替換。如果熟悉物件導向概念的話,就會發現這和介面interface的概念類似,ucore實體記憶體管理器框架就是以物件導向的思維,面向介面開發的,通過函式指標集合的方式實現多型這一特性。

  C語言作為一門較低階的語言,其底層的函式指標功能就是C++/JAVA等面嚮物件語言中虛擬函式表的基礎,只是C語言本身設計上並不支援語言級的物件導向程式設計,而必須由開發者手工的編寫類似的模板程式碼,自己實現面嚮物件語言中由編譯器自動實現的邏輯。

init_pmm_manager函式:

//init_pmm_manager - initialize a pmm_manager instance
static void
init_pmm_manager(void) {
    // pmm_manager預設指向default_pmm_manager 使用第一次適配演算法
    pmm_manager = &default_pmm_manager;
    cprintf("memory management: %s\n", pmm_manager->name);
    pmm_manager->init();
}

pmm_manager定義:

// pmm_manager is a physical memory management class. A special pmm manager - XXX_pmm_manager
// only needs to implement the methods in pmm_manager class, then XXX_pmm_manager can be used
// by ucore to manage the total physical memory space.
struct pmm_manager {
    const char *name;                                 // XXX_pmm_manager's name
                                                      // 管理器的名稱

    void (*init)(void);                               // initialize internal description&management data structure
                                                      // (free block list, number of free block) of XXX_pmm_manager 
                                                      // 初始化管理器

    void (*init_memmap)(struct Page *base, size_t n); // setup description&management data structcure according to
                                                      // the initial free physical memory space
                                                      // 設定可管理的記憶體,初始化可分配的實體記憶體空間

    struct Page *(*alloc_pages)(size_t n);            // allocate >=n pages, depend on the allocation algorithm
                                                      // 分配>=N個連續物理頁,返回分配塊首地址指標

    void (*free_pages)(struct Page *base, size_t n);  // free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
                                                      // 釋放包括自Base基址在內的,起始的>=N個連續實體記憶體頁

    size_t (*nr_free_pages)(void);                    // return the number of free pages 
                                                      // 返回全域性的空閒物理頁數量

    void (*check)(void);                              // check the correctness of XXX_pmm_manager 
};

通過探測出的記憶體佈局設定空閒物理對映空間

  ucore使用一個通用的Page結構,來對映每個被管理的物理頁面。

  其中呼叫的init_memmap函式,會通過pmm_manage框架的init_memmap,由指定的演算法來初始化其內部結構。在ucore lab2的參考答案中,預設使用的是default_pmm_manager,其使用的是效率雖然不高,但簡單、易理解的第一次適配演算法(first fit)。關於default_pmm_manager的細節,會在下面再展開介紹。

Page結構

/* *
 * struct Page - Page descriptor structures. Each Page describes one
 * physical page. In kern/mm/pmm.h, you can find lots of useful functions
 * that convert Page to other data types, such as phyical address.
 * */
struct Page {
    // 當前物理頁被虛擬頁面引用的次數(共享記憶體時,影響物理頁面的回收)
    int ref;                        // page frame's reference counter
    // 標誌位集合(目前只用到了第0和第1個bit位) bit 0表示是否被保留(可否用於實體記憶體分配: 0未保留,1被保留);bit 1表示對於可分配的物理頁,當前是否是已被分配的
    uint32_t flags;                 // array of flags that describe the status of the page frame
    // 在不同分配演算法中意義不同(first fit演算法中表示當前空閒塊中總共所包含的空閒頁個數 ,只有位於空閒塊頭部的Page結構才擁有該屬性,否則為0)
    unsigned int property;          // the num of free block, used in first fit pm manager
    // 空閒連結串列free_area_t的連結串列節點引用
    list_entry_t page_link;         // free list link
};

  通過page_init函式可以利用之前在bootasm.S中探測到的e820map佈局結構,初始化空閒實體記憶體空間。

page_init函式:

/* pmm_init - initialize the physical memory management */
static void
page_init(void) {
    // 通過e820map結構體指標,關聯上在bootasm.S中通過e820中斷探測出的硬體記憶體佈局
    // 之所以加上KERNBASE是因為指標定址時使用的是線性虛擬地址。按照最終的虛實地址關係(0x8000 + KERNBASE)虛擬地址 = 0x8000 實體地址
    struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);
    uint64_t maxpa = 0;

    cprintf("e820map:\n");
    int i;
    // 遍歷memmap中的每一項(共nr_map項)
    for (i = 0; i < memmap->nr_map; i ++) {
        // 獲取到每一個佈局entry的起始地址、截止地址
        uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
        cprintf("  memory: %08llx, [%08llx, %08llx], type = %d.\n",
                memmap->map[i].size, begin, end - 1, memmap->map[i].type);
        // 如果是E820_ARM型別的記憶體空間塊
        if (memmap->map[i].type == E820_ARM) {
            if (maxpa < end && begin < KMEMSIZE) {
                // 最大可用的實體記憶體地址 = 當前項的end截止地址
                maxpa = end;
            }
        }
    }

    // 迭代每一項完畢後,發現maxpa超過了定義約束的最大可用實體記憶體空間
    if (maxpa > KMEMSIZE) {
        // maxpa = 定義約束的最大可用實體記憶體空間
        maxpa = KMEMSIZE;
    }

    // 此處定義的全域性end陣列指標,正好是ucore kernel載入後定義的第二個全域性變數(kern_init處第一行定義的)
    // 其上的高位記憶體空間並沒有被使用,因此以end為起點,存放用於管理實體記憶體頁面的資料結構
    extern char end[];

    // 需要管理的物理頁數 = 最大實體地址/物理頁大小
    npage = maxpa / PGSIZE;
    // pages指標指向->可用於分配的,實體記憶體頁面Page陣列起始地址
    // 因此其恰好位於核心空間之上(通過ROUNDUP PGSIZE取整,保證其位於一個新的物理頁中)
    pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);

    for (i = 0; i < npage; i ++) {
        // 遍歷每一個可用的物理頁,預設標記為被保留無法使用
        SetPageReserved(pages + i);
    }

    // 計算出存放實體記憶體頁面管理的Page陣列所佔用的截止地址
    // freemem = pages(管理資料的起始地址) + (Page結構體的大小 * 需要管理的頁面數量)
    uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);

    // freemem之上的高位物理空間都是可以用於分配的free空閒記憶體
    for (i = 0; i < memmap->nr_map; i ++) {
        // 遍歷探測出的記憶體佈局memmap
        uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
        if (memmap->map[i].type == E820_ARM) {
            if (begin < freemem) {
                // 限制空閒地址的最小值
                begin = freemem;
            }
            if (end > KMEMSIZE) {
                // 限制空閒地址的最大值
                end = KMEMSIZE;
            }
            if (begin < end) {
                // begin起始地址以PGSIZE為單位,向高位取整
                begin = ROUNDUP(begin, PGSIZE);
                // end截止地址以PGSIZE為單位,向低位取整
                end = ROUNDDOWN(end, PGSIZE);
                if (begin < end) {
                    // 進行空閒記憶體塊的對映,將其納入實體記憶體管理器中管理,用於後續的實體記憶體分配
                    // 這裡的begin、end都是探測出來的實體地址
                    // 第一個引數:起始Page結構的虛擬地址base = pa2page(begin)
                    // 第二個引數:空閒頁的個數 = (end - begin) / PGSIZE
                    init_memmap(pa2page(begin), (end - begin) / PGSIZE);
                }
            }
        }
    }
}

初始化完畢後ucore實體記憶體佈局示意圖:

   

  其中page_init中的end指向了BSS段結束處,freemem指向空閒記憶體空間的起始地址。pages(核心頁表)位於"管理空閒空間的區域"這一記憶體塊中。實際可用於分配/釋放的空閒物理頁位於記憶體空間起始地址~實際實體記憶體空間結束地址之間。

對虛擬地址進行對映/解除對映

  ucore的lab2中有兩個練習:

  1. 通過一個線性地址來得到對應的二級頁表項(pmm.c中的get_pte函式)。得到這個二級頁表項地址後,便可以建立起虛擬地址與實體地址的對映關係。

  2. 解除釋放一個二級頁表項與實際實體記憶體的對映關係(pmm.c中的page_remove_pte函式)。

  需要注意的是,開啟了頁機制後,所有程式指令都是以邏輯地址(虛擬地址)的形式工作的,像指標、陣列訪問時等都必須是虛擬地址才能正確的工作(例如使用KADDR巨集進行轉換)。而頁表/頁目錄表中的存放的物理頁面基址對映都是實體地址。

get_pte函式:

//get_pte - get pte and return the kernel virtual address of this pte for la
//        - if the PT contains this pte didn't exist, alloc a page for PT
//        通過線性地址(linear address)得到一個頁表項(二級頁表項)(Page Table Entry),並返回該頁表項結構的核心虛擬地址
//        如果應該包含該線性地址對應頁表項的那個頁表不存在,則分配一個物理頁用於存放這個新建立的頁表(Page Table)
// parameter: 引數
//  pgdir:  the kernel virtual base address of PDT   頁目錄表(一級頁表)的起始核心虛擬地址
//  la:     the linear address need to map              需要被對映關聯的線性虛擬地址
//  create: a logical value to decide if alloc a page for PT   一個布林變數決定對應頁表項所屬的頁表不存在時,是否將頁表建立
// return vaule: the kernel virtual address of this pte  返回值: la引數對應的二級頁表項結構的核心虛擬地址
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
    /* LAB2 EXERCISE 2: YOUR CODE
     *
     * If you need to visit a physical address, please use KADDR()
     * please read pmm.h for useful macros
     *
     * Maybe you want help comment, BELOW comments can help you finish the code
     *
     * Some Useful MACROs and DEFINEs, you can use them in below implementation.
     * MACROs or Functions:
     *   PDX(la) = the index of page directory entry of VIRTUAL ADDRESS la.
     *   KADDR(pa) : takes a physical address and returns the corresponding kernel virtual address.
     *   set_page_ref(page,1) : means the page be referenced by one time
     *   page2pa(page): get the physical address of memory which this (struct Page *) page  manages
     *   struct Page * alloc_page() : allocation a page
     *   memset(void *s, char c, size_t n) : sets the first n bytes of the memory area pointed by s
     *                                       to the specified value c.
     * DEFINEs:
     *   PTE_P           0x001                   // page table/directory entry flags bit : Present
     *   PTE_W           0x002                   // page table/directory entry flags bit : Writeable
     *   PTE_U           0x004                   // page table/directory entry flags bit : User can access
     */
#if 0
    pde_t *pdep = NULL;   // (1) find page directory entry
    if (0) {              // (2) check if entry is not present
                          // (3) check if creating is needed, then alloc page for page table
                          // CAUTION: this page is used for page table, not for common data page
                          // (4) set page reference
        uintptr_t pa = 0; // (5) get linear address of page
                          // (6) clear page content using memset
                          // (7) set page directory entry's permission
    }
    return NULL;          // (8) return page table entry
#endif
    // PDX(la) 根據la的高10位獲得對應的頁目錄項(一級頁表中的某一項)索引(頁目錄項)
    // &pgdir[PDX(la)] 根據一級頁表項索引從一級頁表中找到對應的頁目錄項指標
    pde_t *pdep = &pgdir[PDX(la)];
    // 判斷當前頁目錄項的Present存在位是否為1(對應的二級頁表是否存在)
    if (!(*pdep & PTE_P)) {
        // 對應的二級頁表不存在
        // *page指向的是這個新建立的二級頁表基地址
        struct Page *page;
        if (!create || (page = alloc_page()) == NULL) {
             // 如果create引數為false或是alloc_page分配實體記憶體失敗
            return NULL;
        }
        // 二級頁表所對應的物理頁 引用數為1
        set_page_ref(page, 1);
        // 獲得page變數的實體地址
        uintptr_t pa = page2pa(page);
        // 將整個page所在的物理頁格式胡,全部填滿0
        memset(KADDR(pa), 0, PGSIZE);
        // la對應的一級頁目錄項進行賦值,使其指向新建立的二級頁表(頁表中的資料被MMU直接處理,為了對映效率存放的都是實體地址)
        // 或PTE_U/PTE_W/PET_P 標識當前頁目錄項是使用者級別的、可寫的、已存在的
        *pdep = pa | PTE_U | PTE_W | PTE_P;
    }

    // 要想通過C語言中的陣列來訪問對應資料,需要的是陣列基址(虛擬地址),而*pdep中頁目錄表項中存放了對應二級頁表的一個實體地址
    // PDE_ADDR將*pdep的低12位抹零對齊(指向二級頁表的起始基地址),再通過KADDR轉為核心虛擬地址,進行陣列訪問
    // PTX(la)獲得la線性地址的中間10位部分,即二級頁表中對應頁表項的索引下標。這樣便能得到la對應的二級頁表項了
    return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
}

page_remove_pte函式:

//page_remove_pte - free an Page sturct which is related linear address la
//                - and clean(invalidate) pte which is related linear address la
//note: PT is changed, so the TLB need to be invalidate 
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
    /* LAB2 EXERCISE 3: YOUR CODE
     *
     * Please check if ptep is valid, and tlb must be manually updated if mapping is updated
     *
     * Maybe you want help comment, BELOW comments can help you finish the code
     *
     * Some Useful MACROs and DEFINEs, you can use them in below implementation.
     * MACROs or Functions:
     *   struct Page *page pte2page(*ptep): get the according page from the value of a ptep
     *   free_page : free a page
     *   page_ref_dec(page) : decrease page->ref. NOTICE: ff page->ref == 0 , then this page should be free.
     *   tlb_invalidate(pde_t *pgdir, uintptr_t la) : Invalidate a TLB entry, but only if the page tables being
     *                        edited are the ones currently in use by the processor.
     * DEFINEs:
     *   PTE_P           0x001                   // page table/directory entry flags bit : Present
     */
#if 0
    if (0) {                      //(1) check if page directory is present
        struct Page *page = NULL; //(2) find corresponding page to pte
                                  //(3) decrease page reference
                                  //(4) and free this page when page reference reachs 0
                                  //(5) clear second page table entry
                                  //(6) flush tlb
    }
#endif
    if (*ptep & PTE_P) {
        // 如果對應的二級頁表項存在
        // 獲得*ptep對應的Page結構
        struct Page *page = pte2page(*ptep);
        // 關聯的page引用數自減1
        if (page_ref_dec(page) == 0) {
            // 如果自減1後,引用數為0,需要free釋放掉該物理頁
            free_page(page);
        }
        // 清空當前二級頁表項(整體設定為0)
        *ptep = 0;
        // 由於頁表項發生了改變,需要TLB快表
        tlb_invalidate(pgdir, la);
    }
}

default_pmm.c 第一次適配分配演算法分析

   ucore提供了pmm_manager框架,可以支援靈活的切換多種實體記憶體分配演算法。而為了實驗的簡單性,ucore的參考答案提供了相對好理解的first fit第一次適配演算法作為例子,來展示ucore是的實體記憶體管理功能時如何工作的。

  在ucore的第一次適配分配演算法中,是通過一個雙向連結串列結構來連線各個連續空閒塊的,即定義在default_pmm.c中的free_area_t變數。free_area_t結構十分簡單,一個整數nr_free記錄著全域性儲存著多少空閒物理頁,另一個list_entry_t型別的變數free_list,作為整個空閒連結串列的頭結點。

free_area_t結構:

/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
    list_entry_t free_list;         // the list header
    unsigned int nr_free;           // # of free pages in this free list
} free_area_t;

list_entry_t結構:

struct list_entry {
    struct list_entry *prev, *next;
};

typedef struct list_entry list_entry_t;

  回顧一下Page結構的定義,其中包含了一個屬性page_link,就可以用於掛載到free_area_t空閒連結串列中。

ucore通用雙向連結串列介紹

  如果對資料結構中的雙向連結串列知識有一定了解的話,可能會對ucore中雙向連結串列的實現感到疑惑。

  一般來說,雙向連結串列結構的節點除了前驅和後繼節點的指標/引用之外,還存在一個用於包裹業務資料的data屬性,而ucore中的連結串列節點list_entry卻沒有這個data資料屬性。這是因為ucore中的雙向連結串列結構在設計之初是希望能夠通用的:不但能將Page結構連結起來,還能連結其它任意的資料。而C語言中並沒有c++或是java中的泛型功能,只能定義為某一特定型別的data屬性,如果data域與連結串列的節點定義在一起的話,就沒法做到足夠通用。

  ucore參考了linux中的做法,反其道而行:不再是雙向連結串列的節點包裹資料,而是由資料本身儲存連結串列節點引用。這樣設計的最大好處就是連結串列可以通用,能夠連結各種型別的資料結構到一起;但與此同時也帶來了一些問題,比如其降低了程式碼的可讀性,編譯器也沒法確保連結串列中的資料都是合理的型別。

le2page巨集的原理

  在對傳統的雙向連結串列遍歷時,由於是連結串列節點本身包裹了data,因此可以直接訪問到節點關聯的data資料。而在ucore的雙向連結串列實現中,由於連結串列節點本身沒有儲存data資料,而是反被data資料包裹,因此需要一些比較巧妙(tricky)的方法來實現對節點所屬結構的訪問。

  在空閒連結串列這一實現中,是由Page結構包裹著連結串列節點page_link。ucore提供了le2page巨集,通過le2page可以由page_link反向得到節點所屬的Page結構。

  在ucore中,就有通過struct Page *p = le2page(le, page_link)這樣的邏輯,其中le是連結串列節點的指標。

// convert list entry to page
#define le2page(le, member)                 \
    to_struct((le), struct Page, member)

/* *
 * to_struct - get the struct from a ptr
 * @ptr:    a struct pointer of member
 * @type:   the type of the struct this is embedded in
 * @member: the name of the member within the struct
 * */
#define to_struct(ptr, type, member)                               \
    ((type *)((char *)(ptr) - offsetof(type, member)))

/* Return the offset of 'member' relative to the beginning of a struct type 返回member到結構起始地址的相對偏移*/
#define offsetof(type, member) \
((size_t)(&((type *)0)->member))

  可以看到le2page巨集是依賴to_struct這一通用巨集來實現的。在le2page中,其傳遞給to_struct巨集的三個引數分別是連結串列的指標ptrtype為Page結構體本身的定義,member為page_link。

  C語言中,結構體中資料結構的最終在虛擬記憶體空間中是按照屬性的順序,從低位到高位排列的,而page_link的指標地址必然高於Page結構的基地址,且兩者之間的差值可以通過結構體中的定義得到。在to_struct中,通過ptr(即page_link的指標地址)減去offset(type,member)(即page_link到Struct Page結構的相對偏移),便能夠得到page_link節點所屬Page結構的首地址。最後通過type *,將其強制轉換為對應的Page指標。

  offsetof巨集巧妙的構造了一個位於起始地址0的type型別指標,並通過&獲得其member屬性的地址。由於其結構指標的初始地址為0,則最後得到的就是member欄位相對於type結構基址的相對偏移量了。

  這是C語言中通過結構體中某一屬性地址訪問其所屬結構體的一種巧妙實現。

  le2page巨集對於C語言的初學者來說確實不是很好理解,連註釋中都指出這一做法有些tricky,但在理解其原理之後,未來實驗中更多依賴to_struct巨集的地方就不會再被困擾了。

le2page原理圖:

  

default_pmm.c 第一次適配分配演算法中的分配與釋放功能的分析

  default_pmm.c中完整的實現了pmm_manager所指定的函式介面,限於篇幅,這裡只重點介紹其分配與釋放實體記憶體頁的功能。

  分配實體記憶體頁的功能由default_alloc_pages函式完成;釋放實體記憶體頁的功能由default_free_pages函式完成。

default_alloc_pages函式:

/**
 * 接受一個合法的正整數引數n,為其分配N個物理頁面大小的連續實體記憶體空間.
 * 並以Page指標的形式,返回最低位物理頁(最前面的)。
 * 
 * 如果分配時發生錯誤或者剩餘空閒空間不足,則返回NULL代表分配失敗
 * */
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0); if (n > nr_free) { return NULL; } struct Page *page = NULL; list_entry_t *le = &free_list; // TODO: optimize (next-fit) // 遍歷空閒連結串列 while ((le = list_next(le)) != &free_list) { // 將le節點轉換為關聯的Page結構 struct Page *p = le2page(le, page_link); if (p->property >= n) { // 發現一個滿足要求的,空閒頁數大於等於N的空閒塊 page = p; break; } } // 如果page != null代表找到了,分配成功。反之則分配實體記憶體失敗 if (page != NULL) { if (page->property > n) { // 如果空閒塊的大小不是正合適(page->property != n) // 按照指標偏移,找到按序後面第N個Page結構p struct Page *p = page + n; // p其空閒塊個數 = 當前找到的空閒塊數量 - n p->property = page->property - n; SetPageProperty(p); // 按對應的實體地址順序,將p加入到空閒連結串列中對應的位置 list_add_after(&(page->page_link), &(p->page_link)); } // 在將當前page從空間連結串列中移除 list_del(&(page->page_link)); // 閒連結串列整體空閒頁數量自減n nr_free -= n; // 清楚page的property(因為非空閒塊的頭Page的property都為0) ClearPageProperty(page); } return page; }

default_free_pages函式:

/**
 * 釋放掉自base起始的連續n個物理頁,n必須為正整數
 * */
static void
default_free_pages(struct Page *base, size_t n) {
    assert(n > 0);
    struct Page *p = base;

    // 遍歷這N個連續的Page頁,將其相關屬性設定為空閒
    for (; p != base + n; p ++) {
        assert(!PageReserved(p) && !PageProperty(p));
        p->flags = 0;
        set_page_ref(p, 0);
    }

    // 由於被釋放了N個空閒物理頁,base頭Page的property設定為n
    base->property = n;
    SetPageProperty(base);

    // 下面進行空閒連結串列相關操作
    list_entry_t *le = list_next(&free_list);
    // 迭代空閒連結串列中的每一個節點
    while (le != &free_list) {
        // 獲得節點對應的Page結構
        p = le2page(le, page_link);
        le = list_next(le);
        // TODO: optimize
        if (base + base->property == p) {
            // 如果當前base釋放了N個物理頁後,尾部正好能和Page p連上,則進行兩個空閒塊的合併
            base->property += p->property;
            ClearPageProperty(p);
            list_del(&(p->page_link));
        }
        else if (p + p->property == base) {
            // 如果當前Page p能和base頭連上,則進行兩個空閒塊的合併
            p->property += base->property;
            ClearPageProperty(base);
            base = p;
            list_del(&(p->page_link));
        }
    }
    // 空閒連結串列整體空閒頁數量自增n
    nr_free += n;
    le = list_next(&free_list);

    // 迭代空閒連結串列中的每一個節點
    while (le != &free_list) {
        // 轉為Page結構
        p = le2page(le, page_link);
        if (base + base->property <= p) {
            // 進行空閒連結串列結構的校驗,不能存在交叉覆蓋的地方
            assert(base + base->property != p);
            break;
        }
        le = list_next(le);
    }
    // 將base加入到空閒連結串列之中
    list_add_before(le, &(base->page_link));
}

三、總結

  從ucore lab2的實驗pmm_manager框架的實現中使得我進一步的意識到物件導向,或者說是面向介面/協議程式設計並不是面嚮物件語言的專屬。物件導向這一概念更多的是一種通過抽象、聚合進行模組化,降低系統複雜度的一種思想。在ucore中就用C語言以物件導向的方式,解耦了具體的實體記憶體分配策略與使用實體記憶體管理邏輯的解耦,而在《計算機程式的構造與解釋》SICP一書中,便是用lisp這一被公認為是函數語言程式設計正規化的語言實現了一個物件導向的系統。物件導向與函式式這兩種程式設計正規化並不是水火不容的,而都是作為一種控制系統整體複雜度的抽象手段之一。

  仔細觀察pmm_manager框架的設計,可以明顯感到C的多型實現不如支援物件導向程式設計的語言優雅,需要額外編寫許多模板程式碼,且無法得到編譯器更多的支援。這樣一種類似設計模式的繁瑣實現方式,在某種程度上來說也體現了C語言本身表達能力不足的缺陷,也是後來C++出現的一個主要原因。

  通過ucore的實驗,令我們能從原始碼層面實現不同實體記憶體的分配演算法(挑戰練習中要求實現更復雜的夥伴系統、slab分配器),使得作業系統書籍、原理課上講解的相關理論不再枯燥,而是變得栩栩如生了,

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

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

相關文章