内存池的实现

2024-05-26 15:13:16 浏览数 (1)

内存池

经过了线程池,连接池的作用,内存池也就好理解了。内存池是专门使用数据结构将内存分配的任务交给内存池,不用每次分配内存的时候都自己使用 malloc 之类的。

简要分析

内存池可以分为分配大块内存和小块内存,所以内存池应该维护两个链表,一个是负责小块内存的分配,另一个是大块内存的链表。 c 语言实现相对来说简单一些,先定义数据结构。

这里先把头文件和一些宏定义展示

代码语言:c复制
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <fcntl.h>

#define MP_ALIGNMENT   32
#define MP_PAGE_SIZE   4096
#define MP_MAX_ALLOC_FROM_POOL (MP_PAGE_SIZE - 1)

#define mp_align(n, alignment) (((n)   (alignment - 1)) & ~(alignment - 1))  //求向上 32 位对齐
#define mp_align_ptr(p, alignment) ((void*)(((size_t)p)   (alignment - 1)) //求指针的向上 32 位对齐

内存分配跟其他相比就是需要进行内存对齐,小块内存的数据结构

代码语言:c复制
struct mp_node_s //小块内存,一个指向小块内存开始位置,一个指向末端,一个链表,标志位标志分配结果
{
    unsigned char *last;  //小块内存的初始位置
	unsigned char *end; //小块内存的结尾位置
	struct mp_node_s *next; //下一个小块内存
	size_t failed;  //分配的次数
};

大块内存数据结构如下:

代码语言:c复制
//大块内存组织方式,一个大块 alloc ,一个指向另一个大块的指针
struct mp_large_s
{
   struct mp_large_s *next;   //下一个大块内存链表
   void *alloc  //指向大块内存
};

而内存池的结构

代码语言:c复制
struct mp_pool_s
{
   size_t max;
   struct mp_node_s *current; //指向 head[0] 这个第一个小块,最开始这个快较大,后边链表的块都是 mp_node_s
   struct mp_large_s *large;  // 大块就是直接的指针指向
   struct mp_node_s head[0]  //结构体动态数组,内存池第一个结构是 max 表示内存池分配最大的内存
   
};

有了数据结构,然后就是数据结构的操作方法,所以对于内存池的操作方法定义如下:

代码语言:c复制
struct mp_pool_s *mp_create_pool(size_t size)
{
    struct mp_pool_s *p;
	//可以分配大块内存,第一个结构是两个结构体联合加上需要分配的内存
	int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size   sizeof(struct mp_pool_s)   sizeof(struct mp_node_s));
    if(ret){
		return NULL;
    }

	p->max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;
	p->current = p->head;  //current 指向第一个node 值
	p->large = NULL;

	p->head->last = (unsigned char *)p   sizeof(struct mp_pool_s)   sizeof(struct mp_node_s);
	p->head->end = p->head->last   size;

	p->head->failed = 0;
	return p;
}

创建内存池操作,其中 posix_memalign 是 Linux 负责分配小块内存的方法,然后内存池中 current 指针最开始指向其一起申请的小块 node, 然后就是各种初始化。

代码语言:c复制
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->next)
   {
   	   if(l->alloc){
	   	  free(l->alloc);
   	   	}
   	}
   //释放小块内存
   h = pool->head->next;
   while(h){
   	  n = h->next;
	  free(h);
      h = n;
   	}
   //释放内存池
   free(pool);
}

上述是销毁内存池,先是大块内存销毁,然后是小块内存销毁,最后线程池销毁。

代码语言:c复制
//重置
void mp_reset_pool(struct mp_pool_s *pool)
{
   struct mp_node_s *h;
   struct mp_large_s *l;
   //大块内存释放
   if(l = pool->large; l; l->next)
   {
   	  if(l->alloc){
	  	free(l->alloc);
   	  }
   }

   pool->large = NULL;
   //小块内存内存初始化
   for(h = pool->head; h; h = h->next)
   { 
      h->last = (unsigned char *)h   sizeof(struct mp_node_s);
   	}
}

内存池的重置首先就是释放大块内存,然后将小块内存最开始位置指向初始的 node 最开始位置。

内存分配

内存分配可以分为小块内存分配和大块内存分配,两者是不一样的函数,需要分别进行实现。

代码语言:c复制
//分配一个小块的内存,然后将每个遍历过的内存 failed 标志  1 ,并且如果遍历此处超过 4 次,就将线程池的指向其中
static void *mp_alloc_block(struct mp_pool_s *pool, size_t size)
{
    unsigned char *m;
	struct mp_node_s *h = pool->head;
	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;
	new_node = (struct mp_node_s*)m;

	new_node->next = NULL;
	new_node->failed = 0;

	current = pool->current;

    //分配次数超过 4 次就进行抛弃
    //遍历链表
	for(p = current; p->next; p = p->next){
		if(p->failed   > 4){
			current = p -> next;
		}
	}
    //找到链表的末尾分配
	p->next = new_node;

    //当前指向是否为空,如果是空那么就指向新节点。
	pool->current = current ? current : new_node;
	return m;
}

大块内存分配,我们首先要知道,我们的大块内存是被我们 alloc 指向的,说白了我们定义的数据结构是一个指针,所以这个数据结构占据的内存和 *alloc 指向的内存是分离的,因此我们首先要分配一个指针的内存,然后再分配大块内存。而既然我们要做一个内存池,那么这个指针的数据结构在其他地方分配多少不太合适,因此我们的指针也要在我们内存池分配。

因此先定义一个分配内存的机制。

代码语言:c复制
void *mp_alloc(struct mp_pool_s * pool, size_t size){
     unsigned char* m;
	 struct mp_node_s *p;

	 if(size <= pool->max){  //如果小于内存池最大值在小块内配
	 	p = pool->current;
		do{
			m = mp_align_ptr(p->last, alignment);
			if((size_t)(p->end - m) >= size){
				p->last = m   size;
				return m;
			}
			p = p->next;
		}while(p);

		return mp_alloc_block(pool, size); 
	 }

	 return mp_alloc_large(pool, size); //分配大块
}

而大块内存的定义如下:

代码语言:c复制
static void *mp_alloc_large(struct mp_pool_s *pool, size_t size)
{
    void *p = malloc(size);
	if(p == NULL) return NULL;

	size_t n = 0; 
	struct mp_large_s *large;

    //这里设计了分配的次数,内存池维护的大块最多为 3 块
	for(large = pool->large; large; large = large->next){
		if(large->alloc == NULL){
			large->alloc = p;
			return p;
		}
		if(n   > 3) break;
	}

	large = mp_alloc(pool, sizeof(struct mp_large_s));  //分配这样的结构体 这边有点递归套娃了,我在分配结构体可能会调用大块内存分配
	if(large == NULL){
		free(p);
		return NULL;
	}

    //环形指针
	large->alloc = p;
	large->next = pool->large;
	pool->large = large;

	return p;
}

初次之外,我们需要释放内存可以如下使用:

代码语言:c复制
void mp_free(struct mp_pool_s * pool, void * p){
    struct mp_large_s *l;
	for(l = pool->large; l; l = l -> next){
		if(p == l->alloc){
			free(l -> alloc);
			l->alloc = NULL;

			return ;
		}
	}
}

这个只是负责大块内存的分配。

结语

关于内存池的实现到这里基本就结束了,当然有部分可以优化的地方,比方说最后 mp_alloc 函数,它可以进行对齐分配,也可以分配后进行初始化,也可以多个分配,但是实现思路基本类似,因此不再赘述。

0 人点赞