Linux OOM killer 與相關參數詳解


一、前言
本文是描述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
 
 

OOM killer

當物理內存和交換空間都被用完時,如果還有進程來申請內存,內核將觸發OOM killer,其行為如下:

1.檢查文件/proc/sys/vm/panic_on_oom,如果里面的值為2,那么系統一定會觸發panic
2.如果/proc/sys/vm/panic_on_oom的值為1,那么系統有可能觸發panic(見后面的介紹)
3.如果/proc/sys/vm/panic_on_oom的值為0,或者上一步沒有觸發panic,那么內核繼續檢查文件/proc/sys/vm/oom_kill_allocating_task
3.如果/proc/sys/vm/oom_kill_allocating_task為1,那么內核將kill掉當前申請內存的進程
4.如果/proc/sys/vm/oom_kill_allocating_task為0,內核將檢查每個進程的分數,分數最高的進程將被kill掉(見后面介紹)

進程被kill掉之后,如果/proc/sys/vm/oom_dump_tasks為1,且系統的rlimit中設置了core文件大小,將會由/proc/sys/kernel/core_pattern里面指定的程序生成core dump文件,這個文件里將包含
pid, uid, tgid, vm size, rss, nr_ptes, nr_pmds, swapents, oom_score_adj
score, name等內容,拿到這個core文件之后,可以做一些分析,看為什么這個進程被選中kill掉。

這里可以看看ubuntu默認的配置:

  1.  
    #OOM后不panic
  2.  
    dev@ubuntu :~$ cat /proc/sys/vm/panic_on_oom
  3.  
    0
  4.  
     
  5.  
    #OOM后kill掉分數最高的進程
  6.  
    dev@ubuntu :~$ cat /proc/sys/vm/oom_kill_allocating_task
  7.  
    0
  8.  
     
  9.  
    #進程由於OOM被kill掉后將生成core dump文件
  10.  
    dev@ubuntu :~$ cat /proc/sys/vm/oom_dump_tasks
  11.  
    1
  12.  
     
  13.  
    #默認max core file size是0, 所以系統不會生成core文件
  14.  
    dev@ubuntu :~$ prlimit|grep CORE
  15.  
    CORE max core file size 0 unlimited blocks
  16.  
     
  17.  
    #core dump文件的生成交給了apport,相關的設置可以參考apport的資料
  18.  
    dev@ubuntu:~$ cat /proc/sys/kernel/core_pattern
  19.  
    |/usr/share/apport/apport %p %s %c %P

參考:apport

panic_on_oom

正如上面所介紹的那樣,該文件的值可以取0/1/2,0是不觸發panlic,2是一定觸發panlic,如果為1的話就要看mempolicycpusets,這篇不介紹這方面的內容。

panic后內核的默認行
為是死在那里,目的是給開發人員一個連上去debug的機會。但對於大多數應用層開發人員來說沒啥用,倒是希望它趕緊重啟。為了讓內核panic后重啟,可以修改文件/proc/sys/kernel/panic,里面表示的是panic多少秒后系統將重啟,這個文件的默認值是0,表示永遠不重啟。

  1.  
    #設置panic后3秒重啟系統
  2.  
    dev@ubuntu :~$ sudo sh -c "echo 3 > /proc/sys/kernel/panic"

調整分數

當oom_kill_allocating_task的值為0時(系統默認配置),系統會kill掉系統中分數最高的那個進程,這里的分數是怎么來的呢?該值由內核維護,並存儲在每個進程的/proc/<pid>/oom_score文件中。

每個進程的分數受多方面的影響,比如進程運行的時間,時間越長表明這個程序越重要,所以分數越低;進程從啟動后分配的內存越多,表示越占內存,分數會越高;這里只是列舉了一兩個影響分數的因素,實際情況要復雜的多,需要看內核代碼,這里有篇文章可以參考:Taming the OOM killer

由於分數計算復雜,比較難控制,於是內核提供了另一個文件用來調控分數,那就是文件/proc/<pid>/oom_adj,這個文件的默認值是0,但它可以配置為-17到15中間的任何一個值,內核在計算了進程的分數后,會和這個文件的值進行一個計算,得到的結果會作為進程的最終分數寫入/proc/<pid>/oom_score。計算方式大概如下:

  • 如果/proc/<pid>/oom_adj的值為正數,那么分數將會被乘以2的n次方,這里n是文件里面的值

  • 如果/proc/<pid>/oom_adj的值為負數,那么分數將會被除以2的n次方,這里n是文件里面的值

由於進程的分數在內核中是一個16位的整數,所以-17就意味着最終進程的分數永遠是0,也即永遠不會被kill掉。

當然這種控制方式也不是非常精確,但至少比沒有強多了。

修改配置

上面的這些文件都可以通過下面三種方式來修改,這里以panic_on_oom為例做個示范:

  • 直接寫文件(重啟后失效)

    dev@ubuntu:~$ sudo sh -c "echo 2> /proc/sys/vm/panic_on_oom"
     
  • 通過控制命令(重啟后失效)

    dev@dev:~$ sudo sysctl vm.panic_on_oom=2
     
  • 修改配置文件(重啟后繼續生效)

    1.  
      #通過編輯器將vm.panic_on_oom=2添加到文件sysctl.conf中(如果已經存在,修改該配置項即可)
    2.  
      dev@dev :~$ sudo vim /etc/sysctl.conf
    3.  
       
    4.  
      #重新加載sysctl.conf,使修改立即生效
    5.  
      dev@dev :~$ sudo sysctl -p

日志

一旦OOM killer被觸發,內核將會生成相應的日志,一般可以在/var/log/messages里面看到,如果配置了syslog,日志可能在/var/log/syslog里面,這里是ubuntu里的日志樣例

  1.  
    dev@dev:~$ grep oom /var/log/syslog
  2.  
    Jan 23 21:30:29 dev kernel: [ 490.006836] eat_memory invoked oom-killer: gfp_mask=0x24280ca, order=0, oom_score_adj=0
  3.  
    Jan 23 21:30:29 dev kernel: [ 490.006871] [<ffffffff81191442>] oom_kill_process+0x202/0x3c0

cgroup的OOM killer

除了系統的OOM killer之外,如果配置了memory cgroup,那么進程還將受到自己所屬memory cgroup的限制,如果超過了cgroup的限制,將會觸發cgroup的OOM killer,cgroup的OOM killer和系統的OOM killer行為略有不同,詳情請參考Linux Cgroup系列(04):限制cgroup的內存使用

malloc

malloc是libc的函數,C/C++程序員對這個函數應該都很熟悉,它里面實際上調用的是內核的sbrkmmap,為了避免頻繁的調用內核函數和優化性能,它里面在內核函數的基礎上實現了一套自己的內存管理功能。

既然內存不夠時有OOM killer幫我們kill進程,那么這時調用的malloc還會返回NULL給應用進程嗎?答案是不會,因為這時只有兩種情況:

  1. 當前申請內存的進程被kill掉:都被kill掉了,返回什么都沒有意義了

  2. 其它進程被kill掉:釋放出了空閑的內存,於是內核就能給當前進程分配內存了

那什么時候我們調用malloc的時候會返回NULL呢,從malloc函數的幫助文件可以看出,下面兩種情況會返回NULL:

  • 使用的虛擬地址空間超過了RLIMIT_AS的限制

  • 使用的數據空間超過了RLIMIT_DATA的限制,這里的數據空間包括程序的數據段,BSS段以及heap

關於虛擬地址空間和heap之類的介紹請參考Linux進程的內存使用情況,這兩個參數的默認值為unlimited,所以只要不修改它們的默認配置,限制就不會被觸發。有一種極端情況需要注意,那就是代碼寫的有問題,超過了系統的虛擬地址空間范圍,比如32位系統的虛擬地址空間范圍只有4G,這種情況下不確定系統會以一種什么樣的方式返回錯誤。

rlimit

上面提到的RLIMIT_AS和RLIMIT_DATA都可以通過函數getrlimit和setrlimit來設置和讀取,同時linux還提供了一個prlimit程序來設置和讀取rlimit的配置。

prlimit是用來替代
ulimit的一個程序,除了能設置上面的那兩個參數之外,還有其它的一些參數,比如core文件的大小。關於prlimit的用法請參考它的幫助文件

  1.  
    #默認情況下,RLIMIT_AS和RLIMIT_DATA的值都是unlimited
  2.  
    dev@dev :~$ prlimit |egrep "DATA|AS"
  3.  
    AS address space limit unlimited unlimited bytes
  4.  
    DATA max data size unlimited unlimited bytes

測試代碼

C語言的程序會受到libc的影響,可能在觸發OOM killer之前就觸發了segmentfault錯誤,如果要用C語言程序來測試觸發OOM killer,一定要注意malloc的行為受MMAP_THRESHOLD影響,一次申請分配太多內存的話,malloc會調用mmap映射內存,從而不一定觸發OOM killer,具體細節目前還不太清楚。這里是一個觸發oom killer的例子,供參考:

  1.  
    #include <stdio.h>
  2.  
    #include <stdlib.h>
  3.  
    #include <string.h>
  4.  
    #include <unistd.h>
  5.  
     
  6.  
    #define M (1024 * 1024)
  7.  
    #define K 1024
  8.  
     
  9.  
    int main(int argc, char *argv[])
  10.  
    {
  11.  
    char *p;
  12.  
    int size =0;
  13.  
    while(1) {
  14.  
    p = ( char *)malloc(K);
  15.  
    if (p == NULL){
  16.  
    printf("memory allocate failed!\n");
  17.  
    return -1;
  18.  
    }
  19.  
    memset(p, 0, K);
  20.  
    size += K;
  21.  
    if (size%(100*M) == 0){
  22.  
    printf("%d00M memory allocated\n", size/(100*M));
  23.  
    sleep( 1);
  24.  
    }
  25.  
    }
  26.  
     
  27.  
    return 0;
  28.  
    }

結束語

對一個進程來說,內存的使用受多種因素的限制,可能在系統內存不足之前就達到了rlimit和memory cgroup的限制,同時它還可能受不同編程語言所使用的相關內存管理庫的影響,就算系統處於內存不足狀態,申請新內存也不一定會觸發OOM killer,需要具體問題具體分析。

參考



轉載自0:
https://blog.csdn.net/u011677209/article/details/52769225
https://blog.csdn.net/oDaiLiDong/article/details/81907752


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM