九. 內核的內存分配

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

內存分配的過程如下:

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函數中我申請了三頁的內存,這裡也確實做了三頁的內存映射。

推薦閱讀:

TAG:Linux | Linux內核 | 內存管理 |