文章目录
完善堆内存管理
咱们这一章的目的是实现堆内存管理:也就是实现malloc,free,可以分成以下几个步骤
- 系统调用的实现
- 内存管理系统所需的数据结构实现
- sys_malloc的实现
- sys_free的实现
用户调用系统调用实现的流程
添加一个系统调用的具体过程
![image-20210915083742968](https://gitee.com/wzjia/picture/raw/master/OSimg/202109150937590.png)
上述过程说白了就是在syscall_table中添加了一个函数。
系统调用的一些细节
_syscall是汇编实现的,所以传参可以用寄存器,用寄存器的原因是可以更快,不用访问两个栈
实现中断处理程序syscall_handler 时也得和其他的中断一样先做一遍push
eax存储功能号并存储返回值,ebx,ecx,edx,esi,edi 分别为第1到5个参数的传递,参数多了就压栈,然后传一个栈指针进去即可。
printf的实现
printf的原型为:int printf(const char * format , …); …代表的就是不定长的参数,这里的实现很巧妙,这里面涉及的三个宏,我觉得没什么意义,直接写也是一样的,总之就是根据了栈中每个元素占一个字长,然后%个数就是所谓的不定长参数的个数,实际上我们还可以直接根据这个求出参数的个数,就变成定长了。
- 把这些参数从右到左全部压入到栈中
- 从左往右遍历format
- 遇到%,就停下来,去处理栈中对应位置的数据,然后写入到str中,并pop
- 最后format回读到 ‘\0’ ,那么也就是结束了
下一步再通过write将str写入到某个文件中(fd) ,因为这里没有实现文件系统,所以就直接写入到显示器上了。另外,也可以不调用write,生成str就是sprintf做的事情。
堆内存管理
在这一章我们要对内存进行更细致的分配,前面我们完成的分配主要是针对任务之间的,接下来的内存管理就是针对任务内部的了,因此有很多小内存分配需求需要处理,(咱们都知道内存是分为内核和用户的,但其实从逻辑上来说,这两个没有什么区别,处理的时候加一个判断就可以了),这个内存管理的主要任务是要分配和释放颗粒度更小的内存
内存块描述符
注意:free_list是无限大的,因为blocks_per_arena 只是一个arena能够提供的内存块个数
/* 内存块 */
struct mem_block { // 用来添加到同规格内存块描述符的free_List中
struct list_elem free_elem;
};
/* 内存块描述符 */
struct mem_block_desc {
uint32_t block_size; // 内存块大小
uint32_t blocks_per_arena; // 本arena中可容纳此mem_block的数量.
struct list free_list; // 目前可用的mem_block链表
};
内核和用户进程的 内存块描述符
struct mem_block_desc k_block_descs[DESC_CNT]; // 内核内存块描述符数组
struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
内存仓库
下面是arena的定义,以后咱们的每次分配都是分配某个arena中的块。arena中的块,共有8中类型,16,32,64,128,256,512,1024,大于1024时 large 为 1。
当申请内存大小超过1024KB时,直接返回一个页框,不再从arena中划分,下面是arena的结构:
- 元信息
- 内存池区域,mem_block
/* 内存仓库arena元信息 */
struct arena {
struct mem_block_desc* desc; // 此arena关联的mem_block_desc
/* large为ture时,cnt表示的是页框数。
* 否则cnt表示空闲mem_block数量 */
uint32_t cnt;
bool large;
};
可以看出:arena实际上就分两种
下面我们来看看上面两种arena如何分配内存,假如要分配一个大小为size 字节的内存
对于size>1024KB的内存分配十分简单,直接分配所需要的物理页即可,
主要看看第一种,我们先澄清一个概念,在这种方法中实现的arena实际上就是一个普通的物理页,真正的内存管理是mem_block_desc中free_list;
咱们分两步来理解上面那句话的意思:
- 首先判断有没有内存,是看的mem_block_desc中的free_list,如果free_list是空,就分配一页arena
并在此之后分配给free_liist cnt 个结点,每个都指向在arena中相应块的地址。
- 咱们再看分配内存块,主要有以下两步
- 获取free_list的头结点,就是分配的块的地址
- a ->cnt – ;
可以看出实际上arena就是一个计数器,看这个内存管理的代码最重要的还是free_list
哈哈或许你看到这里,会觉得作者为什么要用这种方法呢?其实这又是计算机思想的一大整合,咱们类比一下虚拟地址和物理地址的映射时,分配虚拟地址必须连续,但是物理地址可以不用连续,这样就减少了内存碎片的产生**。作者这样做,也是为了减少内存碎片。**
咱们在这里可以把arena那个物理页看成物理地址,free_list看成虚拟地址,free_list很可能会由很多不连续的arena中的块来一起构成,但是在使用的时候咱们都只看free_list就行啦,这在后面的释放中显得很是方便,可以在O(1)内做到释放和分配,并且内存利用率基本上拉满了,这真是个妙招!!!
回收内存
回收页物理地址
void pfree(uint32_t pg_phy_addr) ;
直接将pg_phy_addr指向的页在位图中清0即可
回收页虚拟地址
将这页虚拟地址在位图中清0即可
去除映射
让此虚拟地址的pte的P位清0并更新tlb
回收虚拟页指向的物理内存
直接映射成物理内存,pfree即可
回收内存
回收内存才是我们的主要函数,也是系统调用的接口,思路还是很清晰的,顺着申请的思路,对size >1kB的直接当成物理页回收即可,反之则将其放回到相应规格的free_list中,并将其a ->cnt ++ ;
这里要注意的是:如果a->cnt满了,说明此arena没有存任何东西,也一并释放掉这个物理页,和相应的free_list中删去这些结点。
malloc,free
这就是这一章的主要目的啦,这个实现就很简单啦,直接系统调用即哈哈。
执行结果
总结
从上面的管理我们可以发现,在c语言里,为什么咱们free的时候不需要提供大小了哈哈,只用free§即可了,分两种情况
- 因为p在free_list中,那么他的大小就是已知的。
- 因为在p前面有12个字节,这12个字节里就存在着一块arena的内存大小。
特别注意:在对内存进行修改的时候一定要关中断!!
64位下的源代码Makefile脚本修改
https://blog.csdn.net/qq_45923646/article/details/120273571