LXR | KVM | PM | Time | Interrupt | Systems Performance | Bootup Optimization

《Linux/UNIX系统编程手册》第7章 内存分配

关键词:program break、brk()、sbrk()、malloc()、free()、cmalloc()、realloc()、alloca()、mallopt()、mallinfo()

 

1. 在堆上分配内存

所谓堆是一段长度可变的连续虚拟内存,始于进程的未初始化数据段末尾,随着内存的分配和释放而增减。通常将堆的当前内存边界称为“program break”。

C的malloc函数族基于brk()和sbrk()。

1.1 调整program break:brk()和sbrk()

改变堆的大小(即分配或释放内存),其实就像命令内核改变进程的program break位置一样简单。

最初,program break正好位于初始化数据段末尾之后。

在program break的位置抬升后,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配。内核会在进程首次试图访问这些虚拟内存地址是自动分配新的物理内存页。

传统UNIX中提供了两个操作program break的系统调用:brk()和sbrk()

#include <unistd.h>
int brk(void *end_data_segment);
  Returns 0 on success, or –1 on error
void *sbrk(intptr_t increment);
  Returns previous program break on success, or (void *) –1 on error

sbrk()会将program break设置为参数end_data_segment所指定的位置。由于虚拟内存以页为单位进行分配,end_data_segment实际会四舍五入到下一个内存页的边界处。

当试图将program break设置为一个低于其初始值(end)的位置时,有可能会导致无法预知的行为。

program break可以设定的精确上限取决于一些列因素,这包括进程中对数据段大小的资源限制,以及内存映射、共享内存段、共享库的位置。

调用sbrk()将program break在原有地址上增加从参数increment传入的大小。用于声明increment的intptr_t类型属于整数数据类型。

调用成功,sbrk()返回前一个program break的地址;换言之,如果program break增加,那么返回值是指向这块新内存起始位置的指针。

调用sbrk(0)将返回program break的当前位置,对其不做改变。

 

待研究:初始program break和和上图关系,通过sbrk()和maps对应验证。

下面用个测试程序简单验证malloc()/free()和program break一下关系。

在不停的malloc()/free()之后,记录program break的值和系统maps中heap的地址范围。

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

void print_program_break_and_maps(pid_t pid) {
    char cmd[128];

    snprintf(cmd, sizeof(cmd), "cat /proc/%d/maps", pid);
    printf("program break=%10p.\n", sbrk(0));
    system(cmd);
    printf("\n");
    sleep(1);
}


void main(void) {
    int i;
    char *ptr0, *ptr1[256], *ptr2[256], *ptr3[256];
    pid_t pid;

    pid = getpid();
    print_program_break_and_maps(pid);-----1

    ptr0 = malloc(1024*4);
    print_program_break_and_maps(pid);-----2

    free(ptr0);
    print_program_break_and_maps(pid);-----3

    for(i = 0; i < 256; i++) {
        ptr1[i] = malloc(1024*4);
    }
    print_program_break_and_maps(pid);-----4

    for(i = 0; i < 256; i++) {
        ptr2[i] = malloc(1024*4);
    }
    print_program_break_and_maps(pid);-----5

    for(i = 0; i < 256; i++) {
        ptr3[i] = malloc(1024*4);
    }
    print_program_break_and_maps(pid);-----6

    for(i = 0; i < 256; i++) {
        free(ptr3[i]);
    };
    print_program_break_and_maps(pid);-----7

    for(i = 0; i < 256; i++) {
        free(ptr1[i]);
    };
    print_program_break_and_maps(pid);-----8

    for(i = 0; i < 256; i++) {
        free(ptr2[i]);
    };
    print_program_break_and_maps(pid);-----9
}

 

下面结合代码分析一下program break和heap的变化规律。

program break=    0xb000.-----1、此时program break并不是heap的高地址,说明program break和heap高地址并不是一一对应的。
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-0002c000 rwxp 00000000 00:00 0          [heap]
10000000-1001d000 r-xp 00000000 b3:01 960        /lib/ld-2.28.9000.so
1001d000-1001e000 r--p 0001c000 b3:01 960        /lib/ld-2.28.9000.so
1001e000-1001f000 rw-p 0001d000 b3:01 960        /lib/ld-2.28.9000.so
1001f000-10020000 r-xp 00000000 00:00 0          [vdso]
10020000-10022000 rw-p 00000000 00:00 0 
10022000-1014c000 r-xp 00000000 b3:01 952        /lib/libc-2.28.9000.so
1014c000-1014d000 ---p 0012a000 b3:01 952        /lib/libc-2.28.9000.so
1014d000-1014f000 r--p 0012a000 b3:01 952        /lib/libc-2.28.9000.so
1014f000-10150000 rw-p 0012c000 b3:01 952        /lib/libc-2.28.9000.so
10150000-10153000 rw-p 00000000 00:00 0 
7fb77000-7fb98000 rwxp 00000000 00:00 0          [stack]

program break=   0x2c000.-----2、malloc()调用之后,program break和heap高地址一致。但是malloc()只分配了4KB,0x21000是malloc()预分配的空间。
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-0002c000 rwxp 00000000 00:00 0          [heap]
...

program break=   0x2c000.-----3、在free(4KB)之后,program break并没有降低,和第1步不一致。
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-0002c000 rwxp 00000000 00:00 0          [heap]
...

program break=  0x113000.------4、实际分配的空间是4KB*256=1MB,但是heap占用的空间是0x108000,多出了0x8000。
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-00113000 rwxp 00000000 00:00 0          [heap]
...

program break=  0x21b000.-----5、实际malloc()空间时4KB*256*2=2MB,但是heap占用的空间是0x210000,多出了0x10000。
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-0021b000 rwxp 00000000 00:00 0          [heap]
...

program break=  0x323000.-----6、实际malloc()空间是4KB*256*3=3MB,但是heap占用的空间是0x318000,多出了0x18000。
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-00323000 rwxp 00000000 00:00 0          [heap]
...

program break=  0x22e000.-----7、释放了ptr3的1MB空间后,实际heap释放的空间并没有1MB。Why?
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-0022e000 rwxp 00000000 00:00 0          [heap]
...

program break=  0x22e000.-----8、释放的ptr1并不是heap的头部,所以probram break和heap都没有变化。
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-0022e000 rwxp 00000000 00:00 0          [heap]
...

program break=   0x2d000.-----9、释放了ptr2后,heap还保留了0x22000空间。
00008000-00009000 r-xp 00000000 b3:01 1034       /root/sbrk_test
00009000-0000a000 r--p 00000000 b3:01 1034       /root/sbrk_test
0000a000-0000b000 rw-p 00001000 b3:01 1034       /root/sbrk_test
0000b000-0002d000 rwxp 00000000 00:00 0          [heap]
...

 

所以基本上heap的区间比实际malloc()都要大,因为malloc()管理需要额外开销。也验证了1、 program break指向heap高地址;2、sbrk/heap都是以页面对齐的;

1.2 在堆上分配内存:malloc()和free()

C程序使用malloc函数族在堆上分配和释放内存。

较之brk()和sbrk(),malloc函数族具备以下优点:

  • 属于C语言标准的一部分。
  • 更易于在多线程程序中使用。
  • 接口简单,允许分配小块内存。
  • 允许随意释放内存块,他们被维护于一张空闲内存列表中,在后续内存分配调用时循环使用。

malloc()函数在堆上分配参数size字节大小的内存,并返回指向新分配内存起始位置处的指针,其所分配的内存未经初始化。

#include <stdlib.h>
void *malloc(size_t size);
    Returns pointer to allocated memory on success, or NULL on error

由于malloc()的返回类型为void*,因为可以将其赋给任意类型的C指针。

若无法分配内存(或许是因为已经抵达program break所能达到的地址上限),则malloc()返回NULL,并设置errno以返回错误消息。

free()函数释放ptr参数所指向的内存块,该参数应该是之前由malloc(),或者后续其他堆内存分配函数之一所返回的地址。

#include <stdlib.h>
void free(void *ptr);

一般情况下,free()并不降低program break的位置,而是将这块内存添加到空闲内存列表中,供后续malloc()函数循环使用。

  • 被释放的内存通常会位于堆的中间,而非堆的顶部,因为降低program break是不可能的。
  • 它最大限度地减少了程序必须执行的sbrk()调用次数。
  • 在大多数情况下,降低program break的位置不会对那些分配大量内存的程序有多少帮助,因为他们通常倾向于持有已分配内存或是反复释放和重新分配内存,而非释放所有内存后再持续运行一段时间。

传给free()一个空指针,那么函数将什么也不做。

在调用free()后对参数ptr的任何使用,例如将其再次传递给free(),将产生错误,并可能导致不可预知的结果。

调用free()还是不调用free()

当进程终止时,其占用的所有内存都会返回给操作系统,包括堆中由malloc函数所分配的内存。但是最好能够在程序中显式释放所有的已分配内存,因为:

  • 显式调用free()能使程序在未来修改时更具可读性和可维护性。
  • 如果使用malloc调试库来查找程序的内存泄漏问题,那么会将任何未经显式释放处理的内存报告为内存泄漏。这会使发现真正内存泄漏的工作复杂化。

1.3 malloc()和free()的实现

malloc()实现首先会扫描之前由free()所释放的空闲内存块列表,以求找到尺寸大于等于要求的一块空闲内存。

如果这一内存块的尺寸正好与要求相当,就把它直接返回给调用者。如果是一块较大的内存,那么将对其进行分割,在将一块大小相当的内存返回给调用者的同时,把较小的那块空闲内存块保留在空闲列表中。

如果在空闲内存列表中根本找不到足够大的空闲内存块,那么malloc()会调用sbrk()已分配更多的内存。为了减少对sbrk()的调用次数,malloc()并未只是严格按所需字节数来分配内存,而是以更大幅度(以虚拟内存页大小数倍)来增加program brak,并将超出部分置于空闲内存列表。

当free()将内存块置于空闲列表之上时,是如何知晓内存块大小的?当malloc()分配内存块时,会额外分配几个字节来存放记录这块内存大小的整数值。该整数位于内存块的起始处,而实际返回给调用者的内存地址恰好位于这一长度纪录字之后。

当将内存块置于空闲内存列表时,free()会使用内存块本身的空闲来存放链表指针,将自身添加到列表中。

 C语言允许程序创建指向堆中任意位置的指针,并修改其指向的数据,包括由free()和malloc()函数维护的内存块长度、指向前一空闲块和后一空闲块的指针。这就要求malloc()和free()要遵守一下规则。

  • 分配一块内存后,应当小心谨慎,不要改变这块内存范围外的任何内容。
  • 释放同一块已分配内存超过一次是错误的。当两次释放同一块内存时,更常见的后果是导致不可预知的行为。
  • 若非经由malloc函数族中函数所返回的指针,绝不能在free()函数中使用。
  • 在编写需要长时间运行的程序时,如果需要反复分配内存,那么应当确保释放所有已使用完毕的内存。如果不然,堆将稳步增长,直至抵达可用虚拟内存的上限,在此之后分配内存的任何尝试都将以失败告终。

待研究:分析glibc,不同size情况下malloc()/free()看实际内存表现(maps)。

malloc调试的工具和库

glibc提供的malloc调试功能:

mtrace()和muntrace()函数分别在程序中打开和关闭对内存分配调用进行跟踪的功能。这些函数与环境变量MALLOC_TRACE搭配使用,该变量定义了跟踪信息的文件名。

mcheck()和mprobe()函数允许程序对已分配内存块进行一致性检查。当程序试图在已分配内存之外进行写操作时,他们将捕获这个错误。使用这些函数的程序,必须使用cc-lmcheck选项和mcheck库连接。

MALLOC_CHECK_环境变量提供了类似mcheck()和mprobe()函数的功能。设置此变量为不同整数值:0,忽略错误;1,在标准错误输出中打印诊断错误;2,调用abort()来终止程序。

详细介绍参见:《glibc提供的malloc()的调试工具

控制和检测malloc函数包

下面函数用于监测和控制malloc函数族的内存分配:

  • 函数mallopt()能修改各项参数,以控制malloc()所采用的算法。
  • mallinfo()函数返回一个结构,其中包含由malloc()分配内存的各种统计数据。

待研究:mallopt()对malloc()的影响。

1.4 在堆上分配内存的其他方法

用calloc()和realloc()分配内存

calloc()用于给一组相同对象分配内存:

#include <stdlib.h>
void *calloc(size_t numitems, size_t size);
    Returns pointer to allocated memory on success, or NULL on error

与malloc()不同calloc()会将分配的内存初始化为0.

realloc()函数用来调整一块内存的大小,此内存块应是之前由malloc()函数族函数所分配。

#include <stdlib.h>
void *realloc(void *ptr, size_t size);
    Returns pointer to allocated memory on success, or NULL on error

ptr指向需要调整大小的内存块的指针,size指定所需调整大小的期望值。

如果成功,realloc()返回指向大小调整后内存块的指针。如发生错误,realloc()返回NULL,对ptr指针指向的内存块则原封不动。

若realloc()增加了已分配内存块的大小,则不会对额外分配的字节进行初始化

realloc()增大分配内存时几种情况

当realloc()增大已分配内存时,会试图去合并在空闲列表中紧随其后且大小满足要求的内存块。

若原内存快位于堆的顶部,那么realloc()将对对空间进行扩展。

如果内存位于堆中部,且紧临其后的空闲内存空间大小不足,realloc()会分配一块新内存,并将所有数据复制到新内存块中。

由于realloc()可能会移动内存块,任何指向该内存块内部的指针在调用realloc()之后都可能不再可用。

分配对齐的内存:memalign()和posix_memalign()

memalign()和posix_memalign()目的在于分配内存时,起始地址要与2的整数次幂边界对齐。

#include <malloc.h>
void *memalign(size_t boundary, size_t size);
    Returns pointer to allocated memory on success, or NULL on error

boundary必须是2的整数次幂,起始地址是参数boundary的整数倍。

函数返回已分配内存的地址。

#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
    Returns 0 on success, or a positive error number on error

内存与alignment参数的整数倍对齐,alignment必须是sizeof(void*)与2的整数次幂两者间的乘积。

2. 在栈上分配内存:alloca()

 alloca()也可以动态分配内存,不过是从栈中分配。当前调用函数的栈帧位于栈的顶部,因此帧的上方存在扩展空间,只需修改栈指针值即可。

#include <alloca.h>
void *alloca(size_t size);
    Returns pointer to allocated block of memory

 

不需要也绝不可能调用free()来释放由alloca()分配的内存,也不能用realloc()来调整大小。

alloca()的劣势

  • 若调用alloca()造成堆栈溢出,则程序的行为无法预知,特别是在没有收到一个NULL返回值通知错误的情况下。
  • 不能在一个函数的参数列表中调用alloca()。这会使alloca()分配的占空间出现在当前函数参数的空间内,因为函数参数都位于栈内固定位置。

alloca()相对于malloc()的优势

  • alloca()分配内存的速度要快于malloc(),因为编译器将alloca()作为内联代码处理,并且通过直接调整栈指针来实现。alloca()也不需要维护空闲内存列表。
  • alloca()分配的内存随栈帧的溢出而自动释放,亦即当调用alloca的函数返回之时自动释放。
  • 在信号处理程序中调用longjmp()后siglongjmp()一致性非局部跳转时,在起跳函数和落地函数之间的函数中,如果使用malloc()来分配内存,要想避免内存泄漏极其困难,如果使用alloca()则完全可以避免这一问题。

posted on 2020-03-22 00:00  ArnoldLu  阅读(1177)  评论(0编辑  收藏  举报

导航