有時(shí)候我們可能會(huì)碰到系統(tǒng)中某個(gè)進(jìn)程突然掛掉的情況,查看系統(tǒng)日志后發(fā)現(xiàn)是由于系統(tǒng)的 oom(out of memory)機(jī)制 觸發(fā)導(dǎo)致的。
今天我們來討論一下 OOM機(jī)制 是什么,以及如何防止進(jìn)程因?yàn)?OOM機(jī)制 而被終止。
什么是OOM機(jī)制
OOM 是 Out Of Memory 的縮寫,意味著系統(tǒng)內(nèi)存不足。OOM機(jī)制 是指當(dāng)系統(tǒng)內(nèi)存不足時(shí),系統(tǒng)采取的應(yīng)急措施。
當(dāng) Linux 內(nèi)核發(fā)現(xiàn)系統(tǒng)中的物理內(nèi)存不足時(shí),首先會(huì)嘗試回收可回收內(nèi)存,主要包括:
- 用于讀寫文件的頁緩存。
- 為了性能而延遲釋放的空閑 slab 內(nèi)存頁。
內(nèi)核會(huì)優(yōu)先釋放這些內(nèi)存頁,因?yàn)樗鼈兊尼尫挪粫?huì)影響系統(tǒng)的正常運(yùn)行,只是為了提升系統(tǒng)性能。
如果釋放這些內(nèi)存后仍然不足,內(nèi)核將會(huì)采取什么措施呢?它會(huì)觸發(fā) OOM killer,殺掉占用內(nèi)存最多的進(jìn)程,以釋放更多內(nèi)存。以下是一個(gè)示意圖:
可以看出,OOM killer 是防止系統(tǒng)崩潰的最后一個(gè)手段,不到迫不得已的情況是不會(huì)觸發(fā)的。
OOM killer 實(shí)現(xiàn)
接下來,我們分析一下內(nèi)核是如何實(shí)現(xiàn) OOM killer 的。
由于在 Linux 系統(tǒng)中,進(jìn)程申請(qǐng)的都是虛擬內(nèi)存地址。所以當(dāng)程序調(diào)用 malloc() 申請(qǐng)內(nèi)存時(shí),如果虛擬內(nèi)存空間足夠的話,是不會(huì)觸發(fā) OOM 機(jī)制的。
當(dāng)進(jìn)程訪問虛擬內(nèi)存地址時(shí),如果此虛擬內(nèi)存地址還沒有映射到物理內(nèi)存地址的話,那么將會(huì)觸發(fā) 缺頁異常。
在缺頁異常處理例程中,將會(huì)申請(qǐng)新的物理內(nèi)存頁,并且將進(jìn)程的虛擬內(nèi)存地址映射到剛申請(qǐng)的物理內(nèi)存。
如果在申請(qǐng)物理內(nèi)存時(shí),系統(tǒng)中的物理內(nèi)存不足,那么內(nèi)核將會(huì)回收一些能夠被回收的文件頁緩存。如果回收完后,物理內(nèi)存還是不足的話,那么將會(huì)觸發(fā) swapping機(jī)制(如果開啟了的話)。
swapping機(jī)制 會(huì)將某些進(jìn)程不常用的內(nèi)存頁寫入到交換區(qū)(硬盤分區(qū)或文件)中,然后釋放掉這些內(nèi)存頁,從而達(dá)到緩解內(nèi)存不足的情況。
如果通過上面的手段還不能解決內(nèi)存不足的情況,那么內(nèi)核將會(huì)調(diào)用 pagefault_out_of_memory() 函數(shù)來殺掉系統(tǒng)中占用物理內(nèi)存最多的進(jìn)程。
我們來看看 pagefault_out_of_memory() 函數(shù)的實(shí)現(xiàn):
void?pagefault_out_of_memory(void) { ????... ????out_of_memory(NULL,?0,?0,?NULL,?false); ????... }
可以看出,pagefault_out_of_memory() 函數(shù)最終會(huì)調(diào)用 out_of_memory() 來殺死系統(tǒng)中占用內(nèi)存最多的進(jìn)程。
我們繼續(xù)來看看 out_of_memory() 函數(shù)的實(shí)現(xiàn):
void?out_of_memory(struct?zonelist?*zonelist,?gfp_t?gfp_mask,?int?order, ???????????????????nodemask_t?*nodemask,?bool?force_kill) { ????... ????//?1.?從系統(tǒng)中選擇一個(gè)最壞(占用內(nèi)存最多)的進(jìn)程 ????p?=?select_bad_process(&points,?totalpages,?mpol_mask,?force_kill); ????... ????//?2.?如果找到最壞的進(jìn)程,那么調(diào)用?oom_kill_process?函數(shù)殺掉進(jìn)程 ????if?(p?!=?(void?*)-1UL)?{ ????????oom_kill_process(p,?gfp_mask,?order,?points,?totalpages,?NULL, ?????????????????????????nodemask,?"Out?of?memory"); ????????killed?=?1; ????} ????... }
out_of_memory() 函數(shù)的邏輯比較簡(jiǎn)單,主要完成兩個(gè)事情:
- 調(diào)用 select_bad_process() 函數(shù)從系統(tǒng)中選擇一個(gè)最壞(占用物理內(nèi)存最多)的進(jìn)程。
- 如果找到最壞的進(jìn)程,那么調(diào)用 oom_kill_process() 函數(shù)將此進(jìn)程殺掉。
從上面的分析可知,找到最壞的進(jìn)程是 OOM killer 最為重要的事情。
那么我們來看看 select_bad_process() 函數(shù)是怎樣選擇最壞的進(jìn)程的:
static?struct?task_struct?* select_bad_process(unsigned?int?*ppoints,?unsigned?long?totalpages, ???????????????????const?nodemask_t?*nodemask,?bool?force_kill) { ????struct?task_struct?*g,?*p; ????struct?task_struct?*chosen?=?NULL; ????unsigned?long?chosen_points?=?0; ????... ????//?1.?遍歷系統(tǒng)中所有的進(jìn)程和線程 ????for_each_process_thread(g,?p)?{ ????????unsigned?int?points; ????????... ????????//?2.?計(jì)算進(jìn)程最壞分?jǐn)?shù)值,?選擇分?jǐn)?shù)最大的進(jìn)程作為殺掉的目標(biāo)進(jìn)程 ????????points?=?oom_badness(p,?NULL,?nodemask,?totalpages); ????????if?(!points?||?points?continue; ????????... ????????chosen?=?p; ????????chosen_points?=?points; ????} ????... ????return?chosen; }
select_bad_process() 函數(shù)的主要工作如下:
- 遍歷系統(tǒng)中所有的進(jìn)程和線程,并且調(diào)用 oom_badness() 函數(shù)計(jì)算進(jìn)程的最壞分?jǐn)?shù)值。
- 選擇最壞分?jǐn)?shù)值最大的進(jìn)程作為被殺掉的目標(biāo)進(jìn)程。
所以,計(jì)算進(jìn)程的最壞分?jǐn)?shù)值就是 OOM killer 的核心工作。我們接著來看看 oom_badness() 函數(shù)是怎么計(jì)算進(jìn)程的最壞分?jǐn)?shù)值的:
unsigned?long oom_badness(struct?task_struct?*p,?struct?mem_cgroup?*memcg, ????????????const?nodemask_t?*nodemask,?unsigned?long?totalpages) { ????long?points; ????long?adj; ????//?1.?如果進(jìn)程不能被殺掉(init進(jìn)程和內(nèi)核進(jìn)程是不能被殺的) ????if?(oom_unkillable_task(p,?memcg,?nodemask)) ????????return?0; ????... ????//?2.?我們可以通過?/proc/{pid}/oom_score_adj?文件來設(shè)置進(jìn)程的被殺建議值, ????//????這個(gè)值越小,進(jìn)程被殺的機(jī)會(huì)越低。如果設(shè)置為?-1000?時(shí),進(jìn)程將被禁止殺掉。 ????adj?=?(long)p->signal->oom_score_adj; ????if?(adj?==?OOM_SCORE_ADJ_MIN)?{ ????????... ????????return?0; ????} ????//?3.?統(tǒng)計(jì)進(jìn)程使用的物理內(nèi)存數(shù) ????points?=?get_mm_rss(p->mm) ????????????????+?atomic_long_read(&p->mm->nr_ptes) ????????????????+?get_mm_counter(p->mm,?MM_SWAPENTS); ????... ????//?4.?加上進(jìn)程被殺建議值,得出最終的分?jǐn)?shù)值 ????adj?*=?totalpages?/?1000; ????points?+=?adj; ????return?points?>?0???points?:?1; }
oom_badness() 函數(shù)主要按照以下步驟來計(jì)算進(jìn)程的最壞分?jǐn)?shù)值:
- 如果進(jìn)程不能被殺掉(init進(jìn)程和內(nèi)核進(jìn)程是不能被殺的),那么返回分?jǐn)?shù)值為 0。
- 可以通過 /proc/{pid}/oom_score_adj 文件來設(shè)置進(jìn)程的 OOM 建議值(取值范圍為 -1000 ~ 1000)。建議值越小,進(jìn)程被殺的機(jī)會(huì)越低。如果將其設(shè)置為 -1000 時(shí),進(jìn)程將被禁止殺掉。
- 統(tǒng)計(jì)進(jìn)程使用的物理內(nèi)存數(shù),包括實(shí)際使用的物理內(nèi)存、頁表占用的物理內(nèi)存和 swap 機(jī)制占用的物理內(nèi)存。
- 最后加上進(jìn)程的 OOM 建議值,得出最終的分?jǐn)?shù)值。
通過 oom_badness() 函數(shù)計(jì)算出進(jìn)程的最壞分?jǐn)?shù)值后,系統(tǒng)就能從中選擇一個(gè)分?jǐn)?shù)值最大的進(jìn)程殺死,從而解決內(nèi)存不足的情況。
禁止進(jìn)程被 OOM 殺掉
有時(shí)候,我們不希望某些進(jìn)程被 OOM killer 殺掉。例如 mysql 進(jìn)程如果被 OOM killer 殺掉的話,那么可能導(dǎo)致數(shù)據(jù)丟失的情況。
那么如何防止進(jìn)程被 OOM killer 殺掉呢?從上面的分析可知,在內(nèi)核計(jì)算進(jìn)程最壞分?jǐn)?shù)值時(shí),會(huì)加上進(jìn)程的 oom_score_adj(OOM建議值)值。如果將此值設(shè)置為 -1000 時(shí),那么系統(tǒng)將會(huì)禁止 OOM killer 殺死此進(jìn)程。
例如使用如下命令,將會(huì)禁止殺死 PID 為 2000 的進(jìn)程:
$?echo?-1000?>?/proc/2000/oom_score_adj
這樣,我們就能防止一些重要的進(jìn)程被 OOM killer 殺死。