enum oom_constraint {
CONSTRAINT_NONE,
CONSTRAINT_CPUSET,
CONSTRAINT_MEMORY_POLICY,
CONSTRAINT_MEMCG,
};
對於UMA而言, oom_constraint永遠都是CONSTRAINT_NONE,表示系統並沒有什么約束就出現了OOM,不要想太多了,就是內存不足了。在NUMA的情況下,有可能附加了其他的約束導致了系統遇到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
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);
!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干掉當前進程。
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
const nodemask_t *nodemask, unsigned long totalpages)
{……
if (adj == OOM_SCORE_ADJ_MIN) {----------------------(1)
task_unlock(p);
return 0;---------------------------------(2)
}
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;
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殺死該進程。
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
-
dev@ubuntu :~$ cat /proc/sys/vm/panic_on_oom
-
0
-
-
#OOM后kill掉分數最高的進程
-
dev@ubuntu :~$ cat /proc/sys/vm/oom_kill_allocating_task
-
0
-
-
#進程由於OOM被kill掉后將生成core dump文件
-
dev@ubuntu :~$ cat /proc/sys/vm/oom_dump_tasks
-
1
-
-
#默認max core file size是0, 所以系統不會生成core文件
-
dev@ubuntu :~$ prlimit|grep CORE
-
CORE max core file size 0 unlimited blocks
-
-
#core dump文件的生成交給了apport,相關的設置可以參考apport的資料
-
dev@ubuntu:~$ 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的話就要看mempolicy和cpusets,這篇不介紹這方面的內容。
panic后內核的默認行
為是死在那里,目的是給開發人員一個連上去debug的機會。但對於大多數應用層開發人員來說沒啥用,倒是希望它趕緊重啟。為了讓內核panic后重啟,可以修改文件/proc/sys/kernel/panic,里面表示的是panic多少秒后系統將重啟,這個文件的默認值是0,表示永遠不重啟。
-
#設置panic后3秒重啟系統
-
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
-
修改配置文件(重啟后繼續生效)
-
#通過編輯器將vm.panic_on_oom=2添加到文件sysctl.conf中(如果已經存在,修改該配置項即可)
-
dev@dev :~$ sudo vim /etc/sysctl.conf
-
-
#重新加載sysctl.conf,使修改立即生效
-
dev@dev :~$ sudo sysctl -p
-
日志
一旦OOM killer被觸發,內核將會生成相應的日志,一般可以在/var/log/messages里面看到,如果配置了syslog,日志可能在/var/log/syslog里面,這里是ubuntu里的日志樣例
-
dev@dev:~$ 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++程序員對這個函數應該都很熟悉,它里面實際上調用的是內核的sbrk和mmap,為了避免頻繁的調用內核函數和優化性能,它里面在內核函數的基礎上實現了一套自己的內存管理功能。
既然內存不夠時有OOM killer幫我們kill進程,那么這時調用的malloc還會返回NULL給應用進程嗎?答案是不會,因為這時只有兩種情況:
-
當前申請內存的進程被kill掉:都被kill掉了,返回什么都沒有意義了
-
其它進程被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
-
dev@dev :~$ 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的例子,供參考:
-
-
-
-
-
-
-
-
-
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,需要具體問題具體分析。
參考
轉載自0: