linux內核mem_cgroup淺析

轉自 http://hi.baidu.com/_kouu/item/56ffc80934780110addc70d1node

memory cgrouplinux

mem_cgroup是cgroup體系中提供的用於memory隔離的功能。
admin能夠建立若干個mem_cgroup,造成一個樹型結構。能夠將進程加入到這些mem_cgroup中。(相似這樣的管理功能都是由cgroup框架自帶的。)算法

爲了實現memory隔離,每一個mem_cgroup主要有兩個維度的限制:
一、res - 物理內存
二、memsw - memory + swap,物理內存 + swap
其中,memsw確定是大於等於memory的。
另外注意,memory控制是針對於組的,而不是單個進程的。(固然,你也能夠一個進程一個組。)框架

每一個維度又有三個指標:
一、usage - 組內進程已經使用的內存
二、soft_limit - 非強制內存上限。usage超過這個上限後,組內進程使用的內存可能會被加快步伐進行回收
三、hard_limit - 強制內存上限。usage不能超過這個上限。若是試圖超過,則會觸發同步的內存回收過程,或者OOM(挑選並殺掉一個進程,以釋放空間。見《linux頁面回收淺析》)
其中,soft_limit和hard_limit是由admin在mem_cgroup的參數中進行配置的(soft_limit確定是要小於hard_limit才能發揮其做用)。而usage則是由內核實時統計該組所使用的內存值。線程

mem_cgroup有hierarchy的概念。若是設置某個組的hierarchy爲真,則其子組的計數會累加到它身上;而在它須要回收page時,也會嘗試對子組進行回收;OOM時也會考慮殺掉子組中的進程;
反過來,若是hierarchy爲假,則子組跟父組就是形同陌路的兩個組了,僅僅在cgroup的層次結構上有父子關係,實則沒有任何聯繫。計數、回收、OOM都是各顧各的。(另外一個影響在於mem_cgroup的刪除,下文會提到。)
一個mem_cgroup建立的時候老是繼承其父組的hierarchy。指針

usage調試

討論mem_cgroup,第一個問題就是:內存的usage如何統計,也就是如何對res/memsw的usage計數進行charge/uncharge。對象

首先,在mem_cgroup的內存統計邏輯中,有一個基本思想:一個page最多隻會被charge一次,而且通常就charge在第一次使用這個page的那個進程所在的mem_cgroup上。
若是有多個mem_cgroup的進程引用同一個page,也只會有一個mem_cgroup爲它埋單。
其次,uncharge每每是跟page的釋放相對應的。這就意味着mem_cgroup爲它再也不使用的page埋單是正常現象。
一個進程引用了某個page,使其所在的mem_cgroup被charge;隨後該進程再也不引用這個page,不過這個page可能由於某種緣由不能被釋放,因此對應的mem_cgroup就不能獲得uncharge。繼承

page進程

那麼對於usage的統計來講,當進程使用到新的page時,怎麼知道這個page有沒有charge過,是否應該charge相應的mem_cgroup呢?
而當進程釋放page時,又須要知道這個page是由哪一個mem_cgroup charge的,以便給它uncharge。
內核的作法是,給page安排一個指向mem_cgroup的指針,非NULL的指針表示這個page已經charge過了,而page釋放時也能夠經過該指針得知應該uncharge那個mem_cgroup。

不過實際上這個指向mem_cgroup的指針並不存在於page結構,而是在對應的page_cgroup結構中。
爲了支持mem_cgroup,內核維護了一組跟page結構一一對應的page_cgroup,其主要成員爲:
mem_cgroup - 指向一個mem_cgroup
lru - 鏈入mem_cgroup的lru(見後面對reclaim的討論)

由此可知,設一個mem_cgroup-A的res計數爲N,那麼必有N個這樣的page,其對應的page_cgroup->mem_cgroup指向mem_cgroup-A(或其子組)。
(理論上是這樣,而實際會有所出入。見後面關於per-CPU的stock的討論。)

swap

而後,關於swap呢?page的內容可能被swap-out到交換區,從而釋放page。
能夠想象,這將致使對應mem_cgroup的res計數獲得uncharge,memsw計數不變。而當這個swap entry被釋放時,memsw計數才能uncharge。
因此,swap entry也應該有一個相似於page_cgroup->mem_cgroup的指針,可以找到爲它埋單的那個mem_cgroup。
相似的,swap entry會有一個與之對應的swap_cgroup結構,其主要成員爲:
id - 對應mem_cgroup在cgroup體系中的id,經過它可以獲得對應的mem_cgroup

由此可知,設一個mem_cgroup-B在cgroup體系中的id爲id-B,其memsw計數爲M。
那麼必有I個這樣的page,其對應的page_cgroup->mem_cgroup指向mem_cgroup-B(或其子組);和J個這樣的swap entry,其對應的swap_cgroup->id爲id-B(或其子組)。且M == I + J。
(理論上是這樣,而實際會有所出入。見後面關於per-CPU的stock的討論。)

相對應的狀況是swap-in,這時會分配新的page,而後從新charge相應的mem_cgroup的res計數。這個要被charge的mem_cgroup怎麼取得呢?其實並非page_cgroup->mem_cgroup,而是swap_cgroup->id對應的mem_cgroup。由於swap-in時的這個page是從新分配出來的,已經不是當年swap-out時的那個page了(新的page裏面會裝上跟原來同樣的內容,可是沒人保證兩個page是同一個物理頁面),因此此時的page_cgroup->mem_cgroup是無心義的。固然,swap-in完成以後,新的page對應的page_cgroup->mem_cgroup會被賦值,指向swap_cgroup->id對應的mem_cgroup,而swap_cgroup則被回收掉。

mm owner

另外,通常咱們會說某某進程使用了某些page。可是實際上,進程和page並非直接聯繫的,而是:進程 => mm => page。也就是說,對物理內存的計數是跟mm相關的。
而mem_cgroup倒是跟進程相關的(cgroup體系是按進程來分組的)。在一個mm上發生內存使用/釋放時,須要找到對應的進程,再找到對應的mem_cgroup,而後charge/uncharge。
但問題是,mm到進程多是一對多的關係,多個進程引用同一個mm(好比vfork產生的子進程、clone產生的線程、等)。如何定義mm應該對應哪一個進程呢?
這裏就用到了mm->owner的概念,每一個mm有其對應的owner進程。fork時父進程將本身的mm copy一份給子進程,因而子進程擁有了自已的mm,它就是這個新mm的owner。
而若是是vfork、clone致使子進程共享父進程的mm時,mm的owner依然是父進程。而相似這樣的子進程則不是任何mm的owner(未來多是,好比evecve之後)。
因而,經過mm->owner就打通了page => mm => 進程 => mem_cgroup的路徑。同時也意味着,對於那些不是任何mm的owner的進程,它們存在於哪一個mem_cgroup實際上是可有可無的。

charge/uncharge

mem_cgroup統計的對象主要是用戶空間使用的內存,分匿名映射(anon page)和文件映射(page cache)兩種類型的page。而這兩種page又存在swap的狀況。
至於其餘的內存,則是由內核空間使用的,不在統計之列。
下面就分別來看看這些page是如何計數的。

page cache

page cache的計數原則是:誰把page請進了page cache,對應的mem_cgroup就爲此而charge。主要有這麼幾種狀況:
一、read/write系統調用;
二、mmap文件以後,在對應區域進行內存讀寫;
三、伴隨1和2兩種狀況產生的預讀;
反之,當page被釋放(通常就在它離開page cache之時),對應的mem_cgroup得以uncharge。主要有這麼幾種狀況:
一、page回收算法將page cache中的page回收;
二、使用direct-io致使對應區域的page cache被釋放;
三、相似/proc/sys/vm/drop_caches、fadvice(DONTDEED)這樣的方式主動清理page cache;
四、相似文件truncate這樣的事件形成對應區域的page cache被釋放;
五、等等;
注意,使用direct-io方式進行read/write是不跟page cache打交道的,因此mem_cgroup也不會所以而charge。(固然,read/write須要一塊buffer,這個是要charge好的。)

NOTICE:若是某個mem_cgroup內的進程訪問了某些文件,從而填充了它們的page cache。那麼這個mem_cgroup就成了冤大頭,一直要等到page被從page cache裏釋放掉,才能uncharge。就算這個進程早已再也不使用這些數據了。而與此同時,其餘mem_cgoup的進程則能夠無償使用這些page。因此,使用相同數據的進程應該儘量劃分到同一個mem_cgroup中。

page cache的swap狀況。這主要涉及tmpfs和shm的邏輯,它們表面上看跟文件映射沒什麼兩樣,每一個文件(或shmid)都有着本身的page cache,而且均可以按照文件的那一套邏輯來操做。
但它們倒是徹底基於內存的,並無外設做爲存儲介質。當須要回收page的時候,只能swap。
swap-out,在page被釋放時uncharge對應mem_cgroup的res計數,memsw計數不變:
a、page在離開page cache後並不會立刻釋放,而是先被移動到swap cache、而後swap到交換區、最後才能釋放;
b、交換區是有大小限制的,若是分配swap entry不成功,則page不能被回收,依然放在page cache中;
c、直到page釋放,才uncharge;
swap-in,在page從新回到page cache時charge:
a、page先被讀入(或預讀)swap cache,此時並無charge操做;
b、隨後,須要swap-in的page會從swap cache移動到page cache,此時對應mem_cgroup的charge;
c、而其餘被預讀進swap cache的page,並不會引發charge,也不會被移動到page cache,直到它真正須要swap-in時;

NOTICE:swap cache與page cache的不一樣。
二者均可能會有預讀,可是swap cache裏面的page只有當真正要使用的時候纔會charge,而page cache只要讀進cache就charge。
由於文件預讀是爲操做它的進程服務的,而swap預讀則未必,交換區裏的數據多是離散的,屬於不一樣的進程。

anon page

anon的計數原則是:誰分配了page,誰就爲此而charge。主要有這麼幾種狀況:
一、寫一個未創建映射的屬於匿名vma的虛擬內存時,page被分配,並創建映射;
二、寫一個待COW的page時,新page被分配,並從新創建映射。這些待COW的page可能產生於以下場景:
a、讀一個未創建映射的屬於匿名vma的虛擬內存時,page不會被分配,並且將相應地址臨時只讀的映射到一個全0的特殊page,等待COW;
b、fork後,父子進程會共享原來的anon page,而且映射被更改成只讀,等待COW;(在COW以前若是對page的引用已經減爲1,則不須要分配新page,也就不須要再charge。)
c、private文件映射的page是以只讀方式映射到page cache中的page,等待COW;(比較有趣的狀況,新的page是anon的,而對應的vma仍是映射到文件的。)
反之,當page被釋放(通常在對它的映射徹底撤銷時),對應的mem_cgroup得以uncharge。主要有這麼幾種狀況:
一、進程munmap掉一段虛擬內存,則對應的已經映射的page會被減引用,可能致使引用減爲0而釋放;(好比主動munmap、exit退出程序、等。)

NOTICE:若是父子進程不在同一個mem_cgroup,則對於fork後那些還沒有COW的anon page來講,極可能是charge在父進程所對應的mem_cgroup上的。父進程就算撤銷了映射,計數依然會算在它頭上(直到page被釋放)。而若是是由於父進程的寫操做引起了COW,則新分配的page和老的page都要算在父進程頭上。
不過子進程默認是跟父進程在同一個mem_cgroup的,除非刻意去移動它。

anon page可能被page回收算法swap掉,也會致使對應mem_cgroup的res計數uncharge。
swap-out,在page的最後一個映射被撤銷時uncharge;
a、swap-out時,anon page會先放放置在swap cache上,而後對每個映射它的進程進行unmap(前提是分配swap entry成功,不然不會swap-out);
b、在最後一個映射被撤銷時進行uncharge;
c、映射撤銷後,這個page可能還會呆在swap cache上,等待寫回交換區(不過寫不寫回已經不影響mem_cgroup的計數了);
swap-in,在page的第一個映射創建時charge;
a、對swap page的缺頁異常,以及由此觸發的預讀,將致使新page被分配,並放到swap cache,再從交換區讀入數據;
b、新page被放到swap cache並不會致使對應mem_cgroup的charge;
c、等這個新page第一次被映射的時候,對應mem_cgroup纔會charge;

NOTICE:對於共享的anon page,charge在第一次映射它的mem_cgroup上。若是swap-out,再被其餘mem_cgroup的進程swap-in,則仍是計在原來的mem_cgroup上。
由於swap-out後,原mem_cgroup的memsw計數是沒有改變的,因此也不能由於swap-in而改變。
anon page被多個進程共享主要是fork()時父子進程共享這一種狀況。

總的來講:
page cache裏的page,charge/uncharge是以page加入/脫離page cache爲準的;
anon page,charge/uncharge是以page的分配/釋放爲準的;
swap的page,charge/uncharge是以page被使用/未使用爲準的;

reclaim

page回收的過程詳見《linux頁面回收淺析》。

page要被回收,首先是要加入到lru。區別於內核中早已經存在的全局lru,每一個mem_cgroup都獨自維護了一組lru。
mem_cgroup下的lru跟全局lru的構成是相似的,對於每一個NUMA node下的每個zone,會有一套lru。而lru又包含active_file、inactive_file、active_anon、inactive_anon、等若干個list。
page被加入到lru的時候,老是會找到本身所歸屬的NUMA node和zone,而後根據自身屬性,加入其中一個lrulist。

上面提到的兩種page都會被加入到全局的lru,若是它歸屬於某個mem_cgroup的話,也會被加入該mem_cgroup的lru。
一個page怎麼加入兩個lru呢?其實加入全局lru的是page,而加入mem_cgroup的lru的則是其對應的page_cgroup(前面已經介紹了page_cgroup有lru這麼個成員)。

lru

總的來講,anon page和page cache都是在分配的時候分加入lru、釋放前脫離lru。
anon page:
一、alloc => add_lru => del_lru => free
二、alloc => add_lru => add_to_swap_cache => del_from_swap_cache => del_lru => free
page cache:
一、alloc => add_lru => add_to_page_cache => del_from_page_cache => del_lru => free
二、alloc => add_lru => add_to_page_cache => add_to_swap_cache => del_from_page_cache => del_from_swap_cache => del_lru => free

而可以被swap的page,包括anon page和屬於tmpfs/shm的page cache,老是加入anon對應的lrulist。其餘的page cache中的page老是加入file對應的lrulist。

reclaim

reclaim有三條路徑:
一、普通的reclaim流程(包括kswapd和內存緊缺時的主動回收)。
這個是視整個系統的內存使用狀況而定的,有無mem_cgroup都同樣。
注意,在普通的reclaim流程中一樣可能回收掉屬於某個mem_cgroup的page,從而致使對該mem_cgroup的uncharge。
二、普通的reclaim流程中額外會嘗試對soft limit超額最多的幾個mem_cgroup進行回收。
這裏就是soft limit主要產生做用的地方。
三、在試圖對mem_cgroup作charge的時候,若是hard_limit超額,會同步地對其進行頁面回收,以便charge成功;

這三個回收過程走的基本上是同一個邏輯:掃描lru,將active鏈表中的一些老page移動到inactive鏈表、對inactive鏈表中的一些老page進行回收。
略有不一樣之處在於:
一、普通的回收流程關心的是全局的lru,然後兩種則是關心特定mem_cgroup的lru;
二、按照lru的組織結構,在嘗試回收一個mem_cgroup時,要先選定mem_cgroup => NUMA node => zone,才能獲得一個lru:
A、mem_cgroup。若是設置了hierarchy,回收邏輯會在mem_cgroup本身及其子孫mem_cgroup間輪循一個進行回收。不然就只能回收本身;
B、NUMA node。hard limit超限時會輪循一個NUMA node;而soft limit超限時則是使用普通的reclaim流程所針對的NUMA node(好比分別有一個kswapd線程來對每個NUMA node進行回收);
C、zone。hard limit超限時會對全部zone嘗試進行回收;而soft limit超限時則是隨普通的reclaim流程對須要reclaim的zone進行回收;
三、hard limit超限時可能存在no-swap邏輯,若是是memsw超限的話,swap-out是無心義的;
四、hard limit超限時一次回收過程可能沒法釋放足夠的page,則繼續進行回收(會輪循到不一樣的子mem_cgroup和NUMA node),最終回收無果還會進入oom邏輯;而soft limit超限時則沒有回收數目的要求;
五、等等;

oom

就像內核在系統內存不足且回收無果的狀況下會進入oom流程同樣,在嘗試charge超過hard limit狀況下,若是同步的回收過程沒法回收足夠的page,也會進入oom流程。
固然,針對特定mem_cgroup的oom,只會挑選屬於該mem_cgroup的進程來kill。

跟全局的oom同樣,mem_cgroup的oom也分紅select_bad_process和oom_kill_process兩個過程:
一、select_bad_process找出該mem_cgroup下最該被kill的進程(若是mem_cgroup設置了hierarchy,也會考慮子mem_cgroup下的進程);
二、oom_kill_process殺掉選中的進程及與其共用mm的進程(殺進程的目的是釋放內存,因此固然要把mm的全部引用都幹掉);

其中仍是有很多細節的:
一、select_bad_process認爲誰最該死?
select_bad_process會給mem_cgroup(或及其子mem_cgroup)下的每一個進程打一個分,得分最高者被選中。評分因素每一個版本不盡相同,主要會考慮如下因素:
a、進程擁有page和swap entry越多,分得越高;
b、能夠經過/proc/$pid/oom_score_adj進行一些分值干預;
c、擁有CAP_SYS_ADMIN的root進程分值會被調低;
不過我以爲既然是在mem_cgroup中,進程所在的mem_cgroup超出其soft_limit的比例也能夠做爲一個評分因素。YY一下:
d、若是進程所屬的mem_cgroup的soft_limit超限,分值會按超限額增長必定比例的分值;

二、oom時機
oom是在同步的reclaim流程沒法回收足夠的page時觸發的。可是reclaim流程沒法繼續回收,其實並不表明絕對的不可回收。
好比active的page、裝有可執行代碼的page、等都是儘可能不要去回收的。
由於在一個上下文進行reclaim的時候,其餘的上下文還各自在幹其餘的事情,無時不涉及內存的使用。
那麼,若是你把能回收的page都回收了,隨着其餘上下文的運行又會把不少page恢復回來。其結果極可能最終仍是沒能回收到空間,卻徒增了換入換出的開銷。
因此,雖然說oom是在內存回收無果時觸發的,卻也並不是徹底不能再回收。至於其中的「度」,也只能靠調試和經驗來把握了。

三、oom過程同步
oom過程會向選中的進程發送SIGKILL進程。可是距離進程處理信號、釋放空間,仍是須要經歷必定時間的。
若是系統負載較高,則這段時間內極可能有其餘上下文也須要卻得不到page,而觸發新的oom。那麼若是大量oom在短期內爆發,可能會大面積殺死系統中的進程,帶來一場浩劫。
因此oom過程須要同步:在給選中的進程發送SIGKILL後,會設置其TIF_MEMDIE標記。而在select_bad_process的過程當中若是發現記有TIF_MEMDIE的進程,則終止當前的oom過程,並等待上一個oom過程結束。
這樣作能夠避免oom時大面積的kill進程,可是目前並無保證每次oom只會kill一個進程(假設kill的這個進程已經可以釋放足夠的空間)。
由於在一個mem_cgroup下觸發oom時,應該選擇該mem_cgroup下的進程。而一個進程是否屬於這個mem_cgroup,看的是mm->owner是否屬於這個mem_cgroup。
而在進程退出時,會先將task->mm置爲NULL,再mmput(mm)釋放掉引用計數,從而致使內存空間被釋放(若是引用計數減爲0的話)。
因此,只要task->mm被置爲NULL(內存即將開始釋放),就沒人認得它是屬於哪一個mem_cgroup的了,針對那個mem_cgroup的新的oom過程就能夠開始。

others

config change

關於配置更改,mem_cgroup還有不少麻煩的事情須要處理,主要是涉及到mem_cgroup參數的調整以及進程的遷移:

一、hierarchy參數的調整
a、只有當父組的hierarchy爲假時才能設置;
這就規定是繼承關係的斷代是不容許的。貌似實在很差定義斷代了的繼承關係該如何來處理。
b、只有當mem_cgroup沒有子組只才能設置;
這個規定省去了不少麻煩。不然能夠想象,hierarchy調整以後,整棵mem_cgroup子樹上的計數都須要同步地進行調整。

二、進程在mem_cgroup之間移動
按理說,移動進程也是很麻煩的事情。對於進程所佔有的page將在原來的mem_cgroup上uncharge,並在新的mem_cgroup上charge。不過這個邏輯默認是禁止的,也就是說,進程在mem_cgroup間移動,不會觸發charge/uncharge。
也能夠設置mem_cgroup的move_charge_at_immigrate參數來支持進程移動時的charge/uncharge行爲。move_charge_at_immigrate是一個bitmap,bit-0表明anon和swap的行爲、bit-1表明file的行爲。
那麼如何進行計數遷移呢?關鍵的問題是,移動的這個進程應該被認爲帶走了哪些page?注意,page的計數是跟mem_cgroup關聯的,而跟進程沒有直接關係。因此要判斷一個進程應該帶走哪些page,只能反過來,從進程的頁表出發,看看它引用了哪些page(那麼固然,若是沒有mmu,也就不能支持)。另外,固然,須要計數遷移的page,其對應的page_cgroup->mem_cgroup必定是指向源mem_cgroup的。而遷移所須要作的事情就是charge目標mem_cgroup、uncharge源mem_cgroup、再修改page_cgroup->mem_cgroup指向目標mem_cgroup。具體哪些page應該發生計數遷移,大體的規則以下:
a、頁表有引用:若是是映射數目爲1的anon page,或是page cache,則計數遷移;
b、頁表指向swap:若是是swap的引用數目爲1,則計數遷移;
c、頁表項爲空:查看vma映射的文件位置上是否有page cache,有則計數遷移;
總的來講,判斷條件比較暴力,page cache只要被該進程引用,則遷移;而anon和swap則在被且僅被該進程映射的狀況下,才遷移。

三、mem_cgroup的刪除
mem_cgroup可以被刪除,有兩個前提:
a、mem_cgroup下沒有進程;
b、mem_cgroup沒有子組;
刪除時,屬於該mem_cgroup的計數將被增長到其父組上、lru裏面的page也會移動到父組的lru。(無論有沒有設置hierarchy。)
既然mem_cgroup已經沒有了進程,爲何還有計數呢?由於計數是基於mem_cgroup的,進程的退出並不意味着必定會uncharge全部的計數(它有不少當冤大頭的機會)。
若是父組設置了hierarchy,則實際上並不會增長其計數(由於子組的計數已經在它頭上charge過了)。
不然,父組charge,可能致使hard limit超限。這時可能觸發同步的reclaim,可是並不會觸發oom。而若是父組charge失敗,則對子組的rmdir操做將返回-EBUSY。
若是但願乾淨地刪掉一個子組,而避免將計數charge到父組上,則能夠經過echo 0 > memory.force_empty將該組的計數清空。force_empty的前提也是mem_cgroup下沒有進程也沒有子組。force_empty將試圖回收mem_cgroup下全部的page,若是有些page未能回收,則仍是會將其charge到父組上。

stock cache

並不是對於每一個page的charge/uncharge都直接跟mem_cgroup的計數打交道,這樣的話多個CPU可能帶來很多的競爭。 解決辦法是加一個per-CPU的cache,即每一個CPU在須要charge的時候,先charge一個較大的數目(如32),則以後的charge操做就可能直接在本地完成。 這個cache就是memcg_stock_pcp,其主要成員有:一個指向mem_cgroup的指針和一個nr_pages計數。 也就是說,它只cache一個mem_cgroup的計數,若是下一次須要charge的mem_cgroup跟cache中的不一樣,則會將cache替換掉,而cache的計數也會隨之uncharge。只cache一個mem_cgroup也已經足夠了,由於同一個進程幾乎老是跟一個mm打交道的,從而也只會影響到一個mem_cgroup的計數。 由於有這個cache的存在,有時候嘗試charge超過hard limit限制可能並非真正的超限,因此在進行同步的reclaim以前,會先將cache清空。

相關文章
相關標籤/搜索