C/C++内存分配

  1、brk()和sbrk()

// 成功时返回0,出错时返回-1并设置errno为ENOMEM
int brk(void *addr);

// 成功时返回先前的堆结束位置。出错时,返回(void *)-1并设置errno为ENOMEM
void *sbrk(intptr_t increment);

 

  

  如上面两个图所示,堆是一个连续的内存区域,在扩展时自下至上增长。mm_types.h定义的mm_struct结构包含了堆在虚拟地址空间中的起始和当前结束位置(start_brk和brk成员)。在start_brk和brk之间的是已映射区域,表示这部分的虚拟地址已经映射到物理地址,程序可以直接访问,而访问未映射区域则会导致段错误。

  brk()和sbrk()均可调整brk的值。brk()的addr参数指定了堆在虚拟地址空间中新的结束位置,而sbrk()通过增量increment调节堆的结束位置。

  因此,当堆需要扩展时,会调用brk()或sbrk()增大brk,而减小brk则意味着将堆顶部的一部分内存释放给系统(堆的收缩,trim)。brk的值总是系统页长度的倍数(即一页是用brk()能分配的最小内存区域),而C标准库函数(如malloc()和free())的任务便是将页拆分成更小的区域,这样可以避免频繁的brk()系统调用带来的开销。

  示例:

int main()
{
    // sbrk(0)返回堆当前的结束位置
    printf("%p\n", sbrk(0));
sbrk(
4088); int *p = (int *)sbrk(0); *p = 3; *(p + 1) = 3; // sbrk()是按页调整brk的。这里p + 2指针“越界”了 // *(p + 2) = 3; // 根据输出得知,堆每次扩展33*4K for (int i = 0; i < 512; i++) { char *s = (char *)malloc(1024); printf("loop: %p\n", sbrk(0)); } return 0; }

  注意:应避免使用brk()和sbrk()。malloc()是可移植的、更好的分配内存方法。

 

  2、mallopt():调整内存分配参数,以控制内存分配函数(如malloc())的行为。

// 将param指定的参数设置成value
//
成功时返回1,错误时返回0且不设置errno int mallopt(int param, int value);

  一些常用的param和对应的value:

  1)M_CHECK_ACTION:该参数控制当检测到不同类型的编程错误时(如释放同一个指针两次)glibc的行为。value最低三位的值决定了glibc的行为(根据Ubuntu 14.04的实际效果对man mallopt的内容作了调整):

  (1)第0位:设置该位后,在标准出错上打印单行错误详情消息,并调用abort()终止程序。该消息包含程序名、检测到错误的内存分配函数名、错误的简要描述以及检测到错误的内存地址等。

  (2)第1位:设置该位后,程序调用abort()终止。自glibc 2.4以后,若第0位也被设置,则在打印错误消息之后、终止程序之前,以backtrace()的方式打印堆栈跟踪信息(stack  trace),并以/proc/[pid]/maps的方式打印进程的内存映射情况。

  (3)第2位(glibc 2.4以上):仅当第0位被设置时有效。设置该位后,单行的错误消息被简化成只包含检测到错误的函数名和对错误的简短描述。程序然后继续执行。

  其他位被忽略。因此value各个可能值的含义如下:0(忽略错误条件,继续执行,结果未定义。相当于4)、1、2(相当于6)、3、5(打印一个简单的错误消息并继续执行)、7(打印简单的错误消息、堆栈跟踪信息和内存映射情况,并终止程序)。一个例子:

int main(int argc, char *argv[])
{
    if (argc > 1) 
    {
        if (mallopt(M_CHECK_ACTION, atoi(argv[1])) != 1) 
        {
            fprintf(stderr, "mallopt() failed");
            exit(EXIT_FAILURE);
        }
    }

    char *p = (char *)malloc(1000);
    if (p == NULL) 
    {
        fprintf(stderr, "malloc() failed");
        exit(EXIT_FAILURE);
    }

    free(p);
    printf("main(): returned from first free() call\n");

    free(p);
    printf("main(): returned from second free() call\n");

    exit(EXIT_SUCCESS);
}

  编译得到a.out,以不同的命令行参数运行:

~/programming$ ./a.out 1
main(): returned from first free() call
*** Error in `./a.out': double free or corruption (top): 0x09653008 ***
已放弃 (核心已转储)

~/programming$ ./a.out 0
main(): returned from first free() call
main(): returned from second free() call

# 一些环境变量可用来修改mallopt()控制的参数(若它们都设置了同一个参数,则优先取mallopt()的值)
# 使用这些变量的好处是程序的源代码不需要改变
# 比如,变量MALLOC_CHECK_、MALLOC_TRIM_THRESHOLD_、MALLOC_MMAP_MAX_、MALLOC_MMAP_THRESHOLD_
# 分别和mallopt()的M_CHECK_ACTION、M_TRIM_THRESHOLD、M_MMAP_MAX、M_MMAP_THRESHOLD参数对应
~/programming$ MALLOC_CHECK_=0 ./a.out
main(): returned from first free() call
main(): returned from second free() call

  2)M_TRIM_THRESHOLD:当堆顶部的连续空闲内存足够多时,free()使用brk()/sbrk()将这部分内存释放给系统。该参数指定在收缩堆之前,这块空闲内存需要达到的最小字节长度。默认值是128*1024。设置成-1则禁止收缩。

  M_TRIM_THRESHOLD的调整需要权衡:若设置得太低,则增加了系统调用的次数;若设置得太高,则浪费堆顶部的未用内存。

  3)M_MMAP_MAX:该参数指定可以用mmap()同时处理的内存分配请求的最大数目。该参数存在的原因是一些系统只有数量有限的几个内部表可被mmap()使用,而过量使用会导致性能下降。默认值是65536。把它设置为0则禁止使用mmap()处理大的内存分配请求(验证:分两种情况,将该参数设置为0和非0,再分别用malloc()分配一个大内存块。比较这两个块的起始地址。使用mmap()分配的内存块的地址会大得多,参照本文开始的图)。

  4)M_MMAP_THRESHOLD:mmap阀值。使用mmap()满足大块内存(长度>=M_MMAP_THRESHOLD字节)的分配,而不是通过brk()/sbrk()增大brk。mmap()分配的内存不受RLIMIT_DATA限制(getrlimit())。默认值为128 * 1024。

  使用mmap()分配内存有明显的好处:分配的内存块总是可以被独立地释放给系统。相比之下,堆仅在顶部内存被释放的情况下可以收缩。

  但使用mmap()也有一些缺点:释放的空间没有放置到空闲列表以便被之后的分配操作重用;内存可能被浪费,因为mmap()分配的内存必须是页对齐的。

  如今的glibc默认使用一个动态的mmap阀值,其初始值为128*1024。当一个长度大于当前阀值且小于等于DEFAULT_MMAP_THRESHOLD_MAX的内存块被释放时,阀值被向上调整为该块的大小。使用动态mmap阀值时,M_TRIM_THRESHOLD也被动态地调整为mmap阀值的两倍。不过,M_TRIM_THRESHOLD、M_TOP_PAD、M_MMAP_THRESHOLD或M_MMAP_MAX中的任何一个被设置时,mmap阀值的动态调整特性被禁用。

 

  3、malloc_***

  1)malloc_usable_size():获得某个从堆中分配的内存块的大小。

// ptr必须是malloc()或相关函数分配(且未释放)的一个内存块的指针
// 返回ptr指向的已分配内存块的可用字节数。若ptr为NULL,则返回0。
size_t malloc_usable_size (void *ptr);

  由于对齐或最小长度的要求,该函数返回的值可能大于内存分配时申请空间的大小(超过的字节数取决于底层实现)。虽然访问超出的字节范围不会有什么问题,但这不是好的编程习惯。

  2)malloc_trim():调用sbrk()释放堆顶部的空闲内存。

// 内存被释放给系统时返回1,不可能释放任何内存时返回0
void malloc_trim(size_t pad);

  参数pad指定收缩时堆顶部保留的空闲空间大小。若为0,则堆顶部只保留最小限度的内存量(一个页甚至更少);若非0,则保留堆顶部末尾的一些空间,这样后续的内存分配操作不需要使用sbrk()扩展堆。

  在某些情况下,该函数被free()自动调用;该函数只能释放堆顶部的空闲内存。

  3)malloc_stats():在标准出错上打印内存分配统计情况(statistics)。

void malloc_stats(void);

  对每个arena,该函数打印系统分配(映射)的内存总量和实际分配操作消耗的内存总量。如下面的例子:

char *p = (char *)malloc(700);
char *q = (char *)malloc(5205205);

malloc_stats();

  相应的输出:

~/programming$ ./a.out 
Arena 0:
system bytes     =     135168
in use bytes     =        704
Total (incl. mmap):
system bytes     =    5341184
in use bytes     =    5206720
max mmap regions =          1
max mmap bytes   =    5206016

  每个arena(allocation area)是系统内部使用brk()或mmap()分配的一个大的内存区域。它用自己的互斥锁管理。

  为了可扩展地处理多线程应用的内存分配操作,当检测到互斥锁竞争时,glibc创建额外的arena。

  4)malloc调试变量:__malloc_hook、__malloc_initialize_hook、__free_hook和__realloc_hook等。

void *(*__malloc_hook)(size_t size, const void *caller);
void (*__malloc_initialize_hook)(void);
void (*__free_hook)(void *ptr, const void *caller);
void *(*__realloc_hook)(void *ptr, size_t size, const void *caller);

  __malloc_hook、__free_hook和__realloc_hook等指向的函数分别与malloc()、free()和realloc()等函数有着相同的原型,除了最后多了一个caller参数。

  通过指定适当的hook函数,GNU C库允许你修改malloc()、realloc()和free()的行为。一个用处是,你可以使用这些hook帮助你调试使用了动态内存分配的程序。例:

void *(*old_malloc_hook) (size_t, const void *caller);

void *my_malloc_hook(size_t size, const void *caller)
{ 
    // 没有这一句的话会递归调用本函数
    __malloc_hook = old_malloc_hook;
    void *result = malloc(size);
    printf ("malloc (%u) returns %p\n", (unsigned int) size, result);

    // 保证后续的malloc()仍可以使用自定义的hook
    __malloc_hook = my_malloc_hook;

    return result;
}

void malloc_hook_init()
{
    old_malloc_hook = __malloc_hook;
    __malloc_hook = my_malloc_hook;
}

int main()
{
    malloc_hook_init();
    malloc(123);

    return 0;
}

  这些hook函数在多线程程序中使用是不安全的,并且它们现在已被弃用。
  5)malloc_info():将malloc状态导出到一个流中。

// 当前实现下,options必须为0
//
成功时返回0,出错时返回-1并相应地设置errno int malloc_info(int options, FILE *fp);

  该函数导出一个描述调用者当前内存分配状态的XML字符串,该字符串包含所有arena的信息。输出格式如下:

<malloc version="1">
    <heap nr="0">
        <sizes>...</sizes>
        <total type="fast" count="0" size="0"/>
        <total type="rest" count="0" size="0"/>
        <system type="current" size="1081344"/>
        <system type="max" size="1081344"/>
        <aspace type="total" size="1081344"/>
        <aspace type="mprotect" size="1081344"/>
    </heap>
    <heap nr="1">...</heap>
    <heap nr="2">...</heap>
    ...
</malloc>

 

  参考资料:

  《深入Linux内核架构》

  http://blog.jobbole.com/75656/

  http://blog.csdn.net/amwihihc/article/details/7481656

  http://blog.csdn.net/sgbfblog/article/details/7772153

 

 

不断学习中。。。

posted on 2015-06-02 02:47  han'er  阅读(6421)  评论(0编辑  收藏  举报

导航