1.序言
內(nèi)存對計(jì)算機(jī)系統(tǒng)來說是一項(xiàng)非常重要的資源,直接影響著系統(tǒng)運(yùn)行的性能。最初的時(shí)候,系統(tǒng)是直接運(yùn)行在物理內(nèi)存上的,這存在著很多的問題,尤其是安全問題。后來出現(xiàn)了虛擬內(nèi)存,內(nèi)核和進(jìn)程都運(yùn)行在虛擬內(nèi)存上,進(jìn)程與進(jìn)程之間有了空間隔離,增加了安全性。進(jìn)程與內(nèi)核之間有特權(quán)級的區(qū)別,進(jìn)程運(yùn)行在非特權(quán)級,內(nèi)核運(yùn)行在特權(quán)級,進(jìn)程不能訪問內(nèi)核空間,只能通過系統(tǒng)調(diào)用和內(nèi)核進(jìn)行交互,內(nèi)核會(huì)對進(jìn)程進(jìn)行嚴(yán)格的權(quán)限檢查和參數(shù)檢查,使得系統(tǒng)更加安全。通過虛擬內(nèi)存訪問物理內(nèi)存,每次都需要解析頁表,這大大降低了內(nèi)存訪問的性能,為此CPU的MMU里面加入了TLB用來緩存頁表解析的結(jié)果,這樣由于程序的時(shí)間局部性和空間局部性,能極大的提高內(nèi)存訪問的速度。雖然和直接訪問物理內(nèi)存相比,仍然存在著一些性能損耗,但是損耗已經(jīng)降到很低了。因此虛擬內(nèi)存機(jī)制在系統(tǒng)安全和性能之間達(dá)到了最大的平衡。雖然如此,但是虛擬內(nèi)存機(jī)制也使得計(jì)算機(jī)的內(nèi)存系統(tǒng)變得異常復(fù)雜,給我們的編程帶來了巨大的挑戰(zhàn)。內(nèi)存問題,在很多軟件公司里面,都是一個(gè)非常重要非常讓人頭疼的問題,今天我們從OOM的角度來幫大家提高一點(diǎn)內(nèi)存方面的知識,雖然不能說幫助人們來完全解決內(nèi)存問題,但是也能從一個(gè)側(cè)面來提高大家分析內(nèi)存問題相關(guān)的能力。2.內(nèi)存的分配管理
我們已經(jīng)知道了物理內(nèi)存、虛擬內(nèi)存、用戶空間、內(nèi)核空間之間的區(qū)別,下面我們再來深入的了解一下這方面的知識。系統(tǒng)剛啟動(dòng)的時(shí)候是運(yùn)行在物理內(nèi)存之上的,然后系統(tǒng)建立了一段足夠自己繼續(xù)運(yùn)行的恒等映射的頁表,也就是把物理地址映射到相同地址的虛擬地址上。等到系統(tǒng)再進(jìn)一步初始化之后,就會(huì)建立完整的頁表來映射物理內(nèi)存,并把內(nèi)核映射在虛擬地址空間的高部位,對于32位系統(tǒng)來說是3G之上的內(nèi)存空間,對于64系統(tǒng)來說,是映射到比較接近虛擬地址頂端的地方。內(nèi)核初始化之后就會(huì)啟動(dòng)init進(jìn)程,從而啟動(dòng)整個(gè)用戶空間的所有進(jìn)程。內(nèi)核空間和用戶空間的內(nèi)存管理方式的差別是非常大的,首先內(nèi)核是不會(huì)缺頁也不會(huì)換頁的,不會(huì)缺頁是指內(nèi)核的物理內(nèi)存在啟動(dòng)時(shí)就直接映射好了,使用時(shí)直接分配就行了,分配好虛擬內(nèi)存的同時(shí)物理內(nèi)存也分配好了。不會(huì)換頁是指,當(dāng)系統(tǒng)內(nèi)存不足時(shí)內(nèi)核自身使用的物理內(nèi)存不會(huì)被swap出去。與此相反,用戶空間的內(nèi)存分配是先分配虛擬內(nèi)存,此時(shí)并不會(huì)直接分配物理內(nèi)存,而是延遲到程序運(yùn)行時(shí)訪問到哪里的內(nèi)存,如果這個(gè)內(nèi)存還沒有對應(yīng)的物理內(nèi)存,MMU就會(huì)報(bào)缺頁異常從而陷入內(nèi)核,執(zhí)行內(nèi)核的缺頁異常handler給分配物理內(nèi)存,并建立頁表映射,然后再回到用戶空間剛才的那個(gè)指令處繼續(xù)執(zhí)行。當(dāng)系統(tǒng)內(nèi)存不足時(shí),用戶空間使用的物理內(nèi)存會(huì)被swap到磁盤,從而回收物理內(nèi)存。之后如果進(jìn)程再訪問這段內(nèi)存又會(huì)再發(fā)生缺頁異常從swap處把內(nèi)存內(nèi)容加載回來。3.進(jìn)程的內(nèi)存空間布局
明白了上面這些,我們再來看看進(jìn)程的用戶空間內(nèi)存布局。我們都知道進(jìn)程的內(nèi)存空間是由代碼區(qū)、數(shù)據(jù)區(qū)、堆區(qū)、棧區(qū)組成。我們先來看下面的圖,我們以32位進(jìn)程為例進(jìn)行講解,64位的數(shù)值太大不好畫的,但是原理都是一樣的。
進(jìn)程啟動(dòng)之后的內(nèi)存布局如上圖所示,程序file的代碼段被映射到text區(qū),數(shù)據(jù)段映射到data區(qū),內(nèi)核還會(huì)幫進(jìn)程建立堆內(nèi)存區(qū)映射和棧內(nèi)存區(qū)映射,堆一般緊挨著data區(qū)的末尾往上增長,棧區(qū)在3G下面一點(diǎn)點(diǎn)往下增長。數(shù)據(jù)區(qū)和代碼區(qū)是在進(jìn)程啟動(dòng)時(shí)由內(nèi)核之間分配好的,之后大小就不會(huì)再改變,heap區(qū)是隨著程序運(yùn)行中不斷的malloc/free而增長或者縮小的,stack區(qū)是隨時(shí)程序運(yùn)行的局部變量分配釋放而變化的,局部變量的分配釋放是自動(dòng)的,因此這三個(gè)區(qū)域也分別被叫做靜態(tài)內(nèi)存、動(dòng)態(tài)內(nèi)存、自動(dòng)內(nèi)存。由此我們可以看出,我們不必對靜態(tài)內(nèi)存、自動(dòng)內(nèi)存太操心,我們最應(yīng)該關(guān)系的是動(dòng)態(tài)內(nèi)存。我們可以brk系統(tǒng)調(diào)用擴(kuò)大heap區(qū)域來增加堆內(nèi)存,然后再自己管理使用堆內(nèi)存,但是這樣做顯然很麻煩。因此C庫為我們準(zhǔn)備了相關(guān)的API,malloc、free,來分配和釋放堆內(nèi)存,這樣就方便到了。 C庫里面最早的malloc實(shí)現(xiàn)叫做dlmalloc,在計(jì)算機(jī)早期還是單CPU時(shí)代的時(shí)候非常流行,效率也非常高,但是隨著SMP多CPU時(shí)代的到來,dlmalloc的缺點(diǎn)也越來越明顯,尤其是多線程同時(shí)調(diào)用malloc的時(shí)候,鎖沖突越來越嚴(yán)重,嚴(yán)重影響了性能。后來業(yè)界相繼出現(xiàn)了ptmalloc、jemalloc、scudo等優(yōu)秀的malloc庫。 Ptmalloc是Glibc的默認(rèn)malloc實(shí)現(xiàn),jemalloc庫是首先實(shí)現(xiàn)在FreeBSD的malloc庫,后廣泛應(yīng)用于FireFox、Redis、Netty等眾多產(chǎn)品中,也長期是安卓的默認(rèn)malloc庫實(shí)現(xiàn)。目前安卓已經(jīng)把malloc庫替換為scudo了,據(jù)說scudo在安全和性能方面都很不錯(cuò)。 程序簡單的時(shí)候還好說,但是對于很多產(chǎn)品級的軟件來說,其邏輯結(jié)構(gòu)都非常復(fù)雜,進(jìn)而導(dǎo)致其內(nèi)存管理方面也很復(fù)雜,很容易出現(xiàn)棧溢出、野指針、內(nèi)存泄漏等問題。我們有著很多方法和規(guī)則來規(guī)避這些問題,比如誰申請誰釋放,引用計(jì)數(shù),智能指針等,但是仍然不能完全解決這些問題。尤其是內(nèi)存泄漏,在很多公司里面都是令人頭疼的頑疾,對于內(nèi)存泄漏也存在著很多工具,但是都無法完美的解決這個(gè)問題。我們今天要說的不是內(nèi)存泄漏,而是由于內(nèi)存泄漏或者內(nèi)存使用不合理而導(dǎo)致的OOM問題。
4.內(nèi)存回收基本框架
在講OOM之前,我們先來了解一下內(nèi)核內(nèi)存回收的總體框架。內(nèi)存作為系統(tǒng)最寶貴的資源,總是不夠用的,經(jīng)常需要進(jìn)行回收。內(nèi)存回收可分為兩種方式,同步回收和異步回收,同步回收是在分配內(nèi)存時(shí)發(fā)現(xiàn)內(nèi)存不足直接調(diào)用函數(shù)進(jìn)行回收,異步回收是喚醒專門的回收線程kswapd進(jìn)行回收。我們先看一下它們的總體架構(gòu)圖,然后再一一說明。
同步回收的話是在alloc_pages時(shí)發(fā)現(xiàn)內(nèi)存不足就直接進(jìn)行回收,首先嘗試的是內(nèi)存規(guī)整,也就是內(nèi)存碎片整理,比如說系統(tǒng)當(dāng)前有10個(gè)不連續(xù)的空閑page,但是你要分配兩個(gè)連續(xù)的page,顯然是無法分配的,此時(shí)就要進(jìn)行內(nèi)存規(guī)整,通過移動(dòng)movable page,使空閑page盡量連在一起,這樣能有可能分配出多個(gè)連續(xù)的page了。如果內(nèi)存規(guī)整之后還是無法分配到內(nèi)存,此時(shí)就會(huì)進(jìn)行頁幀回收了。用戶空間的物理內(nèi)存可以分為兩種類型,文件頁和匿名頁,文件頁是text data段對應(yīng)的頁幀,它們都有文件做后備存儲,匿名是棧和堆對應(yīng)的內(nèi)存頁,它們沒有對應(yīng)的文件,一般用swap分區(qū)或者swap文件做它們的后備存儲。系統(tǒng)會(huì)首先考慮干凈的文件頁進(jìn)行回收,因?yàn)榛厥账鼈冎灰苯觼G棄內(nèi)容就可以了,需要的時(shí)候再直接從文件里讀取回來,這樣不會(huì)有數(shù)據(jù)丟失。如果沒有干凈的文件頁或者干凈的文件頁不太多,此時(shí)就要從dirty 文件頁和匿名頁進(jìn)行回收了,因?yàn)樗鼈兌家M(jìn)行IO操作,所以會(huì)非常的慢。如果頁幀回收也回收不到內(nèi)存的話,內(nèi)核只能使出最后一招了,OOM Killer,直接殺進(jìn)程進(jìn)行內(nèi)存回收,雖然這招好像不太文雅,但是也是沒有辦法,因?yàn)椴贿@樣做的話,系統(tǒng)沒有多余的內(nèi)存就沒法繼續(xù)運(yùn)行,系統(tǒng)就會(huì)卡死,用戶就會(huì)重啟系統(tǒng),結(jié)果更糟,所以殺進(jìn)程也是最后的無奈之舉。一般能走到這一步都是因?yàn)檫M(jìn)程有長期或者嚴(yán)重的內(nèi)存泄漏導(dǎo)致的。 異步回收線程kswapd是被周期性的喚醒來執(zhí)行回收任務(wù)的,當(dāng)然同步回收的時(shí)候也會(huì)順便喚醒它來一起回收內(nèi)存。有一點(diǎn)需要注意的是kswapd線程不是per CPU的,而是per node的,是一個(gè)NUMA節(jié)點(diǎn)一個(gè)線程,這是因?yàn)閮?nèi)存的分配是per node不是 per CPU的,大部分內(nèi)存分配都是優(yōu)先從本node分配或者只能從本node分配,因此哪個(gè)node的內(nèi)存不足了就喚醒哪個(gè)node的kswapd線程就行內(nèi)存回收工作。對于家庭電腦和手機(jī)來說都是一個(gè)node,所以一般就只有一個(gè)kswapd線程。Kswapd完成回收工作之后,它會(huì)喚醒kcompactd線程進(jìn)行內(nèi)存規(guī)整,對的,內(nèi)存規(guī)整也可以異步執(zhí)行。
5.OOM基本原理
在講內(nèi)核的OOM Killer之前,我們先來說一下OOM基本概念。OOM,out of memory,就是內(nèi)存用完了耗盡了的意思。OOM分為虛擬內(nèi)存OOM和物理內(nèi)存OOM,兩者是不一樣的。虛擬內(nèi)存OOM發(fā)生在用戶空間,因?yàn)橛脩艨臻g分配的就是虛擬內(nèi)存,不能分配物理內(nèi)存,程序在運(yùn)行的時(shí)候觸發(fā)缺頁異常從而需要分配物理內(nèi)存,內(nèi)核自身在運(yùn)行的時(shí)候也需要分配物理內(nèi)存,如果此時(shí)物理內(nèi)存不足了,就會(huì)發(fā)生物理內(nèi)存OOM。用戶空間虛擬內(nèi)存OOM表現(xiàn)為malloc、mmap等內(nèi)存分配接口返回失敗,錯(cuò)誤碼為ENOMEM。大家也許會(huì)想,虛擬內(nèi)存會(huì)OOM嗎,虛擬內(nèi)存那么大,對于32位進(jìn)程來說就有3G,對于64位進(jìn)程來說至少也得有上百G,應(yīng)有盡有,而且很多教科書上都說的是虛擬內(nèi)存可以隨意分配,不受物理內(nèi)存的限制,事實(shí)上真的是這樣嗎,讓我們來看一看。
5.1、虛擬內(nèi)存OOM
虛擬內(nèi)存我們是不是可以隨意分配,虛擬空間有多大我們就能分配多少?事實(shí)不是這樣的。UNIX世界有個(gè)著名的哲學(xué)原理,提供機(jī)制而不是策略,對于這個(gè)問題,Linux也提供了機(jī)制,我們可以通過 /proc/sys/vm/overcommit_memory文件來選擇策略。我們有三種選擇,我們可以往這個(gè)文件里面寫入0、1、2來選擇不同的策略,這三個(gè)值對應(yīng)的宏是:
-
#define OVERCOMMIT_GUESS 0
-
#define OVERCOMMIT_ALWAYS 1
-
#define OVERCOMMIT_NEVER 2
通過宏名我們也可以大概猜出來是啥意思,下面我們一一解析一下,先從最簡單的開始,OVERCOMMIT_ALWAYS,從名字就可以看出來,只要虛擬內(nèi)存空間還有富余,你malloc多少內(nèi)存就給你多少虛擬內(nèi)存,不管它物理內(nèi)存到底還夠不夠用。OVERCOMMIT_GUESS,名為GUESS,實(shí)在不好guess的,通過看代碼發(fā)現(xiàn),這個(gè)模式允許你最多分配的虛擬內(nèi)存不能超過系統(tǒng)總的物理內(nèi)存(這里說的總物理內(nèi)存是物理內(nèi)存加swap的總和,因?yàn)?/span>swap在一定意義上也相當(dāng)于是增加了物理內(nèi)存),也就是說一個(gè)進(jìn)程分配的總虛擬內(nèi)存可以和系統(tǒng)的總物理內(nèi)存相同,還是夠可以的。OVERCOMMIT_NEVER,這個(gè)就比較苛刻了,它像一位勤儉持家的媽媽,總是只給你勉強(qiáng)夠用的零花錢,從來不多給一分。我們來看一下它的計(jì)算過程,它先計(jì)算一個(gè)基準(zhǔn)值,默認(rèn)等于50%的物理內(nèi)存加上swap大小,然后再減去系統(tǒng)管理保留的內(nèi)存,再減去用戶管理保留的內(nèi)存,如果系統(tǒng)所有已分配的虛擬內(nèi)存大于這個(gè)值,就返回分配失敗。具體情況大家可以去看代碼:
linux-src/mm/util.c:__vm_enough_memory。
我們再來看一個(gè)這個(gè)三個(gè)宏的公共部分OVERCOMMIT,過度承諾,這個(gè)詞想表達(dá)什么含義呢,過程承諾always never guess,我們可以看出來,過程承諾指的是,系統(tǒng)允許分配給你的虛擬內(nèi)存是對你的承諾,后面當(dāng)你具體用訪問內(nèi)存的時(shí)候,是要給你分配物理內(nèi)存來實(shí)現(xiàn)對你的承諾的,那么這個(gè)承諾到底能不能實(shí)現(xiàn)呢,如果不能實(shí)現(xiàn)會(huì)怎么樣呢?
5.2、物理內(nèi)存OOM
出來混遲早是要還的,分配出去的虛擬內(nèi)存遲早是要兌現(xiàn)物理內(nèi)存的。內(nèi)核運(yùn)行時(shí)會(huì)分配物理內(nèi)存,程序運(yùn)行時(shí)也會(huì)通過缺頁異常去分配物理。如果此時(shí)沒有足夠的物理內(nèi)存,內(nèi)核會(huì)通過各種手段來收集物理內(nèi)存,比如內(nèi)存規(guī)整、回收緩存、swap等,如果這些手段都用盡了,還是沒有收集到足夠的物理內(nèi)存,那么就只能使出最后一招了,OOM Killer,通過殺死進(jìn)程來回收內(nèi)存。代碼實(shí)現(xiàn)在linux-src/mm/oom_kill.c:out_of_memory,觸發(fā)點(diǎn)在linux-src/mm/page_alloc.c:__alloc_pages_may_oom,當(dāng)使用各種方法都回收不到不到內(nèi)存時(shí)會(huì)調(diào)用out_of_memory函數(shù)。
out_of_memory函數(shù)的實(shí)現(xiàn)還是有點(diǎn)復(fù)雜,我們把各種檢測代碼和輔助代碼都去除之后,高度簡化之后的函數(shù)如下:
這樣就看邏輯就很簡單了,bool out_of_memory(struct oom_control *oc)
{
select_bad_process(oc);
oom_kill_process(oc, "Out of memory");
}
-
1先選擇一個(gè)要?dú)⑺赖倪M(jìn)程
-
2殺死它,就是這么簡單。
下面我們來分析一下select_bad_process函數(shù)的實(shí)現(xiàn):
static void select_bad_process(struct oom_control *oc)
{
oc->chosen_points = LONG_MIN;
struct task_struct *p;
rcu_read_lock();
for_each_process(p)
if (oom_evaluate_task(p, oc))
break;
rcu_read_unlock();
}
函數(shù)首先把chosen_points初始化為最小的Long值,這個(gè)值是用來比較所有的oom_score值,最后誰的值最大就選中哪個(gè)進(jìn)程。然后函數(shù)已經(jīng)遍歷所有進(jìn)程,計(jì)算其oom_score,并更新chosen_points和被選中的task,有點(diǎn)類似于選擇排序。我們繼續(xù)看oom_evaluate_task函數(shù)是如何評估每個(gè)進(jìn)程的函數(shù)。
static int oom_evaluate_task(struct task_struct *task, void *arg)
{
struct oom_control *oc = arg;
long points;
if (oom_unkillable_task(task))
goto next;
/* p may not have freeable memory in nodemask */
if (!is_memcg_oom(oc) && !oom_cpuset_eligible(task, oc))
goto next;
if (oom_task_origin(task)) {
points = LONG_MAX;
goto select;
}
points = oom_badness(task, oc->totalpages);
if (points == LONG_MIN || points < oc->chosen_points)
goto next;
select:
if (oc->chosen)
put_task_struct(oc->chosen);
get_task_struct(task);
oc->chosen = task;
oc->chosen_points = points;
next:
return 0;
abort:
if (oc->chosen)
put_task_struct(oc->chosen);
oc->chosen = (void *)-1UL;
return 1;
}
此函數(shù)首先會(huì)跳軌所有不適合kill的進(jìn)程,如init進(jìn)程、內(nèi)核線程、OOM_DISABLE進(jìn)程等。然后通過select_bad_process算出此進(jìn)程的得分points 也就是oom_score,并和上一次的勝出進(jìn)程進(jìn)行比較,如果小的會(huì)話就會(huì)goto next 返回,如果大的話就會(huì)更新oc->chosen的task 和 chosen_points也就是目前最高的oom_score。那么oom_badness是如何計(jì)算的呢?
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
long points;
long adj;
if (oom_unkillable_task(p))
return LONG_MIN;
p = find_lock_task_mm(p);
if (!p)
return LONG_MIN;
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN ||
&p->mm->flags) ||
{
task_unlock(p);
return LONG_MIN;
}
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
/ PAGE_SIZE;
task_unlock(p);
adj *= totalpages / 1000;
points += adj;
return points;
}
oom_badness首先把unkiller的進(jìn)程也就是init進(jìn)程內(nèi)核線程直接返回LONG_MIN,這樣他們就不會(huì)被選中而殺死了,這里看好像和前面的檢測冗余了,但是實(shí)際上這個(gè)函數(shù)還被/proc/
可能很多會(huì)覺得這里講的不對,和自己在網(wǎng)上的看到的邏輯不一樣,那是因?yàn)榫W(wǎng)上有很多講oom_score算法的文章都是基于2.6版本的內(nèi)核講的,那個(gè)算法比較復(fù)雜,會(huì)考慮進(jìn)程的nice值,nice值小的,oom_score會(huì)相應(yīng)的降低,也會(huì)考慮進(jìn)程的運(yùn)行時(shí)間,運(yùn)行時(shí)間越長,oom_score值也會(huì)相應(yīng)的降低,因?yàn)楫?dāng)時(shí)認(rèn)為進(jìn)程運(yùn)行的時(shí)間長消耗內(nèi)存多是合理的。但是這個(gè)算法會(huì)讓那些緩慢內(nèi)存泄漏的進(jìn)程逃脫制裁。因此后來這個(gè)算法就改成現(xiàn)在這樣的了,只考慮誰用的內(nèi)存多就殺誰,簡潔高效。
5.3、安卓LMK簡介
除了OOM Killer,Android上還開發(fā)了low memory killer機(jī)制,我們在此也簡單介紹一下。LMK是在系統(tǒng)內(nèi)存較低時(shí)就開始?xì)⑦M(jìn)程,而不是等到內(nèi)存不足時(shí)再殺。LMK復(fù)用了OOMKiller 的 /proc/
6.總結(jié)
Linux內(nèi)存管理是一門龐大的學(xué)問,內(nèi)存回收作為其中的一部分也是十分復(fù)雜的,我們今天給大家大概介紹了內(nèi)核的內(nèi)存回收概覽,并詳細(xì)的介紹了OOM Killer機(jī)制,也算是拋磚引玉讓大家對內(nèi)存回收有個(gè)初步的認(rèn)識。另外如果你在工作中遇到你的進(jìn)程莫名其妙掛掉了,如果你能在內(nèi)核log中找到OOM Killer的log的話(搜索 out of memory 關(guān)鍵字并過濾你的進(jìn)程名),那么你就可以快速的斷定你的是因?yàn)橄到y(tǒng)內(nèi)存不足了,而且你的進(jìn)程占用物理內(nèi)存最多,所以被殺了,此時(shí)你就有很大的理由懷疑自己的進(jìn)程內(nèi)存泄漏了,就可以開始進(jìn)行內(nèi)存相關(guān)問題的排查了。
原文標(biāo)題:Linux OOM 基本原理解析
文章出處:【微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
-
Linux
+關(guān)注
關(guān)注
87文章
11292瀏覽量
209331 -
計(jì)算機(jī)
+關(guān)注
關(guān)注
19文章
7488瀏覽量
87852 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
3019瀏覽量
74005
原文標(biāo)題:Linux OOM 基本原理解析
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論