2024年最全【池化技术】内存池技术原理和C语言实现_内存池c语言,Kotlin可能带来的一个深坑

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

鉴于此,malloc采用的是内存池的实现方式,malloc内存池实现方式更类似于STL分配器和memcached的内存池,先申请一大块内存,然后将内存分成不同大小的内存块,然后用户申请内存时,直接从内存池中选择一块相近的内存块即可

由于文首的链接里没图,所以从 glibc内存管理那些事儿 这个链接里盗来几张图,而且这个链接讲的内存池也很不错。

先看内存池的整体结构:(感觉下面的都很核心!)

img(第一眼看上去有没有很像哈希表?哈哈。)

malloc将内存分成了大小不同的chunk,然后通过bins来组织起来。malloc将相似大小的chunk(图中可以看出同一链表上的chunk大小差不多)用双向链表链接起来,这样一个链表被称为一个bin。malloc一共维护了128个bin,并使用一个数组来存储这些bin。数组中第一个为unsorted bin,数组从2开始编号,前64个bin为small bins,同一个small bin中的chunk具有相同的大小,两个相邻的small bin中的chunk大小相差8bytes。small bins后面的bin被称作large bins。large bins中的每一个bin分别包含了一个给定范围内的chunk,其中的chunk按大小序排列。large bin的每个bin相差64字节。(这里我本来想计算一下加深理解的,8x64=512b, 64x64=4096b 然后加起来发现和图片里的不太匹配,所以不知道自己哪里理解错了?可是前面的88又对啊,怎么后面就12k那么大呢?)

malloc除了有unsorted bin,small bin,large bin三个bin之外,还有一个fast bin。一般的情况是,程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的 chunk 之后,也许马上就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样无疑是比较低效的,故而,malloc 中在分配过程中引入了 fast bins,不大于 max_fast(默认值为 64B)的 chunk 被释放后,首先会被放到 fast bins中,fast bins 中的 chunk 并不改变它的使用标志 P。这样也就无法将它们合并,当需要给用户分配的 chunk 小于或等于 max_fast 时,malloc 首先会在 fast bins 中查找相应的空闲块,然后才会去查找 bins 中的空闲 chunk。在某个特定的时候,malloc 会遍历 fast bins 中的 chunk,将相邻的空闲 chunk 进行合并,并将合并后的 chunk 加入 unsorted bin 中,然后再将 usorted bin 里的 chunk 加入 bins 中

unsorted bin 的队列使用 bins 数组的第一个,如果被用户释放的 chunk 大于 max_fast,或者 fast bins 中的空闲 chunk 合并后,这些 chunk 首先会被放到 unsorted bin 队列中,在进行 malloc 操作的时候,如果在 fast bins 中没有找到合适的 chunk,则malloc 会先在 unsorted bin 中查找合适的空闲 chunk,然后才查找 bins。如果 unsorted bin 不能满足分配要求。 malloc便会将 unsorted bin 中的 chunk 加入 bins 中。然后再从 bins 中继续进行查找和分配过程。从这个过程可以看出来,unsorted bin 可以看做是 bins 的一个缓冲区,增加它只是为了加快分配的速度。(其实感觉在这里还利用了局部性原理,常用的内存块大小差不多,从unsorted bin这里取就行了,这个和TLB之类的都是异曲同工之妙啊!)

除了上述四种bins之外,malloc还有三种内存区。

  1. 当fast bin和bins都不能满足内存需求时,malloc会设法在top chunk中分配一块内存给用户;top chunk为在mmap区域分配一块较大的空闲内存模拟sub-heap。(比较大的时候)
  2. 当chunk足够大,fast bin和bins都不能满足要求,甚至top chunk都不能满足时,malloc会从mmap来直接使用内存映射来将页映射到进程空间,这样的chunk释放时,直接解除映射,归还给操作系统。(极限大的时候)
  3. Last remainder是另外一种特殊的chunk,就像top chunk和mmaped chunk一样,不会在任何bins中找到这种chunk。当需要分配一个small chunk,但在small bins中找不到合适的chunk,如果last remainder chunk的大小大于所需要的small chunk大小,last remainder chunk被分裂成两个chunk,其中一个chunk返回给用户,另一个chunk变成新的last remainder chunk。(这个应该是fast bins中也找不到合适的时候,用于极限小的)
malloc内存分配(下面算是正常一般的情况了)

一开始时,brk和start_brk是相等的,这时实际heap大小为0;如果第一次用户请求的内存大小小于mmap分配阈值,则malloc会申请(chunk_size+128kb) align 4kb大小的空间作为初始的heap。初始化heap之后,第二次申请的内存如果还是小于mmap分配阈值时,malloc会先查找fast bins,如果不能找到匹配的chunk,则查找small bins。若还是不行,合并fast bins,把chunk 加入到unsorted bin,在unsorted bin中查找,若还是不行,把unsorted bin中的chunk全加入large bins中,并查找large bins。在fast bins和small bins中查找都需要精确匹配,而在large bins中查找时,则遵循"smalest-first,best-fit"的原则,不需要精确匹配。

若以上都失败了,malloc则会考虑使用top chunk。若top chunk也不能满足分配,且所需的chunk大小大于mmap分配阈值,则使用mmap进行分配。否则增加heap,增加top chunk,以满足分配要求。

现在来不上chunk的结构图:

malloc利用chunk结构来管理内存块,malloc就是由不同大小的chunk链表组成的。一个使用中的chunk的结构如下图:

img

malloc会给用户分配的空间的前后加上一些控制信息,用这样的方法来记录分配的信息,以便完成分配和释放工作。chunk指针指向chunk开始的地方,图中的mem指针才是真正返回给用户的内存指针。

  1. chunk 的第二个域的最低一位为 P,它表示前一个块是否在使用中,P 为 0 则表示前一个 chunk 为空闲,这时chunk的第一个域 prev_size 才有效,prev_size 表示前一个 chunk 的 size,程序可以使用这个值来找到前一个 chunk 的开始地址。当 P 为 1 时,表示前一个 chunk 正在使用中,prev_size程序也就不可以得到前一个 chunk 的大小。不能对前一个 chunk 进行任何操作malloc分配的第一个块总是将 P 设为 1,以防止程序引用到不存在的区域。(这里就很细!)
  2. Chunk 的第二个域的倒数第二个位为 M,他表示当前 chunk 是从哪个内存区域获得的虚拟内存M 为 1 表示该 chunk 是从 mmap 映射区域分配的,否则是从 heap 区域分配的
  3. Chunk 的第二个域倒数第三个位为 A,表示该 chunk 属于主分配区或者非主分配区,如果属于非主分配区,将该位置为 1,否则置为 0。

右边图是空闲的chunk:

当chunk空闲时,其M状态是不存在的,只有AP状态,原本是用户数据区的地方存储了四个指针,指针fd指向后一个空闲的chunk,而bk指向前一个空闲的chunk,malloc通过这两个指针将大小相近的chunk连成一个双向链表。在large bin中的空闲chunk,还有两个指针,fd_nextsize和bk_nextsize,用于加快在large bin中查找最近匹配的空闲chunk。不同的chunk链表又是通过bins或者fastbins来组织的。(这里就很符合网上大多数人说的链表理论了)

以上内容大部分参考阿里华庭写的Glibc内存管理,Ptmalloc2源代码分析。

感觉从这个人这里学到了很多东西,是个大佬。

然后结合网上的一般回答:

链接:https://www.nowcoder.com/questionTerminal/5aae63b290c542f0ab0582d293e6c791
来源:牛客网

malloc可以分别由伙伴系统或基于链表的实现;

1、它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表;

2、 调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。

3、 调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。

虽然这个答案比较粗糙,但是也有大概的思想了。

malloc和free的实现原理 这位大哥讲得也不错,可以参考一下。

最后可以结合 腾讯云技术社区:十问 Linux 虚拟内存管理 (glibc) (一) 腾讯的这个实践来理论联系实践。下面参考于这个腾讯的链接(建议可以去点个赞):

malloc 是 glibc 中内存分配函数,也是最常用的动态内存分配函数,其内存必须通过 free 进行释放,否则导致内存泄露。

关于 malloc 获得虚存空间的实现,与 glibc 的版本有关,但大体逻辑是:

  1. 若分配内存小于 128k ,调用 sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。
  2. 若分配内存大于 128k ,调用 mmap() ,在文件映射区域中分配匿名虚存空间。
  3. 这里讨论的是简单情况,如果涉及并发可能会复杂一些,不过先不讨论。

其中 sbrk 就是修改栈顶指针位置,而 mmap 可用于生成文件的映射以及匿名页面的内存,这里指的是匿名页面。而这个 128k ,是 glibc 的默认配置,可通过函数 mallopt 来设置

接着: VSZ为虚拟内存 RSS为物理内存

  1. VSZ 并不是每次 malloc 后都增长,是与上一节说的堆顶没发生变化有关,因为可重用堆顶内剩余的空间,这样的 malloc 是很轻量快速的。
  2. 但如果 VSZ 发生变化,基本与分配内存量相当,因为 VSZ 是计算虚拟地址空间总大小。
  3. RSS 的增量很少,是因为 malloc 分配的内存并不就马上分配实际存储空间,只有第一次使用,如第一次 memset 后才会分配。
  4. 由于每个物理内存页面大小是 4k ,不管 memset 其中的 1k 还是 5k 、 7k ,实际占用物理内存总是 4k 的倍数。所以 RSS 的增量总是 4k 的倍数
  5. 因此,不是 malloc 后就马上占用实际内存,而是第一次使用时发现虚存对应的物理页面未分配,产生缺页中断,才真正分配物理页面,同时更新进程页面的映射关系。这也是 Linux 虚拟内存管理的核心概念之一

三、使用C语言实现一个内存池

在了解了内存池技术的原理之后,我们就可以自己来实现一个内存池。

1.C语言实现一个内存池

参考这篇文章:用C语言实现的简易内存池

pool.h

#ifndef \_POOL\_H
#define \_POOL\_H
 
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
 
#define LEN sizeof(memblock)//内存块节点大小
#define OPERA\_OK 0//操作失败
#define OPERA\_ERR 1//操作成功
//内存块节点
typedef struct MEMBLOCK
{
	char\* pmem;				//内存指针 
	MEMBLOCK \*next;		//指向下一节点的指针
}memblock;
//内存池节点
typedef struct MEMPOOL
{
	int  cnt;								//数量
	int  usedcnt;						//使用个数
	int blocksize;						//内存块大小
	char\* firstaddr;					//起始地址
	char\* lastaddr;						//结束地址
	MEMBLOCK \*firstblock;		//指向下一节点的指针
}mempool;
 
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*@brief 创建n个节点的内存池链表
\*@param
\* num 数量
\* blocksize 内存块大小
\*@return OPERA\_OK/OPERA\_ERR
\*@see
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
mempool \*CreatePool(int num, int blocksize);
 
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*@brief 销毁内存池
\*@param
\* poolhead 内存池指针
\*@return OPERA\_OK/OPERA\_ERR
\*@see
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
int  DestroyPool(mempool  \*poolhead);
 
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*@brief 得到一个内存块
\*@param
\* poolhead 内存池指针
\*@return 内存块地址
\*@see
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
char \*GetMemblock(mempool \*poolhead);
 
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*@brief 释放一个内存块
\*@param
\* pmem 内存块地址
\* poolhead 内存池指针
\*@return OPERA\_OK/OPERA\_ERR
\*@see
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
int ReleaseMemblock(char\* pmem, mempool \*poolhead);
 
 
#endif

pool.c

#include "pool.h"
 
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*@brief 创建n个节点的内存池链表
\*@param
\* num 数量
\* blocksize 内存块大小
\*@return OPERA\_OK/OPERA\_ERR
\*@see
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
 mempool \*CreatePool(int num,int blocksize )
{
	if(num<=0||blocksize<=0)
	{
		printf("Create input err\n");
		return NULL;
	}
	//内存池指针分配内存
	mempool \*poolhead=NULL;		//内存池指针
	poolhead=(mempool\*)malloc(sizeof(mempool));//池节点申请内存
	if (NULL==poolhead)
	{
		printf("poolhead malloc err!\n");
		return NULL;
	}
	memset(poolhead,0,sizeof(mempool));
	//内存池指针部分初始化赋值,其他默认为0无需初始
	poolhead->cnt=num;
	poolhead->blocksize=blocksize;
	//定义链表操作临时指针
	memblock \*p1 = NULL;	//创建的新节点的地址
	memblock \*p2 = NULL;	
	char\* block;						//内存块指针
	int n = 0;							//创建前链表的节点总数为0:空链表
	//分配第一个内存块
	p1 = ( memblock \*) malloc (LEN);	//开辟第一个新节点
	if (NULL == p1)
	{
		printf("p1 %d malloc err!\n",n+1);
		return NULL;
	}
	memset(p1,0,LEN);
	block=(char\*)malloc(blocksize);//开辟内存块
	if (NULL == block)
	{
		printf("firstblock malloc err!\n");
		return NULL;
	}
	memset(block,0,blocksize);
	//\*内存池,内存块初始化赋值
	p1->pmem=block;
	poolhead->firstaddr=block;
 	p2 = p1;			//如果节点开辟成功,则p2先把它的指针保存下来以备后用
	if(p1==NULL)		//节点开辟不成功
	{
		printf ("\nCann't create it, try it again in a moment!\n");
		return NULL;
	}
	while(n <num)		
	{
		n += 1;			//节点总数增加1个 
		if(n == 1)		//如果节点总数是1,则head指向刚创建的节点p1
		{
			poolhead->firstblock = p1;
			p2->next = NULL;  //此时的p2就是p1,也就是p1->next指向NULL。
		}
		else
		{
			p2->next = p1;	//指向上次下面刚刚开辟的新节点
		}
		p2 = p1;			//把p1的地址给p2保留,然后p1产生新的节点
 
		p1 = ( memblock \*) malloc (LEN);//开辟出新节点
		if (NULL == p1)
		{
			printf("p1 %d malloc err!\n",n+1);
			return NULL;
		}
		memset(p1,0,LEN);
		block=(char\*)malloc(blocksize);//开辟出新内存块
		if (NULL == block)
		{
			printf("block %d malloc err!\n", n + 1);
			return NULL;
		}
		memset(block,0,blocksize);
		p1->pmem=block;
	}
	p2->next = NULL;		//链表的最后一个节点指向NULL
	poolhead->lastaddr=p2->pmem+blocksize;//内存池 末尾地址
 
	free(p1->pmem);
	free(p1);			//跳出了while循环,释放p1 多余的那个空间
	p1 = NULL;			
	return poolhead;	    //返回创建链表的头指针 
}
 
 /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
 \*@brief 销毁内存池
 \*@param
 \* poolhead 内存池指针
 \*@return OPERA\_OK/OPERA\_ERR
 \*@see
 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
int  DestroyPool(mempool  \*poolhead)  
{  
	if(poolhead==NULL)
	{
		return OPERA_ERR;
	}
	memblock  \*p1=poolhead->firstblock;  
	memblock  \*p2=p1;
    while(p1!=NULL)  
    {  
        p2=p1;  
		p1=p1->next;  
		free(p2->pmem);
		p2->pmem=NULL;
        free(p2);  
    }  
	poolhead->firstblock=NULL;	
	free(poolhead);
	poolhead = NULL;
	return OPERA_OK;  
} 
 
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*@brief 得到一个内存块
\*@param
\* poolhead 内存池指针
\*@return 内存块地址
\*@see
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
char \*GetMemblock(mempool \*poolhead)
{
	if(poolhead->usedcnt==poolhead->cnt)
	{
		printf("GetMemblock ERR !Pool Full!\n");
		return NULL;
	}
	if (poolhead==NULL||poolhead->firstblock==NULL)
	{
		printf("pool in err!\n");
		return NULL;
	}
	memblock\* p=poolhead->firstblock;
	poolhead->firstblock = p->next;
	p->next = NULL;
	poolhead->usedcnt++;
	return p->pmem;
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
\*@brief 释放一个内存块
\*@param
\* pmem 内存块地址
\* poolhead 内存池指针
\*@return OPERA\_OK/OPERA\_ERR
\*@see
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
int ReleaseMemblock(char\* pmem, mempool \*poolhead)
{
	if(pmem==NULL)
	{
		printf("Realease Mem input ERR!\n");
		return OPERA_ERR;
	}
	memblock \*ptemp = (memblock \*)malloc(LEN);
	if (NULL == ptemp)
	{


![img](https://img-blog.csdnimg.cn/img_convert/341af7dec5a178c3485131418dd38d87.png)
![img](https://img-blog.csdnimg.cn/img_convert/81a0446fe4cde443ea70ab8bb2515408.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

memblock \*ptemp = (memblock \*)malloc(LEN);
	if (NULL == ptemp)
	{


[外链图片转存中...(img-Y0kU2o3w-1715758081394)]
[外链图片转存中...(img-VBqbafzq-1715758081394)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值