知乎问题:
内存池的优点就是可以减少内存碎片,分配内存更快,可以避免内存泄露等优点。那我们如何去设计一个内存池呢?能不能给个你的大致思考过程或者步骤,比如,首先要设计一个内存分配节点(最小单元),再设计几个内存分配器(函数),等等。不是太清晰。望大神指点。
1. 内存池
redis 在 Linux 上有三种内存池选择:
- glibc 上的 ptmalloc(ptmalloc 文档)。
- 谷歌的 tcmalloc。
- jemalloc。
可以先参考一下这三个内存池,找一个感兴趣的内存池源码进行阅读。
轻量级的也可以参考 nginx 的内存池:ngx_pool_t,但是它的内存回收管理比较弱。
设计图来源:《nginx 内存池结构图》
2. 内存池要点
- 除了考虑从大块内存上高效地将小内存划分出去,还要注意内存碎片问题。
- 当回收内存时要注意是否需要将相邻的空闲内存块进行合并管理。
- 当内存池的空闲内存到达一定的阈值时,要合理地返还系统。
3. 内存池泄漏问题
3.1. 原理
为什么不建议自己写内存池呢?
因为自己曾经遇到过一个棘手的内存泄漏问题,幸运的是当时项目增加的代码量不多,也花了不少精力,才定位在 Linux libc 库里面的 ptmalloc 出现”泄漏“。
主要是它向内核申请了大量内存,但是并不返还系统,原因:申请的都是小内存(<=128k),它都是通过 brk 申请的,ptmalloc 通过 brk 申请的内存,返还系统有个特点:必须是紧挨着当前 brk 申请的空闲内存块的内存空间,它被用户释放了,后面紧挨着的其它空闲内存才会被返还系统。
看下图,只要 n2 这块小内存用户不释放,其它节点内存释放了,也不给返还系统。
所以程序在 Linux 上分配内存,需要避免分阶段分配内存,就是后面分配的内存如果一直不释放,前面申请的内存即便释放了,底层可能不给你返还系统,出现”泄漏”的问题。
如果搞不清楚这些,那些搞个内存池项目,内存资源长期驻留的就更危险了。
3.2. 泄漏 demo
有兴趣的朋友可以在 Linux 上跑一下下面代码。
- 观察程序运行的结果。
- 然后屏蔽掉 addr 的内存申请看看。
- 或者一行一行开启注释掉的两行源码。
- 又或者调换这两行源码顺序。
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;
}