Linux虛擬內存管理(glibc)


轉:https://blog.csdn.net/tengxy_cloud/article/details/53067396

https://www.cnblogs.com/purpleraintear/p/6051562.html

 

在使用mysql作為DB開發的兌換券系統中,隨着分區表的不斷創建,發現mysqld出現了疑似“內存泄露”現象,但通過 valgrind 等工具檢測后,並沒發現類似的問題(最終原因是由於glibc的內存碎片造成)。

最近在做 MySQL 版本升級時( 5.1->5.5 ) , 發現了 mysqld 疑似“內存泄露”現象,但通過 valgrind 等工具檢測后,並沒發現類似的問題。因此,需要深入學習 Linux 的虛擬內存管理方面的內容來解釋這個現象。

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

  1. 每個進程有獨立的虛擬地址空間,進程訪問的虛擬地址並不是真正的物理地址

  2. 虛擬地址可通過每個進程上頁表與物理地址進行映射,獲得真正物理地址

  3. 如果虛擬地址對應物理地址不在物理內存中,則產生缺頁中斷,真正分配物理地址,同時更新進程的頁表;如果此時物理內存已耗盡,則根據內存替換算法淘汰部分頁面至物理磁盤中。

基於以上認識,這篇文章通過本人以前對虛擬內存管理的疑惑由淺入深整理了以下十個問題,並通過例子和系統命令嘗試進行解答。

  1. Linux 虛擬地址空間如何分布? 32 位和 64 位有何不同?

  2. malloc 是如何分配內存的?

  3. malloc 分配多大的內存,就占用多大的物理內存空間嗎?

  4. 如何查看進程虛擬地址空間的使用情況?

  5. free 的內存真的釋放了嗎(還給 OS ) ?

  6. 程序代碼中 malloc 的內存都有相應的 free ,就不會出現內存泄露了嗎?

  7. 既然堆內內存不能直接釋放,為什么不全部使用 mmap 來分配?

  8. 如何查看進程的缺頁中斷信息?

  9. 如何查看堆內內存的碎片情況?

  10. 除了 glibc 的 malloc/free ,還有其他第三方實現嗎?

一.Linux 虛擬地址空間如何分布? 32 位和 64 位有何不同?

Linux 使用虛擬地址空間,大大增加了進程的尋址空間,由低地址到高地址分別為:

  1. 只讀段:該部分空間只能讀,不可寫,包括代碼段、 rodata 段( C 常量字符串和 #define 定義的常量)
  2. 數據段:保存全局變量、靜態變量的空間
  3. 堆 :就是平時所說的動態內存, malloc/new 大部分都來源於此。其中堆頂的位置可通過函數 brk 和 sbrk 進行動態調整。
  4. 文件映射區域 :如動態庫、共享內存等映射物理空間的內存,一般是 mmap 函數所分配的虛擬地址空間。
  5. 棧:用於維護函數調用的上下文空間,一般為 8M ,可通過 ulimit –s 查看。
  6. 內核虛擬空間:用戶代碼不可見的內存區域,由內核管理。

下圖是 32 位系統典型的虛擬地址空間分布(來自《深入理解計算機系統》)。

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

可通過以下代碼驗證進程的地址空間分布,其中 sbrk(0) 函數用於返回棧頂指針。

  1.  
    #include <stdlib.h>
  2.  
    #include <stdio.h>
  3.  
    #include <string.h>
  4.  
    #include <unistd.h>
  5.  
    int global_num = 0;
  6.  
    char global_str_arr [65536] = {'a'};
  7.  
    int main(int argc, char** argv)
  8.  
    {
  9.  
    char* heap_var = NULL;
  10.  
    int local_var = 0;
  11.  
    printf("Address of function main 0x%lx\n", main);
  12.  
    printf("Address of global_num 0x%lx\n", &global_num);
  13.  
    printf("Address of global_str_arr 0x%lx ~ 0x%lx\n", &global_str_arr[0], &global_str_arr[65535]);
  14.  
    printf("Top of stack is 0x%lx\n", &local_var);
  15.  
    printf("Top of heap is 0x%lx\n", sbrk(0));
  16.  
    heap_var = malloc(sizeof(char) * 127 * 1024);
  17.  
    printf("Address of heap_var is 0x%lx\n", heap_var);
  18.  
    printf("Top of heap after malloc is 0x%lx\n", sbrk(0));
  19.  
    free(heap_var);
  20.  
    heap_var = NULL;
  21.  
    printf("Top of heap after free is 0x%lx\n", sbrk(0));
  22.  
    return 1;
  23.  
    }

32 位系統的結果如下,與上圖的划分保持一致,並且棧頂指針在 mallloc 和 free 一個 127K 的存儲空間時都發生了變化(增大和縮小)。

  1.  
    Address of function main 0x8048474
  2.  
    Address of global_num 0x8059904
  3.  
    Address of global_str_arr 0x8049900 ~ 0x80598ff
  4.  
    Top of stack is 0xbfd0886c
  5.  
    Top of heap is 0x805a000
  6.  
    Address of heap_var is 0x805a008
  7.  
    Top of heap after malloc is 0x809a000
  8.  
    Top of heap after free is 0x807b000

但是, 64 位系統結果怎樣呢? 64 位系統是否擁有 2^64 的地址空間嗎?

64 位系統運行結果如下:

  1.  
    Address of function main 0x400594
  2.  
    Address of global_num 0x610b90
  3.  
    Address of global_str_arr 0x600b80 ~ 0x610b7f
  4.  
    Top of stack is 0x7fff2e9e4994
  5.  
    Top of heap is 0x8f5000
  6.  
    Address of heap_var is 0x8f5010
  7.  
    Top of heap after malloc is 0x935000
  8.  
    Top of heap after free is 0x916000

從結果知,與上圖的分布並不一致。而事實上, 64 位系統的虛擬地址空間划分發生了改變:

  1. 地址空間大小不是 2^32 ,也不是 2^64 ,而一般是 2^48 。因為並不需要 2^64 這么大的尋址空間,過大空間只會導致資源的浪費。 64 位 Linux 一般使用 48 位來表示虛擬地址空間, 40 位表示物理地址,這可通過 /proc/cpuinfo 來查看

    address sizes : 40 bits physical, 48 bits virtual

  2. 其中, 0x0000000000000000~0x00007fffffffffff表示用戶空間,0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF表示內核空間,共提供 256TB(2^48) 的尋址空間。這兩個區間的特點是,第 47 位與 48~63 位相同,若這些位為 0 表示用戶空間,否則表示內核空間。

  3. 用戶空間由低地址到高地址仍然是只讀段、數據段、堆、文件映射區域和棧

二.malloc 是如何分配內存的?

malloc 是 glibc 中內存分配函數,也是最常用的動態內存分配函數,其內存必須通過 free 進行釋放,否則導致內存泄露。

關於 malloc 獲得虛存空間的實現,與 glibc 的版本有關,但大體邏輯是:

  1. 若分配內存小於 128k ,調用 sbrk() ,將堆頂指針向高地址移動,獲得新的虛存空間。

  2. 若分配內存大於 128k ,調用 mmap() ,在文件映射區域中分配匿名虛存空間。

  3. 這里討論的是簡單情況,如果涉及並發可能會復雜一些,不過先不討論。

其中 sbrk 就是修改棧頂指針位置,而 mmap 可用於生成文件的映射以及匿名頁面的內存,這里指的是匿名頁面。

而這個 128k ,是 glibc 的默認配置,可通過函數 mallopt 來設置,可通過以下例子說明。

  1.  
    #include <stdlib.h>
  2.  
    #include <stdio.h>
  3.  
    #include <string.h>
  4.  
    #include <unistd.h>
  5.  
    #include <sys/mman.h>
  6.  
    #include <malloc.h>
  7.  
    void print_info(
  8.  
    char* var_name,
  9.  
    char* var_ptr,
  10.  
    size_t size_in_kb
  11.  
    )
  12.  
     
  13.  
    {
  14.  
    printf("Address of %s(%luk) 0x%lx, now heap top is 0x%lx\n",
  15.  
    var_name, size_in_kb, var_ptr, sbrk( 0));
  16.  
     
  17.  
    }
  18.  
    int main(int argc, char** argv)
  19.  
    {
  20.  
    char *heap_var1, *heap_var2, *heap_var3 ;
  21.  
    char *mmap_var1, *mmap_var2, *mmap_var3 ;
  22.  
    char *maybe_mmap_var;
  23.  
    printf("Orginal heap top is 0x%lx\n", sbrk(0));
  24.  
    heap_var1 = malloc(32*1024);
  25.  
    print_info( "heap_var1", heap_var1, 32);
  26.  
    heap_var2 = malloc(64*1024);
  27.  
    print_info( "heap_var2", heap_var2, 64);
  28.  
    heap_var3 = malloc(127*1024);
  29.  
    print_info( "heap_var3", heap_var3, 127);
  30.  
    printf("\n");
  31.  
    maybe_mmap_var = malloc(128*1024);
  32.  
    print_info( "maybe_mmap_var", maybe_mmap_var, 128);
  33.  
    //mmap
  34.  
    mmap_var1 = malloc(128*1024);
  35.  
    print_info( "mmap_var1", mmap_var1, 128);
  36.  
    // set M_MMAP_THRESHOLD to 64k
  37.  
    mallopt(M_MMAP_THRESHOLD, 64*1024);
  38.  
    printf("set M_MMAP_THRESHOLD to 64k\n");
  39.  
    mmap_var2 = malloc(64*1024);
  40.  
    print_info( "mmap_var2", mmap_var2, 64);
  41.  
    mmap_var3 = malloc(127*1024);
  42.  
    print_info( "mmap_var3", mmap_var3, 127);
  43.  
    return 1;
  44.  
    }

這個例子很簡單,通過 malloc 申請多個不同大小的動態內存,同時通過接口 print_info 打印變量大小和地址等相關信息,其中 sbrk(0) 可返回堆頂指針位置。另外,粗體部分是將 MMAP 分配的臨界點由 128k 轉為 64k ,再打印變量地址的不同。

下面是 Linux 64 位機器的執行結果(后文所有例子都是通過 64 位機器上的測試結果)。

  1.  
    Orginal heap top is 0x17da000
  2.  
    Address of heap_var1(32k) 0x17da010, now heap top is 0x1803000
  3.  
    Address of heap_var2(64k) 0x17e2020, now heap top is 0x1803000
  4.  
    Address of heap_var3(127k) 0x17f2030, now heap top is 0x1832000
  5.  
    Address of maybe_mmap_var(128k) 0x1811c40, now heap top is 0x1832000
  6.  
    Address of mmap_var1(128k) 0x7f4a0b1f2010, now heap top is 0x1832000
  7.  
    set M_MMAP_THRESHOLD to 64k
  8.  
    Address of mmap_var2(64k) 0x7f4a0b1e1010, now heap top is 0x1832000
  9.  
    Address of mmap_var3(127k) 0x7f4a0b1c1010, now heap top is 0x1832000

三.malloc 分配多大的內存,就占用多大的物理內存空間嗎?

我們知道, malloc 分配的的內存是虛擬地址空間,而虛擬地址空間和物理地址空間使用進程頁表進行映射,那么分配了空間就是占用物理內存空間了嗎?

首先,進程使用多少內存可通過 ps aux 命令 查看,其中關鍵的兩信息(第五、六列)為:

  1. VSZ , virtual memory size ,表示進程總共使用的虛擬地址空間大小,包括進程地址空間的代碼段、數據段、堆、文件映射區域、棧、內核空間等所有虛擬地址使用的總和,單位是 K

  2. RSS , resident set size ,表示進程實際使用的物理內存空間, RSS 總小於 VSZ 。

可通過一個例子說明這個問題:

  1.  
    #include <stdlib.h>
  2.  
    #include <stdio.h>
  3.  
    #include <string.h>
  4.  
    #include <unistd.h>
  5.  
    #include <sys/mman.h>
  6.  
    #include <malloc.h>
  7.  
    char ps_cmd[1024];
  8.  
    void print_info(
  9.  
    char* var_name,
  10.  
    char* var_ptr,
  11.  
    size_t size_in_kb
  12.  
    )
  13.  
     
  14.  
    {
  15.  
    printf("Address of %s(%luk) 0x%lx, now heap top is 0x%lx\n",
  16.  
    var_name, size_in_kb, var_ptr, sbrk( 0));
  17.  
    system(ps_cmd);
  18.  
    }
  19.  
     
  20.  
    int main(int argc, char** argv)
  21.  
    {
  22.  
    char *non_set_var, *set_1k_var, *set_5k_var, *set_7k_var;
  23.  
    pid_t pid;
  24.  
    pid = getpid();
  25.  
    sprintf(ps_cmd, "ps aux | grep %lu | grep -v grep", pid);
  26.  
    non_set_var = malloc(32*1024);
  27.  
    print_info( "non_set_var", non_set_var, 32);
  28.  
    set_1k_var = malloc(64*1024);
  29.  
    memset(set_1k_var, 0, 1024);
  30.  
    print_info( "set_1k_var", set_1k_var, 64);
  31.  
    set_5k_var = malloc(127*1024);
  32.  
    memset(set_5k_var, 0, 5*1024);
  33.  
    print_info( "set_5k_var", set_5k_var, 127);
  34.  
    set_7k_var = malloc(64*1024);
  35.  
    memset(set_1k_var, 0, 7*1024);
  36.  
    print_info( "set_7k_var", set_7k_var, 64);
  37.  
    return 1;
  38.  
    }

該代碼擴展了上一個例子print_info能力,處理打印變量信息,同時通過 ps aux 命令獲得當前進程的 VSZ 和 RSS 值。並且程序 malloc 一塊內存后,會 memset 內存的若干 k 內容。

執行結果為

  1.  
    Address of non_set_var(32k) 0x502010, now heap top is 0x52b000
  2.  
     
  3.  
    mysql 12183 0.0 0.0 2692 452 pts/3 S+ 20:29 0:00 ./test_vsz
  4.  
     
  5.  
    Address of set_1k_var(64k) 0x50a020, now heap top is 0x52b000
  6.  
     
  7.  
    mysql 12183 0.0 0.0 2692 456 pts/3 S+ 20:29 0:00 ./test_vsz
  8.  
     
  9.  
    Address of set_5k_var(127k) 0x51a030, now heap top is 0x55a000
  10.  
     
  11.  
    mysql 12183 0.0 0.0 2880 464 pts/3 S+ 20:29 0:00 ./test_vsz
  12.  
     
  13.  
    Address of set_7k_var(64k) 0x539c40, now heap top is 0x55a000
  14.  
     
  15.  
    mysql 12183 0.0 0.0 2880 472 pts/3 S+ 20:29 0:00 ./test_vsz

由以上結果知:

  1. VSZ 並不是每次 malloc 后都增長,是與上一節說的堆頂沒發生變化有關,因為可重用堆頂內剩余的空間,這樣的 malloc 是很輕量快速的。

  2. 但如果 VSZ 發生變化,基本與分配內存量相當,因為 VSZ 是計算虛擬地址空間總大小。

  3. RSS 的增量很少,是因為 malloc 分配的內存並不就馬上分配實際存儲空間,只有第一次使用,如第一次 memset 后才會分配。

  4. 由於每個物理內存頁面大小是 4k ,不管 memset 其中的 1k 還是 5k 、 7k ,實際占用物理內存總是 4k 的倍數。所以 RSS 的增量總是 4k 的倍數。

  5. 因此,不是 malloc 后就馬上占用實際內存,而是第一次使用時發現虛存對應的物理頁面未分配,產生缺頁中斷,才真正分配物理頁面,同時更新進程頁面的映射關系。這也是 Linux 虛擬內存管理的核心概念之一。

四. 如何查看進程虛擬地址空間的使用情況?

進程地址空間被分為了代碼段、數據段、堆、文件映射區域、棧等區域,那怎么查詢這些虛擬地址空間的使用情況呢?

Linux 提供了 pmap 命令來查看這些信息,通常使用 pmap -d $pid (高版本可提供 pmap -x $pid)查詢,如下所示:

  1.  
    mysql@ TLOG_590_591:~/vin/test_memory> pmap -d 17867
  2.  
     
  3.  
    17867: test_mmap
  4.  
     
  5.  
    START SIZE RSS DIRTY PERM OFFSET DEVICE MAPPING
  6.  
     
  7.  
    00400000 8K 4K 0K r-xp 00000000 08:01 /home/mysql/vin/test_memory/test_mmap
  8.  
     
  9.  
    00501000 68K 8K 8K rw-p 00001000 08:01 /home/mysql/vin/test_memory/test_mmap
  10.  
     
  11.  
    00512000 76K 0K 0K rw-p 00512000 00:00 [heap]
  12.  
     
  13.  
    0053e000 256K 0K 0K rw-p 0053e000 00:00 [anon]
  14.  
     
  15.  
    2b3428f97000 108K 92K 0K r-xp 00000000 08:01 /lib64/ld-2.4.so
  16.  
     
  17.  
    2b3428fb2000 8K 8K 8K rw-p 2b3428fb2000 00:00 [anon]
  18.  
     
  19.  
    2b3428fc1000 4K 4K 4K rw-p 2b3428fc1000 00:00 [anon]
  20.  
     
  21.  
    2b34290b1000 8K 8K 8K rw-p 0001a000 08:01 /lib64/ld-2.4.so
  22.  
     
  23.  
    2b34290b3000 1240K 248K 0K r-xp 00000000 08:01 /lib64/libc-2.4.so
  24.  
     
  25.  
    2b34291e9000 1024K 0K 0K ---p 00136000 08:01 /lib64/libc-2.4.so
  26.  
     
  27.  
    2b34292e9000 12K 12K 12K r--p 00136000 08:01 /lib64/libc-2.4.so
  28.  
     
  29.  
    2b34292ec000 8K 8K 8K rw-p 00139000 08:01 /lib64/libc-2.4.so
  30.  
     
  31.  
    2b34292ee000 1048K 36K 36K rw-p 2b34292ee000 00:00 [anon]
  32.  
     
  33.  
    7fff81afe000 84K 12K 12K rw-p 7fff81afe000 00:00 [stack]
  34.  
     
  35.  
    ffffffffff600000 8192K 0K 0K ---p 00000000 00:00 [vdso]
  36.  
     
  37.  
    Total: 12144K 440K 96K

從這個結果可以看到進程虛擬地址空間的使用情況,包括起始地址、大小、實際使用內存、臟頁大小、權限、偏移、設備和映射文件等。 pmap 命令就是基於下面兩文件內容進行解析的:

  1.  
    /proc/ $pid/maps
  2.  
     
  3.  
    /proc/ $pid/smaps

並且對於上述每個內存塊區間,內核會使用一個 vm_area_struct 結構來維護,同時通過頁面建立與物理內存的映射關系,如下圖所示。

 

五.free 的內存真的釋放了嗎(還給 OS ) ?

前面所有例子都有一個很嚴重的問題,就是分配的內存都沒有釋放,即導致內存泄露。原則上所有 malloc/new 分配的內存,都需 free/delete 來釋放。但是, free 了的內存真的釋放了嗎?

要說清楚這個問題,可通過下面例子來說明。

  1. 初始狀態:如圖 (1) 所示,系統已分配 ABCD 四塊內存,其中 ABD 在堆內分配, C 使用 mmap 分配。為簡單起見,圖中忽略了如共享庫等文件映射區域的地址空間。

  2. E=malloc(100k) :分配 100k 內存,小於 128k ,從堆內分配,堆內剩余空間不足,擴展堆頂 (brk) 指針。

  3. free(A) :釋放 A 的內存,在 glibc 中,僅僅是標記為可用,形成一個內存空洞 ( 碎片 ) ,並沒有真正釋放。如果此時需要分配 40k 以內的空間,可重用此空間,剩余空間形成新的小碎片。

  4. free(C) : C 空間大於 128K ,使用 mmap 分配,如果釋放 C ,會調用 munmap 系統調用來釋放,並會真正釋放該空間,還給 OS ,如圖 (4) 所示。

  5. free(D) :與釋放 A 類似,釋放 D 同樣會導致一個空洞,獲得空閑空間,但並不會還給 OS 。此時,空閑總空間為 100K ,但由於虛擬地址不連續,無法合並,空閑空間無法滿足大於 60k 的分配請求。

  6. free(E) :釋放 E ,由於與 D 連續,兩者將進行合並,得到 160k 連續空閑空間。同時 E 是最靠近堆頂的空間, glibc 的 free 實現中,只要堆頂附近釋放總空間(包括合並的空間)超過 128k ,即會調用 sbrk(-SIZE) 來回溯堆頂指針,將原堆頂空間還給 OS ,如圖 (6) 所示。而堆內的空閑空間還是不會歸還 OS 的。

由此可見:

  1. malloc 使用 mmap 分配的內存 ( 大於 128k) , free 會調用 munmap 系統調用馬上還給 OS ,實現真正釋放。

  2. 堆內的內存,只有釋放堆頂的空間,同時堆頂總連續空閑空間大於 128k 才使用 sbrk(-SIZE) 回收內存,真正歸還 OS 。

  3. 堆內的空閑空間,是不會歸還給 OS 的。

六. 程序代碼中 malloc 的內存都有相應的 free ,就不會出現內存泄露了嗎?

狹義上的內存泄露是指 malloc 的內存,沒有 free ,導致內存浪費,直到程序結束。而廣義上的內存泄露就是進程使用內存量不斷增加,或大大超出系統原設計的上限。

上一節說到, free 了的內存並不會馬上歸還 OS ,並且堆內的空洞(碎片)更是很難真正釋放,除非空洞成為了新的堆頂。所以,如上一例子情況 (5) ,釋放了 40k 和 60k 兩片內存,但如果此時需要申請大於 60k (如 70k ),沒有可用碎片,必須向 OS 申請,實際使用內存仍然增大。

因此,隨着系統頻繁地 malloc 和 free ,尤其對於小塊內存,堆內將產生越來越多不可用的碎片,導致“內存泄露”。而這種“泄露”現象使用 valgrind 是無法檢測出來的。

下圖是 MySQL 存在大量分區表時的內存使用情況 (RSS 和 VSZ) ,疑似“內存泄露”。

因此,當我們寫程序時,不能完全依賴 glibc 的 malloc 和 free 的實現。更好方式是建立屬於進程的內存池,即一次分配 (malloc) 大塊內存,小內存從內存池中獲得,當進程結束或該塊內存不可用時,一次釋放 (free) ,可大大減少碎片的產生。

七. 既然堆內內存不能直接釋放,為什么不全部使用 mmap 來分配?

由於堆內碎片不能直接釋放,而問題 5 中說到 mmap 分配的內存可以會通過 munmap 進行 free ,實現真正釋放。既然堆內碎片不能直接釋放,導致疑似“內存泄露”問題,為什么 malloc 不全部使用 mmap 來實現呢?而僅僅對於大於 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>)來修改這個臨界值。

八. 如何查看進程的缺頁中斷信息?

可通過以下命令查看缺頁中斷信息

ps -o majflt,minflt -C <program_name> ps -o majflt,minflt -p <pid> 

其中, majflt 代表 major fault ,指大錯誤, minflt 代表 minor fault ,指小錯誤。這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。其中 majflt 與 minflt 的不同是, majflt 表示需要讀寫磁盤,可能是內存對應頁面在磁盤中需要 load 到物理內存中,也可能是此時物理內存不足,需要淘汰部分物理頁面至磁盤中。

例如,下面是 mysqld 的一個例子。

mysql@ TLOG_590_591:~> ps -o majflt,minflt -C mysqld
MAJFLT MINFLT
144856 15296294

如果進程的內核態 CPU 使用過多,其中一個原因就可能是單位時間的缺頁中斷次數多個,可通過以上命令來查看。

如果 MAJFLT 過大,很可能是內存不足。

如果 MINFLT 過大,很可能是頻繁分配 / 釋放大塊內存 (128k) , malloc 使用 mmap 來分配。對於這種情況,可通過 mallopt(M_MMAP_THRESHOLD, <SIZE>)增大臨界值,或程序實現內存池。

九. 如何查看堆內內存的碎片情況?

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) 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 很容易就會導致溢出,應該是歷史遺留問題,使用時要注意!

十. 除了 glibc 的 malloc/free ,還有其他第三方實現嗎?

其實,很多人開始詬病 glibc 內存管理的實現,就是在高並發性能低下和內存碎片化問題都比較嚴重,因此,陸續出現一些第三方工具來替換 glibc 的實現,最著名的當屬 google 的 tcmalloc 和 facebook 的 jemalloc 。

網上有很多資源,可搜索之,這里就不詳述了。

總結

基於以上認識,最后發現 MySQL 的疑似“內存泄露”問題一方面是 MySQL 5.5 分區表使用更多的內存,另一方面跟內存碎片有關,這也是 TMySQL 一個優化方向。

然而,以上主要介紹了 glibc 虛擬內存管理主要內容,事實上,在並發情況下, glibc 的虛存管理會更加復雜,碎片情況也可能更嚴重,這將在另一篇再做介紹。

參考文章:
《深入理解計算機系統》第 10 章
http://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
https://en.wikipedia.org/wiki/X86-64#Canonical_form_addresses
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


免責聲明!

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



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