前面已经准备好了内存池,这里就要正式实现内存的分配了。因为到目前为止,还没有用户进程,所以这里只实现内核中的动态内存分配。
内存分配的过程如下:
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函数中我申请了三页的内存,这里也确实做了三页的内存映射。