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