一文看懂內(nèi)存交換機(jī)制(秒懂)
本文基于 Linux-2.4.16 內(nèi)核版本
由于計(jì)算機(jī)的物理內(nèi)存是有限的, 而進(jìn)程對(duì)內(nèi)存的使用是不確定的, 所以物理內(nèi)存總有用完的可能性. 那么當(dāng)系統(tǒng)的物理內(nèi)存不足時(shí), Linux內(nèi)核使用什么方案來(lái)避免申請(qǐng)不到物理內(nèi)存這個(gè)問(wèn)題呢?
相對(duì)于內(nèi)存來(lái)說(shuō), 磁盤(pán)的容量是非常大的, 所以L(fǎng)inux內(nèi)核實(shí)現(xiàn)了一個(gè)叫 內(nèi)存交換 的功能 -- 把某些進(jìn)程的一些暫時(shí)用不到的內(nèi)存頁(yè)保存到磁盤(pán)中, 然后把物理內(nèi)存頁(yè)分配給更緊急的用戶(hù)使用, 當(dāng)進(jìn)程用到時(shí)再?gòu)拇疟P(pán)讀回到內(nèi)存中即可. 有了 內(nèi)存交換 功能, 系統(tǒng)可使用的內(nèi)存就可以遠(yuǎn)遠(yuǎn)大于物理內(nèi)存的容量.
LRU算法
內(nèi)存交換 過(guò)程首先是找到一個(gè)合適的用戶(hù)進(jìn)程內(nèi)存管理結(jié)構(gòu),然后把進(jìn)程占用的內(nèi)存頁(yè)交換到磁盤(pán)中,并斷開(kāi)虛擬內(nèi)存與物理內(nèi)存的映射,最后釋放進(jìn)程占用的內(nèi)存頁(yè)。由于涉及到IO操作,所以這是一個(gè)比較耗時(shí)的過(guò)程。如果被交換出去的內(nèi)存頁(yè)剛好又被訪(fǎng)問(wèn)了,這時(shí)又需要從磁盤(pán)中把內(nèi)存頁(yè)的數(shù)據(jù)交換到內(nèi)存中。所以,在這種情況下不單不能解決內(nèi)存緊缺的問(wèn)題,而且增加了系統(tǒng)的負(fù)荷。
為了解決這個(gè)問(wèn)題,Linux內(nèi)核使用了一種稱(chēng)為 LRU (Least Recently Used) 的算法, 下面介紹一下 LRU算法 的大體過(guò)程.
LRU 的中文翻譯是 最近最少使用, 顧名思義就是一段時(shí)間內(nèi)沒(méi)有被使用, 那么Linux內(nèi)核怎么知道哪些內(nèi)存頁(yè)面最近沒(méi)有被使用呢? 最簡(jiǎn)單的方法就是把內(nèi)存頁(yè)放進(jìn)一個(gè)隊(duì)列里, 如果內(nèi)存頁(yè)被訪(fǎng)問(wèn)了, 就把內(nèi)存頁(yè)移動(dòng)到鏈表的頭部, 這樣沒(méi)被訪(fǎng)問(wèn)的內(nèi)存頁(yè)在一段時(shí)間后便會(huì)移動(dòng)到隊(duì)列的尾部, 而釋放內(nèi)存頁(yè)時(shí)從鏈表的尾部開(kāi)始. 著名的緩存服務(wù)器 memcached 就是使用這種 LRU算法.
Linux內(nèi)核也使用了類(lèi)似的算法, 但相對(duì)要復(fù)雜一些. Linux內(nèi)核維護(hù)著三個(gè)隊(duì)列: 活躍隊(duì)列, 非活躍臟隊(duì)列和非活躍干凈隊(duì)列. 為什么Linux需要維護(hù)三個(gè)隊(duì)列, 而不是使用一個(gè)隊(duì)列呢? 這是因?yàn)長(zhǎng)inux希望內(nèi)存頁(yè)交換過(guò)程慢慢進(jìn)行, Linux內(nèi)核有個(gè)內(nèi)核線(xiàn)程 kswapd 會(huì)定時(shí)檢查系統(tǒng)的空閑內(nèi)存頁(yè)是否緊缺, 如果系統(tǒng)的空閑內(nèi)存頁(yè)緊缺時(shí)時(shí), 就會(huì)選擇一些用戶(hù)進(jìn)程把其占用的內(nèi)存頁(yè)添加到活躍鏈表中并斷開(kāi)進(jìn)程與此內(nèi)存頁(yè)的映射關(guān)系. 隨著時(shí)間的推移, 如果內(nèi)存頁(yè)沒(méi)有被訪(fǎng)問(wèn), 那么就會(huì)被移動(dòng)到非活躍臟鏈表. 非活躍臟鏈表中的內(nèi)存頁(yè)是需要被交換到磁盤(pán)的, 當(dāng)系統(tǒng)中空閑內(nèi)存頁(yè)緊缺時(shí)就會(huì)從非活躍臟鏈表的尾部開(kāi)始把內(nèi)存頁(yè)刷新到磁盤(pán)中, 然后移動(dòng)到非活躍干凈鏈表中, 非活躍干凈鏈表中的內(nèi)存頁(yè)是可以立刻分配給進(jìn)程使用的. 各個(gè)鏈表之間的移動(dòng)如下圖:

如果在這個(gè)過(guò)程中, 內(nèi)存頁(yè)又被訪(fǎng)問(wèn)了, 那么Linux內(nèi)核會(huì)把內(nèi)存頁(yè)移動(dòng)到活躍鏈表中, 并且建立內(nèi)存映射關(guān)系, 這樣就不需要從磁盤(pán)中讀取內(nèi)存頁(yè)的內(nèi)容.
注意: 內(nèi)核只維護(hù)著一個(gè)活躍鏈表和一個(gè)非活躍臟鏈表, 但是非活躍干凈鏈表是每個(gè)內(nèi)存管理區(qū)都有一個(gè)的. ?這是因?yàn)榉峙鋬?nèi)存是在內(nèi)存管理區(qū)的基礎(chǔ)上進(jìn)行的, 所以一個(gè)內(nèi)存頁(yè)必須屬于某一個(gè)內(nèi)存管理區(qū).
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。∏?00名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書(shū)、實(shí)戰(zhàn)項(xiàng)目及代碼)?

kswapd內(nèi)核線(xiàn)程
在Linux系統(tǒng)啟動(dòng)時(shí)會(huì)調(diào)用 kswapd_init() 函數(shù), 代碼如下:
可以看到, kswapd_init() 函數(shù)會(huì)創(chuàng)建 kswapd 和 kreclaimd 兩個(gè)內(nèi)核線(xiàn)程, 這兩個(gè)內(nèi)核線(xiàn)程負(fù)責(zé)在系統(tǒng)物理內(nèi)存緊缺時(shí)釋放一些物理內(nèi)存頁(yè), 從而使系統(tǒng)的可用內(nèi)存達(dá)到一個(gè)平衡. 下面我們重點(diǎn)來(lái)分析 kswapd 這個(gè)內(nèi)核線(xiàn)程, kswapd() 的源碼如下:
kswapd 內(nèi)核線(xiàn)程由一個(gè)無(wú)限循環(huán)組成, 首先通過(guò) inactive_shortage() 和 free_shortage() 函數(shù)判斷系統(tǒng)的非活躍頁(yè)面和空閑物理內(nèi)存頁(yè)是否短缺, 如果短缺的話(huà), 那么就調(diào)用 do_try_to_free_pages() 函數(shù)試圖釋放一些物理內(nèi)存頁(yè). 然后通過(guò)調(diào)用 refill_inactive_scan() 函數(shù)把一些活躍鏈表中的內(nèi)存頁(yè)移動(dòng)到非活躍臟鏈表中. 最后, 如果空閑物理內(nèi)存頁(yè)或者非活躍內(nèi)存頁(yè)不短缺, 那么就讓 kswapd 內(nèi)核線(xiàn)程休眠一秒.
接下來(lái)我們分析一下 do_try_to_free_pages() 函數(shù)做了一些什么工作, 代碼如下:
do_try_to_free_pages() 函數(shù)第一步先判斷系統(tǒng)中的空閑物理內(nèi)存頁(yè)是否短缺, 或者非活躍臟頁(yè)面的數(shù)量大于空閑物理內(nèi)存頁(yè)和非活躍干凈頁(yè)面的總和, 其中一個(gè)條件滿(mǎn)足了, 就調(diào)用 page_launder() 函數(shù)把非活躍臟鏈表中的頁(yè)面刷到磁盤(pán)中, 然后移動(dòng)到非活躍干凈鏈表中. 接下來(lái)如果內(nèi)存還是緊缺的話(huà), 那么就調(diào)用 shrink_dcache_memory(), shrink_icache_memory() 和 refill_inactive() 函數(shù)繼續(xù)釋放內(nèi)存.
下面我們先來(lái)分析一下 page_launder() 這個(gè)函數(shù), 由于這個(gè)函數(shù)很長(zhǎng), 所以我們分段來(lái)解釋:
上面的代碼首先把 pagemap_lru_lock 上鎖, 然后從尾部開(kāi)始遍歷非活躍臟鏈表.
上面代碼判斷內(nèi)存頁(yè)是否能需要重新移動(dòng)到活躍鏈表中, 依據(jù)有:
內(nèi)存頁(yè)是否設(shè)置了 PG_referenced 標(biāo)志;
內(nèi)存頁(yè)的age字段是否大于0 (age字段是內(nèi)存頁(yè)的生命周期);
內(nèi)存頁(yè)是否還有映射關(guān)系;
內(nèi)存頁(yè)是否用于內(nèi)存磁盤(pán).
如果滿(mǎn)足上面其中一個(gè)條件, 都需要重新把內(nèi)存頁(yè)移動(dòng)到活躍頁(yè)面中.
上面的代碼首先判斷內(nèi)存頁(yè)是否臟的(是否設(shè)置了 PG_dirty 標(biāo)志), 如果是, 那么就需要把內(nèi)存頁(yè)刷新到磁盤(pán)中. 這里有個(gè)要主要的地方是, 當(dāng) launder_loop 變量為0時(shí)只是把內(nèi)存頁(yè)移動(dòng)到非活躍臟鏈表的頭部. 當(dāng) launder_loop 變量為1時(shí)才會(huì)把內(nèi)存頁(yè)刷新到磁盤(pán)中. 為什么要這樣做呢? 這是因?yàn)長(zhǎng)inux內(nèi)核希望第一次掃描先把非活躍臟鏈表中的干凈內(nèi)存頁(yè)移動(dòng)到非活躍干凈鏈表中, 第二次掃描才把臟的內(nèi)存頁(yè)刷新到磁盤(pán)中. 后面的代碼會(huì)對(duì) launder_loop 變量進(jìn)行修改. 而且我們發(fā)現(xiàn), 把臟頁(yè)面刷新到磁盤(pán)后, 并沒(méi)有立刻把內(nèi)存頁(yè)移動(dòng)到非活躍干凈鏈表中, 而是簡(jiǎn)單的清除了 PG_dirty 標(biāo)志.
上面的代碼比較簡(jiǎn)單, 如果內(nèi)存頁(yè)已經(jīng)是干凈的, 那么久移動(dòng)到非活躍干凈鏈表中.
從上面的代碼可以看到, 當(dāng) can_get_io_locks 等于1(gfp_mask 設(shè)置了 __GFP_IO 標(biāo)志), launder_loop 等于0, 并且空閑內(nèi)存頁(yè)還是短缺(free_shortage() 為真)的情況下, 把 launder_loop 變量被設(shè)置為1, 并且跳轉(zhuǎn)到 dirty_page_rescan 處重新掃描, 這是第二次掃描非活躍臟鏈表, 會(huì)把臟的內(nèi)存頁(yè)刷新到磁盤(pán)中.
接下來(lái)我們繼續(xù)分析 refill_inactive() 這個(gè)函數(shù):
在這個(gè)函數(shù)中, 我們主要關(guān)注兩個(gè)地方:
調(diào)用 refill_inactive_scan() 函數(shù), refill_inactive_scan() 函數(shù)的作用是把活躍鏈表中的內(nèi)存頁(yè)移動(dòng)到非活躍臟鏈表中.
調(diào)用 swap_out() 函數(shù), swap_out() 函數(shù)的作用是選擇一個(gè)用戶(hù)進(jìn)程, 并且把其映射的內(nèi)存頁(yè)添加到活躍鏈表中.
先來(lái)看看 refill_inactive_scan() 函數(shù):
refill_inactive_scan() 函數(shù)比較簡(jiǎn)單, 首先從活躍鏈表的尾部開(kāi)始遍歷, 然后判斷內(nèi)存頁(yè)的生命是否已經(jīng)用完(age是否等于0), 并且沒(méi)有進(jìn)程與其有映射關(guān)系(count是否等于1). 如果是, 那么就調(diào)用 deactivate_page_nolock() 函數(shù)把內(nèi)存頁(yè)移動(dòng)到非活躍臟鏈表中.
接著來(lái)看看 swap_out() 函數(shù), swap_out() 函數(shù)比較復(fù)雜, 但最終會(huì)調(diào)用 try_to_swap_out() 函數(shù), 所以我們只分析 try_to_swap_out() 函數(shù):
add_to_swap_cache() 函數(shù)會(huì)調(diào)用 add_to_page_cache_locked() 函數(shù), 而add_to_page_cache_locked() 函數(shù)會(huì)調(diào)用 lru_cache_add() 函數(shù)來(lái)把內(nèi)存頁(yè)添加到活躍鏈表中, lru_cache_add() 函數(shù)代碼如下:
從上面的代碼可以看到, lru_cache_add() 函數(shù)最終會(huì)調(diào)用 list_add(&(page)->lru, &active_list) 這行代碼來(lái)把內(nèi)存頁(yè)添加到活躍鏈表(active_list)中, 并設(shè)置內(nèi)存頁(yè)的 PG_active 標(biāo)志.
最后我們通過(guò)一幅圖來(lái)總結(jié)一下 kswapd 內(nèi)核線(xiàn)程的流程:
swap_out() 函數(shù)會(huì)把進(jìn)程占用的內(nèi)存頁(yè)添加到活躍鏈表中, 而 refill_inactive_scan() 函數(shù)會(huì)把活躍鏈表的內(nèi)存頁(yè)移動(dòng)到非活躍臟鏈表中, 最后 page_launder() 會(huì)把非活躍臟鏈表的內(nèi)存頁(yè)刷新到磁盤(pán)并且移動(dòng)到非活躍干凈鏈表中, 非活躍干凈鏈表中的內(nèi)存頁(yè)是直接可以用來(lái)分配使用的.

一文看懂內(nèi)存交換機(jī)制(秒懂)的評(píng)論 (共 條)
