1. 程式人生 > >Linux OOM killer詳解

Linux OOM killer詳解

Linux OOM killer

作為Linux下的程式設計師,有時不得不面對一個問題,那就是系統記憶體被用光了,這時當程序再向核心申請記憶體時,核心會怎麼辦呢?程式裡面呼叫的malloc函式會返回null嗎?

為了處理記憶體不足時的問題,Linux核心發明了一種機制,叫OOM(Out Of Memory) killer,通過配置它可以控制記憶體不足時核心的行為。

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預設的配置:

#OOM後不panic
[email protected]
:~$ cat /proc/sys/vm/panic_on_oom 0 #OOM後kill掉分數最高的程序 [email protected]:~$ cat /proc/sys/vm/oom_kill_allocating_task 0 #程序由於OOM被kill掉後將生成core dump檔案 [email protected]:~$ cat /proc/sys/vm/oom_dump_tasks 1 #預設max core file size是0, 所以系統不會生成core檔案 [email protected]:~$ prlimit|grep CORE CORE max core file size 0 unlimited blocks #core dump檔案的生成交給了apport,相關的設定可以參考apport的資料
[email protected]
:~$ cat /proc/sys/kernel/core_pattern |/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,表示永遠不重啟。

#設定panic後3秒重啟系統
[email protected]:~$ 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為例做個示範:

  • 直接寫檔案(重啟後失效)

    [email protected]:~$ sudo sh -c "echo 2> /proc/sys/vm/panic_on_oom"
  • 通過控制命令(重啟後失效)

    [email protected]:~$ sudo sysctl vm.panic_on_oom=2
  • 修改配置檔案(重啟後繼續生效)

    #通過編輯器將vm.panic_on_oom=2新增到檔案sysctl.conf中(如果已經存在,修改該配置項即可)
    [email protected]:~$ sudo vim /etc/sysctl.conf
    
    #重新載入sysctl.conf,使修改立即生效
    [email protected]:~$ sudo sysctl -p

日誌

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

[email protected]:~$ grep oom /var/log/syslog
Jan 23 21:30:29 dev kernel: [  490.006836] eat_memory invoked oom-killer: gfp_mask=0x24280ca, order=0, oom_score_adj=0
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的用法請參考它的幫助檔案

#預設情況下,RLIMIT_AS和RLIMIT_DATA的值都是unlimited
[email protected]:~$ prlimit |egrep "DATA|AS"
AS         address space limit                unlimited unlimited bytes
DATA       max data size                      unlimited unlimited bytes

測試程式碼

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define M (1024 * 1024)
#define K 1024

int main(int argc, char *argv[])
{
    char *p;
    int size =0;
    while(1) {
        p = (char *)malloc(K);
        if  (p == NULL){
            printf("memory allocate failed!\n");
            return -1;
        }
        memset(p, 0, K);
        size += K;
        if (size%(100*M) == 0){
            printf("%d00M memory allocated\n", size/(100*M));
            sleep(1);
        }
    }

    return 0;
}

結束語

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

參考