頻繁分配釋放記憶體導致的效能問題的分析

2021-06-03 11:46:43 字數 3286 閱讀 1610

現象

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只有在大量的時候才會對效能產生影響。

頻繁分配釋放記憶體導致的效能問題的分析

徵兆 1.system cpu佔用率偏高。2.執行命令 用ps o majflt,minflt c program命令檢視,majflt或者minflt每秒增量很大。原因 頻繁的分配釋放記憶體,導致缺頁中斷次數很高。而缺頁中斷的處理是由系統在核心態完成的,所以system cpu利用率偏高,導致效能...

頻繁分配釋放記憶體導致的效能問題的分析

現象 1 壓力測試過程中,發現被測物件效能不夠理想,具體表現為 程序的系統態cpu消耗20,使用者態cpu消耗10,系統idle大約70 2 用ps o majflt,minflt c program命令檢視,發現majflt每秒增量為0,而minflt每秒增量大於10000。初步分析 majflt...

頻繁分配釋放記憶體導致的效能問題的分析

現象 1 壓力測試過程中,發現被測物件效能不夠理想,具體表現為 程序的系統態cpu消耗20,使用者態cpu消耗10,系統idle大約70 2 用ps o majflt,minflt c program命令檢視,發現majflt每秒增量為0,而minflt每秒增量大於10000。初步分析 majflt...