前面已经准备好了内存池,这里就要正式实现内存的分配了。因为到目前为止,还没有用户进程,所以这里只实现内核中的动态内存分配。
内存分配的过程如下:
- 在虚拟内存池中申请n个虚拟页
- 在物理内存池中分配物理页
- 在页表中添加虚拟地址与物理地址的映射关系
接下来就是一步步完成这三步
申请虚拟页
1 | // 在虚拟内存池中申请pg_cnt个虚拟页 |
该步只需要在在需要在虚拟内存池的位图结构中找到连续n个空闲的空间即可
虚拟内存池的结构如下
1 | struct virtual_addr |
kernel_vaddr是一个全局的虚拟内存池变量,它的初始化过程是在上一章完成的。
kernel_vaddr中的vaddr_start就是内核堆空间的起始地址,这个地址被设置为0xc0100000。因为在位图中,1bit实际代表1页大小的内存,所以这个地址的转换原理还是很简单的。申请到的空间的起始虚拟地址 就等于 堆空间的起始地址 加 虚拟页的偏移量 * 页大小
分配物理页
1 | // 在m_pool指向的物理内存池中分配一个物理页 |
分配物理页的过程同分配虚拟页的过程差不多,只是这里是在物理内存池中进行分配。而且在分配的过程中,并不需要物理页是连续的,所以在这里一次只分配一个物理页。这样就可以做到虚拟地址连续,而物理地址不需要连续。
添加虚拟地址和物理地址的映射关系
在添加虚拟地址到物理地址映射关系的过程中,肯定要对页表或者页目录进行修改。因为这个对应关系都是写在页表中的,既然此时他们之间没有映射关系,那么就需要在页表中进行添加或者修改,是该虚拟地址能对应到物理地址上。
为了能够在页表中添加或修改数据,就需要访问到该虚拟地址对应的 页目录项地址(PDE) 和 页表项地址(PTE) 通过PDE和PTE对页表进行修改
也就是说,找到该虚拟地址对应的PDE和PTE就成了这步的关键。
下面说一下处理器如何处理一个32位的虚拟地址,使其对应到物理地址上
- 首先通过高10位的pde索引,找到页表的物理地址
- 其次通过中间10位的pte索引,得到物理页的物理地址
- 最后把低12位作为物理页的页内偏移,加上物理页的物理地址,即为最终的物理地址
通过这幅图来说明一下
想要找到一个虚拟地址对应的PDE地址,那么首先要知道页目录表的地址,然后通过该虚拟地址的高10位,得到它相对于页目录表的偏移,便可以最终得到PDE的地址
通过上面的图来说明一下,想要知道0x00c03123的PDE地址,这里假设页目录表的首地址为0xfffff000,0x00c03123的高十位为0x3,而页目录表中,每一个小方框的大小都为4字节,所以最终 PDE=0xfffff000 + 0x3 * 4
而当初在规划页表的时候,最后一个页目录项中存储的是页目录表的物理地址。当高20位全为1的时候访问到的就是最后一个页目录项,所以页目录表的物理地址也就为0xfffff000,代码如下
1 |
|
得到PTE的地址的过程就稍微复杂一点。
首先得知道页目录表中第0个页目录项所对应的页表的物理地址,这里假设是0xffc00000。
然后得知道它是哪张页表,也就是说是哪个页目录项所对应的页表,一个页目录项对应4KB大小的页表
最后根据该虚拟地址在页表中的偏移,也就是虚拟地址的中间10位,得到该PTE
同样通过0x00c03123来举例,它的高十位是0x3,中间十位是0x3
PTE = 0xffc00000 + 高十位 * 0x1000 + 中间十位 * 4
下面代码中的计算方式有点区别但是思路是一致的。
1 |
|
这里放一张地址的映射关系图
解决了最复杂的PTE和PDE的地址获取问题,下面添加虚拟地址到物理地址的映射关系就简单了
1 | // 在页表中添加虚拟地址到物理地址的映射关系 |
这里直接对pde或者pte内部的数据赋值就好了,赋值的数据需要根据pde和pte的结构来。直接上结构图
前二十位是物理地址的高20位,后面的则是一些访问属性。这里不再过多解释
内存分配接口函数
函数已经全部封装好了,接下来是对外接口的提供了
1 | enum pool_flags |
接下来就在bochs中运行看看申请的空间有没有被写入页表中
这个是目前内核的内存布局信息,内核物理内存开始地址为0x200000。并且我们申请的内存开始地址是在0xc010000处,这也是内核堆空间的起始地址
在main函数中我申请了三页的内存,这里也确实做了三页的内存映射。