基础组件(二):内存池


一、内存池的实现与场景分析

1. 为什么要有内存池?

代码长期运行之后,由于使用传统的内存分配方法(malloc,free)在频繁的内存分配和释放过程中会导致内存碎片化,引起coredump。而且频繁的内存分配和释放过程频繁的系统调用会导致较大的系统开销,这时候就需要使用内存池进行统一的管理,预先分配一块连续的内存空间,并在需要时直接从中取出或回收,避免频繁进行系统调用,从而提高了程序性能。

提炼:
内存池是一种高效的内存管理机制,它可以提高性能和可靠性,减少内存分配和释放操作,避免频繁地调用系统的内存分配函数,减少内核态和用户态之间的切换次数,同时还可以减少内存碎片的产生,提高内存的利用率。

内存池的好处 (虚拟内存)

  • 减少内存分配和释放操作
    在传统的内存管理方式中,每次分配内存时都需要调用系统的内存分配函数(如malloc),这会涉及到内核态和用户态之间的频繁切换,造成较大的开销。而内存池则可以预先分配一定数量的内存,将其组织为一组内存块,并在需要时从内存池中分配内存。这样可以避免频繁地调用系统的内存分配函数,减少内核态和用户态之间的切换次数,从而提高程序的性能。

  • 减少内存碎片的产生
    在传统的内存管理方式中,频繁地分配和释放内存会导致内存碎片的产生,从而降低内存的利用率。而内存池可以预先分配一定数量的内存,并将其组织为一组内存块,这样可以避免频繁地分配和释放内存,从而减少内存碎片的产生,提高内存的利用率。

内存池场景分析

不同的场景下,内存池的设计是不一样的,以下是几个举例:

  • (1) 短链接和长连接
    在kv存储中,经常以短链接为key,长链接为value。长链接的长度一般是固定的,短链接的长度一般也在某个区间内。我们在这种场景下只需要考虑固定分配的内存块。大块内存,不均匀内存等情况是不需要考虑的。
  • (2)为每一个连接单独创建内存池
    这种情况下内存池建立的时间和释放的时间和建立连接、断开连接的时点是一致的,内存块的大小根据业务场景的不同而定。
  • (3)实现文章的存储
    每篇文章的长短大小不一,需要的内存大小也不一。设计内存池的不需要考虑得面面俱到,能把单一的场景解决得很好即可。比较通用且开源的内存池框架有jemalloc和tcmalloc等。

内存池框架jemalloc与tcmalloc对比

jemalloc

  • 设计理念: jemalloc不仅考虑了内存分配的性能,还注重对内存碎片的管理。
  • 内存管理策略
    多级分配: 将内存划分为不同的大小类别,每个类别有独立的分配器。
    内存碎片管理:用算法来减少内存碎片, 会尝试将空闲内存块合并,以提供更大的连续内存空间。
  • 使用场景:适用于对内存碎片敏感、需要处理各种大小内存分配的应用程序。

tcmalloc

  • 设计理念: tcmalloc更侧重于为多线程应用程序提供高性能的内存分配。
  • 内存管理策略
    线程本地缓存:每个线程都有自己的本地缓存,当线程需要分配内存时,首先会在本地缓存中查找可用的内存块,避免了线程之间的锁竞争。
    中央自由列表:用于管理全局的空闲内存块。当线程的本地缓存满了或者需要释放内存时,会将内存块放回中央自由列表。
  • 使用场景:更适合多线程应用程序,特别是那些对内存分配速度要求较高的场景。

2. 内存池与malloc/free比较

分配效率方面

内存池:在程序启动时预先从操作系统申请一大块内存。后续程序需要内存时,直接从内存池中分配,无需再进行系统调用,大大减少了时间开销,显著提升了内存分配的效率。

malloc/free:是 C 语言标准库提供的内存分配与释放函数。每次调用malloc时,它会向操作系统发起内存分配请求,这涉及用户态到内核态的切换,需要一定的时间开销。尤其在频繁分配小块内存的场景下,这种开销会不断累积,严重影响程序运行效率。

内存碎片方面

内存池:通过对预先申请的大块内存进行管理,能有效减少内存碎片。它可以采用特定的数据结构和算法,将内存分割成大小固定或按一定规则的小块进行分配。当小块内存被释放时,内存池能方便地将其重新整合,避免了内存碎片化问题,使内存空间始终保持较高的利用率。

malloc/free:频繁的内存分配和释放极易产生内存碎片。当程序分配不同大小的内存块后又释放部分块时,内存空间会变得零散。

外部碎片:表现为内存中存在许多分散的、不连续的小空闲区域,这些空闲区域分布在已分配的内存块之间

内部碎片:表现为每个已分配的内存块内部存在未被利用的空闲部分,它是隐藏在已分配内存块内部的

使用场景区别

内存池:适合对内存分配效率和内存管理要求较高的场景。

malloc/free:适用于内存分配需求较为简单、内存使用量不大且对性能要求不是特别苛刻的场景。

3. 内存池的实现

(1)定长分配(以二的倍数划分内存区域)

定长分配是一种内存管理策略,它将一块连续的内存空间划分为多个固定大小的内存块。

内存池结构体定义

typedef struct mempool_s {
    int blocksize;    // 每个内存块的大小(单位:字节)
    int freecount;    // 当前内存池中未分配(空闲)的内存块数量
    char *free_ptr;   // 指向下一个可分配内存块的指针,实际上构成一个链表,链接所有空闲块
    char *mem;        // 指向整个分配到的内存区域的起始地址
} mempool_t;

内存池初始化:
for循环里的*(char *)ptr = ptr + block_size;这一句,前面表示把ptr强行转换为指向char (字符串指针类型)的指针,然后解引用,使得分配出来的内存块前四个字节储存下一个内存块的首地址。

int memp_create(mempool_t *m, int block_size) {
    if (!m) return -1; // 检查传入的内存池指针是否为空
 
    m->blocksize = block_size;                         // 设置每个块的大小
    m->freecount = MEM_PAGE_SIZE / block_size;           // 计算内存页中可容纳的块数
 
    m->mem = (char *)malloc(MEM_PAGE_SIZE);              // 分配一页内存
    if (!m->mem) {                                       // 分配失败则返回错误码
        return -2;
    }
    memset(m->mem, 0, MEM_PAGE_SIZE);                    // 将分配的内存初始化为0
 
    m->free_ptr = m->mem;                                // 初始化空闲指针,指向内存区域起始位置
 
    // 构建空闲块链表:利用每个内存块的前几个字节存储下一个空闲块的地址
    int i = 0;
    char *ptr = m->mem;
    for (i = 0; i < m->freecount; i++) {
        // 将当前内存块的前几个字节设置为指向下一个块的地址
        // (char **)ptr:将ptr转换为指向char*的指针,再解引用赋值
        *(char **)ptr = ptr + block_size;
        ptr = ptr + block_size; // 移动指针到下一个内存块
    }
    *(char **)ptr = NULL; // 最后一个块的“下一个块地址”置为NULL,表示链表末尾
 
    return 0;
}

销毁内存池:

void memp_destory(mempool_t *m) {
    if (!m) return;         // 参数检查:若m为空则直接返回
    free(m->mem);           // 释放之前分配的内存区域
}

从内存池中分配一个内存块:

void *memp_alloc(mempool_t *m) {
    // 检查内存池是否存在或空闲块数量是否为0
    if (!m || m->freecount == 0) return NULL;
 
    void *ptr = m->free_ptr;  // 保存当前空闲块的地址
 
    // 更新free_ptr,指向链表中下一个空闲块(当前块的前几个字节存储了下一个块的地址)
    m->free_ptr = *(char **)ptr;
    m->freecount--;         // 空闲块数量减1
 
    return ptr;             // 返回分配的内存块地址
}

归还内存块,回收内存块并将其插入到空闲链表头部:

void memp_free(mempool_t *m, void *ptr) {
    // 将归还内存块的前几个字节设置为当前空闲块链表的头部指针
    *(char **)ptr = m->free_ptr;
    m->free_ptr = (char *)ptr;  // 更新空闲块链表头为当前归还的块
    m->freecount++;             // 空闲块数量加1
}

源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
// 定义内存页的大小为 4096 字节
#define MEM_PAGE_SIZE	0x1000  
 
// 定义内存池结构体
typedef struct mempool_s {
    // 每个内存块的大小
    int blocksize;
    // 空闲内存块的数量
    int freecount;
    // 指向空闲内存块链表的头指针
    char *free_ptr;
    // 指向内存池的起始地址
    char *mem;
} mempool_t; 
 
// 创建内存池
// 参数: m 为指向内存池结构体的指针, block_size 为每个内存块的大小
// 返回值: 成功返回 0, 失败返回 -1 或 -2
int memp_create(mempool_t *m, int block_size) {
    // 检查指针是否为空
    if (!m) return -1;
    // 设置每个内存块的大小
    m->blocksize = block_size;
    // 计算空闲内存块的数量
    m->freecount = MEM_PAGE_SIZE / block_size;
    // 分配内存页
    m->mem = (char *)malloc(MEM_PAGE_SIZE); 
    // 检查内存分配是否成功
    if (!m->mem) { 
        return -2;
    }
    // 将分配的内存页初始化为 0
    memset(m->mem, 0, MEM_PAGE_SIZE); 
    // 空闲内存块链表的头指针指向内存池的起始地址
    m->free_ptr = m->mem;
 
    int i = 0;
    char *ptr = m->mem;
    // 构建空闲内存块链表
    for (i = 0; i < m->freecount; i++) {
        // 将当前内存块的下一个指针指向下一个内存块的地址
        *(char **)ptr = ptr + block_size;
        // 移动指针到下一个内存块
        ptr = ptr + block_size;
    } 
    // 最后一个内存块的下一个指针置为 NULL
    *(char **)ptr = NULL;
 
    return 0;
}
 
// 销毁内存池
// 参数: m 为指向内存池结构体的指针
void memp_destory(mempool_t *m) {
    // 检查指针是否为空
    if (!m) return;
    // 释放内存池分配的内存
    free(m->mem);
}
 
// 从内存池中分配一个内存块
// 参数: m 为指向内存池结构体的指针
// 返回值: 成功返回分配的内存块的指针, 失败返回 NULL
void *memp_alloc(mempool_t *m) {
    // 检查指针是否为空或没有空闲内存块
    if (!m || m->freecount == 0) return NULL;
    // 获取当前空闲内存块链表的头指针
    void *ptr = m->free_ptr;
    // 更新空闲内存块链表的头指针为下一个空闲内存块
    m->free_ptr = *(char **)ptr;
    // 空闲内存块数量减 1
    m->freecount--;
    return ptr;
}
 
// 将内存块释放回内存池
// 参数: m 为指向内存池结构体的指针, ptr 为要释放的内存块的指针
void memp_free(mempool_t *m, void *ptr) {
    // 将释放的内存块插入到空闲内存块链表的头部
    *(char **)ptr = m->free_ptr;
    // 更新空闲内存块链表的头指针为释放的内存块
    m->free_ptr = (char *)ptr;
    // 空闲内存块数量加 1
    m->freecount++;
}
 
// 主函数, 用于测试内存池的功能
int main() {
    // 定义一个内存池结构体变量
    mempool_t m;
    // 创建内存池, 每个内存块大小为 32 字节
    memp_create(&m, 32);
    // 从内存池中分配一个内存块
    void *p1 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p1);
    // 从内存池中分配一个内存块
    void *p2 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p2);
    // 从内存池中分配一个内存块
    void *p3 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p3);
    // 将内存块 p2 释放回内存池
    memp_free(&m, p2);
    return 0;
}

1.内存初始化

在memp_create函数中,会分配一块大小为MEM_PAGE_SIZE的连续内存空间作为内存池。然后,根据指定的block_size将内存池划分为多个固定大小的内存块,并通过链表的方式将这些空闲内存块连接起来。
请添加图片描述

2.内存分配

在memp_alloc函数中,当需要分配内存时,会从空闲内存块链表的头部取出一个内存块,并将空闲内存块链表的头指针指向下一个空闲内存块。同时,空闲内存块的数量减 1。

3.内存释放

在memp_free函数中,当需要释放内存时,会将释放的内存块插入到空闲内存块链表的头部,并更新空闲内存块链表的头指针。同时,空闲内存块的数量加 1。


(2)不定长分配 (不同大小的内存块)

源码
#include <stdio.h>
#include <stdlib.h>
 
// 内存池小块节点结构:用于管理内存池中的内存区域(适合小块分配)
// 每个节点管理一段连续内存,其内存范围为 [last, end)
typedef struct mp_node_s {
    unsigned char *last;      // 当前节点中已分配内存的末尾位置,下一次分配从此处开始
    unsigned char *end;       // 当前节点内存区域的结束地址
    struct mp_node_s *next;   // 指向下一个内存节点的指针
} mp_node_t;
 
// 大块内存链表节点结构:用于记录那些大于内存池最大管理范围的分配请求
typedef struct mp_large_s {
    struct mp_large_s *next;  // 指向下一个大块内存记录节点
    void *alloc;              // 实际通过 malloc 分配的大块内存地址
} mp_large_t;
 
// 内存池主结构:包含小块内存链表和大块内存链表信息
typedef struct mp_pool_s {
    size_t max;               // 内存池中单个小块分配的最大尺寸,超过此值的分配被认为是大块分配
    struct mp_node_s *head;   // 小块内存链表头指针,管理连续内存区域(小块分配)
    struct mp_large_s *large; // 大块内存链表头指针,管理通过 malloc 分配的大块内存
} mp_pool_t;
 
 
/**
 * 函数:mp_create
 * 作用:创建并初始化内存池
 * 参数:
 *   pool - 指向内存池主结构的指针
 *   size - 内存池初始分配的总大小(例如 4096 字节)
 * 过程:
 *   - 检查参数有效性
 *   - 分配一块内存,并将其作为第一个内存节点
 *   - 初始化节点的 last 和 end 指针,并将 next 置为 NULL
 *   - 设置内存池的 max 为传入的 size(也可以视需求将 max 设为一个较小值)
 *   - 初始化大块内存链表为空
 * 返回值:
 *   0 表示成功,非0表示错误
 */
int mp_create(mp_pool_t *pool, size_t size) {
    if (!pool || size <= 0) return -1; // 参数检查
 
    // 分配 size 大小的内存空间,作为内存池中的第一个节点
    void *mem = malloc(size);
    if (!mem) return -1; // 检查 malloc 是否成功
 
    // 将分配的内存空间转为内存节点,并初始化节点各字段
    mp_node_t *node = (mp_node_t *)mem;
    // last 指针从节点结构体后面开始,即预留出 mp_node_t 的空间
    node->last = (unsigned char *)mem + sizeof(mp_node_t);
    node->end = (unsigned char *)mem + size; // 结束地址为内存起始地址加上总大小
    node->next = NULL; // 当前节点没有后续节点
 
    // 初始化内存池结构体
    pool->head = node;
    pool->max = size;      // 这里将 max 设置为整个内存池大小,也可以根据需要设置一个阈值
    pool->large = NULL;    // 初始化大块分配链表为空
 
    return 0;
}
 
/**
 * 函数:mp_destory
 * 作用:销毁内存池,释放所有分配的内存(包括大块和小块)
 * 参数:
 *   pool - 指向内存池主结构的指针
 * 过程:
 *   - 遍历大块内存链表,释放所有大块内存
 *   - 遍历小块内存节点链表,释放所有节点(每个节点本身是一块通过 malloc 分配的内存)
 */
void mp_destory(mp_pool_t *pool) {
    mp_large_t *l;
    // 遍历大块内存链表,释放所有大块内存
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            free(l->alloc);
        }
    }
    pool->large = NULL;
 
    // 遍历小块内存节点链表,释放所有节点
    mp_node_t *node = pool->head;
    while (node) {  // 注意:应当是 while(node),而非 while(!node)
        mp_node_t *tmp = node->next;
        free(node);
        node = tmp;
    }
}
 
/**
 * 函数:mp_alloc_block
 * 作用:当现有的小块节点中没有足够空间分配时,
 *       分配一个新的内存节点(块),并将其连接到内存池链表末尾,
 *       然后在新节点中进行内存分配
 * 参数:
 *   pool - 指向内存池主结构的指针
 *   size - 需要分配的内存大小
 * 过程:
 *   - 分配一个新的内存节点,大小同 pool->max(或其它预设大小)
 *   - 初始化新节点的 last、end 和 next 字段
 *   - 将新节点插入到小块内存链表的尾部
 *   - 在新节点中分配内存,并返回分配的地址
 */
static void *mp_alloc_block(mp_pool_t *pool, size_t size) {
    // 这里代码中有个问题,使用了未定义的变量 mem,应调用 malloc 分配新的内存块
    void *mem = malloc(pool->max);
    if (!mem) return NULL; // 检查内存分配是否成功
 
    mp_node_t *node = (mp_node_t *)mem;
    node->last = (unsigned char *)mem + sizeof(mp_node_t);
    node->end = (unsigned char *)mem + pool->max;
    node->next = NULL;
    
    // 在新节点中分配 size 大小内存,分配地址从 last 开始
    void *ptr = node->last;
    node->last += size;
 
    // 将新节点插入到小块内存链表尾部(尾插法)
    mp_node_t *iter = pool->head;
    while (iter->next != NULL) {
        iter = iter->next;
    }
    iter->next = node;
 
    return ptr;
}
 
/**
 * 函数:mp_alloc_large
 * 作用:用于分配大块内存,即分配大小超过内存池最大小块分配尺寸的内存
 * 参数:
 *   pool - 指向内存池主结构的指针
 *   size - 请求分配的大块内存大小
 * 过程:
 *   - 使用 malloc 分配大块内存
 *   - 遍历大块内存链表,查找是否存在空闲节点(alloc 为 NULL)
 *     若存在则复用该节点,否则创建一个新的大块节点,插入到大块内存链表中
 * 返回值:
 *   返回分配的大块内存地址,若分配失败返回 NULL
 */
static void *mp_alloc_large(mp_pool_t *pool, size_t size) {
    if (!pool) return NULL;
 
    // 分配大块内存
    void *ptr = malloc(size);
    if (ptr == NULL) return NULL;
 
    // 遍历大块内存链表,寻找空闲节点(alloc 为 NULL)的节点
    mp_large_t *l;
    for (l = pool->large; l; l = l->next) {
        if (l->alloc == NULL) {
            l->alloc = ptr;
            return ptr;
        }
    }
    
    // 如果没有找到空闲节点,则需要创建新的大块内存记录节点
    l = mp_alloc(pool, sizeof(mp_large_t));
    if (l == NULL) {
        free(ptr);
        return NULL;
    }
    l->alloc = ptr;
 
    // 将新创建的大块节点插入到大块内存链表的头部
    l->next = pool->large;
    pool->large = l;
    
    return ptr;
}
 
/**
 * 函数:mp_alloc
 * 作用:根据请求分配内存
 * 参数:
 *   pool - 指向内存池主结构的指针
 *   size - 请求分配的内存大小
 * 过程:
 *   - 如果请求的大小大于内存池的最大小块分配尺寸(pool->max),
 *     则调用 mp_alloc_large 进行大块分配
 *   - 否则,遍历小块内存节点链表,查找是否有足够空间分配的节点,
 *     如果找到,则在节点中分配内存,并更新节点的 last 指针
 *   - 若所有节点均没有足够空间,则调用 mp_alloc_block 分配一个新的节点,
 *     并在该节点中分配内存
 * 返回值:
 *   返回分配内存的地址,若分配失败则返回 NULL
 */
void *mp_alloc(mp_pool_t *pool, size_t size) {
    if (size > pool->max) {
        // 对于超过最大小块尺寸的分配请求,调用大块分配函数
        return mp_alloc_large(pool, size);
    } 
 
    // 对于小块分配,从内存池的小块内存链表中查找合适的空间
    void *ptr = NULL;
    mp_node_t *node = pool->head;
 
    // 遍历所有节点,查找当前节点是否有足够剩余空间
    do {
        if (node->end - node->last > size) {
            ptr = node->last;       // 找到足够空间,记录分配地址
            node->last += size;     // 更新该节点的 last 指针
            return ptr;
        }
        node = node->next;
    } while (node);
 
    // 如果遍历完所有节点均无足够空间,则分配一个新的节点,并在新节点中分配内存
    return mp_alloc_block(pool, size);
}
 
/**
 * 函数:mp_free
 * 作用:释放大块内存(对于小块内存,由于内存池机制,通常不单独释放)
 * 参数:
 *   pool - 指向内存池主结构的指针
 *   ptr  - 要释放的内存块地址
 * 过程:
 *   - 遍历大块内存链表,查找分配记录中是否有该内存块
 *   - 如果找到,则释放该内存块,并将对应节点的 alloc 置为 NULL
 */
void mp_free(mp_pool_t *pool, void *ptr) {
    mp_large_t *l;
    for (l = pool->large; l; l = l->next) {
        if (l->alloc == ptr) {
            free(l->alloc);
            l->alloc = NULL;
            return;
        }
    }
    // 对于小块内存,内存池一般在销毁时一次性释放,
    // 因此这里不单独处理小块内存的释放操作
}
 
1.内存池结构
 mp_pool_t:这是内存池的核心结构体,包含三个重要成员
     max:表示内存池节点可分配的最大内存大小
     head:指向内存池节点链表的头节点,这些节点用于分配小内存块
     large:指向大内存块链表的头节点,用于管理超过max大小的大内存块
 mp_node_t:小内存块结构体,用于管理小内存块,last指针指向当前节点未使用内存的起始位置,end指针指向当前节点内存的结束位置,next指针指向下一个节点。
 mp_large_t:大内存块结构体,alloc指针指向实际分配的大内存块,next指针指向下一个大内存块。

请添加图片描述

2.内存分配

小内存块分配:当请求的内存大小size小于等于pool->max时,会遍历内存池节点链表,检查每个节点是否有足够的空间。如果有,则从该节点分配内存并更新last指针;如果所有节点都没有足够的空间,则调用mp_alloc_block函数分配一个新的节点块。

大内存块分配:当请求的内存大小size大于pool->max时,直接调用系统的malloc函数分配大内存块。然后会先遍历大内存块链表,查找是否有未使用的节点,如果有则将其指向新分配的大内存块;如果没有,则从内存池分配一个大内存块结构体,并将其插入到大内存块链表头部。

3.内存释放

大内存块释放:当调用mp_free函数释放内存时,会先遍历大内存块链表,找到要释放的大内存块,然后调用free函数释放该大内存块,并将该大内存块结构体的alloc指针置为 NULL。

小内存块释放:小内存块是按节点整体管理的,在销毁内存池时会统一释放所有节点的内存。


Q: 定长/不定长分配区别

  1. 定长分配是一种内存管理策略,它将一块连续的内存空间划分为多个固定大小的内存块。
    定长分配可以有效地减少内存碎片的产生,提高内存分配和释放的效率。因为每个内存块的大小是固定的,所以在分配和释放内存时不需要进行复杂的内存管理操作,只需要简单地操作链表即可。

  2. 不定长分配核心思想是将内存分配分为小内存块和大内存块两种情况分别处理,以提高内存分配和释放的效率,减少内存碎片

    使用不定长内存池可以高效地处理不同大小的内存分配请求,同时减少了频繁调用系统malloc和free带来的开销和内存碎片问题。


优秀笔记:
1. 池式结构—内存池
2. 内存池的实现与场景分析
参考学习:https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值