序
关于malloc/free,我们都不陌生,在最开始学习c语言时就相当了解,包括c++中的new也是封装的malloc。下边我以glibc实现的malloc来讲述一些关于malloc/free的知识点。
malloc/free
- malloc和free并不是系统调用,而是运行时库(eg.libc.so)的api,而运行时库又会去调用系统调用从操作系统申请内存,这里的系统调用在Linux上一般是brk/sbrk或者mmap。
- 一般来讲当调用malloc时,申请下来的内存除了返回给调用者外,还有一部分作为元数据,这里的元数据会存储malloc给用户的内存大小,所以当调用free释放内存时,只需要传递指针,运行时库自然就知道要释放多少内存。
- free调用后,运行时库一般来说并不会立即释放内存给操作系统,而是先将这块内存放到一个内存池中,等待下一次malloc时再从内存池中取出。
int main() {{std::list<int> ll;for (int i = 0; i < 100000000; ++i) {ll.push_back(i);}}for (;;) ;return 0;
}
然后看下top,其实list已经被释放了,但是这个进程的内存还是存在,或者说并没有完全释放:
> top -p 91376
PID %CPU TIME MEM
91376 93.4 01:31.06 7420K
mallopt
该函数针对于malloc进行的一些配置,设置malloc内部的一些参数大小
首先我们简单了解下glibc的ptmalloc的实现逻辑:
ptmalloc中组织内存的数据结构为:
struct malloc_chunk {INTERNAL_SIZE_T mchunk_prev_size;INTERNAL_SIZE_T mchunk_size;struct malloc_chunk* fd; struct malloc_chunk* bk;struct malloc_chunk* fd_nextsize;struct malloc_chunk* bk_nextsize;
};
包括分配给用户是同样指向一个chunk,mchunk_prev_size
和mchunk_size
表示上一个chunk的大小和当前chunk的大小之和,malloc函数返回的指针指向fd这个位置,只有当chunk被释放时,以下的四个字段才会被使用在ptmalloc内部被组织使用,纳入到空闲链表等。
chunk被以下数据结构来组织,malloc_state也被称为是内存区,glibc实现的ptmalloc避免多线程并发引入主分配区和非主分配区,每个进程有一个主分配区,也可以允许有多个非主分配区。主分配区可以使用brk和mmap来分配,而非主分配区只能使用mmap来映射内存块。
struct malloc_state
{__libc_lock_define (, mutex); //互斥锁int flags; //标志 mfastbinptr fastbinsY[NFASTBINS]; //fastbinsmchunkptr top; //top chunk mchunkptr bins[NBINS * 2 - 2]; //unsortedbins,smallbins,largebinsunsigned int binmap[BINMAPSIZE]; //bin位图struct malloc_state *next; //链表指针......
};
当用户调用malloc时,会先去获取一个内存区,然后加锁,在这个内存区上分配内存。加锁失败就会继续找next的内存区,直到找到一个可用的内存区,都不可用,就会创建内存区。超过内存区限制数量,就会循环等待直到找到一个空闲的内存区,进而进入分配流程。
然后我们进一步来看下ptmalloc的内存组织形式:
- fastbin – 小块内存的快速分配,fastbin的分配方式就是直接从fastbin链表上取,所以fastbin的分配效率很高,但是fastbin的分配范围比较小,一般不超过128字节。
- top就是指向从操作系统申请的内存
- bins里有3个链表,分别是unsortedbins,smallbins,largebins。
当用户释放的内存大于max_fast或者fast bins合并后的chunk都会首先进入unsorted bin上。在unsorted bin中的chunk经过合并处理后到达smallbins及largebins。
smallbins – 用于存储32-1008字节的chunk。
largebins – 用于存储大于1024字节的chunk。
(这里的字节大小可能随glibc的版本及SIZE_SZ 取值不同而变化,供参考)
然后来看下各个参数的含义:
- M_MXFAST – fastbin范围的最大值,value值在0~20sizeof(void)之间
- M_TRIM_THRESHOLD – 主内存区的top_chunk的收缩阈值
- M_TOP_PAD – 控制堆顶部的额外空间。堆顶部额外空间可用于缓解堆碎片的问题。默认值为0
- M_MMAP_THRESHOLD – malloc通过mmap直接向系统申请内存的临界值,默认128K
- M_MMAP_MAX 通过mmap分配的内存块个数的上限,默认值为65536
- M_CHECK_ACTION 用于设置内存错误检查时的处理方式。默认值为2(执行abort) 1 – 打印错误信息
- M_PERTURB 控制内存分配时填充内存块的内容。默认值为0。
malloc_stats
输出malloc的内存统计信息,我们看下输出是怎样的:
int main() {std::thread t1([](){int* a = new int[1000];});std::thread t2([](){int* a = new int[128 * 1024];});int* a = new int[1000];t1.join();t2.join();malloc_stats();return 0;
}
然后输出是这样的:
Arena 0:
system bytes = 135168
in use bytes = 78000
Arena 1:
system bytes = 135168
in use bytes = 6256
Total (incl. mmap):
system bytes = 798720
in use bytes = 612640
max mmap regions = 1
max mmap bytes = 528384
大致就是这个进程用了多少内存,Arena是表示内存区的意思。
以上内存统计大概意思是用了两个内存区,system bytes
是从操作系统分配的内存大小,in use bytes
应该就是分配给用户的正在使用的内存大小。
因为涉及到锁,在多线程的情况下ptmalloc性能不是那么优秀,我们自己也可以试试tcmalloc,这里我使用tcmalloc输出下统计信息:
MALLOC: 623232 ( 0.6 MiB) Bytes in use by application
MALLOC: + 393216 ( 0.4 MiB) Bytes in page heap freelist
MALLOC: + 31784 ( 0.0 MiB) Bytes in central cache freelist
MALLOC: + 0 ( 0.0 MiB) Bytes in transfer cache freelist
MALLOC: + 344 ( 0.0 MiB) Bytes in thread cache freelists
MALLOC: + 2490432 ( 2.4 MiB) Bytes in malloc metadata
MALLOC: ------------
MALLOC: = 3539008 ( 3.4 MiB) Actual memory used (physical + swap)
MALLOC: + 0 ( 0.0 MiB) Bytes released to OS (aka unmapped)
MALLOC: ------------
MALLOC: = 3539008 ( 3.4 MiB) Virtual address space used
MALLOC:
MALLOC: 10 Spans in use
MALLOC: 1 Thread heaps in use
MALLOC: 8192 Tcmalloc page size
这是tcmalloc重写了malloc_stats函数的输出,因为实现不一样所以输出也不一样,不过个人看来tcmalloc更加详细一点。
malloc_usable_size
该函数返回使用malloc分配的内存块的可用大小,可能要比malloc传入的参数大,应该是为了做对齐。来看个例子:
int main() {int* a = new int[1000];std::cout << malloc_usable_size(a) << std::endl; // 4008return 0;
}
要比4000大,因为内存对齐,所以多出8个字节
malloc_trim
该函数通过调用sbrk或者madvise来释放内存给操作系统,当你需要显式的释放内存到操作系统时,可以最开始的函数,我们稍微改动下:
int main() {{std::list<int> ll;for (int i = 0; i < 100000000; ++i) {ll.push_back(i);}}malloc_trim(0);for (;;) ;return 0;
}
则可以看到内存已经释放到操作系统了
PID VIRT RES SHR S COMMAND
96270 20096 3144 2932 R main_prog
参数名字是pad
,堆顶部保留不回收的可用空间量,如果该参数为0,则仅在堆顶部保留
最小量的内存。
不过个人认为还是谨慎使用,因为这个就会触发内存回收,及后续相应的系统调用,对于性能来说还是有影响的。
MALLOC_CHECK_
该环境变量可以用来开启内存错误检查,默认是关闭的,开启后,如果内存分配错误,会调用abort,比如调用free一个没有分配的内存,或者调用malloc一个超过内存限制的内存。
- 0: 默认的,关闭检查
- 1: 发生错误时,在stderr打印错误信息
- 2: 发生错误时,调用abort(如果开了core dump,那么会生成core文件)
hook或replace malloc和free函数
有时候我们想要自己来重写malloc和free函数,比如想自己来管理内存,或者在malloc中打印日志什么的,这里提供的有两种方法:
1. __malloc_hook
这是官方提供的hook malloc等函数的方法,在malloc.h的头文件中,已经被标注为废弃的,但是还是可以使用:
/* Hooks for debugging and user-defined versions. */
extern void (*__MALLOC_HOOK_VOLATILE __free_hook) (void *__ptr,const void *)
__MALLOC_DEPRECATED;extern void *(*__MALLOC_HOOK_VOLATILE __malloc_hook)(size_t __size,const void *)
__MALLOC_DEPRECATED;
这是一组函数指针,那也就是说你可以自定义malloc和free函数,这个指针指向你的函数即可:
static void *(*old_malloc_hook)(size_t, const void *);
static void (*old_free_hook)(void *, const void *);static void * my_malloc(size_t size, const void * caller)
{__malloc_hook = old_malloc_hook;void* result = malloc(size);printf("my_malloc return %p, size %d\n", result, (unsigned int)size);__malloc_hook = my_malloc;return result;
}static void my_free(void *ptr, const void *caller)
{__free_hook = old_free_hook;free(ptr);printf("my_free free %p\n", ptr);__free_hook = my_free;
}int main() {__malloc_hook = my_malloc;__free_hook = my_free;void* p1 = malloc(10);free(p1);return 0;
}
为了避免递归调用,我们在进入我们自己hook的malloc函数中首先把hook的指针置空,然后去调用glibc的malloc函数,最后把hook的指针还原。
我这里的例子仅仅是打印一条日志,当然你可以自定义你的函数,比如说重写内存分配方式。
2. LD_PRELOAD及dlsym
- LD_PRELOAD可以设置一个动态链接库,这个库会被预先加载到进程的地址空间,如果这个库中的符号和malloc重名,那么这个库的函数会被调用,否则就调用glibc的函数。这样起到了替换malloc和free的作用。
- dlsym是在动态库中寻找符号的一个函数,我们可以使用它来去glibc中去找malloc和free的函数
void* dlsym(void* handle,const char* symbol)
handle表示动态库句柄,symbol表示要寻找的函数名,返回值是函数指针。
handle一般是使用dlopen打开的动态库句柄,除此之外也可以是RTLD_DEFAULT或RTLD_NEXT:
RTLD_DEFAULT表示按默认的顺序搜索共享库中符号symbol第一次出现的地址
RTLD_NEXT表示在当前库以后按默认的顺序搜索共享库中符号symbol第一次出现的地址
所以我们使用RTLD_NEXT就可以获取到glibc中的malloc和free的函数指针,当然你如果是自己实现内存分配则可以不需要获取glibc的函数指针。
这里我们给出一个简单小项目,用来获取一个进程中某个so内存分配的大小:
static void *(*fn_malloc)(size_t size);
static void (*fn_free)(void *ptr);
声明函数指针去获取glibc的malloc和free函数,到用的时候直接调用:
static void init()
{fn_malloc = (void*(*)(size_t))dlsym(RTLD_NEXT, "malloc");fn_free = (void(*)(void*))dlsym(RTLD_NEXT, "free");if (!fn_malloc || !fn_free) {fprintf(stderr, "Error in dlsym");exit(1);}
}thread_local bool is_collect_info = false;void *malloc(size_t size)
{if (!fn_malloc) {init();}void *ptr = fn_malloc(size);fprintf(stderr, "allocated bytes memory %ld in %p\n", size, ptr);if (!is_collect_info) {is_collect_info = true;collect_info(ptr, size);is_collect_info = false;}
}
我们这里重写了malloc函数,在函数内部我们判断fn_malloc是否存在,不存在的话先去获取fn_malloc和fn_free的函数指针,然后调用glibc的malloc函数分配空间。最后收集指定so的内存情况。
我们使用is_collect_info变量来控制如果在collect_info发生内存申请的情况,避免递归调用。也就是说
collect_info函数中使用的内存不在统计的范围内。
#define STACK_INFO_LEN 1024
#define MAX_STACK_FRAMES 12void collect_info(void* ptr, size_t size)
{void *p_stack[MAX_STACK_FRAMES];char stack_info[STACK_INFO_LEN * MAX_STACK_FRAMES];char** p_stack_list = nullptr;int frames = backtrace(p_stack, MAX_STACK_FRAMES);p_stack_list = backtrace_symbols(p_stack, frames);if (p_stack_list == nullptr) {return;}for (int i = 0; i < frames; ++i) {if (p_stack_list[i] == nullptr) {break;}if (strstr(p_stack_list[i], dso_name) != nullptr) {mem_size_map[dso_name] += size;mem_pos_map[uint64_t(ptr)] = size;fprintf(stderr, "dso_name %s in %p has size: %ld \n", dso_name, ptr, size);}}
这里我们使用backtrace
和backtrace_symbols
,当前调用栈的符号信息,判断是否包含dso_name(要统计的动态库名),如果包含就使用mem_size_map
和mem_pos_map
记录内存分配情况。
void free(void *ptr)
{fn_free(ptr);fprintf(stderr, "deallocated bytes memory in %p\n", ptr);if (!is_collect_info && ptr != nullptr) {if (mem_pos_map.find((uint64_t)ptr) != mem_pos_map.end()) {mem_size_map[dso_name] -= mem_pos_map[(uint64_t)ptr];fprintf(stderr, "dso_name %s in %p free size: %ld \n", dso_name, ptr, mem_pos_map[(uint64_t)ptr]);}}
}
然后我们重写了free函数,在函数内部我们调用glibc的free函数。下边判断是否是指定的so的内存,如果是就从mem_size_map
和mem_pos_map
中移除。
mem_size_map[dso_name]
中存放的就是指定so的内存大小。
mem_pos_map
来存储内存地址下的内存大小
以上则是简单的例子,我把它提交到了https://github.com/leap-ticking/dso_memory_stat位置,如果有需要可以直接使用,仅供参考。
ref
- https://www.cnblogs.com/ho966/p/17671723.html
- https://www.slideshare.net/slideshow/tips-of-malloc-free/16682403?from_search=1#4
- https://github.com/google/tcmalloc
- https://blog.binpang.me/2017/09/22/ptmalloc%E5%A0%86%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/
- https://mp.weixin.qq.com/s/v-lXOFawW5iwZ24O_8f28w