内存池是预先创建一块内存空间,之后需要使用的时候从池中获取空间。
内存池的意义主要有两点,一是方便内存的管理,避免内存泄漏的问题,二是能避免内存碎片。除服务端以外,客户端中也可以使用内存池,特别是当频繁地进行数据统计和更新,需要频繁分配内存的时候。
内存池是主要针对内存小块的策略,大致有三个操作:分配、回收和扩容。
内存池框架
现行内存池框架主要有以下三种
1.伙伴算法
伙伴算法是把整块内存分成小块的做法,类似现实生活中的找零钱的做法,将内存分解成2的整数次幂的大小。伙伴算法存在一个问题,回收内存需要两个空闲空间连续且大小一致,所以不适合以字节为单位分配内存的情况,适用于以页为单位分配内存的情况。
2.slab机制
slab也是将内存分成2的整数次幂的大小,与伙伴算法的区别是slab提前分配好,而不是分配时再拆分,没有就向上“借位”。slab适用于小空间分配的情况。tcmalloc、jemalloc是slab的变种。
3.粗放型设计
先给每个业务或连接分配一页空间,当连接断开或业务结束时释放,这也是“粗放”的原因。这种方法适用于特定业务场景。
这里介绍一下nginx内存池的做法。nginx将内存分配的大小区分为大块和小块两类,以一页大小(4096B)为区分。
struct mp_large_s
{
struct mp_large_s *next;
void *alloc;
};
struct mp_node_s
{
unsigned char *last;
unsigned char *end;
struct mp_node_s *next;
size_t failed;
};
struct mp_pool_s
{
size_t max;
struct mp_node_s *current;
struct mp_large_s *large;
struct mp_node_s head[0];
};
这里注意所有结构体都分配在内存池里,不要malloc(),避免内存碎片。
大块内存是每申请一次空间分配一次相应大小的内存,小块内存是先在已有小块中查询,如果够申请的大小就直接在该小块中分配,如果不够再重新分配一个一页大小的小块。这里只有大块有回收机制,小块只有在当块内全部空间都空闲时才会释放,而且直接释放整个小块。
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;//宏定义,4096
p->current = p->head;
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;
}
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);
}
void mp_reset_pool(struct mp_pool_s *pool)
{
struct mp_node_s *h;
struct mp_large_s *l;
for (l = pool->large; l; 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);
}
}
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->end = m + psize;
new_node->next = NULL;
new_node->failed = 0;
m += sizeof(struct mp_node_s);
m = mp_align_ptr(m, MP_ALIGNMENT);
new_node->last = m + size;
current = pool->current;
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;
}
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;
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;
}
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, MP_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);
}
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 ;
}
}
}
最后再提示一下,面对陌生的服务,如果htop发现内存一直在涨,考虑内存泄漏。如果引入了内存池,加入打印信息查看内存泄漏的地方,如果没有引入内存池,可以尝试tcmalloc和jemalloc。