c语言仿照nginx手写内存池

最近在学习内存池,于是仿照nginx的样子手写一个内存池,学习过程记录一下自己的理解。
完整代码放在github

一 内存池介绍

首先我们需要介绍一下为什么需要用到内存池。

  1. 内存碎片

一个是为了解决内存碎片的问题。这里不详细展开,可以看下面的文章。
内存碎片是什么
我们用malloc等函数频繁申请内存,我们的内存区域会产生很多碎片,这里我画了个图。在学习操作系统的过程中学过类似的,在分区式存储管理的系统中。
我们分配内存可以是按照顺序查找,假如我们是安装上面的方式先分配三个区域。这时候中间的128b的空间被回收了,就留下了一个内存的缝隙,下一次如果申请的内存大小还是1kb,这个缝隙不能被利用,所以我们在分配内存的过程中,内存可能存在很多这样的缝隙,这样是对内存的极大浪费。
在这里插入图片描述

  1. 避免频繁的申请释放
    我们用malloc频繁的申请内存,需要不断在用户态和内核态进行切换,这样开销很大,我们本身也要避免这种开销。

二 如何管理内存

首先内存的管理有两个问题

1.分配大小是不确定的
2.什么时候分配是不确定的

这里针对不同的应用场景我们要采用不同的内存池分配方式

假设我们的应用场景是服务器连接

在这里插入图片描述
我们有很多个客户端去连接我们的服务器。对于每一个连接,我们要考虑这次传输过程传输的数据的缓冲区如何管理,我们可能连接只是发一句话,也可能是传输很大的文件,那我们如何针对传输的内容不同分配不同的内存呢。

解决方案

我们可以为每一个连接分配一个内存池,如果传输的是大文件,我们就分配一个大块内存,如果是很小的字符串,我们把这些小字符串全部放到一个内存区域。

三 实现内存池

我们首先知道对于大块内存和小块内存我们用不同的分配方式。

1.大块内存
我们用一个类似链表的结构,如果用户申请的空间大于4k我们就分配一个4k的块。
在这里插入图片描述
2.小块内存
我们也用一个类似链表的结构存储,但是对于每一个块,我们用两个指针来区分已分配区域和待分配区域。
在这里插入图片描述
具体的,我们用head指针来指向内存区域的头,用last指向下一个可以分配的的起点,end指向内存区域的末端。
在这里插入图片描述

然后我们就可以进行代码的编写,首先是三个结构体

typedef struct mp_large_s {  //大块内存
	struct mp_large_s *next;
	void *alloc;
}mp_large_s;

typedef struct mp_node_s {  //小块内存

	unsigned char *last;  //指向可用内存的开头
	unsigned char *end;	  //指向可用内存的结尾
	
	struct mp_node_s *next;
	// size_t failed;
}mp_node_s;

typedef struct mp_pool_s { 

	 size_t max;

	struct mp_node_s *current;  //指向当前使用的小内存块
	struct mp_large_s *large;   //指向大块内存链表的开头

	 struct mp_node_s head[0];

}mp_pool_s;

前面两个结构体也很好理解。最后一个mp_pool_s的结构我们需要结合下面的初始化内存池的函数来讲解。

我们的内存池需要下面6个接口
用来进行初始化销毁和分配释放

struct mp_pool_s *mp_create_pool(size_t size);

void mp_destory_pool(struct mp_pool_s *pool);

void *mp_alloc(struct mp_pool_s *pool, size_t size);

void *mp_nalloc(struct mp_pool_s *pool, size_t size);

void *mp_calloc(struct mp_pool_s *pool, size_t size);

void mp_free(struct mp_pool_s *pool, void *p);
  1. mp_create_pool
//创建一个内存池 size是内存池的小块用户空间的大小
struct mp_pool_s *mp_create_pool(size_t size){
	struct mp_pool_s *p; //内存池的指针
	//给内存池结构体分配空间
	//传入的是二级指针,因为我们要修改指针的内容  
	//MP_ALIGNMENT是对齐的地址大小,代表以32的倍数对齐
	int ret =posix_memalign((void**)&p,MP_ALIGNMENT,size + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s));
	if(ret){
		return NULL;
	}
	//MP_MAX_ALLOC_FROM_POOL=4095 4096就分配大块 
	//size 小于4095 max 就等于size 否则就等于4095
	p->max=(size<MP_MAX_ALLOC_FROM_POOL)?size:MP_MAX_ALLOC_FROM_POOL;
	//这一部分结合图理解 p的current指针指向当前正在用的小块内存区域
	p->current=p->head;
	//大块内存区域还没有数据
	p->large=NULL;
	//转化为char 类型指针是为了让指针的加法以1为偏移量
	//last指向了用户空间的 开头位置
	p->head->last=(unsigned char *)p+sizeof(struct mp_pool_s)+sizeof(mp_node_s);
	//end指向了用户空间的末尾
	p->head->end=p->head->last+size;
	return p;
}

我们来讲解一下这个函数。

我们先创建一个内存池的指针p,然后我们用posix_memalign分配对齐的内存,为什么要使用这个函数,因为malloc 4k这么大的空间有时候会分配不了。
posix_memalign

第一个参数是一个二级指针,因为我们需要修改一级指针的内容。
第二个参数是对齐值,必须是2的n次方,这里我们传入自己定义的常量
MP_ALIGNMENT,值为32
第三个参数就是分配的size大小。这里为什么等于
size + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s)
这个size是作为函数参数传入的,我们传入的是4k。这个4k我们是作为用户的小块内存空间的。我们要确保用户空间一定有4k。为什么呢,我们先讲如何区分分配大块内存还是小块内存。

如何区分大块内存和小块内存
大块内存
size>=4096
小块内存
size<4096

所以如果用户需要的内存是4095 属于小块内存,但是我们程序最初申请的空间留给用户的没有4k,就会发生错误。
我们看一下这个结构体,然后我画了个示意图。

typedef struct mp_pool_s { 

	 size_t max;

	struct mp_node_s *current;  //指向当前使用的小内存块
	struct mp_large_s *large;   //指向大块内存链表的开头

	 struct mp_node_s head[0];

}mp_pool_s;

假设我们只分配size大小的空间。实际上分配给用户的空间就是
4k-sizeof(struct mp_pool_s) - sizeof(struct mp_node_s)
在这里插入图片描述

所以我们要分配这么大的空间变成下面这样
在这里插入图片描述

此时head是一个柔性数组,指向的位置就是mp_node_t的开头。
然后我们看下head里面的指针

typedef struct mp_node_s {  //小块内存

	unsigned char *last;  //指向可用内存的开头
	unsigned char *end;	  //指向可用内存的结尾
	
	struct mp_node_s *next;
	// size_t failed;
}mp_node_s;

因为空间已经分配好了,所以接下来的代码都是用来初始化指针
我们让head->last指向用户区域的开头位置,end指向了末尾

	//转化为char 类型指针是为了让指针的加法以1为偏移量
	//last指向了用户空间的 开头位置
	p->head->last=(unsigned char *)p+sizeof(struct mp_pool_s)+sizeof(mp_node_s);
	//end指向了用户空间的末尾
	p->head->end=p->head->last+size;

就像前面那个图
在这里插入图片描述
2. mp_alloc
初始化完了我们来看分配内存的函数

void *mp_alloc(struct mp_pool_s *pool, size_t size) {

	unsigned char *m;
	struct mp_node_s *p;

	if (size <= pool->max) { //4095 分配小块内存

		p = pool->current;	

		do {
			//函数将 p->last 指针对齐到 MP_ALIGNMENT 的倍数
			m = mp_align_ptr(p->last, MP_ALIGNMENT);
			//并检查剩余空间是否足够容纳 size 大小的数据。
			//如果足够,就将 p->last 更新为新的内存块的起始地址,并返回该地址作为分配的内存空间。
			if ((size_t)(p->end - m) >= size) {
				p->last = m + size;
				return m;
			}
			p = p->next;
		} while (p);
		//如果遍历完所有节点仍然没有找到足够的空间,那么会调用 mp_alloc_block 函数来分配一个新的内存块,并返回分配的内存空间。
		return mp_alloc_block(pool, size);
	}
	//分配大块内存
	return mp_alloc_large(pool, size);
	
}

核心的逻辑很简单,先判断一下分配内存的大小,决定是否分配大块内存。

如果是小块内存,我们先遍历小块内存的链表结构,找到一个可以分配对齐内存池的空间。

如果找到我们就直接将空间地址m返回。如果没有,我们就用mp_alloc_block函数创建一个新的小块空间,然后分配内存。

那我们接着看着里面函数的具体实现。

mp_align_ptr

是一个宏定义

#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))

具体的步骤如下:

  1. 将指针 p 转换为 size_t 类型,以便进行位运算。

  2. 将 alignment 减去 1,得到一个掩码,用于将指针向上对齐到 alignment 的倍数。

  3. 将指针 p 加上掩码,实现向上对齐。

  4. 使用按位与运算符 & 将对齐后的指针与掩码进行按位与操作,以确保最终的指针值是 alignment 的倍数。

  5. 最后,将对齐后的指针转换回 void* 类型,并返回对齐后的指针。

实现的效果就是将指针对齐到alignment指定的数值,这里我们是32,那么最后指针会是32字节的倍数。这里根据实际需求可以调整。

mp_alloc_large

这里执行分配大块内存的操作

static void *mp_alloc_large(struct mp_pool_s *pool, size_t size) {
	
	//分配 size 字节大小的内存块,并将返回的指针存储在变量 p 中
	void *p = malloc(size);
	if (p == NULL) return NULL;
	//函数遍历内存池 pool 中的大块内存链表 pool->large
	//直到找到一个未分配的大块内存或者遍历了超过 3 个大块内存。查找太多影响效率
	//如果找到了未分配的大块内存,将其 alloc 字段设置为 p,即将分配的内存块指针存储在其中,并返回该指针。
	size_t n = 0;
	struct mp_large_s *large;
	for (large = pool->large; large; large = large->next) {
		if (large->alloc == NULL) {
			large->alloc = p;
			return p;
		}
		if (n ++ > 3) break;
	}

	//在这段代码中,调用 mp_alloc 函数分配一个 sizeof(struct mp_large_s) 大小的内存块,用于存储新的大块内存结构。
	large = mp_alloc(pool, sizeof(struct mp_large_s));
	//如果内存分配失败,会释放之前分配的内存块 p,然后返回 NULL,表示分配失败。
	if (large == NULL) {
		free(p);
		return NULL;
	}
	//头插法
	large->alloc = p;
	large->next = pool->large;
	pool->large = large;

	return p;
}

这个函数本身没什么难度,就是在链表中找到未分配的区域或者创建一个新的大块内存。
申请完空间后我们将这部分内存的管理结构存入链表中,头插法

mp_alloc_block

static void *mp_alloc_block(struct mp_pool_s *pool, size_t size) {
	//定义一个指针m 它指向被分配内存的开头
	unsigned char *m;
	//拿到头节点的控制块 也是小块指针链表的入口位置
	struct mp_node_s*h =pool->head;
	//计算要分配内存 应该等于 4k +sizeof(mp_node_s)
	size_t psize= (size_t)(h->end-(unsigned char *)h);

	//这里分配内存
	int ret =posix_memalign((void**)&m,MP_ALIGNMENT,psize);	
	if (ret) return NULL;

	//创建新的节点
	struct mp_node_s*p,*new_node,*current;
	//这里先将指针 m 转换为 struct mp_node_s* 类型
	//这样保证用户空间为4k
	new_node=(struct mp_node_s*)m;
	//指向用户空间的结尾
	new_node->end=m+psize;
	//next指针置为null
	new_node->next=NULL;
	new_node->failed=0;

	//这里给用户分配内存
	//将m移动到用户空间的开头位置
	m+=sizeof(struct mp_node_s);
	//对m进行指针的对齐
	m = mp_align_ptr(m, MP_ALIGNMENT);
	//更新新节点的last
	new_node->last=m+size;
	//拿到当前的current
	current = pool->current;

	//这里将刚刚创建的内存块插入链表中 尾插法
	for (p = current; p->next; p = p->next) {
		//在遍历过程中,如果节点 p 的失败计数器 failed 大于 4,则将当前节点 current 更新为 p 的下一个节点 p->next。
		//这个计数器会使得current的值前移
		//因为current之前的节点也可能存在没有用完的内存
		if (p->failed++ > 4) { 
			current = p->next;
		}
	}
	p->next = new_node;

	//current非空就把内存池的current更新
	pool->current = current ? current : new_node;

	return m;
}

3 .mp_destory_pool

在这里进行资源回收,代码很简单,free掉我们分配的内存就行了

void mp_destory_pool(struct mp_pool_s *pool){
	struct mp_node_s *h, *n;
	struct mp_large_s *l;
	
	for (l = pool->large; l; l = l->next) {
		if (l->alloc) {
			free(l->alloc);
		}
	}

	h = pool->head->next;

	while (h) {
		n = h->next;
		free(h);
		h = n;
	}

	free(pool);
}
  • 45
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值