内存池的实现

简介: 内存池的实现

1、使用内存池的原因

开源组件:tcmalloc/jemalloc

内存池是对堆上的内存池进行管理。服务器长时间运行,会产生莫名其妙的问题,大多数都是内存问题 。

为什么需要对内存管理?

  • 避免频繁的系统调用带来的开销。
  • 减少了频繁分配和释放小块内存产生的内存碎片。

解决上述问题,最好的方法就是内存池。

2、内存池的工作流程

内存池的具体做法是固定大小、提前申请、重复利用。

  • 固定大小。在调用内存分配函数的时候,小块内存每次都分配固定大小的内存块,这样避免了内存碎片产生的可能性。
  • 提前申请一块大的内存,内存不够用时再二次分配,减少了malloc的次数,提高了效率。

本文参考 nginx 内存池的设计,实现一个简易版的内存池,工作流程如图所示

1704877404498.jpg

nginx 内存池工作流程


3、内存池的实现

3.1、数据结构设计


1704877417957.jpg

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

1704877436624.jpg

小块内存分配


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、参考

相关文章
|
机器学习/深度学习 人工智能 编解码
AI人像特效之「一键生成N次元虚拟形象」
为了零成本低门槛地提供极致酷炫的人像玩法,我们提出了一套人像风格化通用框架「AI Maleonn」AI 版神笔马良,用于一键生成风格百变的人物虚拟形象,在风格上涵盖手绘、3D、日漫、艺术特效、铅笔画等多种风格,同时可以支持面向小样本的专属风格定制,利用少量目标风格图即可实现快速迁移拓展;在处理维度上,不仅适用于生成头部效果,更支持全图精细化纹理转换,兼容多人场景;在模型鲁棒性上,有效克服了多角度姿态、面部遮挡等各类复杂场景,整体稳定性大大提升。
|
存储 Prometheus 监控
评估系统的可用性时间
评估系统可用性时间是指对系统在预定时间内正常运行的能力进行测量和分析,以确保其稳定性和可靠性满足用户需求。这通常涉及对系统故障率、恢复时间和维护周期的综合考量。
|
数据采集 Python
数据爬取技术进阶:从表单提交到页面点击的实现
本文介绍了如何使用 Python 和代理 IP 技术,从表单提交到页面点击,实现动态网页的数据爬取。以百度贴吧为例,详细讲解了登录、发帖和数据采集的实现流程,并提供了完整的代码示例。通过代理 IP 确保数据获取的稳定性和安全性。
475 3
|
算法 数据可视化 图形学
网络通信系统的voronoi图显示与能耗分析matlab仿真
在MATLAB2022a中,该程序模拟了两层基站网络,使用泊松分布随机生成Macro和Micro基站,并构建Voronoi图。它计算每个用户的信号强度,选择最强连接,并分析SINR和数据速率。程序还涉及能耗计算,包括传输、接收、处理和空闲能耗的分析。Voronoi图帮助可视化网络连接和优化能源效率。
|
人工智能 API 决策智能
【AI Agent系列】【阿里AgentScope框架】实战1:利用AgentScope实现动态创建Agent和自由组织讨论
【AI Agent系列】【阿里AgentScope框架】实战1:利用AgentScope实现动态创建Agent和自由组织讨论
1995 3
|
数据采集 机器学习/深度学习 搜索推荐
Python自动化:关键词密度分析与搜索引擎优化
Python自动化:关键词密度分析与搜索引擎优化
|
机器学习/深度学习 数据可视化 数据挖掘
R语言包管理:如何使用CRAN与Bioconductor
【8月更文挑战第28天】CRAN和Bioconductor是R语言包的两个重要来源,分别覆盖了广泛的科学计算和生物信息学领域。通过掌握CRAN和Bioconductor的包管理技巧,用户可以更加高效地利用R语言进行数据分析、统计建模和生物信息学研究。在实际应用中,建议根据具体需求选择合适的包,并合理设置镜像站点以提高下载速度。同时,定期更新和卸载不再需要的包,有助于保持R环境的整洁和高效。
|
Web App开发
Chrome 126版本 打印预览失败 #85
Chrome 126版中出现了打印预览功能失效的问题(#85)。目前有两种解决方案:一是在chrome.exe目录为“ALL APPLICATION PACKAGES”用户组设置适当权限;二是等待内核修复,或通过添加启动参数&quot;--disable-features=PrintCompositorLPAC&quot;来暂时解决此问题。
1853 1
|
弹性计算 运维 负载均衡
SLB介绍
SLB产品介绍
1021 4
|
人工智能 安全 Ubuntu
操作系统加码主动防护:数智化有了“安全底座”
操作系统的发展遵循约20年的周期律,从大型机到个人电脑,再到互联网时代,每次变革都催生了新的应用和市场。当前,随着数字化、智能化的浪潮,操作系统再次迎来革新机遇。以Linux为代表的开源操作系统,凭借其开放性和安全性,在服务器市场占据主导地位。面对AI时代的安全挑战,openEuler提出“OS for AI,AI for OS”理念,通过复式内核设计和AI技术,强化了系统的安全性和性能,展现了中国在开源领域的创新实力。
201 0