九. 内核的内存分配

前面已经准备好了内存池,这里就要正式实现内存的分配了。因为到目前为止,还没有用户进程,所以这里只实现内核中的动态内存分配。

内存分配的过程如下:

  1. 在虚拟内存池中申请n个虚拟页
  2. 在物理内存池中分配物理页
  3. 在页表中添加虚拟地址与物理地址的映射关系

接下来就是一步步完成这三步

申请虚拟页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 在虚拟内存池中申请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个空闲的空间即可

虚拟内存池的结构如下

1
2
3
4
5
struct virtual_addr
{
struct bitmap vaddr_bitmap;
uint32_t vaddr_start;
};

kernel_vaddr是一个全局的虚拟内存池变量,它的初始化过程是在上一章完成的。

kernel_vaddr中的vaddr_start就是内核堆空间的起始地址,这个地址被设置为0xc0100000。因为在位图中,1bit实际代表1页大小的内存,所以这个地址的转换原理还是很简单的。申请到的空间的起始虚拟地址 就等于 堆空间的起始地址虚拟页的偏移量 * 页大小

分配物理页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在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,代码如下

1
2
3
4
5
6
7
8
9
#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

下面代码中的计算方式有点区别但是思路是一致的。

1
2
3
4
5
6
7
8
9
#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的地址获取问题,下面添加虚拟地址到物理地址的映射关系就简单了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 在页表中添加虚拟地址到物理地址的映射关系
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位,后面的则是一些访问属性。这里不再过多解释

内存分配接口函数

函数已经全部封装好了,接下来是对外接口的提供了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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函数中我申请了三页的内存,这里也确实做了三页的内存映射。

页表信息