【C/C++】手写内存池(三):大块内存的申请、记录与单独释放

前两篇介绍了内存池的基本结构,以及小块内存如何通过 last 指针顺序分配。

小块内存可以直接从内存池节点中取得,但如果一次申请的空间很大,继续从固定大小的节点中分配就不太合适。因此,内存池通常会将申请分为两种情况:

复制代码
小块内存:直接从内存池节点中顺序分配
大块内存:单独调用 malloc 或 posix_memalign 申请

本文重点介绍大块内存为什么需要单独处理,以及内存池如何记录和释放这些大块内存。

一、为什么要区分小块内存和大块内存

内存池创建时,会设置一个小块内存的最大申请值:

复制代码
#define MP_PAGE_SIZE              4096
#define MP_MAX_ALLOC_FROM_POOL    (MP_PAGE_SIZE - 1)

内存池结构中使用 max 保存该值:

复制代码
struct mp_pool_s {
    size_t max;                  // 小块内存的最大申请值
    struct mp_node_s *current;   // 当前优先分配的节点
    struct mp_large_s *large;    // 大块内存记录链表
    struct mp_node_s head[0];    // 第一个小块内存节点
};

创建内存池时:

复制代码
pool->max = size < MP_MAX_ALLOC_FROM_POOL
          ? size
          : MP_MAX_ALLOC_FROM_POOL;

假设创建了一个大小为 4096 字节的内存池,那么:

复制代码
pool->max = 4095

调用 mp_alloc() 时,会根据申请大小选择不同的分配方式:

复制代码
void *mp_alloc(struct mp_pool_s *pool, size_t size)
{
    if (pool == NULL || size == 0) {
        return NULL;
    }

    if (size <= pool->max) {
        // 小块内存:从已有内存节点中分配
        return mp_alloc_small(pool, size);
    }

    // 大块内存:单独调用malloc申请
    return mp_alloc_large(pool, size);
}

之所以不让大块内存直接占用小块内存节点,主要有以下几个原因:

复制代码
1. 大块申请可能一次占满整个节点;

2. 会造成节点中剩余空间难以继续利用;

3. 频繁的大块申请会导致节点链表快速增长;

4. 大块内存通常需要支持单独释放;

5. malloc更适合处理较大的独立内存申请。

例如,一个内存节点只有 4096 字节,如果申请 8192 字节,显然无法直接从当前节点中分配。

即使申请 3000 字节可以勉强放下,也可能导致剩余空间过小,后续很难继续利用。

因此,可以将内存申请分成两条路径:

复制代码
mp_alloc(pool, 128)
        ↓
128 <= pool->max
        ↓
从小块内存节点中分配


mp_alloc(pool, 8192)
        ↓
8192 > pool->max
        ↓
调用malloc单独申请

二、大块内存链表如何记录申请结果

大块内存由 malloc() 单独申请,但内存池销毁时必须知道这些地址,否则无法统一释放。

因此,需要定义一个大块内存记录结构:

复制代码
struct mp_large_s {
    struct mp_large_s *next; // 指向下一条大块内存记录
    void *alloc;             // 保存实际申请到的内存地址
};

需要注意,mp_large_s 本身不是那块大内存,它只是一个管理记录。

假设申请了三块大内存:

复制代码
void *p1 = mp_alloc(pool, 8192);
void *p2 = mp_alloc(pool, 10000);
void *p3 = mp_alloc(pool, 16384);

内存之间的关系可以表示为:

复制代码
pool->large
     |
     v
+-------------+     +-------------+     +-------------+
| alloc = p3  | --> | alloc = p2  | --> | alloc = p1  |
| next        |     | next        |     | next = NULL |
+-------------+     +-------------+     +-------------+
      |                    |                    |
      v                    v                    v
  16384字节             10000字节              8192字节

其中:

复制代码
large节点:记录大块内存的信息
alloc指针:指向真正由malloc申请的空间

大块内存记录采用头插法:

复制代码
large->next = pool->large;
pool->large = large;

每次新增记录都会成为链表头节点。

这样做不需要遍历链表尾部,插入操作的时间复杂度为:

复制代码
O(1)

另外,大块内存记录结构本身比较小,可以直接从小块内存池中申请:

复制代码
large = mp_alloc(
    pool,
    sizeof(struct mp_large_s)
);

因为 sizeof(struct mp_large_s) 通常远小于 pool->max,所以这次调用会进入小块内存分配流程,不会再次进入大块分配,因而不会形成无限递归。


三、大块内存的完整申请过程

大块内存申请函数如下:

复制代码
/**
 * 单独申请一块大内存,并将地址记录到large链表中
 */
static void *mp_alloc_large(
    struct mp_pool_s *pool,
    size_t size)
{
    if (pool == NULL || size == 0) {
        return NULL;
    }

    // 第一步:使用malloc申请真正的大块内存
    void *p = malloc(size);

    if (p == NULL) {
        return NULL;
    }

    /*
     * 第二步:尝试复用之前已经被释放的管理节点。
     *
     * 大块内存被单独释放后,
     * 对应记录节点的alloc会被设置为NULL。
     */
    size_t count = 0;
    struct mp_large_s *large;

    for (large = pool->large;
         large != NULL;
         large = large->next) {

        if (large->alloc == NULL) {
            // 找到空闲记录,直接保存新地址
            large->alloc = p;
            return p;
        }

        /*
         * 不需要遍历特别长的链表。
         * 检查有限数量的记录后仍未找到,
         * 就直接创建新的管理节点。
         */
        if (count++ > 3) {
            break;
        }
    }

    /*
     * 第三步:没有可复用的记录节点,
     * 从小块内存池中申请一个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;
}

整个过程可以概括为:

复制代码
申请大小超过pool->max
        ↓
调用malloc申请大块内存
        ↓
检查large链表中是否存在空闲记录
        ↓
存在:复用原来的记录节点
        ↓
不存在:从小块内存池申请新的记录节点
        ↓
将记录插入large链表头部
        ↓
返回大块内存地址

这里有一个非常重要的错误处理:

复制代码
large = mp_alloc(pool, sizeof(struct mp_large_s));

if (large == NULL) {
    free(p);
    return NULL;
}

如果大块内存已经通过 malloc() 申请成功,但管理节点申请失败,必须立即释放大块内存。

否则,函数返回后就没有任何指针能够管理这块空间,会产生内存泄漏。

正确的资源创建顺序是:

复制代码
申请大块内存
        ↓
创建管理节点
        ↓
管理节点创建失败
        ↓
释放已经申请的大块内存

四、大块内存如何单独释放和对齐申请

小块内存采用顺序分配,通常不会逐个释放;大块内存由 malloc() 单独申请,因此可以支持单独释放。

释放函数如下:

复制代码
/**
 * 释放由当前内存池申请的大块内存
 *
 * 注意:该函数只负责释放large链表中的大块内存,
 * 不能单独释放小块内存。
 */
void mp_free(struct mp_pool_s *pool, void *p)
{
    if (pool == NULL || p == NULL) {
        return;
    }

    struct mp_large_s *large;

    // 遍历大块内存记录链表
    for (large = pool->large;
         large != NULL;
         large = large->next) {

        // 找到对应的大块内存地址
        if (p == large->alloc) {
            free(large->alloc);

            /*
             * 管理节点本身属于小块内存池,
             * 不进行单独释放。
             *
             * 将alloc设置为NULL,
             * 后续申请大块内存时可以复用该记录。
             */
            large->alloc = NULL;

            return;
        }
    }
}

假设链表原来为:

复制代码
pool->large
     |
     v
large3 → large2 → large1
  |         |         |
  p3        p2        p1

调用:

复制代码
mp_free(pool, p2);

之后:

复制代码
pool->large
     |
     v
large3 → large2 → large1
  |         |         |
  p3       NULL       p1

这里不会删除 large2 节点,而是将:

复制代码
large2->alloc = NULL;

下一次申请大块内存时,可以直接复用这条记录:

复制代码
申请新的大块内存p4
        ↓
发现large2->alloc == NULL
        ↓
large2->alloc = p4

这样可以避免频繁申请新的管理节点。

对齐的大块内存申请

有些场景不仅要求申请一块大内存,还要求地址满足特定的对齐值,例如:

  • 32 字节对齐;
  • 64 字节对齐;
  • 页对齐;
  • SIMD 指令使用的内存;
  • 某些硬件缓冲区。

可以提供 mp_memalign()

复制代码
/**
 * 申请一块满足指定对齐值的大内存
 */
void *mp_memalign(
    struct mp_pool_s *pool,
    size_t size,
    size_t alignment)
{
    if (pool == NULL ||
        size == 0 ||
        alignment == 0) {
        return NULL;
    }

    void *p = NULL;

    // 申请满足alignment要求的内存
    int ret = posix_memalign(
        &p,
        alignment,
        size
    );

    if (ret != 0) {
        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链表头部
    large->next = pool->large;
    pool->large = large;

    return p;
}

使用方式如下:

复制代码
// 申请8192字节,并保证起始地址按64字节对齐
void *buffer = mp_memalign(pool, 8192, 64);

if (buffer == NULL) {
    printf("对齐内存申请失败\n");
}

这块内存同样会被记录在 large 链表中,因此可以使用:

复制代码
mp_free(pool, buffer);

进行单独释放。


五、完整使用示例与注意事项

下面给出一个大块内存申请和释放的简单示例:

复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    // 创建4096字节的小块内存池
    struct mp_pool_s *pool =
        mp_create_pool(4096);

    if (pool == NULL) {
        printf("内存池创建失败\n");
        return -1;
    }

    /*
     * 128字节小于pool->max,
     * 从小块内存节点中分配。
     */
    void *small = mp_alloc(pool, 128);

    /*
     * 8192字节大于pool->max,
     * 通过malloc单独申请,
     * 并记录到large链表。
     */
    void *large1 = mp_alloc(pool, 8192);

    /*
     * 申请一块64字节对齐的大内存。
     */
    void *large2 =
        mp_memalign(pool, 16384, 64);

    printf("small  = %p\n", small);
    printf("large1 = %p\n", large1);
    printf("large2 = %p\n", large2);

    /*
     * 大块内存支持单独释放。
     */
    mp_free(pool, large1);
    mp_free(pool, large2);

    /*
     * small属于小块内存,
     * 不能通过mp_free单独释放。
     *
     * 最终销毁或重置内存池时统一处理。
     */

    mp_destory_pool(pool);

    return 0;
}

大块内存的完整管理流程为:

复制代码
调用mp_alloc申请内存
        ↓
判断size是否超过pool->max
        ↓
超过:调用malloc单独申请
        ↓
创建或复用large管理节点
        ↓
将地址记录到large链表
        ↓
返回给用户使用
        ↓
调用mp_free单独释放
        ↓
将管理节点的alloc设置为NULL
        ↓
后续大块申请可以复用该记录

使用时需要注意以下几点:

复制代码
1. mp_free主要用于释放大块内存;

2. 小块内存不能单独归还,只能统一重置或销毁;

3. 不能使用mp_free释放不属于当前内存池的地址;

4. 同一个大块地址不能重复释放;

5. 管理节点申请失败时,必须释放已经申请的大块内存;

6. 内存池销毁时,还要遍历large链表释放未手动释放的大块内存。

另外,在创建第一个小块内存节点时,需要确保链表指针被正确初始化:

复制代码
pool->head->next = NULL;

否则后续遍历节点链表时,可能访问未初始化的随机地址。

大块内存管理的核心可以总结为:

复制代码
小块内存从池内顺序分配;
大块内存通过malloc单独申请;
large链表记录所有大块地址;
大块内存可以通过mp_free单独释放;
空闲的large记录可以继续复用;
销毁内存池时统一清理剩余大块内存。

0voice · GitHub

相关推荐
一只小菜鸡..3 小时前
Stanford CS144 学习笔记 (二):传输层与数据通信机制
网络·笔记·学习
超防局3 小时前
网络安全渗透测试常用工具详解
网络·安全·web安全
峥嵘life4 小时前
Andorid 打开热点,关闭有线网,ifconfig卡死分析解决
java·服务器·网络
瓦学妹4 小时前
代理IP连接失败怎么办?2026代理IP异常排查与解决方法
网络·网络协议·tcp/ip
hehelm5 小时前
AI 大模型接入 SDK —项目概述
linux·服务器·网络·数据库·c++
国科安芯5 小时前
抗辐射微控制器在卫星电源管理分系统中的控制架构与可靠性研究——基于AS32S601型MCU的星载能源系统智能化管理技术分析
网络·单片机·嵌入式硬件·安全·架构·系统架构·能源
大郭鹏宇5 小时前
下篇:LangChain Agent 智能体从入门到实战(二)—— 记忆管理、中间件与最佳实践
网络·数据库·人工智能
Fnetlink15 小时前
Fnet 云网安 260717
网络·安全·网络安全
小白学大数据6 小时前
企业内网场景下 Python 自定义 CA 证书信任链的正确配置方法
开发语言·网络·python·信任链