OOM Killer機(jī)制學(xué)習(xí)
當(dāng)系統(tǒng)內(nèi)存不足以分配時,Linux內(nèi)核會使用一種OOM Killer(Out-Of-Memory Killer)機(jī)制釋放內(nèi)存,該機(jī)制通過一系列比較選擇出最適合的進(jìn)程并將其kill掉,從而達(dá)到保障系統(tǒng)穩(wěn)定運(yùn)行的目的。那么在內(nèi)核中,OOM Killer具體是怎么運(yùn)轉(zhuǎn)的呢?
一、觸發(fā)過程
在申請內(nèi)存時,必然會調(diào)用alloc_page(),在__alloc_pages中有以下調(diào)用關(guān)系:

其中,在__alloc_pages_slowpath中,當(dāng)反復(fù)嘗試reclaim和compact后仍不成功,就會調(diào)用__alloc_pages_may_oom進(jìn)行內(nèi)存釋放。
/*
* If we failed to make any progress reclaiming, then we are
* running out of options and have to consider going OOM
*/
if (!did_some_progress) {
if (oom_gfp_allowed(gfp_mask)) {
if (oom_killer_disabled)
goto nopage;
/* Coredumps can quickly deplete all memory reserves */
if ((current->flags & PF_DUMPCORE) &&
!(gfp_mask & __GFP_NOFAIL))
goto nopage;
page = __alloc_pages_may_oom(gfp_mask, order,
zonelist, high_zoneidx,
nodemask, preferred_zone,
classzone_idx, migratetype);
......
}
如果定義了oom_killer_disabled,就會直接goto到nopage,不會觸發(fā)OOM機(jī)制(此值默認(rèn)為0).
二、工作過程(基于Linux-3.18)
當(dāng)內(nèi)核檢測到內(nèi)存不足,執(zhí)行到out_of_memory時,OOM Killer會選擇一個進(jìn)程并把他kill掉:
p = select_bad_process(&points, totalpages, mpol_mask, force_kill);
具體的選擇過程在select_bad_process中進(jìn)行:
/*
* Simple selection loop. We chose the process with the highest
* number of 'points'. Returns -1 on scan abort.
*
* (not docbooked, we don't want this one cluttering up the manual)
*/
static struct task_struct *select_bad_process(unsigned int *ppoints,
unsigned long totalpages, const nodemask_t *nodemask,
bool force_kill)
{
struct task_struct *g, *p;
struct task_struct *chosen = NULL;
unsigned long chosen_points = 0;
rcu_read_lock();
for_each_process_thread(g, p) {
unsigned int points;
switch (oom_scan_process_thread(p, totalpages, nodemask,
force_kill)) {
case OOM_SCAN_SELECT:
chosen = p;
chosen_points = ULONG_MAX;
/* fall through */
case OOM_SCAN_CONTINUE:
continue;
case OOM_SCAN_ABORT:
rcu_read_unlock();
return (struct task_struct *)(-1UL);
case OOM_SCAN_OK:
break;
};
points = oom_badness(p, NULL, nodemask, totalpages);
if (!points || points < chosen_points)
continue;
/* Prefer thread group leaders for display purposes */
if (points == chosen_points && thread_group_leader(chosen))
continue;
chosen = p;
chosen_points = points;
}
if (chosen)
get_task_struct(chosen);
rcu_read_unlock();
*ppoints = chosen_points * 1000 / totalpages;
return chosen;
}
select_bad_process會選擇一個points數(shù)值最高的進(jìn)程并返回。在宏for_each_process_thread循環(huán)里,通過switch和oom_scan_process_thread對一些進(jìn)程做特殊化處理,如一些進(jìn)程不適合被結(jié)束,就跳過本次循環(huán)。如果該進(jìn)程沒有特殊狀態(tài),oom_scan_process_thread返回OOM_SCAN_OK,繼續(xù)向下進(jìn)行判斷。這里使用了oom_badness對其points值進(jìn)行計算。
/**
* oom_badness - heuristic function to determine which candidate task to kill
* @p: task struct of which task we should calculate
* @totalpages: total present RAM allowed for page allocation
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
const nodemask_t *nodemask, unsigned long totalpages)
{
long points;
long adj;
if (oom_unkillable_task(p, memcg, nodemask))
return 0;
p = find_lock_task_mm(p);
if (!p)
return 0;
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN) {
task_unlock(p);
return 0;
}
/*
* The baseline for the badness score is the proportion of RAM that each
* task's rss, pagetable and swap space use.
*/
points = get_mm_rss(p->mm) + atomic_long_read(&p->mm->nr_ptes) +
get_mm_counter(p->mm, MM_SWAPENTS);
task_unlock(p);
/*
* Root processes get 3% bonus, just like the __vm_enough_memory()
* implementation used by LSMs.
*/
if (has_capability_noaudit(p, CAP_SYS_ADMIN))
points -= (points * 3) / 100;
/* Normalize to oom_score_adj units */
adj *= totalpages / 1000;
points += adj;
/*
* Never return 0 for an eligible task regardless of the root bonus and
* oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
*/
return points > 0 ? points : 1;
}
在oom_badness的上半部分,對進(jìn)程做了一些判斷,排除了不可進(jìn)行kill的進(jìn)程以及oom_score_adj為OOM_SCORE_ADJ_MIN(-1000)的進(jìn)程,進(jìn)行了return 0。接著是進(jìn)行比重計算,將rss、nr_ptes、swap空間使用量占RAM比重相加。如果是Root進(jìn)程則去掉3%的比重points -= (points * 3) / 100;。之后對adj進(jìn)行歸一化并與points相加,在返回值計算時,使用了一個三目運(yùn)算符,即當(dāng)points大于0時,返回points,否則返回1。這里注釋給出的原因是,對于有資格的進(jìn)程(即可以被OOM Killer掉的進(jìn)程),是絕不能返回0的。(這里我的理解是,如果points返回0,這個進(jìn)程可能在之后的比較中就處于劣勢,成為漏網(wǎng)之魚)

再回到select_bad_process中看,之后跟的一個if比較就是為了進(jìn)行取最大值的判斷,再之后判斷該進(jìn)程是否為thread_group_leader,若是則continue跳過本次循環(huán),否則該進(jìn)程就是被chosen的進(jìn)程。
再回到out_of_memory中,得到p值后,需要對其進(jìn)行判斷:
if (!p) {
dump_header(NULL, gfp_mask, order, NULL, mpol_mask);
panic("Out of memory and no killable processes...\n");
}
if (p != (void *)-1UL) {
oom_kill_process(p, gfp_mask, order, points, totalpages, NULL,
nodemask, "Out of memory");
killed = 1;
}
當(dāng)p是0時,即沒有找到可以kill掉的進(jìn)程,內(nèi)核發(fā)出一個panic。當(dāng)p不是0時,即找到了可以kill掉的進(jìn)程,則通過oom_kill_process將其kill。
在oom_kill_process中有個“有意思”的事是,在kill之前,會先遍歷其子進(jìn)程,重新通過oom_badness計算出一個最適合被kill掉的子進(jìn)程,該子進(jìn)程會有限考慮被kill掉,從而避免kill父進(jìn)程導(dǎo)致的接管子進(jìn)程的工作開銷。并且最終被kill掉的進(jìn)程的名字叫victim,這個單詞的中文含義是犧牲者,有點是為了整個系統(tǒng)的穩(wěn)定運(yùn)轉(zhuǎn)而犧牲的意思。在這之后OOM Killer會kill掉和victim使用相同虛擬內(nèi)存的進(jìn)程,并通過發(fā)送SIGKILL信號將其終止。

三、到底為什么會發(fā)生Out Of Memory?
因為物理內(nèi)存頁的分配發(fā)生在使用的瞬間而非分配的瞬間。若某個進(jìn)程申請了200MB內(nèi)存,但實際上只使用了100MB,未使用到的100MB根本沒有分配物理內(nèi)存頁。當(dāng)進(jìn)程需要內(nèi)存時,進(jìn)程從內(nèi)核得到的只是虛擬地址的使用權(quán),而不是實際的物理地址,實際的物理內(nèi)存只有當(dāng)進(jìn)程真的去訪問新獲取的虛擬地址時,產(chǎn)生缺頁異常,從而進(jìn)入分配實際物理地址的過程,之后系統(tǒng)返回產(chǎn)生異常的地址,重新執(zhí)行內(nèi)存訪問。虛擬內(nèi)存需要物理內(nèi)存作為支撐,當(dāng)分配了太多虛擬內(nèi)存,導(dǎo)致物理內(nèi)存不夠時,就發(fā)生了Out Of Memory。這種允許超額commit的機(jī)制就是overcommit。
overcommit即操作系統(tǒng)在應(yīng)用申請內(nèi)存空間時不去檢查是否超出當(dāng)前可用量,隨意滿足申請要求,應(yīng)用也不管實際是否有足夠多的內(nèi)存可使用,認(rèn)為我申請了2G,OS肯定就給我2G使用。最后,隨著內(nèi)存越用越多,OS發(fā)現(xiàn)內(nèi)存不夠用了,必須要收回一些內(nèi)存才行,就觸發(fā)了上述的OOM Killer機(jī)制回收內(nèi)存。
Linux根據(jù)參數(shù) vm.overcommit_memory設(shè)置overcommit:
0 ——默認(rèn)值,啟發(fā)式overcommit,它允許overcommit,但太明顯的overcommit會被拒絕,比如malloc一次性申請的內(nèi)存大小就超過了系統(tǒng)總內(nèi)存。
1 ——Always overcommit. 允許overcommit,對內(nèi)存申請來者不拒。
2 ——不允許overcommit,提交給系統(tǒng)的總地址空間大小不允許超過CommitLimit。(CommitLimit 就是overcommit的閾值,申請的內(nèi)存總數(shù)超過CommitLimit的話就算是overcommit)
四、總結(jié)
由于物理內(nèi)存的分配機(jī)制,以及overcommit的存在,導(dǎo)致了在物理內(nèi)存不夠時的OOM Killer。OOM Killer機(jī)制很有意思,它為了保護(hù)整個系統(tǒng)的安全穩(wěn)定運(yùn)行,需要找出一個最合適的進(jìn)程kill掉。這是不得已而為之,內(nèi)核必須在kill掉進(jìn)程和系統(tǒng)崩潰之間選擇其中一個。內(nèi)核代碼中out_of_memory注釋中也體現(xiàn)了這種無奈。> * If we run out of memory, we have the choice between either
killing a random task (bad), letting the system crash (worse)
OR try to be smart about which process to kill. Note that we
don't have to be perfect here, we just have to be good.
在選擇合適的進(jìn)程時,OOM Killer會挑選一個占用內(nèi)存最大的進(jìn)程,這也很好理解,畢竟kill掉一個大的可以獲得更多的物理內(nèi)存,并且損失也比較小。如果kill掉多個小的,損失會比較大。Linux內(nèi)核總是去選擇更高效的方法。
鏈接:https://www.codingsky.com/m/doc/2021/10/19/925.html
(版權(quán)歸原作者所有,侵刪)

