細說 Linux Out Of Memory 機制
有時候我們會發現系統中某個進程會突然掛掉,通過查看系統日誌發現是由於 OOM機制
導致進程被殺掉。
今天我們就來介紹一下什麼是 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 < chosen_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 殺死。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/YgA4x6kmhvOpgYQtZ9_A0g