【轉】linux環境內存分配原理 malloc info

Linux的虛擬內存管理有幾個關鍵概念html

Linux 虛擬地址空間如何分佈?malloc和free是如何分配和釋放內存?如何查看堆內內存的碎片狀況?既然堆內內存brk和sbrk不能直接釋放,爲何不所有使用 mmap 來分配,munmap直接釋放呢 ?linux

Linux 的虛擬內存管理有幾個關鍵概念: 
1
、每一個進程都有獨立的虛擬地址空間,進程訪問的虛擬地址並非真正的物理地址; 
二、虛擬地址可經過每一個進程上的頁表(在每一個進程的內核虛擬地址空間)與物理地址進行映射,得到真正物理地址; 
三、若是虛擬地址對應物理地址不在物理內存中,則產生缺頁中斷,真正分配物理地址,同時更新進程的頁表;若是此時物理內存已耗盡,則根據內存替換算法淘汰部分頁面至物理磁盤中。 

1、Linux 虛擬地址空間如何分佈? 
Linux 使用虛擬地址空間,大大增長了進程的尋址空間,由低地址到高地址分別爲: 
1、只讀段:該部分空間只能讀,不可寫;(包括:代碼段、rodata 段(C常量字符串和#define定義的常量) ) 
2、數據段:保存全局變量、靜態變量的空間; 
3、堆 :就是平時所說的動態內存, malloc/new 大部分都來源於此。其中堆頂的位置可經過函數 brk 和 sbrk 進行動態調整。 
4、文件映射區域:如動態庫、共享內存等映射物理空間的內存,通常是 mmap 函數所分配的虛擬地址空間。 
5、棧:用於維護函數調用的上下文空間,通常爲 8M ,可經過 ulimit –s 查看。 
6、內核虛擬空間:用戶代碼不可見的內存區域,由內核管理(頁表就存放在內核虛擬空間)。 
下圖是 32 位系統典型的虛擬地址空間分佈(來自《深刻理解計算機系統》)。ios

clip_image001

32 位系統有4G 的地址空間::算法

其中 0x08048000~0xbfffffff 是用戶空間,0xc0000000~0xffffffff 是內核空間,包括內核代碼和數據、與進程相關的數據結構(如頁表、內核棧)等。另外,%esp 執行棧頂,往低地址方向變化;brk/sbrk 函數控制堆頂_edata往高地址方向變化sql

64位系統結果怎樣呢? 64 位系統是否擁有 2^64 的地址空間嗎? 
事實上, 64 位系統的虛擬地址空間劃分發生了改變: 
1、地址空間大小不是2^32,也不是2^64,而通常是2^48。數組

由於並不須要 2^64 這麼大的尋址空間,過大空間只會致使資源的浪費。64位Linux通常使用48位來表示虛擬地址空間,40位表示物理地址, 
這可經過#cat  /proc/cpuinfo 來查看: 
clip_image002 
二、其中,0x0000000000000000~0x00007fffffffffff 表示用戶空間, 0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示內核空間,共提供 256TB(2^48) 的尋址空間。 
這兩個區間的特色是,第 47 位與 48~63 位相同,若這些位爲 0 表示用戶空間,不然表示內核空間。 
三、用戶空間由低地址到高地址仍然是只讀段、數據段、堆、文件映射區域和棧數據結構

2、malloc和free是如何分配和釋放內存?併發

如何查看進程發生缺頁中斷的次數app

用# ps -o majflt,minflt -C program 命令查看nosql

clip_image003

majflt表明major fault,中文名叫大錯誤,minflt表明minor fault,中文名叫小錯誤

這兩個數值表示一個進程自啓動以來所發生的缺頁中斷的次數。

能夠用命令ps -o majflt minflt -C program來查看進程的majflt, minflt的值,這兩個值都是累加值,從進程啓動開始累加。在對高性能要求的程序作壓力測試的時候,咱們能夠多關注一下這兩個值。 
若是一個進程使用了mmap將很大的數據文件映射到進程的虛擬地址空間,咱們須要重點關注majflt的值,由於相比minflt,majflt對於性能的損害是致命的,隨機讀一次磁盤的耗時數量級在幾個毫秒,而minflt只有在大量的時候纔會對性能產生影響。

發成缺頁中斷後,執行了那些操做?

當一個進程發生缺頁中斷的時候,進程會陷入內核態,執行如下操做: 
1、檢查要訪問的虛擬地址是否合法 
二、查找/分配一個物理頁 
三、填充物理頁內容(讀取磁盤,或者直接置0,或者啥也不幹) 
四、創建映射關係(虛擬地址到物理地址) 
從新執行發生缺頁中斷的那條指令 
若是第3步,須要讀取磁盤,那麼此次缺頁中斷就是majflt,不然就是minflt。

內存分配的原理

從操做系統角度來看,進程分配內存有兩種方式,分別由兩個系統調用完成:brk和mmap(不考慮共享內存)。

1、brk是將數據段(.data)的最高地址指針_edata往高地址推;

2、mmap是在進程的虛擬地址空間中(堆和棧中間,稱爲文件映射區域的地方)找一塊空閒的虛擬內存

這兩種方式分配的都是虛擬內存,沒有分配物理內存在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操做系統負責分配物理內存,而後創建虛擬內存和物理內存之間的映射關係。


在標準C庫中,提供了malloc/free函數分配釋放內存,這兩個函數底層是由brk,mmap,munmap這些系統調用實現的。

下面以一個例子來講明內存分配的原理:

狀況1、malloc小於128k的內存,使用brk分配內存,將_edata往高地址推(只分配虛擬空間,不對應物理內存(所以沒有初始化),第一次讀/寫數據時,引發內核缺頁中斷,內核才分配對應的物理內存,而後虛擬地址空間創建映射關係),以下圖:

clip_image004

1進程啓動的時候,其(虛擬)內存空間的初始佈局如圖1所示。

其中,mmap內存映射文件是在堆和棧的中間(例如libc-2.2.93.so,其它數據文件等),爲了簡單起見,省略了內存映射文件。

_edata指針(glibc裏面定義)指向數據段的最高地址。 
2
進程調用A=malloc(30K)之後,內存空間如圖2:

      malloc函數會調用brk系統調用,將_edata指針往高地址推30K,就完成虛擬內存分配。

你可能會問:只要把_edata+30K就完成內存分配了?

事實是這樣的,_edata+30K只是完成虛擬地址的分配,A這塊內存如今仍是沒有物理頁與之對應的,等到進程第一次讀寫A這塊內存的時候,發生缺頁中斷,這個時候,內核才分配A這塊內存對應的物理頁。也就是說,若是用malloc分配了A這塊內容,而後歷來不訪問它,那麼,A對應的物理頁是不會被分配的。 
三、
進程調用B=malloc(40K)之後,內存空間如圖3。

狀況2、malloc大於128k的內存,使用mmap分配內存,在堆和棧之間找一塊空閒內存分配(對應獨立內存,並且初始化爲0),以下圖:

clip_image005

4進程調用C=malloc(200K)之後,內存空間如圖4:

默認狀況下,malloc函數分配內存,若是請求內存大於128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指針了,而是利用mmap系統調用,從堆和棧的中間分配一塊虛擬內存。

這樣子作主要是由於::

brk分配的內存須要等到高地址內存釋放之後才能釋放(例如,在B釋放以前,A是不可能釋放的,這就是內存碎片產生的緣由,何時緊縮看下面),而mmap分配的內存能夠單獨釋放。

固然,還有其它的好處,也有壞處,再具體下去,有興趣的同窗能夠去看glibc裏面malloc的代碼了。 
5進程調用D=malloc(100K)之後,內存空間如圖5; 
6進程調用free(C)之後,C對應的虛擬內存和物理內存一塊兒釋放。

clip_image006

7進程調用free(B)之後,如圖7所示:

        B對應的虛擬內存和物理內存都沒有釋放,由於只有一個_edata指針,若是往回推,那麼D這塊內存怎麼辦呢

固然,B這塊內存,是能夠重用的,若是這個時候再來一個40K的請求,那麼malloc極可能就把B這塊內存返回回去了。 
8進程調用free(D)之後,如圖8所示:

        B和D鏈接起來,變成一塊140K的空閒內存。

9默認狀況下:

當最高地址空間的空閒內存超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行內存緊縮操做(trim)。在上一個步驟free的時候,發現最高地址空閒內存超過128K,因而內存緊縮,變成圖9所示。

真相大白 
說完內存分配的原理,那麼被測模塊在內核態cpu消耗高的緣由就很清楚了:每次請求來都malloc一塊2M的內存,默認狀況下,malloc調用 mmap分配內存,請求結束的時候,調用munmap釋放內存。假設每一個請求須要6個物理頁,那麼每一個請求就會產生6個缺頁中斷,在2000的壓力下,每 秒就產生了10000屢次缺頁中斷,這些缺頁中斷不須要讀取磁盤解決,因此叫作minflt;缺頁中斷在內核態執行,所以進程的內核態cpu消耗很大。缺 頁中斷分散在整個請求的處理過程當中,因此表現爲分配語句耗時(10us)相對於整條請求的處理時間(1000us)比重很小。 
解決辦法 
將動態內存改成靜態分配,或者啓動的時候,用malloc爲每一個線程分配,而後保存在threaddata裏面。可是,因爲這個模塊的特殊性,靜態分配,或者啓動時候分配都不可行。另外,Linux下默認棧的大小限制是10M,若是在棧上分配幾M的內存,有風險。 
禁止malloc調用mmap分配內存,禁止內存緊縮。 
在進程啓動時候,加入如下兩行代碼: 
mallopt(M_MMAP_MAX, 0); // 禁止malloc調用mmap分配內存 
mallopt(M_TRIM_THRESHOLD, -1); // 禁止內存緊縮 
效果:加入這兩行代碼之後,用ps命令觀察,壓力穩定之後,majlt和minflt都爲0。進程的系統態cpu從20降到10。

3、如何查看堆內內存的碎片狀況 ?

glibc 提供瞭如下結構和接口來查看堆內內存和 mmap 的使用狀況。 
struct mallinfo { 
  int arena;            /* non-mmapped space allocated from system */ 
  int ordblks;         /* number of free chunks */ 
  int smblks;          /* number of fastbin blocks */ 
  int hblks;             /* number of mmapped regions */ 
  int hblkhd;           /* space in mmapped regions */ 
  int usmblks;        /* maximum total allocated space */ 
  int fsmblks;         /* space available in freed fastbin blocks */ 
  int uordblks;        /* total allocated space */ 
  int fordblks;         /* total free space */ 
  int keepcost;       /* top-most, releasable (via malloc_trim) space */ 
};

/*返回heap(main_arena)的內存使用狀況,以 mallinfo 結構返回 */ 
struct mallinfo mallinfo();

/* 將heap和mmap的使用狀況輸出到stderr*/ 
void malloc_stats();

可經過如下例子來驗證mallinfo和malloc_stats輸出結果。 

#include <stdlib.h> 
#include <stdio.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/mman.h> 
#include <malloc.h>

size_t  heap_malloc_total, heap_free_total,mmap_total, mmap_count;

void print_info() { 
    struct mallinfo mi = mallinfo(); 
    printf("count by itself:\n"); 
    printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\tmmap_total=%lu mmap_count=%lu\n", 
              heap_malloc_total*1024, heap_free_total*1024, heap_malloc_total*1024-heap_free_total*1024, 
              mmap_total*1024, mmap_count); 
    printf("count by mallinfo:\n"); 
    printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\tmmap_total=%lu mmap_count=%lu\n", 
             mi.arena, mi.fordblks, mi.uordblks, 
             mi.hblkhd, mi.hblks); 
    printf("from malloc_stats:\n"); 
    malloc_stats(); 
}

#define ARRAY_SIZE 200 
int main(int argc, char** argv) 
{ 
    char** ptr_arr[ARRAY_SIZE]; 
    int i;  
    for( i = 0; i < ARRAY_SIZE; i++) 
    { 
            ptr_arr[i] = malloc(i * 1024);  
            if ( i < 128)                                      //glibc默認128k以上使用mmap 
            { 
                    heap_malloc_total += i; 
            } 
            else 
            { 
                    mmap_total += i; 
                    mmap_count++; 
            } 
    }  
    print_info();

    for( i = 0; i < ARRAY_SIZE; i++) 
    { 
           if ( i % 2 == 0) 
                continue; 
           free(ptr_arr[i]);

           if ( i < 128) 
           { 
                   heap_free_total += i; 
           } 
           else 
           { 
                  mmap_total -= i; 
                  mmap_count--; 
           } 
    }  
    printf("\nafter free\n"); 
    print_info();

    return 1; 
}

該例子第一個循環爲指針數組每一個成員分配索引位置 (KB) 大小的內存塊,並經過 128 爲分界分別對 heap 和 mmap 內存分配狀況進行計數; 
第二個循環是 free 索引下標爲奇數的項,同時更新計數狀況。經過程序的計數與mallinfo/malloc_stats 接口獲得結果進行對比,並經過 print_info打印到終端。

下面是一個執行結果: 
count by itself: 
        heap_malloc_total=8323072 heap_free_total=0 heap_in_use=8323072 
        mmap_total=12054528 mmap_count=72 
count by mallinfo: 
        heap_malloc_total=8327168 heap_free_total=2032 heap_in_use=8325136 
        mmap_total=12238848 mmap_count=72

from malloc_stats: 
Arena 0: 
system bytes     =    8327168 
in use bytes     =    8325136 
Total (incl. mmap): 
system bytes     =   20566016 
in use bytes     =   20563984 
max mmap regions =         72 
max mmap bytes   =   12238848

after free 
count by itself: 
        heap_malloc_total=8323072 heap_free_total=4194304 heap_in_use=4128768 
        mmap_total=6008832 mmap_count=36

count by mallinfo: 
        heap_malloc_total=8327168 heap_free_total=4197360 heap_in_use=4129808 
        mmap_total=6119424 mmap_count=36

from malloc_stats: 
Arena 0: 
system bytes     =    8327168 
in use bytes     =    4129808 
Total (incl. mmap): 
system bytes     =   14446592 
in use bytes     =   10249232 
max mmap regions =         72 
max mmap bytes   =   12238848

由上可知,程序統計和mallinfo 獲得的信息基本吻合,其中 heap_free_total 表示堆內已釋放的內存碎片總和。 
若是想知道堆內究竟有多少碎片,可經過 mallinfo 結構中的 fsmblks 、smblks 、ordblks 值獲得,這些值表示不一樣大小區間的碎片總個數,這些區間分別是 0~80 字節,80~512 字節,512~128k。若是 fsmblks 、 smblks 的值過大,那碎片問題可能比較嚴重了。 
不過, mallinfo 結構有一個很致命的問題,就是其成員定義所有都是 int ,在 64 位環境中,其結構中的 uordblks/fordblks/arena/usmblks 很容易就會致使溢出,應該是歷史遺留問題,使用時要注意!

4、既然堆內內存brk和sbrk不能直接釋放,爲何不所有使用 mmap 來分配,munmap直接釋放呢? 
既然堆內碎片不能直接釋放,致使疑似「內存泄露」問題,爲何 malloc 不所有使用 mmap 來實現呢(mmap分配的內存能夠會經過 munmap 進行 free ,實現真正釋放)?而是僅僅對於大於 128k 的大塊內存才使用 mmap ?

其實,進程向 OS 申請和釋放地址空間的接口 sbrk/mmap/munmap 都是系統調用,頻繁調用系統調用都比較消耗系統資源的。而且, mmap 申請的內存被 munmap 後,從新申請會產生更多的缺頁中斷。例如使用 mmap 分配 1M 空間,第一次調用產生了大量缺頁中斷 (1M/4K 次 ) ,當munmap 後再次分配 1M 空間,會再次產生大量缺頁中斷。缺頁中斷是內核行爲,會致使內核態CPU消耗較大。另外,若是使用 mmap 分配小內存,會致使地址空間的分片更多,內核的管理負擔更大。 
同時堆是一個連續空間,而且堆內碎片因爲沒有歸還 OS ,若是可重用碎片,再次訪問該內存極可能不需產生任何系統調用和缺頁中斷,這將大大下降 CPU 的消耗。 所以, glibc 的 malloc 實現中,充分考慮了 sbrk 和 mmap 行爲上的差別及優缺點,默認分配大塊內存 (128k) 才使用 mmap 得到地址空間,也可經過 mallopt(M_MMAP_THRESHOLD, <SIZE>) 來修改這個臨界值。

5、如何查看進程的缺頁中斷信息? 
可經過如下命令查看缺頁中斷信息 
ps -o majflt,minflt -C <program_name> 
ps -o majflt,minflt -p <pid> 
其中:: majflt 表明 major fault ,指大錯誤;

           minflt 表明 minor fault ,指小錯誤。

這兩個數值表示一個進程自啓動以來所發生的缺頁中斷的次數。 
其中 majflt 與 minflt 的不一樣是::

        majflt 表示須要讀寫磁盤,多是內存對應頁面在磁盤中須要load 到物理內存中,也多是此時物理內存不足,須要淘汰部分物理頁面至磁盤中。

參看:: http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201210975312473/

6、除了 glibc 的 malloc/free ,還有其餘第三方實現嗎?

其實,不少人開始詬病 glibc 內存管理的實現,特別是高併發性能低下和內存碎片化問題都比較嚴重,所以,陸續出現一些第三方工具來替換 glibc 的實現,最著名的當屬 google 的tcmalloc和facebook 的jemalloc 。 
網上有不少資源,能夠本身查(只用使用第三方庫,代碼不用修改,就可使用第三方庫中的malloc)。

參考資料: 
《深刻理解計算機系統》第 10 章 
http://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt

https://www.ibm.com/developerworks/cn/linux/l-lvm64/

http://www.kerneltravel.net/journal/v/mem.htm

http://blog.csdn.net/baiduforum/article/details/6126337

http://www.nosqlnotes.net/archives/105

http://www.man7.org/linux/man-pages/man3/mallinfo.3.html

原文地址:http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201210975312473/

 

測試程序代碼

#include <malloc.h>
#include <string.h>
#include <stdlib.h>
#include <iostream>

static void display_mallinfo(void){
    struct mallinfo mi;
    mi = mallinfo();
    printf("Total non-mmapped bytes (arena):       %d\n", mi.arena);
    printf("# of free chunks (ordblks):            %d\n", mi.ordblks);
    printf("# of free fastbin blocks (smblks):     %d\n", mi.smblks);
    printf("# of mapped regions (hblks):           %d\n", mi.hblks);
    printf("Bytes in mapped regions (hblkhd):      %d\n", mi.hblkhd);
    printf("Max. total allocated space (usmblks):  %d\n", mi.usmblks);
    printf("Free bytes held in fastbins (fsmblks): %d\n", mi.fsmblks);
    printf("Total allocated space (uordblks):      %d\n", mi.uordblks);
    printf("Total free space (fordblks):           %d\n", mi.fordblks);
    printf("Topmost releasable block (keepcost):   %d\n", mi.keepcost);
}

int main(int argc, char *argv[]) {
#define MAX_ALLOCS 2000000
    char *alloc[MAX_ALLOCS];
    int numBlocks, j, freeBegin, freeEnd, freeStep;
    size_t blockSize;

    if (argc < 3 || strcmp(argv[1], "--help") == 0) {
        printf("%s num-blocks block-size [free-step [start-free "
                "[end-free]]]\n", argv[0]);
        return 0;
    }
    numBlocks = atoi(argv[1]);
    blockSize = atoi(argv[2]);
    freeStep = (argc > 3) ? atoi(argv[3]) : 1;
    freeBegin = (argc > 4) ? atoi(argv[4]) : 0;
    freeEnd = (argc > 5) ? atoi(argv[5]) : numBlocks;

    printf("============== Before allocating blocks ==============\n");
    display_mallinfo();

    for (j = 0; j < numBlocks; j++) {
        if (numBlocks >= MAX_ALLOCS)
            std::cout<<"Too many allocations"<<std::endl;

        alloc[j] = (char *)malloc(blockSize);
        if (alloc[j] == NULL)
            std::cout<<"malloc"<<std::endl;
    }

    printf("\n============== After allocating blocks ==============\n");
    display_mallinfo();

    for (j = freeBegin; j < freeEnd; j += freeStep)
        free(alloc[j]);
   
    printf("\n============== After freeing blocks ==============\n");
    display_mallinfo();
    exit(EXIT_SUCCESS);
}

 

=====================上面是拷貝別人的基礎知識,有了基礎纔好繼續領悟========================

 

1.經過gdb查找main的棧起始地址(能夠考慮增長一個全局變量,在它調用構造函數時記錄下其地址。)

操做系統棧的地址分配是每一個程序分配127T(64位)虛擬內存,程序看到的只是虛擬地址,任何程序線程棧入口都是接近0x7fffffffffff(由0x7fffffffffff加上一個隨機值)。 進入main函數時的棧指針並非真正棧起始地址,由於編譯器添加的其餘準備代碼處理在調用到main以前已經消耗一部分的棧空間。

2.進一步考慮

因爲虛擬內存的存在,系統整理內存也就成爲可能。 分析即便系統內存整理調整後,虛擬內存的地址也不會變,各個線程的棧內存也應該不會發生變化,只是每一個內存頁對應的物理內存發生變化。進程啓動後增長各個線程棧起始地址打印,應該能夠用於某些core以後無棧問題的定位。

相關文章
相關標籤/搜索