版權聲明:本文由陳福榮原創文章,轉載請註明出處:
文章原文連接:https://www.qcloud.com/community/article/184mysql
來源:騰雲閣 https://www.qcloud.com/communitylinux
接上篇:十問 Linux 虛擬內存管理 (glibc) (一)sql
前面全部例子都有一個很嚴重的問題,就是分配的內存都沒有釋放,即致使內存泄露。原則上全部 malloc/new 分配的內存,都需 free/delete 來釋放。可是, free 了的內存真的釋放了嗎?數組
要說清楚這個問題,可經過下面例子來講明。併發
初始狀態:如圖 (1) 所示,系統已分配 ABCD 四塊內存,其中 ABD 在堆內分配, C 使用 mmap 分配。爲簡單起見,圖中忽略瞭如共享庫等文件映射區域的地址空間。app
E=malloc(100k) :分配 100k 內存,小於 128k ,從堆內分配,堆內剩餘空間不足,擴展堆頂 (brk) 指針。nosql
free(A) :釋放 A 的內存,在 glibc 中,僅僅是標記爲可用,造成一個內存空洞 ( 碎片 ) ,並無真正釋放。若是此時須要分配 40k 之內的空間,可重用此空間,剩餘空間造成新的小碎片。
高併發
free(C) : C 空間大於 128K ,使用 mmap 分配,若是釋放 C ,會調用 munmap 系統調用來釋放,並會真正釋放該空間,還給 OS ,如圖 (4) 所示。工具
free(D) :與釋放 A 相似,釋放 D 一樣會致使一個空洞,得到空閒空間,但並不會還給 OS 。此時,空閒總空間爲 100K ,但因爲虛擬地址不連續,沒法合併,空閒空間沒法知足大於 60k 的分配請求。性能
free(E) :釋放 E ,因爲與 D 連續,二者將進行合併,獲得 160k 連續空閒空間。同時 E 是最靠近堆頂的空間, glibc 的 free 實現中,只要堆頂附近釋放總空間(包括合併的空間)超過 128k ,即會調用 sbrk(-SIZE) 來回溯堆頂指針,將原堆頂空間還給 OS ,如圖 (6) 所示。而堆內的空閒空間仍是不會歸還 OS 的。
因而可知:
malloc 使用 mmap 分配的內存 ( 大於 128k) , free 會調用 munmap 系統調用立刻還給 OS ,實現真正釋放。
堆內的內存,只有釋放堆頂的空間,同時堆頂總連續空閒空間大於 128k 才使用 sbrk(-SIZE) 回收內存,真正歸還 OS 。
堆內的空閒空間,是不會歸還給 OS 的。
狹義上的內存泄露是指 malloc 的內存,沒有 free ,致使內存浪費,直到程序結束。而廣義上的內存泄露就是進程使用內存量不斷增長,或大大超出系統原設計的上限。
上一節說到, free 了的內存並不會立刻歸還 OS ,而且堆內的空洞(碎片)更是很難真正釋放,除非空洞成爲了新的堆頂。因此,如上一例子狀況 (5) ,釋放了 40k 和 60k 兩片內存,但若是此時須要申請大於 60k (如 70k ),沒有可用碎片,必須向 OS 申請,實際使用內存仍然增大。
所以,隨着系統頻繁地 malloc 和 free ,尤爲對於小塊內存,堆內將產生愈來愈多不可用的碎片,致使「內存泄露」。而這種「泄露」現象使用 valgrind 是沒法檢測出來的。
下圖是 MySQL 存在大量分區表時的內存使用狀況 (RSS 和 VSZ) ,疑似「內存泄露」。
所以,當咱們寫程序時,不能徹底依賴 glibc 的 malloc 和 free 的實現。更好方式是創建屬於進程的內存池,即一次分配 (malloc) 大塊內存,小內存從內存池中得到,當進程結束或該塊內存不可用時,一次釋放 (free) ,可大大減小碎片的產生。
因爲堆內碎片不能直接釋放,而問題 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 內存管理的實現,就是在高併發性能低下和內存碎片化問題都比較嚴重,所以,陸續出現一些第三方工具來替換 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