1. 程式人生 > >Linux vm運行參數 - OOM相關的參數

Linux vm運行參數 - OOM相關的參數

換算 資源 功能 處理 同學 roo emc lis like

一、前言

本文是描述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

Linux vm運行參數 - OOM相關的參數