九. 內核的內存分配
前面已經準備好了內存池,這裡就要正式實現內存的分配了。因為到目前為止,還沒有用戶進程,所以這裡只實現內核中的動態內存分配。
內存分配的過程如下:
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函數中我申請了三頁的內存,這裡也確實做了三頁的內存映射。
推薦閱讀: