[知乎回答] 如何设计内存池?

2021-11-14

知乎问题

内存池的优点就是可以减少内存碎片,分配内存更快,可以避免内存泄露等优点。那我们如何去设计一个内存池呢?能不能给个你的大致思考过程或者步骤,比如,首先要设计一个内存分配节点(最小单元),再设计几个内存分配器(函数),等等。不是太清晰。望大神指点。


1. 内存池

redis 在 Linux 上有三种内存池选择:

  1. glibc 上的 ptmalloc(ptmalloc 文档)。
  2. 谷歌的 tcmalloc。
  3. jemalloc。

可以先参考一下这三个内存池,找一个感兴趣的内存池源码进行阅读。


轻量级的也可以参考 nginx 的内存池:ngx_pool_t,但是它的内存回收管理比较弱。

nginx 内存池

设计图来源:《nginx 内存池结构图


2. 内存池要点

  1. 除了考虑从大块内存上高效地将小内存划分出去,还要注意内存碎片问题。
  2. 当回收内存时要注意是否需要将相邻的空闲内存块进行合并管理。
  3. 当内存池的空闲内存到达一定的阈值时,要合理地返还系统。

3. 内存池泄漏问题

3.1. 原理

为什么不建议自己写内存池呢?

因为自己曾经遇到过一个棘手的内存泄漏问题,幸运的是当时项目增加的代码量不多,也花了不少精力,才定位在 Linux libc 库里面的 ptmalloc 出现”泄漏“。

主要是它向内核申请了大量内存,但是并不返还系统,原因:申请的都是小内存(<=128k),它都是通过 brk 申请的,ptmalloc 通过 brk 申请的内存,返还系统有个特点:必须是紧挨着当前 brk 申请的空闲内存块的内存空间,它被用户释放了,后面紧挨着的其它空闲内存才会被返还系统。

看下图,只要 n2 这块小内存用户不释放,其它节点内存释放了,也不给返还系统。

所以程序在 Linux 上分配内存,需要避免分阶段分配内存,就是后面分配的内存如果一直不释放,前面申请的内存即便释放了,底层可能不给你返还系统,出现”泄漏”的问题。

如果搞不清楚这些,那些搞个内存池项目,内存资源长期驻留的就更危险了。


3.2. 泄漏 demo

有兴趣的朋友可以在 Linux 上跑一下下面代码。

  1. 观察程序运行的结果。
  2. 然后屏蔽掉 addr 的内存申请看看。
  3. 或者一行一行开启注释掉的两行源码。
  4. 又或者调换这两行源码顺序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* test_memory.c
 * gcc -g -O0 -W test_memory.c -o tm123 && ./tm123 */

#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BLOCK_CNT (256 * 1024)
#define BLOCK_SIZE (4 * 1024)

int main() {
    int i;
    char *addr, *blks[BLOCK_CNT];

    for (i = 0; i < BLOCK_CNT; i++) {
        blks[i] = (char *)malloc(BLOCK_SIZE * sizeof(char));
    }

    addr = (char *)malloc(2 * sizeof(char));

    for (i = 0; i < BLOCK_CNT; i++) {
        free(blks[i]);
    }

    // free(addr);
    // malloc_trim(0);

    malloc_stats();
    for (;;) {
        sleep(1);
    }

    return 0;
}

4. 参考