1. 程式人生 > >Linux vm執行引數之(二):OOM相關的引數

Linux vm執行引數之(二):OOM相關的引數

一、前言

本文是描述Linux virtual memory執行引數的第二篇,主要是講OOM相關的引數的。為了理解OOM引數,第二章簡單的描述什麼是OOM。如果這個名詞對你毫無壓力,你可以直接進入第三章,這一章是描述具體的引數的,除了描述具體的引數,我們引用了一些具體的核心程式碼,本文的程式碼來自4.0核心,如果有興趣,可以結合程式碼閱讀,為了縮減篇幅,文章中的程式碼都是刪減版本的。按照慣例,最後一章是參考文獻,本文的參考文獻都是來自linux核心的Documentation目錄,該目錄下有大量的文件可以參考,每一篇都值得細細品味。

二、什麼是OOM

OOM就是out of memory的縮寫,雖然linux kernel有很多的記憶體管理技巧(從cache中回收、swap out等)來滿足各種應用空間的vm記憶體需求,但是,當你的系統配置不合理,讓一匹小馬拉大車的時候,linux kernel會執行非常緩慢並且在某個時間點分配page frame的時候遇到記憶體耗盡、無法分配的狀況。應對這種狀況首先應該是系統管理員,他需要首先給系統增加記憶體,不過對於kernel而言,當面對OOM的時候,咱們也不能慌亂,要根據OOM引數來進行相應的處理。

三、OOM引數

1、panic_on_oom

當kernel遇到OOM的時候,可以有兩種選擇:

(1)產生kernel panic(就是死給你看)。

(2)積極面對人生,選擇一個或者幾個最“適合”的程序,啟動OOM killer,幹掉那些選中的程序,釋放記憶體,讓系統勇敢的活下去。

panic_on_oom這個引數就是控制遇到OOM的時候,系統如何反應的。當該引數等於0的時候,表示選擇積極面對人生,啟動OOM killer。當該引數等於2的時候,表示無論是哪一種情況,都強制進入kernel panic。panic_on_oom等於其他值的時候,表示要區分具體的情況,對於某些情況可以panic,有些情況啟動OOM killer。kernel的程式碼中,enum oom_constraint 就是一個進一步描述OOM狀態的引數。系統遇到OOM總是有各種各樣的情況的,kernel中定義如下:

enum oom_constraint { 
    CONSTRAINT_NONE, 
    CONSTRAINT_CPUSET, 
    CONSTRAINT_MEMORY_POLICY, 
    CONSTRAINT_MEMCG, 
};

對於UMA而言, oom_constraint永遠都是CONSTRAINT_NONE,表示系統並沒有什麼約束就出現了OOM,不要想太多了,就是記憶體不足了。在NUMA的情況下,有可能附加了其他的約束導致了系統遇到OOM狀態,實際上,系統中還有充足的記憶體。這些約束包括:

(1)CONSTRAINT_CPUSET。cpusets是kernel中的一種機制,通過該機制可以把一組cpu和memory node資源分配給特定的一組程序。這時候,如果出現OOM,僅僅說明該程序能分配memory的那個node出現狀況了,整個系統有很多的memory node,其他的node可能有充足的memory資源。

(2)CONSTRAINT_MEMORY_POLICY。memory policy是NUMA系統中如何控制分配各個memory node資源的策略模組。使用者空間程式(NUMA-aware的程式)可以通過memory policy的API,針對整個系統、針對一個特定的程序,針對一個特定程序的特定的VMA來制定策略。產生了OOM也有可能是因為附加了memory policy的約束導致的,在這種情況下,如果導致整個系統panic似乎有點不太合適吧。

(3)CONSTRAINT_MEMCG。MEMCG就是memory control group,Cgroup這東西太複雜,這裡不適合多說,Cgroup中的memory子系統就是控制系統memory資源分配的控制器,通俗的將就是把一組程序的記憶體使用限定在一個範圍內。當這一組的記憶體使用超過上限就會OOM,在這種情況下的OOM就是CONSTRAINT_MEMCG型別的OOM。

OK,瞭解基礎知識後,我們來看看核心程式碼。核心中sysctl_panic_on_oom變數是和/proc/sys/vm/panic_on_oom對應的,主要的判斷邏輯如下:

void check_panic_on_oom(enum oom_constraint constraint, gfp_t gfp_mask, 
            int order, const nodemask_t *nodemask) 

    if (likely(!sysctl_panic_on_oom))----0表示啟動OOM killer,因此直接return了 
        return; 
    if (sysctl_panic_on_oom != 2) {----2是強制panic,不是2的話,還可以商量 
        if (constraint != CONSTRAINT_NONE)---在有cpuset、memory policy、memcg的約束情況下
            return;                                                  的OOM,可以考慮不panic,而是啟動OOM killer 
    } 
    dump_header(NULL, gfp_mask, order, NULL, nodemask); 
    panic("Out of memory: %s panic_on_oom is enabled\n", 
        sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide");---死給你看啦 
}

2、oom_kill_allocating_task

當系統選擇了啟動OOM killer,試圖殺死某些程序的時候,又會遇到這樣的問題:幹掉哪個,哪一個才是“合適”的哪那個程序?系統可以有下面的選擇:

(1)誰觸發了OOM就幹掉誰

(2)誰最“壞”就幹掉誰

oom_kill_allocating_task這個引數就是控制這個選擇路徑的,當該引數等於0的時候選擇(2),否則選擇(1)。具體的程式碼可以在參考__out_of_memory函式,具體如下:

static void __out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask, 
        int order, nodemask_t *nodemask, bool force_kill)   {

…… 
    check_panic_on_oom(constraint, gfp_mask, order, mpol_mask);

    if (sysctl_oom_kill_allocating_task && current->mm && 
        !oom_unkillable_task(current, NULL, nodemask) && 
        current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) { 
        get_task_struct(current); 
        oom_kill_process(current, gfp_mask, order, 0, totalpages, NULL, 
                 nodemask, "Out of memory (oom_kill_allocating_task)"); 
        goto out; 
    }

…… 
}

當然也不能說殺就殺,還是要考慮是否使用者空間程序(不能殺核心執行緒)、是否unkillable task(例如init程序就不能殺),使用者空間是否通過設定引數(oom_score_adj)阻止kill該task。如果萬事俱備,那麼就呼叫oom_kill_process幹掉當前程序。

3、oom_dump_tasks

當系統的記憶體出現OOM狀況,無論是panic還是啟動OOM killer,做為系統管理員,你都是想保留下線索,找到OOM的root cause,例如dump系統中所有的使用者空間程序關於記憶體方面的一些資訊,包括:程序標識資訊、該程序使用的total virtual memory資訊、該程序實際使用實體記憶體(我們又稱之為RSS,Resident Set Size,不僅僅是自己程式使用的實體記憶體,也包含共享庫佔用的記憶體),該程序的頁表資訊等等。拿到這些資訊後,有助於瞭解現象(出現OOM)之後的真相。

當設定為0的時候,上一段描述的各種程序們的記憶體資訊都不會打印出來。在大型的系統中,有幾千個程序,逐一列印每一個task的記憶體資訊有可能會導致效能問題(要知道當時已經是OOM了)。當設定為非0值的時候,在下面三種情況會呼叫dump_tasks來列印系統中所有task的記憶體狀況:

(1)由於OOM導致kernel panic

(2)沒有找到適合的“bad”process

(3)找適合的並將其幹掉的時候

4、oom_adj、oom_score_adj和oom_score

準確的說這幾個引數都是和具體程序相關的,因此它們位於/proc/xxx/目錄下(xxx是程序ID)。假設我們選擇在出現OOM狀況的時候殺死程序,那麼一個很自然的問題就浮現出來:到底幹掉哪一個呢?核心的演算法倒是非常簡單,那就是打分(oom_score,注意,該引數是read only的),找到分數最高的就OK了。那麼怎麼來算分數呢?可以參考核心中的oom_badness函式:

unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg, 
              const nodemask_t *nodemask, unsigned long totalpages) 
{……

    adj = (long)p->signal->oom_score_adj; 
    if (adj == OOM_SCORE_ADJ_MIN) {----------------------(1) 
        task_unlock(p); 
        return 0;---------------------------------(2) 
    }

    points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) + 
        atomic_long_read(&p->mm->nr_ptes) + mm_nr_pmds(p->mm);---------(3) 
    task_unlock(p);


    if (has_capability_noaudit(p, CAP_SYS_ADMIN))-----------------(4) 
        points -= (points * 3) / 100;

    adj *= totalpages / 1000;----------------------------(5) 
    points += adj;  

    return points > 0 ? points : 1; 
}

(1)對某一個task進行打分(oom_score)主要有兩部分組成,一部分是系統打分,主要是根據該task的記憶體使用情況。另外一部分是使用者打分,也就是oom_score_adj了,該task的實際得分需要綜合考慮兩方面的打分。如果使用者將該task的 oom_score_adj設定成OOM_SCORE_ADJ_MIN(-1000)的話,那麼實際上就是禁止了OOM killer殺死該程序。

(2)這裡返回了0也就是告知OOM killer,該程序是“good process”,不要幹掉它。後面我們可以看到,實際計算分數的時候最低分是1分。

(3)前面說過了,系統打分就是看實體記憶體消耗量,主要是三部分,RSS部分,swap file或者swap device上佔用的記憶體情況以及頁表佔用的記憶體情況。

(4)root程序有3%的記憶體使用特權,因此這裡要減去那些記憶體使用量。

(5)使用者可以調整oom_score,具體如何操作呢?oom_score_adj的取值範圍是-1000~1000,0表示使用者不調整oom_score,負值表示要在實際打分值上減去一個折扣,正值表示要懲罰該task,也就是增加該程序的oom_score。在實際操作中,需要根據本次記憶體分配時候可分配記憶體來計算(如果沒有記憶體分配約束,那麼就是系統中的所有可用記憶體,如果系統支援cpuset,那麼這裡的可分配記憶體就是該cpuset的實際額度值)。oom_badness函式有一個傳入引數totalpages,該引數就是當時的可分配的記憶體上限值。實際的分數值(points)要根據oom_score_adj進行調整,例如如果oom_score_adj設定-500,那麼表示實際分數要打五折(基數是totalpages),也就是說該任務實際使用的記憶體要減去可分配的記憶體上限值的一半。

瞭解了oom_score_adj和oom_score之後,應該是塵埃落定了,oom_adj是一箇舊的介面引數,其功能類似oom_score_adj,為了相容,目前仍然保留這個引數,當操作這個引數的時候,kernel實際上是會換算成oom_score_adj,有興趣的同學可以自行了解,這裡不再細述了。

四、參考文獻

1、Documentation/vm/numa_memory_policy.txt

2、Documentation/sysctl/vm.txt

3、Documentation/cgroup/cpusets.txt

4、Documentation/cgroup/memory.txt

5、Documentation/filesystems/proc.txt