1. 程式人生 > 實用技巧 >作業系統——記憶體管理(十七)

作業系統——記憶體管理(十七)

作業系統——記憶體管理(十七)

2020-10-0216:06:04 hawk


概述

  這篇文章,我們將會接著前面的步驟,實現簡單的記憶體管理。


字串操作

  實際上這裡和記憶體管理關係並不是很大,但是這些字串操作函式又確實是後面作業系統的基石,因此這裡我們單獨插入這一章,用來實現一下和字串相關的操作函式。這裡主要包括memset、memcpy、memcpy、strcpy、strlen、strcmp、strchr、strrchr、strcat和strchrs等操作。

  這裡稍微簡單介紹一下這些函式的作用,如下表所示

函式原型 函式簡介
void memset(void *dst_, uint8_t value, uint32_t size); 用來將dst_起始的size個位元組設定為value
void memcpy(void *dst_, const void *src_, uint32_t size); 將src_起始地址的size個位元組複製到dst_處
int memcmp(const void *a_, const void *b_, uint32_t size); 連續比較以地址a_和地址b_開頭的size個位元組,若相等則返回0。若a_大於b_。則返回1。否則返回-1
char *strcpy(char* dst_, const char* src_); 將字串從src_複製到dst_
uint32_t strlen(const char *str); 返回字串的長度
int8_t strcmp(const char *a, const char *b); 比較兩個字串,若a_中的字元大於b_中的字元,返回1.若a_等於b_,返回0。否則,返回-1
char *strchr(const char *str, const uint8_t ch); 從左到右查詢字串str中首次出現ch的地址,沒找到的話返回NULL
char *strrchr(const char *str, const uint8_t ch); 從後往前查詢字串str中首次出現字元ch的地址,沒找到的話返回NULL
char *strcat(char *dst_, const char* src_); 將字串src_拼接到dst_後,返回拼接的字串地址
uint32_t strchrs(const char *str, uint8_t ch); 在字串str中查詢字元ch出現的次數

  其相關的原始碼如下所示

#include "string.h"
#include "global.h"
#include "debug.h"


//    用來將dst_起始的size個位元組設定為value
void memset(void *dst_, uint8_t value, uint32_t size) {

    ASSERT(dst_ != NULL);        //這裡強制dst_不為NULL,否則直接中斷

    uint8_t *dst = (uint8_t*)dst_;

    while(size-- > 0) {*(dst++) = value;}
}



//    將src_起始地址的size個位元組複製到dst_處
void memcpy(void *dst_, const void *src_, uint32_t size) {
    ASSERT(dst_ != NULL && src_ != NULL);

    const uint8_t *src = (const uint8_t*)src;
    uint8_t *dst = (uint8_t*)dst_;

    while(size-- > 0) {*(dst++) = *(src++);}
}


//    連續比較以地址a_和地址b_開頭的size個位元組,若相等則返回0。若a_大於b_。則返回1。否則返回-1
int memcmp(const void *a_, const void *b_, uint32_t size) {
    
    ASSERT(a_ != NULL && b_ != NULL);

    const uint8_t *a = (const uint8_t*)a_, *b = (const uint8_t*)b_;

    while(size-- > 0) {
        if(*a != *b) {return *a > *b ? 1 : -1;}
        
        ++a;    ++b;
    }

    return 0;
}


//    將字串從src_複製到dst_
char *strcpy(char *dst_, const char *src_) {
    ASSERT(dst_ != NULL && src_ != NULL);

    char *dst = dst_;
    const char *src = src_;

    while(*src) {*(dst++) = *(src++);}

    return dst_;
}



//    返回字串的長度
uint32_t strlen(const char *str) {
    ASSERT(str != NULL);

    const char *s = str;

    while(*(s++)) {}

    return (s - str - 1);
}



//    比較兩個字串,若a_中的字元大於b_中的字元,返回1.若a_等於b_,返回0。否則,返回-1
int8_t strcmp(const char *a, const char *b) {
    ASSERT(a != NULL && b != NULL);

    while(*a && (*a == *b)) {
        ++a;    ++b;
    }

    return *a > *b ? 1 : (*a < *b);
}

//    從左到右查詢字串str中首次出現ch的地址,沒找到的話返回NULL
char *strchr(const char *str, const uint8_t ch) {
    ASSERT(str != NULL);    

    while(*str && *str != ch) { ++str;}

    return *str ? str : NULL;
}


//    從後往前查詢字串str中首次出現字元ch的地址,沒找到的話返回NULL
char *strrchr(const char *str, const uint8_t ch) {
    ASSERT(str != NULL);

    const char *res = NULL;

    while(*str) {
        if(*str == ch) {res = str;}
    }

    return (char*)res;
}




//    將字串src_拼接到dst_後,返回拼接的字串地址
char *strcat(char *dst_, const char *src_) {
    ASSERT(dst_ != NULL && src_ != NULL);

    char *dst = dst_;

    while(*(dst++)) {;}

    --dst;

    while(*(dst++) = *(src_++)) {;}

    return dst_;
}


//    在字串str中查詢字元ch出現的次數
uint32_t strchrs(const char *str, uint8_t ch) {
    ASSERT(str != NULL);

    uint32_t res = 0;

    while(*str) {
        if(*(str++) == ch) {++res;}
    }

    return res;
}

  都是一些比較簡單的函式,這裡就不過多贅述了。下面開始正式的記憶體管理的實現。


點陣圖bitmap

  首先簡單介紹一下點陣圖的概念——點陣圖,即bitmap,是一種管理資源的方式、手段。

  點陣圖實際上包含兩個概念,位和圖。位是指1bit,即位元組中的位,1位元組中包含8個位。而圖是指map,即一種對映關係。所以,實際上點陣圖就是用位元組中的1位去對映其他單位大小的資源,按位與資源之間是一對一的對應關係。

  那麼換算到我們的記憶體中,實際上就是點陣圖中的每一位代表實際實體記憶體中的4KB,也就是一頁——那麼如果點陣圖中每一位為0,則表示對應的位未分配,可以使用;否則表示已經分配出去浪了,在回收之前不可再分配了。

  對於點陣圖資料的定義,實際上很簡單——就是一個位元組陣列和該陣列長度組成的結構即可,其定義程式碼如下所示

    #define BITMAP_MASK (1)


    /*
        定義點陣圖資料
    */
    typedef struct BITMAP {
        uint32_t bitmap_bytes_len;
        uint8_t *bits;
    } BitMap;

  對於其操作,實際上也很簡單,但對於我們已經夠用了——初始化,判斷、申請和置位,其程式碼如下所示

#include "bitmap.h"
#include "stdint.h"
#include "string.h"
#include "print.h"
#include "debug.h"
#include "interrupt.h"



/*
    將點陣圖bitmap進行初始化
*/
void bitmap_init(BitMap *bitmap) {
    memset(bitmap->bits, 0, bitmap->bitmap_bytes_len);
}



/*
    判斷點陣圖中第bit_idx位是否為1
        若為1,返回true
        否則,返回false
*/
bool bitmap_scan_test(BitMap *bitmap, uint32_t bit_idx) {
    
    uint32_t byte_idx = bit_idx >> 3, bit_ord = bit_idx & 0x7;

    return bitmap->bits[byte_idx] & (BITMAP_MASK << bit_ord);
}


/*
    在點陣圖中連續申請cnt個位,
    如果成功返回起始位的下標
    失敗的話返回  -1
*/
int bitmap_scan(BitMap *bitmap, uint32_t cnt) {

    //    這裡直接使用暴力法進行查詢
    
    uint32_t byte_idx = 0, bit_start, bit_idx;



    while(byte_idx < bitmap->bitmap_bytes_len) {
        
        //首先獲取第一個0位
        bit_start = 0;
        while(bit_start < 8 && (bitmap->bits[byte_idx] & (BITMAP_MASK << bit_start))) {++bit_start;}


        if(bit_start >= 8) {break;}


        //此時找到了第1個0位,下面開始查詢是否存在連續cnt個0位
        bit_start += byte_idx * 8;
        bit_idx = bit_start + 1;
        
        //為了避免遍歷超出點陣圖的範圍,需要給出最大的界限, bit_limit = MIN((bitmap->bitmap_bytes_len - byte_idx) * 8, cnt + bit_start)
        uint32_t bit_limit = (bitmap->bitmap_bytes_len * 8 < (cnt + bit_start) ? bitmap->bitmap_bytes_len * 8 : (cnt + bit_start)) - 1;

        while(bit_idx < bit_limit && !bitmap_scan_test(bitmap, bit_idx)) {++bit_idx;}

        if(bit_idx - bit_start == cnt) {return (int)bit_start;}
        
        byte_idx = (bit_idx / 8) + 1;
    }

    return -1;
}




/*
    將點陣圖bitmap中的bit_idx設定為value
*/
void bitmap_set(BitMap *bitmap, uint32_t bit_idx, uint8_t value) {
    ASSERT(value == 1 || value == 0);

    uint32_t byte_idx = bit_idx / 8, bit_ord = bit_idx & 0x7;

    if(value) {
        bitmap->bits[byte_idx] |= BITMAP_MASK << bit_ord;
    }else {
        bitmap->bits[byte_idx] &= ~(BITMAP_MASK << bit_ord);
    }
}

  整體上邏輯還是比較簡單的,就是各種位操作而已。


記憶體管理

  實際上,使用者程式所佔用的記憶體空間是由作業系統分配的,而記憶體具體是如何分配並且給使用者程序分配多少位元組,則是我們要實現的記憶體管理。下面我們首先簡單的分析一下整體思路。

記憶體池規劃

  我們知道,核心和使用者程序實際上分別執行在自己的地址空間中。在真實模式下,程式的線性地址就是實際上的實體地址。而在保護模式下,由於開了分頁機制,因此實際上線性地址變成了虛擬地址,最後CPU通過頁表將虛擬地址轉換到實體地址,並進行實際上的訪問。而對於作業系統來說,管理虛擬地址和實體地址的記憶體池,則是其主要職責之一。

  我們首先分析一下如何規劃實體記憶體池。對於實體記憶體,可行的方案就是將實體記憶體劃分為兩部分,一部分只用來執行核心,另一部分只用來執行使用者程序。每次從記憶體池申請記憶體的話,按照記憶體的單位大小——頁,即4KB的倍數進行進行分配和回收。為了方便起見,我們就直接將這兩部分對半分,也就是一半實體記憶體用於核心記憶體池,一半實體記憶體用於使用者記憶體池,如下圖所示

  下面我們討論另一個,即虛擬記憶體的地址池。對於我們採取的分頁機制來說,一方面,在我們的32位環境下,虛擬地址空間是4GB;零一方面,每個任務都有自己的4GB虛擬地址空間,這樣子避免了程式間記憶體地址的衝突。當然,程式(程序、核心執行緒)等在執行的過程中,也會有申請記憶體的需求,這種動態申請記憶體一般是在堆中申請記憶體,當作業系統接受申請後,為程序或者核心,在堆中選擇一個空閒的虛擬地址,然後再找個空閒的實體地址進行對映即可,之後把虛擬地址返回給程式即可。

  對於核心的記憶體申請,核心也通過核心管理系統申請記憶體,然後核心從自己的虛擬地址池中分配虛擬地址,然後再從核心專用的實體記憶體池分配實體記憶體,最後核心自己的頁表將這兩個地址建立對映關係,即可完成核心的記憶體申請。

  對於使用者程序來說,它同樣向核心管理系統,即作業系統申請記憶體。然後作業系統首先從使用者程序自己的虛擬地址池中分配空閒虛擬地址,然後再從使用者實體記憶體池(所有使用者程序共享)中分配空閒的實體記憶體,最後在該使用者程序自己的頁表中,將這兩個地址建立好對映關係即可。

  當然,為了方便管理,虛擬地址池中的地址單位同樣是4KB,從而可以方便的進行對映。實際上計算機中的虛擬地址池與實體地址池的示意圖如下所示

實體記憶體池構建

  下面分析了一下虛擬地址和實體地址的記憶體池的整體規劃,下面我們介紹一下實體記憶體的記憶體池。

  根據前面的示意圖,我們容易看出來——實際上實體記憶體是全域性的,也就是整個計算機系統中只保留一份實體記憶體池的管理結構,這也是很自然的(不類似於每一個程序一個虛擬地址空間,整個作業系統、程式等公用一個實體記憶體空間,也就是真實的實體記憶體,自然管理的資料結構只可能會有一部分,並且所有程序共享)。

  當然,前面也分析過了——整個實體記憶體池是需要被均分分為兩部分,核心記憶體池,用來核心申請實體記憶體;使用者記憶體池,使用者程序申請實體記憶體的。這裡還是說一下我一開始的疑問——前面的部落格一直再講,核心佔用高1GB的記憶體空間,使用者佔用剩下的3GB記憶體空間,到這裡怎麼又被平分了?這實際上是由於搞混了概念。首先對於記憶體池的管理,我們是分成了虛擬記憶體池的管理和實體記憶體池的管理的。對於虛擬記憶體池的管理,是每一個程序自己管理自己的虛擬記憶體池,其大小就是虛擬記憶體空間的大小,這裡就是4GB,其中佈局往往是高1GB的記憶體空間屬於核心空間,剩下3GB的空間是真正的使用者程序使用的,因此這僅僅是虛擬記憶體。而對於實體記憶體池的管理,其管理的是實際的實體記憶體,在我們的虛擬機器中是被設定為了32MB的,這裡的佈局往往是一半是核心記憶體池,另一半是使用者程序記憶體池。因此,一定需要分清虛擬記憶體池中的使用者空間、核心空間,和實體記憶體池中的核心記憶體池、使用者記憶體池這些概念。

  下面我們接著將實體記憶體池均分為核心記憶體池和使用者記憶體池部分講解。首先,這裡的實體記憶體池是用來管理初始化後空閒的記憶體的,即初始化後已經被使用的,是已經被賦予了重要任務了,所以基本不可能在空閒了,所以不需要管理。這裡“初始化後已經被使用的"記憶體指的是作業系統相關的資料結構,在這裡就是低端1MB資料(包含核心、核心資料、GDT、IDT等)和頁目錄表、頁表等,值得慶幸的是,我們前面實現這些結構的時候,將頁目錄表、頁表緊鄰且緊接著低端1MB資料。因此,實際上我們需要管理的實體記憶體池的起始地址就是低端1MB資料 + 頁目錄表 + 頁表大小,也就是0x100000 + (1 + 1 + 254) * 4K這個地址,而其管理的實體記憶體池的大小就是總的實體記憶體大小(這裡是32MB)-使用的實體記憶體大小,也就是前面的值。

  最後則是使用者記憶體的地址。首先,我們是通過作業系統對實體記憶體進行管理,也就是這些管理實體記憶體的核心記憶體池和使用者記憶體池結構——點陣圖,都儲存在核心空間中。而另一方面,實際上在我們對核心記憶體池和使用者記憶體池的初始化時,我們已經指定好了已使用的記憶體,也就是我們這些用於管理實體記憶體的這些資料結構只能放置在前面已經指明的已使用記憶體中。而頁目錄表和頁表都有其他用處,因此我們只能將其放置在低端1MB的記憶體空間中。而我們的虛擬機器實體記憶體僅僅32MB,因此我們僅需要32MB / (4K * 8) = 1KB大小的點陣圖,但是考慮到棧空間,我們將實體記憶體池相關的點陣圖起始地址放置在原始棧頂下5個頁,也就是點陣圖的起始地址為0xc009f000 - 4K * 5= 0xc009a000即可。

  這樣,我們就基本描述完了實體記憶體池的資料,然後將其在等分為核心記憶體池和使用者記憶體池,即可基本完成實體記憶體池的構建,其相關程式碼如下所示

#include "memory.h"
#include "stdint.h"
#include "print.h"
#include "debug.h"


#define PG_SIZE    (4096)                //即每一個頁的大小為4KB,即4096位元組



/*
    實際上前面已經分析過了,核心是從虛擬地址3G開始,即0xc0000000
    但是實際上最開始的分頁機制中,我們將低端1MB記憶體對映給了核心空間了,也就是0xc0000000 - 0xc00fffff已經對映到了實體地址0 - 0xfffff中

    因此核心申請的堆的起始地址我們就設定為0xc0100000即可
*/

#define K_HEAP_START (0xc0100000)


Virtual_Pool kernel_vir_pool;                    //核心的虛擬記憶體池
Physical_Pool kernel_phy_pool, user_phy_pool;            //實體記憶體池中的核心記憶體池和使用者記憶體池


/*
    初始化實體記憶體池,也就是初始化核心記憶體池和使用者記憶體池
    輸入引數:    all_mem,此引數表示整個實體記憶體容量
*/
static void mem_pool_init(uint32_t all_mem) {
    
    put_str("[*] mem_pool_init start\n");


    
    uint32_t page_table_size = PG_SIZE * 256;
    /*
        page_table_size用來記錄頁目錄表和頁表佔用的總大小
        也就是1頁目錄表(頁目錄表) + 第0個頁目錄項/第768頁目錄項(指向第1個頁表,頁表) + 第769 ~ 1022個頁目錄項(指向第2個頁表- 第254頁表,共254個頁表)
        即256個頁
    */

    uint32_t used_mem = page_table_size + 0x100000;            //即已經被使用的實體記憶體大小,這裡值得說明的是,這些被使用的實體記憶體是緊鄰的

    uint32_t free_mem = all_mem - used_mem;                //剩餘需要通過實體記憶體池進行管理的記憶體大小
    
    uint16_t all_free_pages = free_mem / PG_SIZE;            //最終實體記憶體池通過bitmap,即點陣圖進行管理,獲取代管理的實體記憶體的點陣圖位數


    /*
        實體記憶體池會被均分為核心記憶體池和使用者記憶體池
    */
    uint16_t kernel_free_pages = all_free_pages / 2, user_free_pages = all_free_pages - kernel_free_pages;


    //    這裡為了管理的方便,餘數不做處理,因此實際上位圖表示的可用記憶體可能會少於實際的可用記憶體
    uint32_t kbm_length = kernel_free_pages / 8, ubm_length = user_free_pages / 8;

    
    /*
        前面已經分析過了,已使用的實體記憶體都是從0開始的,緊鄰的。
        因此待管理的實體記憶體池的起始地址數值上就等於已使用的實體記憶體的大小
        這裡低端記憶體分配各核心記憶體池,剩下的待管理的實體記憶體就是使用者記憶體池
    */
    uint32_t kp_start = used_mem, up_start = kp_start + kernel_free_pages * PG_SIZE;

    
    /*
        下面初始化實體記憶體中的用來管理核心記憶體池和使用者記憶體池的資料結構即可
    */
    /****************************************************************************************************************************************************/
    kernel_phy_pool.phy_addr_start = kp_start;    kernel_phy_pool.pool_size = kernel_free_pages * PG_SIZE;    kernel_phy_pool.phy_bitmap.bitmap_bytes_len = kbm_length;
    user_phy_pool.phy_addr_start = up_start;    user_phy_pool.pool_size = user_free_pages * PG_SIZE;    user_phy_pool.phy_bitmap.bitmap_bytes_len = ubm_length;


    /*        實體記憶體的核心記憶體池和使用者記憶體池的管理資料結構的存放位置
            畢竟實體記憶體的管理資料結構仍然是需要存放到實體記憶體中的。實際上,一定位於低端的1MB物理空間——因為我們前面已經分析完了當前的可用實體記憶體,
            也就是我們這些實體記憶體的管理資料能存放的位置只能是前面討論過的已使用的實體記憶體部分。而頁目錄表和頁表自然不能使用,則只能放置在低端的1MB物理空間中
            在虛擬地址中,也就是0xc0000000 - 0xc00fffff

            在虛擬地址中,核心的棧被設定為0xc009f000,我們就將管理實體記憶體的點陣圖放置在這個地址附近

            而我們的虛擬機器實體記憶體僅僅32MB,因此我們僅需要32MB / (4K * 8) = 1KB大小的點陣圖,因此考慮到棧空間,我們將實體記憶體池相關的點陣圖起始地址放置在原始棧頂下5個頁
            也就是
                0xc009f000 - 4K * 5 = 0xc009a000
            這個大小已經足夠放置32MB實體記憶體的點陣圖

            實際上後面的虛擬記憶體的點陣圖同樣會放置在這裡
    
    */
    #define BITMAP_BASE    (0xc009a000)

    kernel_phy_pool.phy_bitmap.bits = (uint8_t*)BITMAP_BASE;    user_phy_pool.phy_bitmap.bits = (uint8_t*)(BITMAP_BASE + kbm_length);
    bitmap_init(&kernel_phy_pool.phy_bitmap);            bitmap_init(&user_phy_pool.phy_bitmap);


    /*
            這裡順便設定一下核心程序的虛擬記憶體池
    */
    kernel_vir_pool.vir_addr_start = K_HEAP_START;    kernel_vir_pool.vir_bitmap.bitmap_bytes_len = kbm_length;
    kernel_vir_pool.vir_bitmap.bits = (uint8_t*)(BITMAP_BASE + kbm_length + ubm_length);
    bitmap_init(&kernel_vir_pool.vir_bitmap);


    /*
        輸出實體記憶體池的相關資訊
    */
    put_str("[*]  kernel_phy_pool_bitmap_start: 0x");    put_uHex(kernel_phy_pool.phy_bitmap.bits);    put_str(";kernel_phy_pool_phy_addr_start: 0x");    put_uHex(kernel_phy_pool.phy_addr_start); put_char('\n');
    put_str("[*]  user_phy_pool_bitmap_start: 0x");    put_uHex(user_phy_pool.phy_bitmap.bits);    put_str(";user_phy_pool_phy_addr_start: 0x");    put_uHex(user_phy_pool.phy_addr_start); put_char('\n');
    
            
    /*
        輸出核心虛擬記憶體池的相關資訊
    */    
    put_str("[*]  kernel_vir_pool_bitmap_start: 0x");    put_uHex(kernel_vir_pool.vir_bitmap.bits); put_char('\n');



    put_str("[*] mem_pool_init done\n");
}




/*
    記憶體管理的初始化入口
*/
void mem_init() {
    put_str("[*] mem_init start\n");
    

    //    在loader程式中,我們在GDT後面儲存了實體記憶體總容量,其地址即為loader載入地址 + 偏移(指令對齊)
    //    即0x700 + (60 + 4) * 8 + 4 = 0x900,則對應的虛擬地址為0xc0000900
    uint32_t mem_bytes_total = *(uint32_t*)0xc0000904;

    mem_pool_init(mem_bytes_total);


    put_str("[*] mem_init done\n");
}

  註釋實際上已經非常詳細了,這裡我最後說一點——一定分清楚實體記憶體和虛擬記憶體,這裡我們管理的是實體記憶體池,也就是實際的虛擬機器的32MB的記憶體,這個是全域性唯一的。這裡說一下,實際上這個0xc0000904地址是在loader中定義的資料,其實體地址是0x904,其通過BIOS提供的中斷功能獲取當前的實體記憶體容量。

虛擬記憶體池構建

  實際上前面也已經分析過了,虛擬記憶體是用來管理每個程序的記憶體空間的,也就是每一個程序都會有自己的虛擬記憶體池,其中其高1GB是分配給核心空間的,低3GB才是使用者程序自己申請的。自然的,由於每個程序都有自己的虛擬記憶體池,也就是核心程序也會有自己的虛擬記憶體池。由於這裡我們作業系統中僅僅包含核心程序一個,而不包含其他的程序,因此這裡我們只需要實現核心程序的虛擬記憶體池即可。

  實際上雖然虛擬地址空間確保了不同程序間相同地址不會衝突,但是無法保證相同程式的相同地址不衝突,因此我們自然需要使用資料結構——這裡就是虛擬記憶體池,對虛擬地址空間進行管理,從而保證分配到的虛擬記憶體是唯一的。而由於核心也是程式,自然其在執行的過程中也可能需要申請額外的記憶體,所以我們同樣通過虛擬記憶體池來管理核心虛擬地址空間中的記憶體分配情況。

  對於核心來說,實際上其申請的記憶體空間就是堆空間。這裡實際上我們需要管理的就是這個堆空間,首先是其起始虛擬地址。我們知道,在前面開啟分頁機制的時候,我們將0xc0000000~0xc00fffff已經對映到實體地址的低端1MB記憶體中,也就是實際上虛擬地址的0xc0000000~0xc00fffff已經被使用了。那麼為了讓虛擬地址可以連續使用,我們就不妨設定堆的起始地址就為0xc0010000,之後申請的虛擬地址空間都從這裡開始申請,即可相對的保證連續地址的連續。

  而我們考慮到,前面實體記憶體池的資料管理結構中,我們制定了記憶體池的大小。按理說,對於虛擬記憶體池的資料管理結構,應該和實體記憶體池的是類似的,但實際上虛擬記憶體池並沒有指定記憶體池的大小——實際上之所以指定實體記憶體池的大小,是因為其大小是十分有限的,在我們的虛擬機器中,其為32MB,因此需要小心,避免申請超過總大小。而對於虛擬記憶體池來說,其和地址線寬度是一樣的,因此不需要再指定記憶體池的大小了,其基本上可以看作是無限的。雖然說是無限的,但實際上為了方便,我們將核心的虛擬記憶體池大小設定為和實體記憶體池的核心記憶體池一樣的大小,這樣極其方便管理——一一對應即可,畢竟只有一個核心程序。

  由於我們這裡僅僅只需要實現核心的虛擬記憶體池,其程式碼也十分簡單,如下所示

    /*
            這裡順便設定一下核心程序的虛擬記憶體池
    */
    kernel_vir_pool.vir_addr_start = K_HEAP_START;    kernel_vir_pool.vir_bitmap.bitmap_bytes_len = kbm_length;
    kernel_vir_pool.vir_bitmap.bits = (uint8_t*)(BITMAP_BASE + kbm_length + ubm_length);

  可以看到,確實很簡單。


核心程序分配記憶體頁

  既然我們已經有了實體記憶體池和核心的虛擬記憶體池,自然的,我們就可以給核心進行記憶體的分配了。這裡我們要實現的,是一個基礎的“整頁分配”,也就是我們支援核心申請一次分配n個頁的記憶體,即申請n * 4096位元組。

  下面我們簡單的介紹一下申請記憶體,也就是分配記憶體頁的大體思路,方便我們有一個更清晰的瞭解,從而更方便的實現。實際上申請記憶體頁,就是要將虛擬記憶體池中的空閒記憶體和實體記憶體池對應的空閒記憶體建立對映關係,因此實際上我們需要做三件事

  1.  在虛擬記憶體池中申請足夠大小的虛擬記憶體

  2.  在實體記憶體池中申請足夠大小的物理頁

  3.  將上面兩步得到的虛擬地址和實體地址在頁表中完成對映。

  這樣,實際上就相當於我們完成了分配記憶體頁的過程,下面我們給出對應的原始碼

/*
    在flag表示的虛擬記憶體池中申請pg_cnt個虛擬頁
    成功則返回虛擬頁的起始地址,失敗則返回NULL
*/
static void* vir_addr_get(enum pool_flags flag, uint32_t pg_cnt) {

    //    根據傳入的flag獲取核心記憶體池或者使用者記憶體池
    Virtual_Pool pool = (flag == PF_KERNEL) ? kernel_vir_pool : kernel_vir_pool;        //這裡還沒有實現使用者池,同樣設定為核心記憶體池

    int idx = bitmap_scan(&pool.vir_bitmap, pg_cnt);

    //    如果返回小於0,則表明此時對應的記憶體池中的空閒記憶體不足夠,則返回NULL
    if(idx < 0) {return NULL;}

    //    將對應的點陣圖的位進行標記
    for(int i = 0; i < pg_cnt; ++i) {bitmap_set(&pool.vir_bitmap, i + idx, 1);}

    return (void*)(pool.vir_addr_start + idx * PG_SIZE);
}




/*
    獲取虛擬地址對應的頁表項的指標,也就是對應的頁表項的虛擬地址
*/
uint32_t* pte_ptr(uint32_t vir_addr) {
    /*
        首先高10位應該是1023,從而訪問頁目錄表的最後一項,仍然是頁目錄表(當做頁表)
        其次是中間10位即為虛擬地址的頁目錄項索引,從而訪問上述頁表(仍然是頁目錄表)的索引項,即訪問到虛擬地址對應的頁表
        最後12位則是對應的記憶體的偏移,由於記憶體指向的是頁表,因此偏移即為虛擬地址在頁表中的索引項 * 4,這從而訪問的是虛擬地址對應的頁表項
    */
    return (uint32_t*)(0xffc00000 + ((vir_addr & 0xffc00000) >> 10) + ((vir_addr & 0x003ff000) >> 10));
}




/*
    獲取虛擬地址對應的頁目錄項的指標,也就是對應的頁目錄項的虛擬地址
*/
uint32_t* pde_ptr(uint32_t vir_addr) {
    /*
        首先高10位應該是1023,從而訪問頁目錄表的最後一項,仍然是頁目錄表(當做頁表)
        其次是中間10位也應該是1023,從而訪問上述頁表(仍然是頁目錄表)的最後一項,仍然是頁目錄表
        最後12位則是對應的記憶體的偏移,由於記憶體指向的是頁目錄表,因此偏移即為虛擬地址在頁目錄表的索引項 * 4,這從而訪問的是虛擬地址對應的頁目錄項
    */
    return (uint32_t*)(0xfffff000 + ((vir_addr & 0xffc00000) >> 20));
}






/*
    在pool指向的實體記憶體池中分配1個物理頁
    如果成功,則返回物理頁的實體地址
    如果失敗,則返回NULL即可
*/
static void* palloc(Physical_Pool *pool) {
    int idx = bitmap_scan(&pool->phy_bitmap, 1);

    //    如果返回索引小於0,則表明記憶體池不足,分配失敗
    if(idx < 0) {return NULL;}

    //    將對應的點陣圖的位進行標記
    bitmap_set(&pool->phy_bitmap, idx, 1);

    return (void*)(pool->phy_addr_start + PG_SIZE * idx);
}





/*
    下面在對應的頁表中完成虛擬地址_vir_addr和實體地址_phy_addr的對映關係
*/
static void page_table_add(void* _vir_addr, void* _phy_addr) {
    uint32_t *pte = pte_ptr((uint32_t)_vir_addr), *pde = pde_ptr((uint32_t)_vir_addr);
    uint32_t phy_addr = _phy_addr;

    /******************************************************************
        因為pde對應的頁目錄項可能為空,從而導致訪問*pte會引發page_fault。
        所以確保pde建立完後,在執行相關的*pte操作
    ********************************************************************/
    
    //    首先判斷pde是否已經被建立

    if(!(*pde & PG_P_1)) {//頁目錄項不存在,首先初始化頁目錄項,然後在初始化對應的頁表項
        uint32_t pte_phy_addr = (uint32_t)palloc(&kernel_phy_pool);
        *pde = (pte_phy_addr | (PG_US_U << 2) | (PG_RW_W << 1) | PG_P_1);
        
        /*
            下面我們將分配的pde_phy_addr進行清0,避免裡面的原始資料當做頁表項
            從而避免造成不必要的麻煩

            這裡我們使用memset進行清空,那麼我們需要一個通過兩次頁錶轉換的記憶體指向pde_phy_addr即可
            
            實際上我們考慮一下pte變數,其指向當前虛擬地址的頁表項。如果我們將其低12位置為0,則指向當前頁表的起始項
            從而我們可以通過遍歷低12位完成虛擬地址對應的頁表的清空
        */
        memset((void*)((uint32_t)pte & 0xfffff000), 0, PG_SIZE);
    }


    //    寫入虛擬地址對應的pte的對映的實體地址
    if(*pde & PG_P_1) {
        
        //如果pde已經被建立,則可以直接訪問*pte,由於_vir_addr沒有被分配,因此再確認一下
        ASSERT(!(*pte && PG_P_1));

        //    如果確實不存在,向頁表中寫入相關資訊即可
        if(!(*pte && PG_P_1)) {
            //    當前分配的頁的屬性為存在與記憶體、可讀寫、使用者級別
            *pte = (phy_addr | (PG_US_U << 2) | (PG_RW_W << 1) | PG_P_1);
        }else {    //基本不會執行到這裡,因為前面已經有ASSERT判斷了
            PANIC("pte repeat");
            //    覆蓋該頁表項
            *pte = (phy_addr | (PG_US_U << 2) | (PG_RW_W << 1) | PG_P_1);
        }
    }
}    



/*
    從flag對應的虛擬地址池和flag對應的實體記憶體池中分配pg_cnt個頁空間,並完成虛擬記憶體池、實體記憶體池以及頁表的對映
*/
void* malloc_page(enum pool_flags flag, uint32_t pg_cnt) {


    //    確保申請的虛擬記憶體頁個數是有效的,因為實際上我們一次性最多可以申請的記憶體頁也就是實體記憶體的容量
    //    即15MB / (4K) = 3840
    ASSERT(pg_cnt > 0 && pg_cnt < 3840);


    void* vir_addr_start = vir_addr_get(flag, pg_cnt), *phy_addr;

    //    如果此時返回的虛擬地址的起始地址為空,則表明虛擬記憶體池不足以分配這些記憶體頁
    if(!vir_addr_start) {return NULL;}

    uint32_t vir_addr = (uint32_t)vir_addr_start;

    Physical_Pool pool = (flag == PF_KERNEL) ? kernel_phy_pool : user_phy_pool;

    //    由於實體記憶體池往往很小,無法分配到連續的頁,因此需要一頁一頁進行分配
    while(pg_cnt-- > 0) {

        phy_addr = palloc(&pool);
    
        if(!phy_addr) {    //說明即使一頁一頁分配,物理頁仍然不足夠進行分配,則需要將佔用的虛擬記憶體池和實體記憶體池中的記憶體全部釋放掉

            //    這裡等實現記憶體回收後在實現

            return NULL;
        }

        page_table_add((void*)vir_addr, phy_addr);

        //    對映下一個虛擬頁和其對應的物理頁
        vir_addr += PG_SIZE;    
    }

    return vir_addr_start;
}




/*
    從核心空間申請pg_cnt個頁空間
    成功返回對應的起始虛擬地址,失敗則返回NULL即可
*/
void *malloc_kernel_page(uint32_t pg_cnt) {
    void *vir_addr = malloc_page(PF_KERNEL, pg_cnt);

    //    如果成功分配到了pg_cnt個頁空間,則將該空間進行清0,避免重要的舊資料洩露
    if(!vir_addr) {
        memset(vir_addr, 0, PG_SIZE * pg_cnt);
    }

    return vir_addr;
}

  註釋已經非常詳盡了。這裡在具體說明幾點

  1.  對於程序(核心程序/使用者程序),其申請記憶體的實質,是在該程序的虛擬記憶體池中申請對應的資源(點陣圖上進行查詢和標註),然後再全域性的實體記憶體池中申請物理頁(根據程序類別在核心記憶體池/使用者記憶體池中進行申請),最後在將該程序中申請的虛擬地址對應的頁表填充上申請的物理頁即可。目前由於僅僅實現了核心程序,因此這裡的頁表就是核心程序的頁表,也就是我們前面實現的全域性頁表,則我們申請物理頁,自然在核心記憶體池中進行申請即可。

  2.  由於虛擬記憶體池是足夠大的,每一個程序的虛擬記憶體池理論上都是4GB,因此我們可以一次申請大量的連續虛擬頁,基本不會出現問題。實體記憶體池一般沒有那麼大,至少在我們實驗的虛擬機器中,僅僅設定了32MB,因此我們不一定可以申請到大量連續的物理頁,因此我們一般一次申請一個單位,也就是一個物理頁,然後多次申請。因此我們一般申請虛擬記憶體和實體記憶體的方式是不同的,向虛擬記憶體池中申請,我們直接一次性申請連續的虛擬記憶體頁即可;而對於實體記憶體頁,我們多次申請不一定連續的物理頁,每一次僅僅申請一個物理頁。當然,通過頁表對映,我們可以通過連續的虛擬地址訪問不連續的物理頁,從而使不連續的物理頁邏輯上連續起來。

  3.  注意到,雖然我們說虛擬記憶體池是足夠大的。但是這裡我們實現的記憶體管理比較簡單,因此對於核心程序來說,其虛擬記憶體池大小就和其物理的核心記憶體池大小相同即可。這裡特別說明一下。

  4.  最後一點,就是在虛擬地址上進行直接讀寫頁表、頁目錄表。實際上前面分頁機制部分已經講過了,這裡只說明以下原因——實際上可以簡單理解為開啟分頁機制後,CPU的所有記憶體訪問,其地址都會通過頁部件,而頁部件會進行兩次頁目錄表、頁表對映,頁部件中所有的地址訪問都是真實的。那麼問題就在於兩次對映,即使我們擁有真實的頁表實體地址,但是隻要是從CPU中傳遞的,統統認為是虛擬地址(即使其值為真實的實體地址),則也要經過兩次對映,則最後訪問的大概率不是前面傳送的實體地址。這裡具體的細節不再贅述,直接給出更為使用的結論。

  如果我們訪問記憶體地址為虛擬地址(0xfffff000 + offset),則我們實際上讀寫的是頁目錄表的offset偏移的資料,也就是通過*(0xfffff000 + offset)修改的是頁目錄表的offset偏移處的資料

  如果我們訪問記憶體地址為虛擬地址(0xffc00000 + (idx1 << 12) + offset),則我們實際上讀寫的是頁目錄表的第idx1頁目錄項指向的頁表的offset偏移的資料。也就是通過*(0xffc00000 + (idx1 << 12) + offset),我們可以修改頁目錄表的第idx1頁表的offset偏移的資料。按照這個結論,則我們完全可以將核心程序的虛擬地址空間的指定虛擬地址的頁表項、頁目錄表項的值進行修改,從而指定其項的真實實體地址,完成實體地址和虛擬地址的對映。

  這裡給出倉庫連結。下面給出我們的測試程式碼,如下所示

#include "print.h"
#include "init.h"
#include "memory.h"

int main(void) {

    /*
        初始化所有的模組
    */    
    init_all();


    //    嘗試申請頁
    void *vir_addr = malloc_kernel_page(2);
    put_str("[*]  malloc_kernel_page's vir_addr_start1: 0x");
    put_uHex(vir_addr);

    put_char('\n');

    vir_addr = malloc_kernel_page(3);
    put_str("[*]  malloc_kernel_page's vir_addr_start2: 0x");
    put_uHex(vir_addr);
    while(1);

    return 0;
}

  則我們通過make命令進行編譯,最後在虛擬機器上進行執行,結果如圖所


確實從核心虛擬地址空間的虛擬記憶體池的起始地址處分類了兩個虛擬頁,我們首先觀察一下此時的核心空間的頁表和頁目錄表,如下所示

  可以看到,和剛剛開啟分頁機制相比,多了虛擬地址0xc0100000 - 0xc0104fff對映到0x200000 - 0x204fff的實體地址的對映,也就是核心的虛擬記憶體池的5個虛擬頁對映到了核心記憶體池的5個物理頁,和預期的是一樣的。下面則是再檢視一下核心的虛擬地址空間的點陣圖情況,其地址位於0xc009a3c0(前面的輸出),如下所示

  這表明確實bitmap中連續的5個位被置為1,下面則是核心記憶體池,其地址位於0xc009a000(前面的輸出)

  其點陣圖也被成功置位。表明最後成功了。