九. 內核的內存分配

前面已經準備好了內存池,這裏就要正式實現內存的分配了。因爲到目前爲止,還沒有用戶進程,所以這裏只實現內核中的動態內存分配。

內存分配的過程如下:
1. 在虛擬內存池中申請n個虛擬頁
2. 在物理內存池中分配物理頁
3. 在頁表中添加虛擬地址與物理地址的映射關係

接下來就是一步步完成這三步

申請虛擬頁

// 在虛擬內存池中申請pg_cnt個虛擬頁
static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt)
{
    int vaddr_start = 0;
    int bit_idx_start = -1;
    uint32_t cnt = 0;

    if(pf == PF_KERNEL)
    {
        bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
        if(bit_idx_start == -1)
        {
            return NULL;
        }

        while (cnt < pg_cnt)
        {
            bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
        }

        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
    }
    else
    {
        // 用戶內存池
    }

    return (void *)vaddr_start;
}

該步只需要在在需要在虛擬內存池的位圖結構中找到連續n個空閒的空間即可

虛擬內存池的結構如下

struct virtual_addr
{
    struct bitmap vaddr_bitmap;
    uint32_t vaddr_start;
};

kernel_vaddr是一個全局的虛擬內存池變量,它的初始化過程是在上一章完成的。

kernel_vaddr中的vaddr_start就是內核堆空間的起始地址,這個地址被設置爲0xc0100000。因爲在位圖中,1bit實際代表1頁大小的內存,所以這個地址的轉換原理還是很簡單的。申請到的空間的起始虛擬地址 就等於 堆空間的起始地址虛擬頁的偏移量 * 頁大小

分配物理頁

// 在m_pool指向的物理內存池中分配一個物理頁
static void *palloc(struct pool *m_pool)
{
    int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1);
    if(bit_idx == -1)
    {
        return NULL;
    }

    bitmap_set(&m_pool->pool_bitmap, bit_idx, 1);
    uint32_t page_phyaddr = bit_idx * PG_SIZE + m_pool->phy_addr_start;

    return (void*)page_phyaddr;
}

分配物理頁的過程同分配虛擬頁的過程差不多,只是這裏是在物理內存池中進行分配。而且在分配的過程中,並不需要物理頁是連續的,所以在這裏一次只分配一個物理頁。這樣就可以做到虛擬地址連續,而物理地址不需要連續。

添加虛擬地址和物理地址的映射關係

在添加虛擬地址到物理地址映射關係的過程中,肯定要對頁表或者頁目錄進行修改。因爲這個對應關係都是寫在頁表中的,既然此時他們之間沒有映射關係,那麼就需要在頁表中進行添加或者修改,是該虛擬地址能對應到物理地址上。

爲了能夠在頁表中添加或修改數據,就需要訪問到該虛擬地址對應的 頁目錄項地址(PDE)頁表項地址(PTE) 通過PDE和PTE對頁表進行修改

也就是說,找到該虛擬地址對應的PDE和PTE就成了這步的關鍵。

下面說一下處理器如何處理一個32位的虛擬地址,使其對應到物理地址上
1. 首先通過高10位的pde索引,找到頁表的物理地址
2. 其次通過中間10位的pte索引,得到物理頁的物理地址
3. 最後把低12位作爲物理頁的頁內偏移,加上物理頁的物理地址,即爲最終的物理地址

通過這幅圖來說明一下

虛擬地址到物理地址的映射

想要找到一個虛擬地址對應的PDE地址,那麼首先要知道頁目錄表的地址,然後通過該虛擬地址的高10位,得到它相對於頁目錄表的偏移,便可以最終得到PDE的地址

mark

通過上面的圖來說明一下,想要知道0x00c03123的PDE地址,這裏假設頁目錄表的首地址爲0xfffff000,0x00c03123的高十位爲0x3,而頁目錄表中,每一個小方框的大小都爲4字節,所以最終 PDE=0xfffff000 + 0x3 * 4

而當初在規劃頁表的時候,最後一個頁目錄項中存儲的是頁目錄表的物理地址。當高20位全爲1的時候訪問到的就是最後一個頁目錄項,所以頁目錄表的物理地址也就爲0xfffff000,代碼如下

#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)

// 得到虛擬地址對應的pde指針
uint32_t *pde_ptr(uint32_t vaddr)
{
    uint32_t *pde = (uint32_t*)(0xfffff000 + PDE_IDX(vaddr) * 4);

    return pde;
}

得到PTE的地址的過程就稍微複雜一點。

首先得知道頁目錄表中第0個頁目錄項所對應的頁表的物理地址,這裏假設是0xffc00000。

然後得知道它是哪張頁表,也就是說是哪個頁目錄項所對應的頁表,一個頁目錄項對應4KB大小的頁表

最後根據該虛擬地址在頁表中的偏移,也就是虛擬地址的中間10位,得到該PTE

同樣通過0x00c03123來舉例,它的高十位是0x3,中間十位是0x3

PTE = 0xffc00000 + 高十位 * 0x1000 + 中間十位 * 4

下面代碼中的計算方式有點區別但是思路是一致的。

#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)

// 得到虛擬地址對應的pte指針
uint32_t *pte_ptr(uint32_t vaddr)
{
    uint32_t *pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
    // 0xffc00000 + 0x3 >> 10
    return pte;
}

這裏放一張地址的映射關係圖

mark

解決了最複雜的PTE和PDE的地址獲取問題,下面添加虛擬地址到物理地址的映射關係就簡單了

// 在頁表中添加虛擬地址到物理地址的映射關係
static void page_table_add(void *_vaddr, void *_page_phyaddr)
{
    uint32_t vaddr = (uint32_t)_vaddr;
    uint32_t page_phyaddr = (uint32_t)_page_phyaddr;

    uint32_t *pde = pde_ptr((uint32_t)vaddr);
    uint32_t *pte = pte_ptr((uint32_t)vaddr);

    // 在頁目錄內判斷目錄項的P位,若爲1,表示該表已存在
    if(*pde & 0x01)
    {
        // 創建頁表的時候,pte不應該存在
        ASSERT(!(*pte & 0x01));

        if(!(*pte & 0x01))
        {
            *pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1;
        }
    }
    else
    {// 頁目錄項不存在,此時先創建頁目錄項
        uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);

        *pde = pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1;
        memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);

        ASSERT(!(*pte & 0x01));
        *pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1;
    }
}

這裏直接對pde或者pte內部的數據賦值就好了,賦值的數據需要根據pde和pte的結構來。直接上結構圖

PDE與PTE的結構圖

前二十位是物理地址的高20位,後面的則是一些訪問屬性。這裏不再過多解釋

內存分配接口函數

函數已經全部封裝好了,接下來是對外接口的提供了

enum pool_flags
{
    PF_KERNEL=1,
    PF_USER
};


// 分配pg_cnt 個頁空間
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt)
{
    ASSERT(pg_cnt > 0 && pg_cnt < 3840);

    void *vaddr_start = vaddr_get(pf, pg_cnt);
    if(vaddr_start == NULL)
    {
        return NULL;
    }

    uint32_t vaddr = (uint32_t)vaddr_start;
    uint32_t cnt = pg_cnt;

    struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;

    while (cnt-- > 0)
    {
        void *page_phyaddr = palloc(mem_pool);
        if(page_phyaddr == NULL)
        {// 此處分配失敗需要釋放已申請的虛擬頁和物理頁
            return NULL;
        }
        page_table_add((void*)vaddr, page_phyaddr);
        vaddr += PG_SIZE;
    }
    return vaddr_start;
}

// 在內核物理內存池中申請pg_cnt頁內存
void *get_kernel_pages(uint32_t pg_cnt)
{
    void *vaddr = malloc_page(PF_KERNEL, pg_cnt);

    if(vaddr != NULL)
    {
        memset(vaddr,0, pg_cnt * PG_SIZE);
    }
    return vaddr;
}

接下來就在bochs中運行看看申請的空間有沒有被寫入頁表中

這個是目前內核的內存佈局信息,內核物理內存開始地址爲0x200000。並且我們申請的內存開始地址是在0xc010000處,這也是內核堆空間的起始地址

內存佈局信息

在main函數中我申請了三頁的內存,這裏也確實做了三頁的內存映射。

頁表信息

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章