关于malloc/free的一些知识点

13 篇文章 5 订阅

关于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_sizemchunk_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];  //fastbins
  mchunkptr top;                     //top chunk 
  mchunkptr bins[NBINS * 2 - 2];     //unsortedbins,smallbins,largebins
  unsigned int binmap[BINMAPSIZE];   //bin位图
  struct malloc_state *next;         //链表指针
  ......
};

当用户调用malloc时,会先去获取一个内存区,然后加锁,在这个内存区上分配内存。加锁失败就会继续找next的内存区,直到找到一个可用的内存区,都不可用,就会创建内存区。超过内存区限制数量,就会循环等待直到找到一个空闲的内存区,进而进入分配流程。

然后我们进一步来看下ptmalloc的内存组织形式:

  1. fastbin – 小块内存的快速分配,fastbin的分配方式就是直接从fastbin链表上取,所以fastbin的分配效率很高,但是fastbin的分配范围比较小,一般不超过128字节。
  2. top就是指向从操作系统申请的内存
  3. 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; // 4008
    return 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 12

void 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);
        }
    }

这里我们使用backtracebacktrace_symbols,当前调用栈的符号信息,判断是否包含dso_name(要统计的动态库名),如果包含就使用mem_size_mapmem_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_mapmem_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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值