【百度分享】頻繁分配釋放內存致使的性能問題的分析

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

現象佈局

1 壓力測試過程當中,發現被測對象性能不夠理想,具體表現爲:  
進程的系統態CPU消耗20,用戶態CPU消耗10,系統idle大約70  
2 用ps -o majflt,minflt -C program命令查看,發現majflt每秒增量爲0,而minflt每秒增量大於10000。

初步分析
majflt表明major fault,中文名叫大錯誤,minflt表明minor fault,中文名叫小錯誤。
這兩個數值表示一個進程自啓動以來所發生的缺頁中斷的次數。
當一個進程發生缺頁中斷的時候,進程會陷入內核態,執行如下操做:  
檢查要訪問的虛擬地址是否合法  
查找/分配一個物理頁  
填充物理頁內容(讀取磁盤,或者直接置0,或者啥也不幹)  
創建映射關係(虛擬地址到物理地址)  
從新執行發生缺頁中斷的那條指令  
若是第3步,須要讀取磁盤,那麼此次缺頁中斷就是majflt,不然就是minflt。  
此進程minflt如此之高,一秒10000屢次,不得不懷疑它跟進程內核態cpu消耗大有很大關係。

分析代碼
查看代碼,發現是這麼寫的:一個請求來,用malloc分配2M內存,請求結束後free這塊內存。看日誌,發現分配內存語句耗時10us,平均一條請求處理耗時1000us 。 緣由已找到!  
雖然分配內存語句的耗時在一條處理請求中耗時比重不大,可是這條語句嚴重影響了性能。要解釋清楚緣由,須要先了解一下內存分配的原理。  

內存分配的原理
從操做系統角度來看,進程分配內存有兩種方式,分別由兩個系統調用完成:brk和mmap(不考慮共享內存)。brk是將數據段(.data)的最高地址指針_edata往高地址推,mmap是在進程的虛擬地址空間中(通常是堆和棧中間)找一塊空閒的。這兩種方式分配的都是虛擬內存,沒有分配物理內存。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操做系統負責分配物理內存,而後創建虛擬內存和物理內存之間的映射關係。  
在標準C庫中,提供了malloc/free函數分配釋放內存,這兩個函數底層是由brk,mmap,munmap這些系統調用實現的。  
下面以一個例子來講明內存分配的原理:
  
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對應的物理頁是不會被分配的。  
3進程調用B=malloc(40K)之後,內存空間如圖3.  

  
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對應的虛擬內存和物理內存一塊兒釋放  
  

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。

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