細說 Linux Out Of Memory 機制

有時候我們會發現系統中某個進程會突然掛掉,通過查看系統日誌發現是由於 OOM機制 導致進程被殺掉。

今天我們就來介紹一下什麼是 OOM機制 以及怎麼防止進程因爲 OOM機制 而被殺掉。

什麼是 OOM 機制

OOM 是 Out Of Memory 的縮寫,中文意思是內存不足。而 OOM機制 是指當系統內存不足時,系統觸發的應急機制。

當 Linux 內核發現系統中的物理內存不足時,首先會對系統中的可回收內存進行回收,能夠被回收的內存有如下:

當系統內存不足時,內核會優先釋放這些內存頁。因爲使用這些內存頁只是爲了提升系統的性能,釋放這些內存頁也不會影響系統的正常運行。

如果釋放上述的內存後,還不能解決內存不足的情況,那麼內核會如何處理呢?答案就是:觸發 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() 函數的邏輯比較簡單,主要完成兩個事情:

  1. 調用 select_bad_process() 函數從系統中選擇一個最壞(佔用物理內存最多)的進程。

  2. 如果找到最壞的進程,那麼調用 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() 函數的主要工作如下:

  1. 遍歷系統中所有的進程和線程,並且調用 oom_badness() 函數計算進程的最壞分數值。

  2. 選擇最壞分數值最大的進程作爲被殺掉的目標進程。

所以,計算進程的最壞分數值就是 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() 函數主要按照以下步驟來計算進程的最壞分數值:

  1. 如果進程不能被殺掉(init 進程和內核進程是不能被殺的),那麼返回分數值爲 0。

  2. 可以通過 /proc/{pid}/oom_score_adj 文件來設置進程的 OOM 建議值(取值範圍爲 -1000 ~ 1000)。建議值越小,進程被殺的機會越低。如果將其設置爲 -1000 時,進程將被禁止殺掉。

  3. 統計進程使用的物理內存數,包括實際使用的物理內存、頁表佔用的物理內存和 swap 機制佔用的物理內存。

  4. 最後加上進程的 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