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

 

 

不斷學習中。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM