前面已經準備好了內存池,這裏就要正式實現內存的分配了。因爲到目前爲止,還沒有用戶進程,所以這裏只實現內核中的動態內存分配。
內存分配的過程如下:
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的地址
通過上面的圖來說明一下,想要知道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;
}
這裏放一張地址的映射關係圖
解決了最複雜的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的結構來。直接上結構圖
前二十位是物理地址的高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函數中我申請了三頁的內存,這裏也確實做了三頁的內存映射。