1、記憶體配置設定的過程是怎樣的?
應用程式通過 malloc 函數(全稱是memory allocation,中文叫動态記憶體配置設定)申請記憶體的時候,實際上申請的是虛拟記憶體,此時并不會配置設定實體記憶體。
當應用程式讀寫了這塊虛拟記憶體,CPU 就會去通路這個虛拟記憶體, 這時會發現這個虛拟記憶體沒有映射到實體記憶體, CPU 就會産生缺頁中斷,程序會從使用者态切換到核心态,并将缺頁中斷交給核心的 Page Fault Handler (缺頁中斷函數)處理。
缺頁中斷處理函數會看是否有空閑的實體記憶體,如果有,就直接配置設定實體記憶體,并建立虛拟記憶體與實體記憶體之間的映射關系。
如果沒有空閑的實體記憶體,那麼核心就會開始進行回收記憶體的工作,回收的方式主要是兩種:直接記憶體回收和背景記憶體回收。
- 背景記憶體回收(kswapd):在實體記憶體緊張的時候,會喚醒 kswapd 核心線程來回收記憶體,這個回收記憶體的過程異步的,不會阻塞程序的執行。
- 直接記憶體回收(direct reclaim):如果背景異步回收跟不上程序記憶體申請的速度,就會開始直接回收,這個回收記憶體的過程是同步的,會阻塞程序的執行。
如果直接記憶體回收後,空閑的實體記憶體仍然無法滿足此次實體記憶體的申請,那麼核心就會放最後的大招了 ——觸發 OOM (Out of Memory)機制。
OOM Killer 機制會根據算法選擇一個占用實體記憶體較高的程序,然後将其殺死,以便釋放記憶體資源,如果實體記憶體依然不足,OOM Killer 會繼續殺死占用實體記憶體較高的程序,直到釋放足夠的記憶體位置。
申請實體記憶體的過程如下圖:
2、哪些記憶體可以被回收?
系統記憶體緊張的時候,就會進行回收記憶體的工作,那具體哪些記憶體是可以被回收的呢?
主要有兩類記憶體可以被回收,而且它們的回收方式也不同。
- 檔案頁(File-backed Page):核心緩存的磁盤資料(Buffer)和核心緩存的檔案資料(Cache)都叫作檔案頁。大部分檔案頁,都可以直接釋放記憶體,以後有需要時,再從磁盤重新讀取就可以了。而那些被應用程式修改過,并且暫時還沒寫入磁盤的資料(也就是髒頁),就得先寫入磁盤,然後才能進行記憶體釋放。是以,回收幹淨頁的方式是直接釋放記憶體,回收髒頁的方式是先寫回磁盤後再釋放記憶體。
- 匿名頁(Anonymous Page):這部分記憶體沒有實際載體,不像檔案緩存有硬碟檔案這樣一個載體,比如堆、棧資料等。這部分記憶體很可能還要再次被通路,是以不能直接釋放記憶體,它們回收的方式是通過 Linux 的 Swap 機制,Swap 會把不常通路的記憶體先寫到磁盤中,然後釋放這些記憶體,給其他更需要的程序使用。再次通路這些記憶體時,重新從磁盤讀入記憶體就可以了。
檔案頁和匿名頁的回收都是基于 LRU 算法,也就是優先回收不常通路的記憶體。LRU 回收算法,實際上維護着 active 和 inactive 兩個雙向連結清單,其中:
- active_list活躍記憶體頁連結清單,這裡存放的是最近被通路過(活躍)的記憶體頁;
- inactive_list不活躍記憶體頁連結清單,這裡存放的是很少被通路(非活躍)的記憶體頁;
越接近連結清單尾部,就表示記憶體頁越不常通路。這樣,在回收記憶體時,系統就可以根據活躍程度,優先回收不活躍的記憶體。
活躍和非活躍的記憶體頁,按照類型的不同,又分别分為檔案頁和匿名頁。可以從 /proc/meminfo 中,查詢它們的大小,比如:
# grep表示隻保留包含active的名額(忽略大小寫)
# sort表示按照字母順序排序
[root@xiaolin ~]# cat /proc/meminfo | grep -i active | sort
Active: 901456 kB
Active(anon): 227252 kB
Active(file): 674204 kB
Inactive: 226232 kB
Inactive(anon): 41948 kB
Inactive(file): 184284 kB
3、回收記憶體帶來的性能影響
在前面我們知道了回收記憶體有兩種方式。
- 一種是背景記憶體回收,也就是喚醒 kswapd 核心線程,這種方式是異步回收的,不會阻塞程序。
- 一種是直接記憶體回收,這種方式是同步回收的,會阻塞程序,這樣就會造成很長時間的延遲,以及系統的 CPU 使用率會升高,最終引起系統負荷飙高。
可被回收的記憶體類型有檔案頁和匿名頁:
- 檔案頁的回收:對于幹淨頁是直接釋放記憶體,這個操作不會影響性能,而對于髒頁會先寫回到磁盤再釋放記憶體,這個操作會發生磁盤 I/O 的,這個操作是會影響系統性能的。
- 匿名頁的回收:如果開啟了 Swap 機制,那麼 Swap 機制會将不常通路的匿名頁換出到磁盤中,下次通路時,再從磁盤換入到記憶體中,這個操作是會影響系統性能的。
可以看到,回收記憶體的操作基本都會發生磁盤 I/O 的,如果回收記憶體的操作很頻繁,意味着磁盤 I/O 次數會很多,這個過程勢必會影響系統的性能,整個系統給人的感覺就是很卡。
下面針對回收記憶體導緻的性能影響,說說常見的解決方式。
1)調整檔案頁和匿名頁的回收傾向
從檔案頁和匿名頁的回收操作來看,檔案頁的回收操作對系統的影響相比匿名頁的回收操作會少一點,因為檔案頁對于幹淨頁回收是不會發生磁盤 I/O 的,而匿名頁的 Swap 換入換出這兩個操作都會發生磁盤 I/O。
Linux 提供了一個
/proc/sys/vm/swappiness
選項,用來調整檔案頁和匿名頁的回收傾向。
swappiness 的範圍是 0-100,數值越大,越積極使用 Swap,也就是更傾向于回收匿名頁;數值越小,越消極使用 Swap,也就是更傾向于回收檔案頁。
[root@xiaolin ~]# cat /proc/sys/vm/swappiness
0
一般建議 swappiness 設定為 0(預設值是 60),這樣在回收記憶體的時候,會更傾向于檔案頁的回收,但是并不代表不會回收匿名頁。
2)盡早觸發 kswapd 核心線程異步回收記憶體
如何檢視系統的直接記憶體回收和背景記憶體回收的名額?
我們可以使用
sar -B 1
指令來觀察:
圖中紅色框住的就是背景記憶體回收和直接記憶體回收的名額,它們分别表示:
- pgscank/s : kswapd(背景回收線程) 每秒掃描的 page 個數。
- pgscand/s: 應用程式在記憶體申請過程中每秒直接掃描的 page 個數。
- pgsteal/s: 掃描的 page 中每秒被回收的個數(pgscank+pgscand)。
如果系統時不時發生抖動,并且在抖動的時間段裡如果通過 sar -B 觀察到 pgscand 數值很大,那大機率是因為「直接記憶體回收」導緻的。
針對這個問題,解決的辦法就是,可以通過盡早的觸發「背景記憶體回收」來避免應用程式進行直接記憶體回收。
什麼條件下才能觸發 kswapd 核心線程回收記憶體呢?
核心定義了三個記憶體門檻值(watermark,也稱為水位),用來衡量目前剩餘記憶體(pages_free)是否充裕或者緊張,分别是:
- 頁最小門檻值(pages_min);
- 頁低門檻值(pages_low);
- 頁高門檻值(pages_high);
這三個記憶體門檻值會劃分為四種記憶體使用情況,如下圖:
kswapd 會定期掃描記憶體的使用情況,根據剩餘記憶體(pages_free)的情況來進行記憶體回收的工作。
- 圖中綠色部分:如果剩餘記憶體(pages_free)大于 頁高門檻值(pages_high),說明剩餘記憶體是充足的;
- 圖中藍色部分:如果剩餘記憶體(pages_free)在頁高門檻值(pages_high)和頁低門檻值(pages_low)之間,說明記憶體有一定壓力,但還可以滿足應用程式申請記憶體的請求;
- 圖中橙色部分:如果剩餘記憶體(pages_free)在頁低門檻值(pages_low)和頁最小門檻值(pages_min)之間,說明記憶體壓力比較大,剩餘記憶體不多了。這時 kswapd0 會執行記憶體回收,直到剩餘記憶體大于高門檻值(pages_high)為止。雖然會觸發記憶體回收,但是不會阻塞應用程式,因為兩者關系是異步的。
- 圖中紅色部分:如果剩餘記憶體(pages_free)小于頁最小門檻值(pages_min),說明使用者可用記憶體都耗盡了,此時就會觸發直接記憶體回收,這時應用程式就會被阻塞,因為兩者關系是同步的。
可以看到,當剩餘記憶體頁(pages_free)小于頁低門檻值(pages_low),就會觸發 kswapd 進行背景回收,然後 kswapd 會一直回收到剩餘記憶體頁(pages_free)大于頁高門檻值(pages_high)。
也就是說 kswapd 的活動空間隻有 pages_low 與 pages_min 之間的這段區域,如果剩餘記憶體低于了 pages_min 會觸發直接記憶體回收,高于了 pages_high 又不會喚醒 kswapd。
頁低門檻值(pages_low)可以通過核心選項
/proc/sys/vm/min_free_kbytes
(該參數代表系統所保留白閑記憶體的最低限)來間接設定。
min_free_kbytes 雖然設定的是頁最小門檻值(pages_min),但是頁高門檻值(pages_high)和頁低門檻值(pages_low)都是根據頁最小門檻值(pages_min)計算生成的,它們之間的計算關系如下:
pages_min = min_free_kbytes
pages_low = pages_min*5/4
pages_high = pages_min*3/2