内存池
经过了线程池,连接池的作用,内存池也就好理解了。内存池是专门使用数据结构将内存分配的任务交给内存池,不用每次分配内存的时候都自己使用 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 函数,它可以进行对齐分配,也可以分配后进行初始化,也可以多个分配,但是实现思路基本类似,因此不再赘述。