日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

操作系统实验报告11:ucore Lab 2

發布時間:2024/6/3 windows 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 操作系统实验报告11:ucore Lab 2 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

ucore實驗報告2

實驗內容

  • uCore Lab 2:物理內存管理
    (1) 編譯運行 uCore Lab 2 的工程代碼;
    (2) 完成 uCore Lab 2 練習 1-3 的編程作業;
    (3) 思考如何實現 uCore Lab 2 擴展練習 1-2。
  • 實驗環境

    • 架構:Intel x86_64 (虛擬機)
    • 操作系統:Ubuntu 20.04
    • 匯編器:gas (GNU Assembler) in AT&T mode
    • 編譯器:gcc

    (1)編譯運行 uCore Lab 2 的工程代碼

    在lab2的makefile文件目錄下, 輸入命令:

    make

    即可編譯運行 uCore Lab 2 的工程代碼

    執行截圖:

    如果輸入make,程序報錯,提示make: Nothing to be done forTARGETS.,那么說明文件沒有更新而且已經編譯過了,想要再次強制編譯,只要輸入make clean,然后再輸入make就可以編譯了:

    (2) uCore Lab 2 練習 1-3 實驗報告

    lab2 練習0:填寫已有實驗

    本實驗依賴實驗1。請把你做的實驗1的代碼填入本實驗中代碼中有LAB1的注釋相應部分。提示:可采用diff和patch工具進行半自動的合并(merge),也可用一些圖形化的比較/merge工具來手動合并,比如meld,eclipse中的diff/merge工具,understand中的diff/merge工具等。

    lab2 練習1:實現 first-fit 連續物理內存分配算法(需要編程)

    在實現first fit 內存分配算法的回收函數時,要考慮地址連續的空閑塊之間的合并操作。提示:在建立空閑頁塊鏈表時,需要按照空閑頁塊起始地址來排序,形成一個有序的鏈表。可能會修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相關函數。請仔細查看和理解default_pmm.c中的注釋。

    請在實驗報告中簡要說明你的設計實現過程。請回答如下問題:

    • 你的first fit算法是否有進一步的改進空間

    first-fit 連續物理內存分配算法

    在First-Fit算法中,空間配置器allocator保留一個空閑塊列表(稱為空閑列表)。一旦接收到內存分配請求,它將沿著列表掃描第一個足夠大以滿足請求的塊。如果選擇的塊明顯大于請求的塊,則通常將其拆分,剩余的塊將作為另一個空閑塊添加到列表中。

    設計實現過程

    可以根據注釋中的過程部分按照注釋內容逐步實現算法設計。

    1. 準備工作:

    在附錄中,可以找到實驗用到的許多數據結構的定義和作用含義。

    (1)、物理頁結構Page:

    為了與以后的分頁機制配合,我們首先需要建立對整個計算機的每一個物理頁的屬性用結構Page來表示,Page在文件kern/mm/memlayout.h中定義,它包含了映射此物理頁的虛擬頁個數,描述物理頁屬性的flags和雙向鏈接各個Page結構的page_link雙向鏈表:

    struct Page {int ref; // page frame's reference counteruint32_t flags; // array of flags that describe the status of the page frameunsigned int property; // the num of free block, used in first fit pm managerlist_entry_t page_link; // free list link };

    成員變量含義:

    ref:表示這頁被頁表的引用記數,如果這個頁被頁表引用了,即在某頁表中有一個頁表項設置了一個虛擬頁到這個Page管理的物理頁的映射關系,就會把Page的ref加一;反之,若頁表項取消,即映射關系解除,就會把Page的ref減一。

    flags:表示此物理頁的狀態標記,在kern/mm/memlayout.h中的定義,可以看到:

    /* Flags describing the status of a page frame */ #define PG_reserved 0 // the page descriptor is reserved for kernel or unusable #define PG_property 1 // the member 'property' is valid

    這表示flags目前用到了兩個bit表示頁目前具有的兩種屬性,bit 0表示此頁是否被保留(reserved),如果是被保留的頁,則bit 0會設置為1,且不能放到空閑頁鏈表中,即這樣的頁不是空閑頁,不能動態分配與釋放。比如目前內核代碼占用的空間就屬于這樣“被保留”的頁。在本實驗中,bit 1表示此頁是否是free的,如果設置為1,表示這頁是free的,可以被分配;如果設置為0,表示這頁已經被分配出去了,不能被再二次分配。另外,本實驗這里取的名字PG_property比較不直觀 ,主要是我們可以設計不同的頁分配算法(best fit, buddy system等),那么這個PG_property就有不同的含義了。

    property:用來記錄某連續內存空閑塊的大小(即地址連續的空閑頁的個數)。這里需要注意的是用到此成員變量的這個Page比較特殊,是這個連續內存空閑塊地址最小的一頁(即頭一頁, Head Page)。連續內存空閑塊利用這個頁的成員變量property來記錄在此塊內的空閑頁的個數。這里去的名字property也不是很直觀,原因與上面類似,在不同的頁分配算法中,property有不同的含義。

    page_link:這是便于把多個連續內存空閑塊鏈接在一起的雙向鏈表指針(可回顧在lab0實驗指導書中有關雙向鏈表數據結構的介紹)。這里需要注意的是用到此成員變量的這個Page比較特殊,是這個連續內存空閑塊地址最小的一頁(即頭一頁, Head Page)。連續內存空閑塊利用這個頁的成員變量page_link來鏈接比它地址小和大的其他連續內存空閑塊。

    (2)、管理所有的連續內存空閑塊的雙向鏈表結構free_area_t:

    在初始情況下,也許這個物理內存的空閑物理頁都是連續的,這樣就形成了一個大的連續內存空閑塊。但隨著物理頁的分配與釋放,這個大的連續內存空閑塊會分裂為一系列地址不連續的多個小連續內存空閑塊,且每個連續內存空閑塊內部的物理頁是連續的。那么為了有效地管理這些小連續內存空閑塊。所有的連續內存空閑塊可用一個雙向鏈表管理起來,便于分配和釋放,為此定義了一個free_area_t數據結構,包含了一個list_entry結構的雙向鏈表指針和記錄當前空閑頁的個數的無符號整型變量nr_free。其中的鏈表指針指向了空閑的物理頁。

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

    (3)、物理內存頁管理器框架pmm_manager:

    struct pmm_manager {const char *name; //物理內存頁管理器的名字void (*init)(void); //初始化內存管理器void (*init_memmap)(struct Page *base, size_t n); //初始化管理空閑內存頁的數據結構struct Page *(*alloc_pages)(size_t n); //分配n個物理內存頁void (*free_pages)(struct Page *base, size_t n); //釋放n個物理內存頁size_t (*nr_free_pages)(void); //返回當前剩余的空閑頁數void (*check)(void); //用于檢測分配/釋放實現是否正確的輔助函數 };

    (4)、雙向鏈表list結構:

    為了實現First-Fit內存分配(FFMA),需要維護一個查找有序(地址按從小到大排列)空閑塊(以頁為最小單位的連續地址空間)的數據結構,而雙向鏈表是一個很好的選擇。我們應該使用一個列表來管理空閑內存塊。使用free_area_t結構用于管理可用內存塊。

    libs/list.h定義了可掛接任意元素的通用雙向鏈表結構和對應的操作,所以需要了解如何使用這個文件提供的各種函數,從而可以完成對雙向鏈表的初始化/插入/刪除等。需要熟悉頭文件list.h中的內容,包括list的定義,以及對其的一些基本操作,如list_init、list_add(list_add_after)、list_add_before、list_del、list_next、list_prev等函數的作用。

    list.h:

    // 雙向鏈表,包括兩個分別指向前一個結點和后一個結點的指針 struct list_entry {struct list_entry *prev, *next; };// list_entry雙向鏈表和list_entry_t等價 typedef struct list_entry list_entry_t;// 初始化一個新的雙向鏈表,雙向鏈表的表頭指針和表尾指針都指向elm,即只有一個結點elm的雙向鏈表 static inline void list_init(list_entry_t *elm) __attribute__((always_inline)); // 將結點elm插到鏈表項listelm的后面 static inline void list_add(list_entry_t *listelm, list_entry_t *elm) __attribute__((always_inline)); // 將結點elm插到鏈表項listelm的前面 static inline void list_add_before(list_entry_t *listelm, list_entry_t *elm) __attribute__((always_inline)); // 等價于list_add static inline void list_add_after(list_entry_t *listelm, list_entry_t *elm) __attribute__((always_inline)); // 刪除鏈表項listem static inline void list_del(list_entry_t *listelm) __attribute__((always_inline)); // 返回listelm后面的鏈表項 static inline list_entry_t *list_next(list_entry_t *listelm) __attribute__((always_inline)); // 返回listelm前面的鏈表項 static inline list_entry_t *list_prev(list_entry_t *listelm) __attribute__((always_inline));

    __attribute__((always_inline))代表強制內聯。

    2. 修改default_init函數:

    kern/mm/pmm.h中定義了一個通用的分配算法的函數列表,用pmm_manager表示。其中init函數就是用來初始化free_area變量的, first_fit分配算法可直接重用default_init函數的實現。來初始化free_area_t結構中的free_list,并將nr_free設置為0。free_list用于記錄可用內存塊,nr_free是可用內存塊的總數。

    default_init函數:

    static void default_init(void) {// 初始化鏈表list_init(&free_list);// 將可用內存塊數目設置為0nr_free = 0; }

    3. 修改default_init_memmap函數:

    init_memmap函數需要根據現有的內存情況構建空閑塊列表的初始狀態。通過分析代碼,可以知道:

    kern_init --> pmm_init --> page_init --> init_memmap --> pmm_manager --> init_memmap

    所以,default_init_memmap需要根據page_init函數中傳遞過來的參數(某個連續地址的空閑塊的起始頁,頁個數)來建立一個連續內存空閑塊的雙向鏈表。這里有一個假定page_init函數是按地址從小到大的順序傳來的連續內存空閑塊的。鏈表頭是free_area.free_list,鏈表項是Page數據結構的base->page_link。這樣我們就依靠Page數據結構中的成員變量page_link形成了連續內存空閑塊列表。

    default_init_memmap函數將根據每個物理頁幀的情況來建立空閑頁鏈表,且空閑頁塊應該是根據地址高低形成一個有序鏈表。根據上述變量的定義,default_init_memmap可大致實現如下:

    static void default_init_memmap(struct Page *base, size_t n) {// n要大于0assert(n > 0);// 令p為連續地址的空閑塊的起始頁struct Page *p = base;// 將這個空閑塊的每個頁面初始化for (; p != base + n; p ++) {// 每次循環首先檢查p的PG_reserved位是否設置為1,表示空閑可分配assert(PageReserved(p));// 設置這一頁的flag為0,表示這頁空閑p->flags = 0;// 將這一頁的ref設為0,因為這頁現在空閑,沒有引用set_page_ref(p, 0);// 如果是空閑塊的起始頁if (p == base) {// 空閑塊的第一頁的連續空頁值property設置為塊中的總頁數p->property = n;// 將空閑塊的第一頁的PG_property位設置為1,表示是起始頁,可以被用作分配內存SetPageProperty(p);} else {// 設置非起始頁的property為0,表示不是起始頁p->property = 0;}}// 將base->page_link此頁鏈接到free_list中list_add_before(&free_list, &(base->page_link));// 將空閑頁的數目加nnr_free += n; }

    4. 修改default_alloc_pages函數:

    firstfit需要從空閑鏈表頭開始查找最小的地址,通過list_next找到下一個空閑塊元素,通過le2page宏可以由鏈表元素獲得對應的Page指針p。通過p->property可以了解此空閑塊的大小。如果p->property >= n,這就找到了!如果p->property < n,則list_next,繼續查找。直到list_next == &free_list,這表示找完了一遍了。找到后,就要從新組織空閑塊,然后把找到的page返回。所以default_alloc_pages可大致實現如下:

    static struct Page * default_alloc_pages(size_t n) {// n要大于0assert(n > 0);// 考慮邊界情況,當n大于可以分配的內存數時,直接返回,確保分配不會超出范圍,保證軟件的魯棒性if (n > nr_free) {return NULL;}struct Page *page = NULL;// 指針le指向空閑鏈表頭,開始查找最小的地址list_entry_t *le = &free_list;// 遍歷空閑鏈表while ((le = list_next(le)) != &free_list) {// 由鏈表元素獲得對應的Page指針pstruct Page *p = le2page(le, page_link);// 如果當前頁面的property大于等于n,說明空閑塊的連續空頁數大于等于n,可以分配,令page等于p,直接退出if (p->property >= n) {page = p;break;}}// 如果找到了空閑塊,進行重新組織,否則直接返回NULLif (page != NULL) {// 在空閑頁鏈表中刪除剛剛分配的空閑塊list_del(&(page->page_link));// 如果可以分配的空閑塊的連續空頁數大于nif (page->property > n) {// 創建一個地址為page+n的新物理頁struct Page *p = page + n;// 頁面的property設置為page多出來的空閑連續頁數p->property = page->property - n;// 設置p的Page_property位,表示為新的空閑塊的起始頁SetPageProperty(p);// 將新的空閑塊的頁插入到空閑頁鏈表的后面list_add(&free_list, &(p->page_link));}// 剩余空閑頁的數目減nnr_free -= n;// 清除page的Page_property位,表示page已經被分配ClearPageProperty(page);}return page; }

    5. 修改default_free_pages函數:

    default_free_pages函數的實現其實是default_alloc_pages的逆過程,不過需要考慮空閑塊的合并問題。將頁面重新鏈接到空閑列表中,可以將小的空閑塊合并到大的空閑塊中。

    static void default_free_pages(struct Page *base, size_t n) {// n要大于0assert(n > 0);// 令p為連續地址的釋放塊的起始頁struct Page *p = base;// 將這個釋放塊的每個頁面初始化for (; p != base + n; p ++) {// 檢查每一頁的Page_reserved位和Page_property是否都未被設置assert(!PageReserved(p) && !PageProperty(p));// 設置每一頁的flags都為0,表示可以分配p->flags = 0;// 設置每一頁的ref都為0,表示這頁空閑set_page_ref(p, 0);}// 釋放塊起始頁的property連續空頁數設置為nbase->property = n;// 設置起始頁的Page_property位SetPageProperty(base);// 指針le指向空閑鏈表頭,開始查找最小的地址list_entry_t *le = &free_list;// 遍歷空閑鏈表,查看能否將釋放塊合并到合適的頁塊中while ((le = list_next(le)) != &free_list) {// 由鏈表元素獲得對應的Page指針pp = le2page(le, page_link);// 如果釋放塊在下一個空閑塊起始頁的前面,那么進行合并if (base + base->property == p) {// 釋放塊的連續空頁數要加上空閑塊起始頁p的連續空頁數base->property += p->property;// 清除p的Page_property位,表示p不再是新的空閑塊的起始頁ClearPageProperty(p);// 將原來的空閑塊刪除list_del(&(p->page_link));}// 如果釋放塊的起始頁在上一個空閑塊的后面,那么進行合并else if (p + p->property == base) {// 空閑塊的連續空頁數要加上釋放塊起始頁base的連續空頁數p->property += base->property;// 清除base的Page_property位,表示base不再是起始頁ClearPageProperty(base);// 新的空閑塊的起始頁變成pbase = p;// 將原來的空閑塊刪除list_del(&(p->page_link));}}le = &free_list;// 遍歷空閑鏈表,將合并好之后的頁塊加回空閑鏈表while ((le = list_next(le)) != &free_list) {// 由鏈表元素獲得對應的Page指針pp = le2page(le, page_link);// 找到能夠方向新的合并塊的位置if (base + base->property <= p) {break;}}// 將空閑頁的數目加nnr_free += n;// 將base->page_link此頁鏈接到le中,插入合適位置list_add_before(le, &(base->page_link)); }

    6、檢驗:

    在終端輸入make qemu指令:

    可以看到,內存成功分配,算法通過測試。

    firstfit算法的改進

    在進行分配以及釋放內存的時候,在雙向鏈表上進行操作的時間復雜度為O(n),如果使用二叉搜索樹對地址進行排序,從而對進程進行管理,就可以在查找頁塊時將時間復雜度降到O(logn);

    將較小的內存塊及時合并到其它內存塊中,也可以提高空間的利用率,從而使算法得到優化。

    lab2 練習2:實現尋找虛擬地址對應的頁表項(需要編程)

    通過設置頁表和對應的頁表項,可建立虛擬內存地址和物理內存地址的對應關系。其中的get_pte函數是設置頁表項環節中的一個重要步驟。此函數找到一個虛地址對應的二級頁表項的內核虛地址,如果此二級頁表項不存在,則分配一個包含此項的二級頁表。本練習需要補全get_pte函數 in kern/mm/pmm.c,實現其功能。請仔細查看和理解get_pte函數中的注釋。get_pte函數的調用關系圖如下所示:

    請在實驗報告中簡要說明你的設計實現過程。請回答如下問題:

    • 請描述頁目錄項(Page Directory Entry)和頁表項(Page Table Entry)中每個組成部分的含義以及對ucore而言的潛在用處。
    • 如果ucore執行過程中訪問內存,出現了頁訪問異常,請問硬件要做哪些事情?

    段頁式管理基本概念

    在保護模式中,x86 體系結構將內存地址分成三種:邏輯地址(也稱虛地址)、線性地址和物理地址。邏輯地址即是程序指令中使用的地址,物理地址是實際訪問內存的地址。邏 輯地址通過段式管理的地址映射可以得到線性地址,線性地址通過頁式管理的地址映射得到物理地址。

    段頁式管理總體框架圖:

    段式管理前一個實驗已經討論過。在 ucore 中段式管理只起到了一個過渡作用,它將邏輯地址不加轉換直接映射成線性地址,所以我們在下面的討論中可以對這兩個地址不加區分(目前的 OS 實現也是不加區分的)。

    頁式管理將線性地址分成三部分(分頁機制管理圖中的 Linear Address 的 Directory 部分、 Table 部分和 Offset 部分)。ucore 的頁式管理通過一個二級的頁表實現。一級頁表的起始物理地址存放在 cr3 寄存器中,這個地址必須是一個頁對齊的地址,也就是低 12 位必須為 0。目前,ucore 用boot_cr3(mm/pmm.c)記錄這個值。

    分頁機制管理:

    為了實現分頁機制,需要建立好虛擬內存和物理內存的頁映射關系,即正確建立二級頁表。此過程涉及硬件細節,不同的地址映射關系組合,相對比較復雜。

    建立虛擬頁和物理頁幀的地址映射關系

    建立二級頁表

    整個頁目錄表和頁表所占空間大小取決與二級頁表要管理和映射的物理頁數。假定當前物理內存0~16MB,每物理頁(也稱Page Frame)大小為4KB,則有4096個物理頁,也就意味這有4個頁目錄項和4096個頁表項需要設置。一個頁目錄項(Page Directory Entry,PDE)和一個頁表項(Page Table Entry,PTE)占4B。即使是4個頁目錄項也需要一個完整的頁目錄表(占4KB)。而4096個頁表項需要16KB(即4096*4B)的空間,也就是4個物理頁,16KB的空間。所以對16MB物理頁建立一一映射的16MB虛擬頁,需要5個物理頁,即20KB的空間來形成二級頁表。

    完成前一節所述的前兩個階段的地址映射變化后,為把0~KERNSIZE(明確ucore設定實際物理內存不能超過KERNSIZE值,即0x38000000字節,896MB,3670016個物理頁)的物理地址一一映射到頁目錄項和頁表項的內容,其大致流程如下:

  • 指向頁目錄表的指針已存儲在boot_pgdir變量中。
  • 映射0~4MB的首個頁表已經填充好。
  • 調用boot_map_segment函數進一步建立一一映射關系,具體處理過程以頁為單位進行設置,即
  • linear addr = phy addr + 0xC0000000

    設一個32bit線性地址la有一個對應的32bit物理地址pa,如果在以la的高10位為索引值的頁目錄項中的存在位(PTE_P)為0,表示缺少對應的頁表空間,則可通過alloc_page獲得一個空閑物理頁給頁表,頁表起始物理地址是按4096字節對齊的,這樣填寫頁目錄項的內容為

    頁目錄項內容 = (頁表起始物理地址 & ~0x0FFF) | PTE_U | PTE_W | PTE_P

    進一步對于頁表中以線性地址la的中10位為索引值對應頁表項的內容為

    頁表項內容 = (pa & ~0x0FFF) | PTE_P | PTE_W

    其中:

  • PTE_U:位3,表示用戶態的軟件可以讀取對應地址的物理內存頁內容
  • PTE_W:位2,表示物理內存頁內容可寫
  • PTE_P:位1,表示物理內存頁存在
  • ucore的內存管理經常需要查找頁表:給定一個虛擬地址,找出這個虛擬地址在二級頁表中對應的項。通過更改此項的值可以方便地將虛擬地址映射到另外的頁上。可完成此功能的這個函數是get_pte函數。它的原型為

    pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create)

    1. 準備工作:

    根據注釋的指引,可以找到實驗用到的許多數據結構的定義和作用含義。

    PDX(la):返回虛擬地址la的頁目錄項索引
    KADDR(pa):獲取pa的物理地址并返回相應的內核虛擬地址
    set_page_ref(page,1):表示此頁被引用一次
    page2pa(page):獲得page管理的那一頁的物理內存地址
    struct Page * alloc_page():分配一頁
    memset(void * s, char c, size_t n):設置s指向內存區域的前面n個字節為字符c
    PTE_P 0x001:位1,表示物理內存頁存在
    PTE_W 0x002:位2,表示物理內存頁內容可寫
    PTE_U 0x004:位3,表示用戶態的軟件可以讀取對應地址的物理內存頁內容

    涉及到的三個類型pte_t、pde_t和uintptr_t。通過參見mm/mmlayout.h和libs/types.h,可知它們其實都是unsigned int類型。在此做區分,是為了分清概念。

    typedef unsigned int uint32_t;typedef uint32_t uintptr_t;typedef uintptr_t pte_t;typedef uintptr_t pde_t;

    2. 按照注釋的步驟,實現get_pte函數:

    pde_t全稱為page directory entry,也就是一級頁表的表項(注意:pgdir實際不是表項,而是一級頁表本身。實際上應該新定義一個類型pgd_t來表示一級頁表本身)。pte_t全稱為 page table entry,表示二級頁表的表項。uintptr_t表示為線性地址,由于段式管理只做直接映射,所以它也是邏輯地址。

    pgdir給出頁表起始地址。通過查找這個頁表,我們需要給出二級頁表中對應項的地址。雖然目前我們只有boot_pgdir一個頁表,但是引入進程的概念之后每個進程都會有自己的頁表。

    有可能根本就沒有對應的二級頁表的情況,所以二級頁表不必要一開始就分配,而是等到需要的時候再添加對應的二級頁表。如果在查找二級頁表項時,發現對應的二級頁表不存在,則需要根據create參數的值來處理是否創建新的二級頁表。如果create參數為0,則get_pte返回NULL;如果create參數不為0,則get_pte需要申請一個新的物理頁(通過alloc_page來實現,可在mm/pmm.h中找到它的定義),再在一級頁表中添加頁目錄項指向表示二級頁表的新物理頁。注意,新申請的頁必須全部設定為零,因為這個頁所代表的虛擬地址都沒有被映射。

    當建立從一級頁表到二級頁表的映射時,需要注意設置控制位。這里應該設置同時設置上PTE_U、PTE_W和PTE_P(定義可在mm/mmu.h)。如果原來就有二級頁表,或者新建立了頁表,則只需返回對應項的地址即可。

    pte_t * get_pte(pde_t *pgdir, uintptr_t la, bool create) {pde_t *pdep = &pgdir[PDX(la)]; // (1) 首先找到頁目錄項,嘗試獲得頁表if (!(*pdep & PTE_P)) { // (2) 檢查這個頁目錄項是否存在,存在則直接返回找到的頁表項,如果不存在if (!create) { // (3) 頁目錄項不存在且參數不要求創建新的頁表,那么返回NULLreturn NULL;}struct Page *page = alloc_page(); // (3) 否則分配一個物理頁存儲創建的頁表if (page == NULL) { // (3) 如果分配失敗,那么返回NULLreturn NULL;}set_page_ref(page, 1); // (4) 設置物理頁被引用一次uintptr_t pa = page2pa(page); // (5) 獲得物理頁的線性物理地址memset(KADDR(pa), 0, PGSIZE); // (6) 將物理地址轉換成虛擬地址后,用memset函數清除頁目錄進行初始化*pdep = pa | PTE_U | PTE_W | PTE_P; // (7) 設置頁目錄項的權限}return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)]; // (8) 返回虛擬地址la對應的頁表項入口地址 }

    請描述頁目錄項(Page Directory Entry)和頁表項(Page Table Entry)中每個組成部分的含義以及對ucore而言的潛在用處。

    頁目錄項

    組成如圖所示:

    每個組成部分的含義:

    地址頁目錄項組成部分ucore中的對應以及對ucore而言的潛在用處
    31:12Page Table 4-kB aligned Address這個頁目錄項對應的頁表指向的物理頁的物理地址,用于定位頁表位置
    11:9AvailPTE_AVAIL,保留給OS使用
    8Ignored可忽略
    7Page Size(0 for 4kb)PTE_PS,用于確認頁的大小,0表示4kb
    60PTE_MBZ,恒為0,保留位信息
    5AccessedPTE_A,用來表示頁表是否被使用
    4Cache DisabledPTE_PCD,表示是否對頁表進行緩存
    3Write ThroughPTE_PWT,表示緩存是否使用write through寫策略
    2Use\SupervisorPTE_U,表示訪問該頁需要的特權級
    1Read\WritePTE_W,表示頁表是否允許讀寫。內存分配和釋放時需要置位
    0PresentPTE_P,是存在位,如果為1表示存在,如果為0表示不存在,需要再分配一個物理頁給頁表
    頁表項

    組成如圖所示:

    每個組成部分的含義:

    地址頁目錄項組成部分ucore中的對應以及對ucore而言的潛在用處
    31:12PAGE FRAME ADDRESS頁表項指向的物理頁的物理地址,用于定位頁表位置
    11:9AVAILPTE_AVAIL,保留給OS使用
    8:70PTE_MBZ,恒為0,保留位信息
    6Dirty表示是否要在swap out的時候寫回外存
    5AccessedPTE_A,用來表示頁表是否被訪問
    4:30恒為0,保留位信息
    2Use\SupervisorPTE_U,表示訪問該頁需要的特權級
    1Read\WritePTE_W,表示頁表是否允許讀寫。內存分配和釋放時需要置位
    0PresentPTE_P,是存在位,如果為1表示存在,如果為0表示不存在,需要再分配一個物理頁給頁表

    對ucore而言的潛在用處:

    頁目錄項和頁表項中的保留位可以幫助ucore實現功能的擴展,可以用來進行內存管理,完成一些內存管理相關的算法,比如記錄一段時間內被訪問的次數,實現LRU等算法。

    如果ucore執行過程中訪問內存,出現了頁訪問異常,請問硬件要做哪些事情?

    • 將發生頁訪問異常的地址保存在cr2寄存器中;
    • 設置錯誤代碼,向棧中壓入EFLAGS,CS, EIP,和錯誤代碼error code,如果發生在用戶態,則之前還要先壓入ss和esp,并切換到內核態;
    • 引發Page Fault,根據中斷描述符表查詢到對應Page Fault的ISR,跳轉到對應的ISR處執行,進行Page Fault處理,將外存的數據換到內存中;
    • 進行上下文切換,返回中斷之前的狀態

    lab2 練習3:釋放某虛地址所在的頁并取消對應二級頁表項的映射(需要編程)

    當釋放一個包含某虛地址的物理內存頁時,需要讓對應此物理內存頁的管理數據結構Page做相關的清除處理,使得此物理內存頁成為空閑;另外還需把表示虛地址與物理地址對應關系的二級頁表項清除。請仔細查看和理解page_remove_pte函數中的注釋。為此,需要補全在kern/mm/pmm.c中的page_remove_pte函數。page_remove_pte函數的調用關系圖如下所示:

    請在實驗報告中簡要說明你的設計實現過程。請回答如下問題:

    • 數據結構Page的全局變量(其實是一個數組)的每一項與頁表中的頁目錄項和頁表項有無對應關系?如果有,其對應關系是啥?
    • 如果希望虛擬地址與物理地址相等,則需要如何修改lab2,完成此事? 鼓勵通過編程來具體完成這個問題

    1. 準備工作:

    根據注釋的指引,可以找到實驗用到的許多數據結構的定義和作用含義。

    struct Page *page pte2page(*ptep):獲取ptep頁表項對應的物理頁
    free_page:釋放一頁
    page_ref_dec(page):使得此頁的引用數減一,并返回引用數,如果此頁的引用數為0,那么應該被釋放
    tlb_invalidate(pde_t *pgdir, uintptr_t la):當修改的頁表是那些正在使用的頁表,那么無效

    PTE_P 0x001:位1,表示物理內存頁存在

    涉及到的兩個個類型pde_t和uintptr_t。通過參見mm/mmlayout.h和libs/types.h,可知它們其實都是unsigned int類型。在此做區分,是為了分清概念。

    typedef unsigned int uint32_t;typedef uint32_t uintptr_t;typedef uintptr_t pde_t;

    2. 按照注釋的步驟,實現page_remove_pte函數:

    只有當一級二級頁表的項都設置了用戶寫權限后,用戶才能對對應的物理地址進行讀寫。所以我們可以在一級頁表先給用戶寫權限,再在二級頁表上面根據需要限制用戶的權限,對物理頁進行保護。由于一個物理頁可能被映射到不同的虛擬地址上去(譬如一塊內存在不同進程間共享),當這個頁需要在一個地址上解除映射時,操作系統不能直接把這個頁回收,而是要先看看它還有沒有映射到別的虛擬地址上。這是通過查找管理該物理頁的Page數據結構的成員變量ref(用來表示虛擬頁到物理頁的映射關系的個數)來實現的,如果ref為0了,表示沒有虛擬頁到物理頁的映射關系了,就可以把這個物理頁給回收了,從而這個物理頁是free的了,可以再被分配。page_insert函數將物理頁映射在了頁表上。可參看page_insert函數的實現來了解ucore內核是如何維護這個變量的。當不需要再訪問這塊虛擬地址時,可以把這塊物理頁回收并在將來用在其他地方。取消映射由page_remove來做,這其實是page_insert的逆操作。

    static inline void page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {if (*ptep & PTE_P) { // (1) 檢查這個頁目錄項是否存在struct Page *page = pte2page(*ptep); // (2) 找到這個頁目錄項對應的頁if (page_ref_dec(page) == 0) { // (3) 將這個頁的引用數減一free_page(page); // (4) 如果這個頁的引用數為0,那么釋放此頁}*ptep = 0; // (5) 清除頁目錄項tlb_invalidate(pgdir, la); // (6) 當修改的頁表正在使用時,那么無效 } }

    數據結構Page的全局變量(其實是一個數組)的每一項與頁表中的頁目錄項和頁表項有無對應關系?如果有,其對應關系是啥?

    答:有對應關系,因為每個頁目錄項記錄了一個頁表項的信息,每一個頁表項也記錄了一個物理頁的信息,而數據結構Page的全局變量的每一項也記錄一個物理頁的信息,那么頁目錄項和頁表項中保存的物理頁面地址也會對應數據結構Page的全局變量的某一頁。

    如果希望虛擬地址與物理地址相等,則需要如何修改lab2,完成此事? 鼓勵通過編程來具體完成這個問題

    根據附錄,lab1中通過ld工具形成的ucore的起始虛擬地址從0x100000開始,這個地址是虛擬地址,建立的段地址映射關系為對等關系,物理地址也是從0x100000開始,虛擬地址、線性地址以及物理地址相同,在lab2中建立了從虛擬地址到物理地址的映射,那么只需要取消映射,就可以實現虛擬地址以及物理地址相等。

    首先找到tools/kernel.ld,將tools/kernel.ld改回lab1的數字,將鏈接腳本的0xC0100000改為0x100000:

    ENTRY(kern_init)SECTIONS {/* Load the kernel at this address: "." means the current address */. = 0x100000;.text : {*(.text .stub .text.* .gnu.linkonce.t.*)}

    然后按照lab1,將偏移量從0xC0000000改為0,并將開啟頁表關閉:

    #define KERNBASE 0x0

    驗證 lab2 練習1~3正確性

    輸入命令make qemu和make grade:

    可以看到,程序編寫正確。

    lab2 擴展練習Challenge:buddy system(伙伴系統)分配算法(需要編程)

    Buddy System算法把系統中的可用存儲空間劃分為存儲塊(Block)來進行管理, 每個存儲塊的大小必須是2的n次冪(Pow(2, n)), 即1, 2, 4, 8, 16, 32, 64, 128…

    • 參考伙伴分配器的一個極簡實現, 在ucore中實現buddy system分配算法,要求有比較充分的測試用例說明實現的正確性,需要有設計文檔。

    思考如何實現

    伙伴分配的實質就是一種特殊的“分離適配”,即將內存按2的冪進行劃分,相當于分離出若干個塊大小一致的空閑鏈表,搜索該鏈表并給出同需求最佳匹配的大小。其優點是快速搜索合并(O(logN)時間復雜度)以及低外部碎片(最佳適配best-fit);其缺點是內部碎片,因為按2的冪劃分塊,如果碰上66單位大小,那么必須劃分128單位大小的塊。但若需求本身就按2的冪分配,比如可以先分配若干個內存池,在其基礎上進一步細分就很有吸引力了。

    根據參考文章《伙伴分配器的一個極簡實現》,我們可以使用一個數組形式的完全二叉樹來管理內存,二叉樹的節點用于標記相應內存塊的使用狀態,高層節點對應大的塊,低層節點對應小的塊,在分配和釋放中我們就通過這些節點的標記屬性來進行塊的分離合并。如圖所示,假設總大小為16單位的內存,我們就建立一個深度為5的滿二叉樹,根節點從數組下標[0]開始,監控大小16的塊;它的左右孩子節點下標[12],監控大小8的塊;第三層節點下標[36]監控大小4的塊……依此類推。

    在分配階段,首先要搜索大小適配的塊,假設第一次分配3,轉換成2的冪是4,我們先要對整個內存進行對半切割,從16切割到4需要兩步,那么從下標[0]節點開始深度搜索到下標[3]的節點并將其標記為已分配。第二次再分配3那么就標記下標[4]的節點。第三次分配6,即大小為8,那么搜索下標[2]的節點,因為下標[1]所對應的塊被下標[3~4]占用了。

    在釋放階段,我們依次釋放上述第一次和第二次分配的塊,即先釋放[3]再釋放[4],當釋放下標[4]節點后,我們發現之前釋放的[3]是相鄰的,于是我們立馬將這兩個節點進行合并,這樣一來下次分配大小8的時候,我們就可以搜索到下標[1]適配了。若進一步釋放下標[2],同[1]合并后整個內存就回歸到初始狀態。

    lab2 擴展練習Challenge:任意大小的內存單元slub分配算法(需要編程)

    slub算法,實現兩層架構的高效內存單元分配,第一層是基于頁大小的內存分配,第二層是在第一層基礎上實現基于任意大小的內存分配。可簡化實現,能夠體現其主體思想即可。

    • 參考linux的slub分配算法實現slub分配算法。要求有比較充分的測試用例說明實現的正確性,需要有設計文檔。

    思考如何實現

    根據參考文章《linux的slub分配算法》中的SLUB分配器,可以實現分配和釋放等功能。

    SLAB 分配器為每種使用的內核對象建立單獨的緩沖區。每種緩沖區由多個 slab 組成,每個 slab就是一組連續的物理內存頁框,被劃分成了固定數目的對象。根據對象大小的不同,缺省情況下一個 slab 最多可以由 1024 個物理內存頁框構成。

    內核使用 kmem_cache 數據結構管理緩沖區。由于 kmem_cache 自身也是一種內核對象,所以需要一個專門的緩沖區。所有緩沖區的 kmem_cache 控制結構被組織成以 cache_chain 為隊列頭的一個雙向循環隊列,同時 cache_cache 全局變量指向kmem_cache 對象緩沖區的 kmem_cache 對象。每個 slab 都需要一個類型為 struct slab 的描述符數據結構管理其狀態,同時還需要一個 kmem_bufctl_t(被定義為無符號整數)的結構數組來管理空閑對象。如果對象不超過 1/8 個物理內存頁框的大小,那么這些 slab 管理結構直接存放在 slab 的內部,位于分配給 slab 的第一個物理內存頁框的起始位置;否則的話,存放在 slab 外部,位于由 kmalloc 分配的通用對象緩沖區中。

    slab 中的對象有 2 種狀態:已分配或空閑。為了有效地管理 slab,根據已分配對象的數目,slab 可以有 3 種狀態,動態地處于緩沖區相應的隊列中:

  • Full 隊列,此時該 slab 中沒有空閑對象。
  • Partial 隊列,此時該 slab 中既有已分配的對象,也有空閑對象。
  • Empty 隊列,此時該 slab 中全是空閑對象。
  • 在 SLUB 分配器中,一個 slab 就是一組連續的物理內存頁框,被劃分成了固定數目的對象。slab 沒有額外的空閑對象隊列(這與 SLAB 不同),而是重用了空閑對象自身的空間。slab 也沒有額外的描述結構,因為 SLUB 分配器在代表物理頁框的 page 結構中加入 freelist,inuse 和 slab 的 union 字段,分別代表第一個空閑對象的指針,已分配對象的數目和緩沖區 kmem_cache 結構的指針,所以 slab 的第一個物理頁框的 page 結構就可以描述自己。

    每個處理器都有一個本地的活動 slab,由 kmem_cache_cpu 結構描述。

    分配時:

    slab_alloc函數:

    static __always_inline void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, void *addr) {void **object;struct kmem_cache_cpu *c;unsigned long flags;local_irq_save(flags);c = get_cpu_slab(s, smp_processor_id()); // (a)if (unlikely(!c->freelist || !node_match(c, node)))object = __slab_alloc(s, gfpflags, node, addr, c); // (b)else {object = c->freelist; // (c)c->freelist = object[c->offset];stat(c, ALLOC_FASTPATH);}local_irq_restore(flags);if (unlikely((gfpflags & __GFP_ZERO) && object))memset(object, 0, c->objsize);return object; // (d) }
  • 獲取本處理器的 kmem_cache_cpu 數據結構。
  • 假如當前活動 slab 沒有空閑對象,或本處理器所在節點與指定節點不一致,則調用 __slab_alloc 函數。
  • 獲得第一個空閑對象的指針,然后更新指針使其指向下一個空閑對象。
  • 返回對象地址。
  • __slab_alloc函數:

    static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, void *addr, struct kmem_cache_cpu *c) {void **object;struct page *new;gfpflags &= ~__GFP_ZERO;if (!c->page) // (a)goto new_slab;slab_lock(c->page);if (unlikely(!node_match(c, node))) // (b)goto another_slab;stat(c, ALLOC_REFILL);load_freelist:object = c->page->freelist;if (unlikely(!object)) // (c)goto another_slab;if (unlikely(SlabDebug(c->page)))goto debug;c->freelist = object[c->offset]; // (d)c->page->inuse = s->objects;c->page->freelist = NULL;c->node = page_to_nid(c->page);unlock_out:slab_unlock(c->page);stat(c, ALLOC_SLOWPATH);return object;another_slab:deactivate_slab(s, c); // (e)new_slab:new = get_partial(s, gfpflags, node); // (f)if (new) {c->page = new;stat(c, ALLOC_FROM_PARTIAL);goto load_freelist;}if (gfpflags & __GFP_WAIT) // (g)local_irq_enable();new = new_slab(s, gfpflags, node); // (h)if (gfpflags & __GFP_WAIT)local_irq_disable();if (new) {c = get_cpu_slab(s, smp_processor_id());stat(c, ALLOC_SLAB);if (c->page)flush_slab(s, c);slab_lock(new);SetSlabFrozen(new);c->page = new;goto load_freelist;}if (!(gfpflags & __GFP_NORETRY) && (s->flags & __PAGE_ALLOC_FALLBACK)) {if (gfpflags & __GFP_WAIT)local_irq_enable();object = kmalloc_large(s->objsize, gfpflags); // (i)if (gfpflags & __GFP_WAIT)local_irq_disable();return object;}return NULL;debug:if (!alloc_debug_processing(s, c->page, object, addr))goto another_slab;c->page->inuse++;c->page->freelist = object[c->offset];c->node = -1;goto unlock_out; }
  • 如果沒有本地活動 slab,轉到 (f) 步驟獲取 slab 。
  • 如果本處理器所在節點與指定節點不一致,轉到 (e) 步驟。
  • 檢查處理器活動 slab 沒有空閑對象,轉到 (e) 步驟。
  • 此時活動 slab 尚有空閑對象,將 slab 的空閑對象隊列指針復制到 kmem_cache_cpu 結構的 freelist 字段,把 slab 的空閑對象隊列指針設置為空,從此以后只從 kmem_cache_cpu 結構的 freelist 字段獲得空閑對象隊列信息。
  • 取消當前活動 slab,將其加入到所在 NUMA 節點的 Partial 隊列中。
  • 優先從指定 NUMA 節點上獲得一個 Partial slab。
  • 加入 gfpflags 標志置有 __GFP_WAIT,開啟中斷,故后續創建 slab 操作可以睡眠。
  • 創建一個 slab,并初始化所有對象。
  • 如果內存不足,無法創建 slab,調用 kmalloc_large(實際調用物理頁框分配器)分配對象。
  • 釋放時:

    slab_free函數:

    static __always_inline void slab_free(struct kmem_cache *s, struct page *page, void *x, void *addr) {void **object = (void *)x;struct kmem_cache_cpu *c;unsigned long flags;local_irq_save(flags);c = get_cpu_slab(s, smp_processor_id());debug_check_no_locks_freed(object, c->objsize);if (likely(page == c->page && c->node >= 0)) { // (a)object[c->offset] = c->freelist;c->freelist = object;stat(c, FREE_FASTPATH);} else__slab_free(s, page, x, addr, c->offset); // (b)local_irq_restore(flags); }
  • 如果對象屬于處理器當前活動的 slab,或處理器所在 NUMA 節點號不為 -1(調試使用的值),將對象放回空閑對象隊列。
  • 否則調用 __slab_free 函數。
  • __slab_free函數:

    static void __slab_free(struct kmem_cache *s, struct page *page, void *x, void *addr, unsigned int offset) {void *prior;void **object = (void *)x;struct kmem_cache_cpu *c;c = get_cpu_slab(s, raw_smp_processor_id());stat(c, FREE_SLOWPATH);slab_lock(page);if (unlikely(SlabDebug(page)))goto debug;checks_ok:prior = object = page->freelist; // (a)page->freelist = object;page->inuse--;if (unlikely(SlabFrozen(page))) {stat(c, FREE_FROZEN);goto out_unlock;}if (unlikely(!page->inuse)) // (b)goto slab_empty;if (unlikely(!prior)) { // (c)add_partial(get_node(s, page_to_nid(page)), page, 1);stat(c, FREE_ADD_PARTIAL);}out_unlock:slab_unlock(page);return;slab_empty:if (prior) { // (d)remove_partial(s, page);stat(c, FREE_REMOVE_PARTIAL);}slab_unlock(page);stat(c, FREE_SLAB);discard_slab(s, page);return;debug:if (!free_debug_processing(s, page, x, addr))goto out_unlock;goto checks_ok; }
  • 執行本函數表明對象所屬 slab 并不是某個活動 slab。保存空閑對象隊列的指針,將對象放回此隊列,最后把已分配對象數目減一。
  • 如果已分配對象數為 0,說明 slab 處于 Empty 狀態,轉到 (d) 步驟。
  • 如果原空閑對象隊列的指針為空,說明 slab 原來的狀態為 Full,那么現在的狀態應該是 Partial,將該 slab 加到所在節點的 Partial 隊列中。
  • 如果 slab 狀態轉為 Empty,且先前位于節點的 Partial 隊列中,則將其剔出并釋放所占內存空間。
  • 總結

    以上是生活随笔為你收集整理的操作系统实验报告11:ucore Lab 2的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。