1、使用内存池的原因
开源组件:tcmalloc/jemalloc
内存池是对堆上的内存池进行管理。服务器长时间运行,会产生莫名其妙的问题,大多数都是内存问题 。
为什么需要对内存管理?
- 避免频繁的系统调用带来的开销。
- 减少了频繁分配和释放小块内存产生的内存碎片。
解决上述问题,最好的方法就是内存池。
2、内存池的工作流程
内存池的具体做法是固定大小、提前申请、重复利用。
- 固定大小。在调用内存分配函数的时候,小块内存每次都分配固定大小的内存块,这样避免了内存碎片产生的可能性。
- 提前申请一块大的内存,内存不够用时再二次分配,减少了
malloc
的次数,提高了效率。
本文参考 nginx 内存池的设计,实现一个简易版的内存池,工作流程如图所示
nginx 内存池工作流程
3、内存池的实现
3.1、数据结构设计
nginx 内存池
3.1.1、内存池
管理整个内存池的头部信息,包括内存池结点(小块内存)链表,大块内存链表。
typedef struct mp_pool_s { size_t max; // 小块内存能分配的最大空间,超过该值使用大块内存分配 mp_node_t *current; // 指向当前内存池 struct mp_large_s *large; // 指向大块内存链表 mp_node_t head[0]; // 指向小块内存链表 } mp_pool_t;
两个链表为真正的内存分配的地方。在分配内存的时候,根据内存池设定的max
值,判断当前要分配的内存块是小块内存还是大块内存。小块内存直接在内存池内 mp_node_s
分配,大块内存向 OS 申请 mp_large_s
。
3.1.2、小块内存
内存池结点,用于小块内存的分配。
typedef struct mp_node_s { unsigned char *last; // 指向内存池结点已分配的末尾地址,下一次内存分配的起始地址 unsigned char *end; // 指向内存池结点的末尾地址 struct mp_node_s *next; // 指向下一个内存池结点 size_t failed; // 当前内存块分配空间失败的次数 } mp_node_t;
对于小块内存的分配,先尝试从内存池的当前结点(p->current
)的剩余空间分配。
- 若空间足够,则返回
last
的地址作为内存分配的起始地址,并更新last = last + size
。 - 若空间不足,则创建一个新的
mp_node_s
结点,并更新p->next
,返回last
的地址作为内存分配的起始地址,并更新last = last + size
。
小块内存分配
3.1.3、大块内存
struct mp_large_s { struct mp_large_s *next; // 指向下一个大块内存,所有大块内存通过链表串联起来 void *alloc; // 指向实际分配的大块内存 };
3.2、接口设计
3.2.1、创建内存池
申请的内存由两部分组成,一部分用来容纳mp_pool_s
结构体,另一部分内存则是用于满足用户申请。last
指针和end
指针之间的内存是空闲的,当用户申请小块内存时,如果空闲的内存大小满足用户的需求,则可以分配给用户
// 内存对齐,以 alignmen 为单位,如:以4B对齐,17->20, 30->32 #define mp_align(n, alignment) (((n)+(alignment-1)) & ~(alignment-1)) #define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1)) mp_pool_t *mp_create_pool(size_t size) { mp_pool_t *p; // posix_memalign申请空间,参数:首地址,对齐方式,大小。 // 空间大小:申请的空间 + 内存池管理信息(mp_pool_t) + 内存池结点(mp_node_t) int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size + sizeof(mp_pool_t) + sizeof(mp_node_t)); if (ret) { return NULL; } // 设置小块内存能分配的最大空间 p->max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL; // 设置当前指向的内存池结点 p->current = p->head; // 设置大块内存链表 p->large = NULL; // 设置内存池结点信息 p->head->last = (unsigned char *)p + sizeof(mp_pool_t) + sizeof(mp_node_t); p->head->end = p->head->last + size; p->head->failed = 0; return p; }
3.2.2、内存分配
小块内存分配
用户可以调用mp_alloc()
向内存池申请内存。分配内存的时候,根据本次申请空间的大小 size
和内存池设定的pool->max
,判断当前要分配的内存块是小块内存还是大块内存。
如果用户申请的是小块内存,遍历现有的小块内存链表,寻找是否有满足需求的内存块
- 有可分配的内存块,返回待分配空间的首地址
- 没有可分配的内存块,创建一个新的小块内存
void *mp_alloc(mp_pool_t *pool, size_t size) { unsigned char *m; mp_node_t *p; // 1、用户申请的是小块内存 if (size <= pool->max) { // 指向第一个要遍历的内存池结点 p = pool->current; // 遍历小块内存链表,寻找可用的空间分配内存 do { // 指针内存对齐 m = mp_align_ptr(p->last, MP_ALIGNMENT); // 当前结点的剩余空间足够分配 if ((size_t)(p->end - m) >= size) { // 更新last指针 p->last = m + size; return m; } // 若当前内存块的剩余内存小于所需内存,则到下一个内存块中寻找 p = p->next; } while (p); // 没找到合适的小块内存,则创建一个新的小块内存 return mp_alloc_block(pool, size); } // 2、用户申请的是大块内存 return mp_alloc_large(pool, size); }
mp_alloc_block
用来创建新的小块内存,并插入到小块内存链表的末尾。
每次调用mp_alloc_block
函数,代表内存池现有小块内存的空间分配失败,此时,所有可用的小块内存的 failed + 1
,表示不满足用户的需求增加1次。 由于采取的是尾插法,所以链表中结点的 failed
计数值依次递减。若某个小块内存若连续 5 次不满足用户需求,则不再使用它,遍历时跳过。
static void *mp_alloc_block(mp_pool_t *pool, size_t size) { unsigned char *m; mp_node_t *p, *new_node, *current; mp_node_t *h = pool->head; // 计算第一个内存块中共计可分配内存的大小 size_t psize = (size_t)(h->end - (unsigned char *)h); // 申请新的内存块(和第一个内存块大小相同) int ret = posix_memalign((void **)&m, MP_ALIGNMENT, psize); if (ret == NULL) return NULL; // 初始化内存块 new_node = (mp_node_t*)m; new_node->end = m + psize; new_node->next = NULL; new_node->failed = 0; // 将指针m移动到可分配内存的开始位置 m += sizeof(mp_node_t); // 对指针做内存对齐 m = mp_align_ptr(m, MP_ALIGNMENT); // 设置新内存块的last new_node->last = m + size; // 当前指向的内存池结点(该结点及后面结点都可分配内存,前面结点无法分配) current = pool->current; // 寻找最后一个链表结点,结点的查找从 current 开始,不需要每次从头开始查找 for (p = current; p->next; p = p->next) { // 所有小块内存的 failed+1,表示不满足用户的需求 +1 次 // 若某个小块内存若连续 5 次都不满足用户需求,则跳过这个小块内存,不再遍历它 if (p->failed++ > 4) { current = p->next; // 调整 current 指向下一个内存块 } } // 将新创建的内存块,尾插到小块内存链表 p->next = new_node; // 更新pool->current指针 pool->current = current ? current : new_node; return m; }
大块内存分配
内存池mp_pool_t
的成员large
指向大块内存链表。如果用户申请的是大块内存,那么内存池就会调用mp_alloc_large()
分配一个新的大块内存,插入到链表的开头,头插法。
static void *mp_alloc_large(mp_pool_t *pool, size_t size) { // 调用 malloc 申请大块内存 void *p = malloc(size); if (p == NULL) return NULL; size_t n = 0; struct mp_large_s *large; // 遍历大块内存链表,找到可以挂载 p 的位置 for (large = pool->large; large; large = large->next) { // 找到可以挂载的地方 if (large->alloc == NULL) { large->alloc = p; return p; } // 若连续 4 次都没找到,就不再寻找了 if (n ++ > 3) break; } // 创建一个新的大块内存结点,用来挂载p 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; }
3.2.3、内存池释放
Nginx 内存池内部仅提供大块内存的释放接口
void mp_free(mp_pool_t *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 ; } } }
3.2.4、内存池销毁
void mp_destory_pool(mp_pool_t *pool) { mp_node_t *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); }
3.3、代码实现
#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) // 内存对齐,以 alignmen 为单位,如:17->20, 30->32 #define mp_align(n, alignment) (((n)+(alignment-1)) & ~(alignment-1)) #define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1)) // 大块内存 typedef struct mp_large_s { struct mp_large_s *next; // 指向大块内存链表的下一个大块内存 void *alloc; // 指向实际分配的大块内存 } mp_large_t; // 内存池结点:小块内存 typedef struct mp_node_s { unsigned char *last; // 指向内存池结点已分配的末尾地址,下一次内存分配的起始地址 unsigned char *end; // 指向内存池结点的末尾地址 struct mp_node_s *next; // 指向下一个内存池结点 size_t failed; // 当前内存块分配空间失败的次数 } mp_node_t; // 内存池 typedef struct mp_pool_s { size_t max; // 小块内存能分配的最大空间,超过该值使用大块内存分配 mp_node_t *current; // 指向当前内存池 struct mp_large_s *large; // 指向大块内存链表 mp_node_t head[0]; // 指向小块内存链表 } mp_pool_t; mp_pool_t *mp_create_pool(size_t size); void mp_destory_pool(mp_pool_t *pool); void *mp_alloc(mp_pool_t *pool, size_t size); void *mp_nalloc(mp_pool_t *pool, size_t size); void *mp_calloc(mp_pool_t *pool, size_t size); void mp_free(mp_pool_t *pool, void *p); // 创建内存池 mp_pool_t *mp_create_pool(size_t size) { mp_pool_t *p; // posix_memalign申请空间,参数:首地址,对齐方式,大小。malloc无法分配4k空间 // 空间大小:申请的空间 + 内存池管理信息(mp_pool_s) + 内存池结点(mp_node_s) int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size + sizeof(mp_pool_t) + sizeof(mp_node_t)); if (ret) { return NULL; } // 设置小块内存能分配的最大空间 p->max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL; // 设置当前指向的内存池结点 p->current = p->head; // 设置大块内存链表 p->large = NULL; // 设置内存池结点信息 p->head->last = (unsigned char *)p + sizeof(mp_pool_t) + sizeof(mp_node_t); p->head->end = p->head->last + size; p->head->failed = 0; return p; } // 内存池销毁 void mp_destory_pool(mp_pool_t *pool) { mp_node_t *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(mp_pool_t *pool) { mp_node_t *h; struct mp_large_s *l; // 释放大块内存 for (l = pool->large; l; l = l->next) { if (l->alloc) { free(l->alloc); } } pool->large = NULL; //重置小块内存,并不调用free将内存交还给系统,只是指针的复位操作。 for (h = pool->head; h; h = h->next) { h->last = (unsigned char *)h + sizeof(mp_node_t); } } // 小块内存分配 static void *mp_alloc_block(mp_pool_t *pool, size_t size) { unsigned char *m; mp_node_t *p, *new_node, *current; mp_node_t *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; // 初始化内存块 new_node = (mp_node_t*)m; new_node->end = m + psize; new_node->next = NULL; new_node->failed = 0; // 将指针m移动到可分配内存的开始位置 m += sizeof(mp_node_t); // 对指针做内存对齐 m = mp_align_ptr(m, MP_ALIGNMENT); // 设置新内存块的last new_node->last = m + size; // 当前指向的内存池结点(该结点及后面结点都可分配内存,前面结点无法分配) current = pool->current; // 寻找最后一个链表结点,结点的查找从 current 开始,不需要每次从头开始查找 for (p = current; p->next; p = p->next) { // 每次调用mp_alloc_block函数,代表之前所有小块内存空间分配失败 // 此时,所有小块内存的 failed+1,表示不满足用户的需求 +1 次 // 若某个小块内存若连续 5 次都不满足用户需求,则跳过这个小块内存,下次不再遍历它 // 由于链表结点的插入是尾插法,所以先插入的结点内存分配失败次数多 if (p->failed++ > 4) { current = p->next; // 调整 current 指向下一个内存块 } } // 将新创建的内存块,尾插到小块内存链表 p->next = new_node; // 更新pool->current指针,判断 current 是否为空? // 若非空,指向current指向的结点;若为空,代表之前所有的内存块都分配失败,则指向新的内存块 pool->current = current ? current : new_node; return m; } // 分配大块内存 static void *mp_alloc_large(mp_pool_t *pool, size_t size) { // 调用 malloc 申请大块内存 void *p = malloc(size); if (p == NULL) return NULL; size_t n = 0; struct mp_large_s *large; // 遍历大块内存链表,找到可以挂载 p 的位置 for (large = pool->large; large; large = large->next) { // 找到可以挂载的地方 if (large->alloc == NULL) { large->alloc = p; return p; } // 若连续 4 次都没找到,就不再寻找了 if (n ++ > 3) break; } // 创建一个新的大块内存结点,用来挂载p 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_memalign(mp_pool_t *pool, size_t size, size_t alignment) { void *p; int ret = posix_memalign(&p, alignment, size); if (ret) { return NULL; } struct mp_large_s *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(mp_pool_t *pool, size_t size) { unsigned char *m; mp_node_t *p; // 1、用户申请的是小块内存 if (size <= pool->max) { // 指向第一个要遍历的内存池结点 p = pool->current; // 遍历小块内存链表,寻找可用的空间分配内存 do { // 指针内存对齐 m = mp_align_ptr(p->last, MP_ALIGNMENT); // 当前结点的剩余空间足够分配 if ((size_t)(p->end - m) >= size) { // 更新last指针 p->last = m + size; return m; } // 若当前内存块的剩余内存小于所需内存,则到下一个内存块中寻找 p = p->next; } while (p); // 没找到合适的小块内存,则创建一个新的小块内存 return mp_alloc_block(pool, size); } // 2、用户申请的是大块内存 return mp_alloc_large(pool, size); } void *mp_nalloc(mp_pool_t *pool, size_t size) { unsigned char *m; mp_node_t *p; if (size <= pool->max) { p = pool->current; do { m = p->last; 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_calloc(mp_pool_t *pool, size_t size) { void *p = mp_alloc(pool, size); if (p) { memset(p, 0, size); } return p; } // 内存池释放,只针对大块内存 void mp_free(mp_pool_t *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 ; } } } int main(int argc, char *argv[]) { int size = 1 << 12; mp_pool_t *p = mp_create_pool(size); int i = 0; for (i = 0; i < 10; i++) { void *mp = mp_alloc(p, 512); // mp_free(mp); } // printf("mp_create_pool: %ld\n", p->max); printf("mp_align(123, 32): %d, mp_align(17, 32): %d\n", mp_align(24, 32), mp_align(17, 32)); // printf("mp_align_ptr(p->current, 32): %lx, p->current: %lx, mp_align(p->large, 32): %lx, p->large: %lx\n", mp_align_ptr(p->current, 32), p->current, mp_align_ptr(p->large, 32), p->large); int j = 0; for (i = 0; i < 5; i++) { char *pp = mp_calloc(p, 32); for (j = 0;j < 32;j ++) { if (pp[j]) { printf("calloc wrong\n"); } printf("calloc success\n"); } } //printf("mp_reset_pool\n"); for (i = 0;i < 5;i ++) { void *l = mp_alloc(p, 8192); mp_free(p, l); } mp_reset_pool(p); //printf("mp_destory_pool\n"); for (i = 0;i < 58;i ++) { mp_alloc(p, 256); } mp_destory_pool(p); return 0; }
4、参考
- Nginx 内存池 - 原理解析
- Nginx 内存池源码分析
- 深入理解Nginx(第2版)