本文主要讲内存池的原理和实现
池式结构的作用:
主要起到缓冲作用。也就是需要使用资源时,在已有的资源结构中去拿,避免重新创建资源。
背景:
在网络数据中,建立io连接后,recv收到数据后,会讲数据放置在buffer中。若数据周期较长,或者将处理数据进行解耦,放置在另一个线程中处理,这样buffer就是不可用的。我们通常会malloc一个内存,将数据拷贝进去,再在新线程中 处理数据。
问题:
当大量io事件发生时,会出现:
1、不利于内存管理;
2、内存碎片;
什么是内存池?
拿取内存时,从内存池中拿取内存,用完了就还回去
什么时候使用内存池?
服务端和客户端都可用内存池。
数据频繁的需要分配的地方,都可使用内存池。
内存池是针对小块内存的分配,因为其分配和释放的频率很高。
如何做内存池:
伙伴算法:
分配空间是2^n分配数据,回收数据会将两块合并成一起回收
做法是先分一个大块内存,然后在这个内存中分小块给其他人
1、针对4k如何分配?2^n
2、针对4K如何回收?需要将两个快连接到一起擦回收,要遵循物理内存空间。以页为单位操作,4k一页。伙伴算法是以页为单位操作。
伙伴算法适合在物理内存的分配与回收。
参考一
参考二
slab
分配4K时一开始就将其分配成若干个8,16,32,64,128,256等小空间的组成 。
参考一
参考二
实现内存池的策略
1、由整块散成小块;(伙伴算法) github可找到
2、划分好若干小块;(slab)
3、在特定业务场景里面:更实用,主要讲这个
建立fd连接时,就创建内存池给其使用,断开连接就将内存池释放。
内存池分为
4K内处理;
大于4K空间的处理;
将小于4K和大于4K的处理,连接起来;
内存池的创建;
销毁线程池;
从内存池中分配内存;
代码实现:
4K内存空间:
//4K内存
struct mp_node_s {
unsigned char *last; //指向已经分配后的内存结尾
unsigned char *end; //指向4k内存的结尾
struct mp_node_s *next; //不同4K内存块,如何组成起来,通过指针连接起来
size_t failed;
};
大于4K的空间:
//大于4K的内存
struct mp_large_s {
struct mp_large_s *next;
void *alloc; //分配块
};
内存池
//内存池
struct mp_pool_s {
size_t max; //能分配最大的块
struct mp_node_s *current; //当前从哪个节点分配
struct mp_large_s *large; //大块数据
struct mp_node_s head[0]; //指向4K小块的头节点
};
内存池的创建
//内存池的创建
struct mp_pool_s *mp_create_pool(size_t size) {
struct mp_pool_s *p;
//分配4K以上的大数据:参数:返回的指针,以多少对齐,分配大小。
//这个空间就是4K数据前还加上结构体的大小,返回的是最开始的地方
//放在一起是为了避免内存碎片
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; //指向4K节点一段
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_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) { //在4K空间中分配空间,若剩余的空间大于要分配的空间
p->last = m + size;
return m;
}
p = p->next; //在下一个4K空间中分配
} while (p);
return mp_alloc_block(pool, size); //分配新的4K小块数据
}
//否则分配大块数据内存
return mp_alloc_large(pool, size);
}
分配4K以内的内存:
//分配新的4k小块数据
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;
//遍历上一个4K空间剩下的小窗口
//不断的去遍历小窗口的大小
for (p = current; p->next; p = p->next) {
if (p->failed++ > 4) { //大于尝试的失败次数,4是个经验值
current = p->next;
}
}
p->next = new_node;
pool->current = current ? current : new_node;
return m;
}
分配大于4K的内存:
//分配大块数据
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;
//不考虑大块回收,只用last指针就好了
//
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {//不考虑大块回收,拿到尾结点即可
large->alloc = p;
return p;
}
if (n ++ > 3) break;
}
//把大块的结构放在4K空间里,避免出现内存碎片
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_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);
}
}
//释放 Nodes
h = pool->head->next;
while (h) {
n = h->next;
free(h);
h = n;
}
free(pool);
}
面试题:
接触一个陌生的系统(服务器),htop看虚拟内存在涨,请问你怎么判断和解决问题?
1、判断是否内存池有内存泄露:打开打印信息,一般释放时会有打印信息
若没有引入内存池, tcmalloc/jemalloc全局内存池的做法来解决。
2、是否第三方lib库有内存泄漏,排查哪些业务会大量涨内存,再找对应的库