图解式讲解内存池:告别内存碎片与随机coredump

1. 为什么要内存池?

1.1 一个很常见的场景

想象一下,你的程序运行了很长时间 (比如服务器进程),过程中不断 mallocfree

复制代码
malloc(32)  → free()
malloc(64)  → free()
malloc(128) → free()
malloc(32)  → free()
malloc(16)  → free()
...

每次分配和释放的大小不一,释放后的空间又可能被下次不同大小的请求填上,大量不连续的空闲小块 就产生了------这就是内存碎片

1.2 内存碎片会导致什么?

  • 随机 coredump:进程明明还有大量空闲内存,但分配请求却失败了(找不到连续空间)
  • 内存利用率下降:明明还有 200 bytes 的空闲空间,但请求 100 bytes 的连续空间却失败
  • 性能下降:碎片化越严重,内存分配器越难找到合适的块

1.3 业界通用方案 vs 定制方案

方案 代表 特点
通用内存分配器 jemalloc、tcmalloc 通用、成熟,但不一定贴合你的业务
自研内存池 业务定制 针对业务场景深度优化,性能更高

通用方案不是万能的,举个例子:

  • 游戏服务器按 16/32/64 字节分块,通用分配器可能做不到这么精细
  • 高频交易系统要求极低延迟,自研内存池可以省掉锁和查找开销

2. 虚拟内存布局 --- 理解堆管理

在聊内存池之前,先搞清楚程序运行时的内存布局:

复制代码
+------------------+  0xFFFFFFFF (4GB)
|    内核空间       |  ← 操作系统内核用,进程不能直接访问
+------------------+
|      栈          |  ← 向低地址增长,函数局部变量放这里
|       ↓          |
|      ...         |
|       ↑          |
|      堆          |  ← 向高地址增长,malloc/free 在这里管理
|   BSS 段         |  ← 未初始化的全局变量
|   Data 段        |  ← 已初始化的全局变量
|   Code 段        |  ← 程序代码
+------------------+  0x00000000

malloc / free 管理的区域。每次调用 malloc,向内核申请内存;调用 free,把内存归还堆。

Linux 中,每个进程有一个 task_struct(进程描述符),其中的 mm_struct 管理内存:

c 复制代码
struct mm_struct {
    unsigned long start_code, end_code;    // 代码段
    unsigned long start_data, end_data;    // 数据段
    unsigned long start_brk, brk;           // 堆
    unsigned long start_stack;              // 栈
    ...
};

3. 内存池的核心思想

3.1 什么是内存池?

预先申请一大块内存,后续分配都从这一大块里切,不用每次都找内核要。

复制代码
不用内存池:
  内核 ← malloc(32)   ← malloc(64)   ← malloc(128)   (每次都找内核)
  内核 ← free()        ← free()       ← free()        (碎片散落)

用内存池:
  内核 ← 一次申请 4096 字节的大块
  内存池内部 ← 切成 32 字节的小块,按需分配
  (碎片被"池"兜住了)

3.2 定长 vs 不定长

类型 说明 适用场景
定长内存池 每块大小固定(如 32 字节),分配简单 对象池、连接池
不定长内存池 块大小不固定,更灵活但实现复杂 通用场景

本文聚焦定长内存池,因为实现更清晰、也更常见。


4. 内存池结构设计

4.1 整体架构

复制代码
mp_pool_s(内存池)
├── max        :每块的大小上限(如 4096)
├── head       :mp_node_s 链表头(管理所有块)
│   ├── last   :当前可用位置指针
│   ├── end    :块末尾指针
│   └── next   :指向下一个节点
└── large      :mp_large_s 链表头(管理超大块 > max)
    ├── alloc  :实际分配的内存指针
    └── next   :指向下一个大块

4.2 为什么要分"小块"和"大块"?

  • max 是阈值:请求 ≤ max 的走小块池;请求 > max 的走大块直接 malloc
  • 原因:太大的请求不适合切成小块放池子里,浪费空间

4.3 最小内存块设计

我们把内存池的基本单元设计成这样:

c 复制代码
struct mempool_s {
    size_t   blocksize;   // 每一块的大小(如 32)
    int      freecount;   // 还有多少块可以用
    unsigned char *free_ptr; // 下次分配的起始位置
    void    *mem;         // 整块内存
};

关键问题:释放完的块如何再次分配?

两种策略:

  1. 空闲链表:每个块头部存一个指针,指向下一个空闲块 → O(1) 分配
  2. 位置标记:从头开始扫描,找到第一个够大的空闲块 → 简单但 O(n)

我们这里用的是从头分配free_ptr 不断后移),简单直接。


5. 代码实现详解

5.1 数据结构

c 复制代码
// 小块节点(链表中每个节点是一整块内存)
typedef struct mp_node_s {
    unsigned char *last;  // 当前可用位置(下一个分配从这里开始)
    unsigned char *end;   // 块的结束位置
    struct mp_node_s *next;
} mp_node_t;

// 大块节点(超过 max 的请求走这里)
typedef struct mp_large_s {
    struct mp_large_s *next;
    void *alloc;          // 实际分配的内存
} mp_large_t;

// 内存池
typedef struct mp_pool_s {
    size_t max;                    // 阈值:超过这个大小走大块
    struct mp_node_s *head;        // 小块链表头
    struct mp_large_s *large;       // 大块链表头
} mp_pool_t;

图示:

复制代码
mp_pool_s
├── max = 4096
├── head ──→ [mp_node] ──→ [mp_node] ──→ NULL
│            last↑      last↑
│            end↑       end↑
│            next──→    next──→
└── large ──→ [mp_large] ──→ NULL
             alloc↑      alloc↑

5.2 创建内存池 mp_create

c 复制代码
int mp_create(mp_pool_t *pool, size_t size) {
    if (pool == NULL || size <= sizeof(mp_node_t)) return -1;
    
    void *mem = malloc(size);
    if (!mem) return -2;
    
    // 第一个节点就是这块内存本身
    mp_node_t *node = (mp_node_t *)mem;
    node->last = (unsigned char *)mem + sizeof(mp_node_t);
    node->end  = (unsigned char *)mem + size;
    node->next = NULL;

    pool->head  = node;
    pool->max   = size;
    pool->large = NULL;
    return 0;
}

逻辑: 第一次 malloc(size) 得到一整块内存,把前 sizeof(mp_node_t) 字节当作节点头,剩下的给 last 开始往后分配。

复制代码
[  mp_node_t (头)  |  可分配区域 last ─────────────── end  ]
 ↑ sizeof(node)               ↑ node->last

5.3 分配内存 mp_alloc

c 复制代码
void *mp_alloc(mp_pool_t *pool, size_t size) {
    if (!pool || size == 0) return NULL;

    // 超过阈值,走大块
    if (size > pool->max) {
        return mp_alloc_large(pool, size);
    }

    // 在已有节点中找一块够大的
    mp_node_t *node = pool->head;
    do {
        if (node->end - node->last >= size) {
            void *ptr = node->last;
            node->last += size;
            return ptr;
        }
        node = node->next;
    } while (node);

    // 找不到,申请新节点
    return mp_alloc_block(pool, size);
}

分配策略:

  1. 先在现有节点里找空间(从头扫描,找第一个够大的)
  2. 找不到 → 分配一个新节点

5.4 分配新块 mp_alloc_block

c 复制代码
static void *mp_alloc_block(mp_pool_t *pool, size_t size) {
    // 分配一个完整的新节点(大小为 pool->max)
    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;

    // 切一块给调用者
    void *ptr = node->last;
    node->last += size;

    // 尾插到链表
    mp_node_t *iter = pool->head;
    if (iter) {
        while (iter->next) iter = iter->next;
        iter->next = node;
    } else {
        pool->head = node;
    }

    return ptr;
}

5.5 分配大块 mp_alloc_large

c 复制代码
static void *mp_alloc_large(mp_pool_t *pool, size_t size) {
    if (!pool) return NULL;

    void *ptr = malloc(size);          // 直接找内核要
    if (!ptr) 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;
        }
    }

    // 没有可复用的,分配新的 mp_large_t 头节点
    // ⚠️ 注意:这里直接 malloc(sizeof(mp_large_t)),而不是 mp_alloc()
    // 因为 mp_alloc() 在 size > max 时会递归调 mp_alloc_large(),会导致死循环
    l = malloc(sizeof(mp_large_t));
    if (!l) {
        free(ptr);
        return NULL;
    }

    l->alloc = ptr;
    l->next  = pool->large;
    pool->large = l;
    return ptr;
}

⚠️ 关键注意点:

  • mp_large_t 头结构体本身用 malloc 而不是 mp_alloc 分配
  • 否则会触发 mp_allocmp_alloc_largemp_alloc 递归死循环

5.6 释放内存 mp_free

c 复制代码
void mp_free(mp_pool_t *pool, void *ptr) {
    if (!pool || !ptr) return;

    // 在大块链表里找
    mp_large_t *l = pool->large;
    mp_large_t *prev = NULL;
    while (l) {
        if (l->alloc == ptr) {
            free(l->alloc);         // 释放实际内存
            l->alloc = NULL;       // 标记为可复用

            // 从链表移除并释放 mp_large_t 头
            if (prev) prev->next = l->next;
            else pool->large = l->next;
            free(l);
            return;
        }
        prev = l;
        l = l->next;
    }
}

5.7 销毁内存池 mp_destroy

c 复制代码
void mp_destroy(mp_pool_t *pool) {
    if (!pool) return;

    // 先释放所有大块
    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) {
        mp_node_t *tmp = node->next;
        free(node);
        node = tmp;
    }
    pool->head = NULL;
}

6. 进阶:用面向对象的思想封装内存池

6.1 为什么需要面向对象?

想象你要管理多个不同类型的内存池(比如有的按 32 字节分块,有的按 64 字节分块),如果把属性方法分离,代码会更清晰:

c 复制代码
// 属性
struct body {
    void   *_;
    int     high;
    int     weight;
};

// 方法(函数指针)
struct method {
    void *(*eat)(struct body *);
    void *(*play)(struct body *);
};

// 创建实例
struct body *b = malloc(sizeof(struct body));
*(struct method **)b = method_ptr;  // 方法存在 body 头部

这样设计的好处:属性和数据隔离,方法通过函数指针调用,内存池可以更灵活扩展。

6.2 内存池的 OOP 封装思路

c 复制代码
typedef struct mp_pool_s {
    void *(*alloc)(struct mp_pool_s *, size_t);
    void (*free)(struct mp_pool_s *, void *);
    void (*destroy)(struct mp_pool_s *);
    // ... 其他方法
    size_t max;
    mp_node_t *head;
    mp_large_t *large;
} mp_pool_t;

7. 常见 Bug 总结

# Bug 后果
1 while(!node) 写成 while(node) 节点无法释放,内存泄漏
2 mp_alloc_large 里调 mp_alloc 分配 header 递归死循环
3 mp_freefree(alloc)free(l) mp_large_t 结构体泄漏
4 mp_alloc_block 里用未声明的 mem 编译错误/段错误
5 mp_create 返回 int* 而非 int 类型不匹配

8. 总结

复制代码
内存池核心思想:
  → 一次向内核申请一大块
  → 按需切成小片分配出去
  → 避免碎片化 + 减少系统调用

两个核心场景:
  → 小块(≤ max):从节点链表分配
  → 大块(> max):直接 malloc/free

设计要点:
  → max 阈值决定走哪条路径
  → 大块 header 不能递归调 mp_alloc
  → free 时要同时释放 alloc 和 header

完整代码

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

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;
} mp_large_t;

typedef struct mp_pool_s {
    size_t max;
    struct mp_node_s *head;
    struct mp_large_s *large;
} mp_pool_t;

void *mp_alloc(mp_pool_t *pool, size_t size);
static void *mp_alloc_block(mp_pool_t *pool, size_t size);
static void *mp_alloc_large(mp_pool_t *pool, size_t size);

int mp_create(mp_pool_t *pool, size_t size) {
    if (pool == NULL || size <= sizeof(mp_node_t)) return -1;
    void *mem = malloc(size);
    if (!mem) return -2;

    mp_node_t *node = (mp_node_t *)mem;
    node->last = (unsigned char *)mem + sizeof(mp_node_t);
    node->end  = (unsigned char *)mem + size;
    node->next = NULL;

    pool->head  = node;
    pool->max   = size;
    pool->large = NULL;
    return 0;
}

void mp_destroy(mp_pool_t *pool) {
    if (!pool) return;

    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) {
        mp_node_t *tmp = node->next;
        free(node);
        node = tmp;
    }
    pool->head = NULL;
}

static void *mp_alloc_block(mp_pool_t *pool, size_t size) {
    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;

    void *ptr = node->last;
    node->last += size;

    mp_node_t *iter = pool->head;
    if (iter) {
        while (iter->next) iter = iter->next;
        iter->next = node;
    } else {
        pool->head = node;
    }
    return ptr;
}

static void *mp_alloc_large(mp_pool_t *pool, size_t size) {
    if (!pool) return NULL;

    void *ptr = malloc(size);
    if (!ptr) return NULL;

    mp_large_t *l;
    for (l = pool->large; l; l = l->next) {
        if (l->alloc == NULL) {
            l->alloc = ptr;
            return ptr;
        }
    }

    l = malloc(sizeof(mp_large_t));
    if (!l) {
        free(ptr);
        return NULL;
    }
    l->alloc = ptr;
    l->next  = pool->large;
    pool->large = l;
    return ptr;
}

void *mp_alloc(mp_pool_t *pool, size_t size) {
    if (!pool || size == 0) return NULL;

    if (size > pool->max) {
        return mp_alloc_large(pool, size);
    }

    mp_node_t *node = pool->head;
    do {
        if (node->end - node->last >= size) {
            void *ptr = node->last;
            node->last += size;
            return ptr;
        }
        node = node->next;
    } while (node);

    return mp_alloc_block(pool, size);
}

void mp_free(mp_pool_t *pool, void *ptr) {
    if (!pool || !ptr) return;

    mp_large_t *l = pool->large;
    mp_large_t *prev = NULL;
    while (l) {
        if (l->alloc == ptr) {
            free(l->alloc);
            l->alloc = NULL;
            if (prev) prev->next = l->next;
            else pool->large = l->next;
            free(l);
            return;
        }
        prev = l;
        l = l->next;
    }
}

int main() {
    mp_pool_t pool;
    if (mp_create(&pool, 4096) != 0) return -1;

    int *a = mp_alloc(&pool, sizeof(int));
    *a = 100;

    int *b = mp_alloc(&pool, sizeof(int));
    *b = 200;

    char *str = mp_alloc(&pool, 32);

    // 分配大块(超过 max)
    char *big = mp_alloc(&pool, 5000);
    if (big) big[0] = 'B';

    mp_free(&pool, big);

    char *big2 = mp_alloc(&pool, 6000);
    if (big2) big2[0] = 'C';

    mp_destroy(&pool);
    return 0;
}

相关推荐
北山有鸟1 天前
【学习笔记】MIPI CSI-2 协议全解析:从底层封包到像素解析
linux·驱动开发·笔记·学习·相机
mounter6251 天前
深度解析:Linux 内核为何要移除“直接映射” (Direct Map)?
linux·运维·服务器·security·linux kernel·direct mem map
bugu___1 天前
Linux系统、网络知识点回顾1
linux·网络
keyipatience1 天前
7.Linux1权限-开发工具
linux
j_xxx404_1 天前
万字长文爆肝:彻底弄懂Linux文件系统(Ext2),从Inode、Block到Dentry核心机制全解析
linux·运维·服务器
2401_841495641 天前
Linux C++ TCP 服务端经典的监听骨架
linux·网络·c++·网络编程·ip·tcp·服务端
楼田莉子1 天前
同步/异步日志系统:日志器管理器模块\全局接口\性能测试
linux·服务器·开发语言·c++·后端·设计模式
奇妙之二进制1 天前
zmq源码分析之io_thread_t
linux·服务器
cui_ruicheng1 天前
Linux IO入门(三):手写一个简易的 mystdio 库
linux·运维·服务器
telllong1 天前
MCP协议实战:30分钟给Claude接上你公司的内部API
linux·运维·服务器