Cgroup-memory子系統分析(1)

1      概述

1.1      應用背景

Cgroup的memory子系統,即memory cgroup(本文以下簡稱memcg),提供了對系統中一組進程的內存行爲的管理,從而對整個系統中對內存有不用需求的進程或應用程序區分管理,實現更有效的資源利用和隔離。

在實際業務場景中,爲了防止一些應用程序對資源的濫用(可能因爲應用本身的bug,如內存泄露),導致對同一主機上其他應用造成影響,我們往往希望可以控制應用程序的內存使用量,這是memcg提供的主要功能之一,當然它還可以做的更多。

Memcg的應用場景,往往來自一些虛擬化的業務需求,所以memcg往往作爲cgroup的一個子系統與容器方案一起應用。在容器方案中,與一般的虛擬化方案不同,memcg在管理內存時,並不會在物理內存上對每個容器做區分,也就是說所有的容器使用的是同一個物理內存(有一種例外情況,如果存在多個內存節點,則可以通過cgroup中的cpuset子系統將不同的內存節點應用到不同的容器中)。對於共用的物理內存,memcg也不會對不同的容器做物理頁面的預分配,也就是說同一個內存page,可能會被容器A使用,也可能被容器B使用。

所以memcg應用在容器方案中,雖然沒有實現真正意義上的內存虛擬化,但是通過內核級的內存管理,依然可以實現某種意義上的虛擬化的內存管理,而且是真正的輕量級的。

(注:本文的功能及代碼基於Linux 3.4)

1.2      功能簡介

Memcg的主要應用場景有:

a.     隔離一個或一組應用程序的內存使用

對於內存飢渴型的應用程序,我們可以通過memcg將其可用內存限定在一定的數量以內,實現與其他應用程序內存使用上的隔離。

b.    創建一個有內存使用限制的控制組

比如在啓動的時候就設置mem=XXXX。

c.     在虛擬化方案中,控制虛擬機的內存大小

比如可應用在LXC的容器方案中。

d.    確保應用的內存使用量

比如在錄製CD/DVD時,通過限制系統中其他應用可以使用的內存大小,可以保證錄製CD/DVD的進程始終有足夠的內存使用,以避免因爲內存不足導致錄製失敗。

e.     其他

各種通過memcg提供的特性可應用到的場景。

 

爲了支撐以上場景,這裏也簡單列舉一下memcg可以提供的功能特性:

a.     統計anonymous pages, file caches, swap caches的使用並限制它們的使用;

b.    所有page都鏈接在per-memcg的LRU鏈表中,將不再存在global的LRU;

c.     可以選擇統計和限制memory+swap的內存;

d.    對hierarchical的支持;

e.     Soft limit;

f.     可以選擇在移動一個進程的時候,同時移動對該進程的page統計計數;

g.     內存使用量的閾值超限通知機制;

h.    可以選擇關閉oom-killer,並支持oom的通知機制;

i.      Root cgroup不存在任何限制;

2      總體設計

2.1      Memcg

Memcg在cgroup體系中提供memory隔離的功能,它跟cgroup中其他子系統一樣可以由admin創建,形成一個樹形結構,可以將進程加入到這些memcg中管理。

Memcg設計的核心是一個叫做res_counter的結構體,該結構體跟蹤記錄當前的內存使用和與該memcg關聯的一組進程的內存使用限制值,每個memcg都有一個與之相關的res_counter結構。

 

而mem_cgroup結構體中通常會有兩個res_counter結構,這是因爲爲了實現memory隔離,每個memcg主要要有兩個維度的限制:

a.     Res – 物理內存;

b.    Memsw – memory + swap,即物理內存 + swap內存;

其中,memsw肯定是大於等於memory的。

 

另外,從res_counter結構中可以看出,每個維度又有三個指標:

a.     Usage – 組內進程已經使用的內存;

b.    Soft_limit – 軟限制,非強制內存上限,usage超過這個上限後,組內進程使用的內存可能會加快步伐進行回收;

c.     Hard_limit – 硬限制,強制內存上限,usage不能超過這個上限,如果試圖超過,則會觸發同步的內存回收,或者觸發OOM(詳見OOM章節)。

其中,soft_limit和hard_limit都是admin在memcg的配置文件中進行配置的(soft_limit必須要小於hard_limit才能發揮作用),hard_limit是真正的內存限制,soft_limit只是爲了實現更好的內存使用效果而做的輔助,而usage則是內核實時統計該組進程內存的使用值。

 

對於統計功能的實現,可以用一個簡單的圖表示:

概括爲三點:

a.     統計針對每一個cgroup進行;

b.    每個cgroup中的進程,它的mm_struct知道自己屬於哪個cgroup;

c.     每個page對應一個page_cgroup,而page_cgroup知道自己屬於哪個memcg;

 

而整個統計和限制的實現過程可簡單描述爲:

某進程在需要統計的地方調用mem_cgroup_charge()來進行必要的結構體設置(增加計數等),判斷增加計數後進程所在的cgroup的內存使用是否超過限制,如果超過了,則觸發reclaim機制進行內存回收,如果回收後依然超過限制,則觸發oom或阻塞機制等待;如果增加計數後沒有超過限制,則更新相應page對應的page_cgroup,完成統計計數的修改,並將相應的page放到對應的LRU中進行管理。

該過程中涉及的各種實現細節,將在後面的章節進行分解描述。

2.2      Page & swap

討論memcg,必然涉及對內存的管理和統計,而這些操作都是針對page來做的,所以一些針對page的設計需要先理清楚。

首先,在memcg的內存統計邏輯中,有幾個基本思想:

a.     一個page最多隻會被charge一次,並且一般就charge在第一次使用這個page的那個進程所在的memcg上。

b.    如果有多個memcg的進程引用了同一個page,該page也只會被統計在一個memcg中。

c.     Unchage往往跟page的釋放相對應,所以可能存在某個進程不再使用某個page,但是對該page的統計還是記錄在進程所在的memcg中,因爲可能還有其他memcg中的進程在使用這個page,只要page無法釋放,memcg就無法unchage。

 

那麼對於usage的統計來說,當進程使用到新的page時,怎麼知道這個page有沒有chage過,是否該chage相應的memcg呢?而當進程釋放page時,又需要知道這個page是由哪個memcg chage的,以便給它uncharge呢?

內核的做法是,給page安排一個指向memcg的指針,非NULL的指針表示這個page已經charge過了,而page釋放時也可以通過該指針得知應該uncharge哪個memcg。不過實際上這個指向memcg的指針並不存在於page結構中,而是在對應的page_cgroup結構中:

Page_cgroup:每個page對應一個,跟page結構體一樣,在系統啓動的時候或內存熱插入的時候分配,在內存熱拔除的時候釋放。

 

然後,關於swap呢?page的內容可能被swap-out到交換區,從而釋放page。

可以想象,這將導致對應memcg的res計數得到uncharge,memsw計數保持不變。而當這個swap entry被釋放時,memsw計數才能uncharge。

所以,swap entry也應該有一個類似於page_cgroup->mem_cgroup的指針,能夠找到它是被統計到哪個memcg中的。

類似的,swap entry會有一個與之對應的swap_cgroup結構:

Swap_cgroup:每個對應一個swp_entry,在swapon()的時候分配,swapoff()的時候釋放。

其中的id是對應memcg在cgroup體系中的id,通過它可以找到對應的memcg。

而在swap-in的時候,這時會分配新的page,然後重新charge相應的memcg的res計數。這個要被charge的memcg怎麼取得呢?其實並不是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則被回收掉。

2.3      Hierarchy

2.3.1      簡介

Hierarchy是cgroup對子系統層級支持提出的概念,一個cgroup文件系統掛載點形成一個hierarchy,在該掛載點下創建的cgroup及其子cgroup都屬於同一個hierarchy,該hierarchy可以附加多個子系統。對於hierarchy,有幾個基本規則:

a.     規則1:一個或多個Subsystem只能附加到一個hierarchy中;

如下圖所示:CPUSubsystem無法附加到2個不同hierarchy。

b.    規則2:一個hierarchy可以附加一個或多個subsystem;

如下圖所示,cpu和memory subsystem可以同時附加到hierarchy中。

c.     規則3:在系統創建新hierarchy時,系統中所有任務都是這個hierarchy的默認cgroup的初始成員。對於單一hierarchy來說,系統中每個任務都可以是該hierarchy的唯一一個cgroup的成員。單一任務可以在多個cgroup中,每個cgroup都在不同的hierarchy,如果任務成爲同一hierarchy中第二個cgroup的成員,將會從該層級中第一個cgroup中刪除,一個任務永遠不會同時位於同一hierarchy的不同cgroup中。

如下圖所示:進程httpd可以在hierarchyA和hierarchyB中同時存在,但是不能在hierarchyA中存在多個。

2.3.2      Memcg的hierarchy

對於memcg,作爲一個cgroup的subsystem,它遵循hierarchy的所有規則,另外,對於hierarchy中cgroup的層級對memcg管理規則的影響,主要分兩方面:

1、 如果不啓用hierarchy,即mem_cgroup->use_hierarchy =false,則所有的memcg之間都是互相獨立,互不影響的,即使是父子cgroup之間,也跟兩個單獨創建的cgroup一樣。

2、 如果啓用hierarchy,即mem_cgroup->use_hierarchy =true,則memcg的統計需要考慮hierarchy中的層級關係,其影響因素主要有:

a.     Charge/uncharge

如果子cgroup中charge/uncharge了一個page,則其父cgroup和所有祖先cgroup都要charge/uncharge該page。

b.    Reclaim

因爲父cgroup的統計中包含了所有子cgroup中charge的page,所以在回收父cgroup中使用的內存時,也可以回收子cgroup中進程使用的內存。

c.     Oom

因爲父cgroup的統計中包含了所有子cgroup中charge的page,所以如果父cgroup需要出發oom,則oom可以考慮殺死子cgroup中的進程,達到釋放內存的效果。

3      技術點分解

3.1      Charge/Uncharge

3.1.1      Page cache & anon page

1.   Page cache

上面說了memcg管理和統計內存,都是以page爲最小單位的,那麼內核中使用page的地方那麼多,我們怎麼去一一統計呢?

實際上,對於用戶態應用程序使用的內存,在內核中一般只需要統計兩類內存:

a.     Page cache

b.    Anon page

(另外,memcg還會統計kmem,即kernel memory,這個後面單獨講)

Page cache是內核對磁盤文件內容在內存中做的緩存,比如在我們做read操作時,會先去讀page cache中的內容,如果不存在,再通過IO去磁盤上讀,並把讀到的內容寫到page cache中;相應的,在write操作時(非direct IO),我們也是先操作page cache中的內容,再異步的從page cache中刷回磁盤。

Page cache的計數原則是:誰把page請進了page cache,對應的memcg就爲此而charge。主要有這麼幾種情況:

1、read/write系統調用;

2、mmap做文件映射之後,在對應區域進行內存讀寫;

3、伴隨1和2兩種情況產生的預讀;

反之,當page被釋放(一般就在它離開pagecache之時),對應的memcg得以uncharge。主要有這麼幾種情況:

1、page回收算法將page cache中的page回收;

2、使用direct-io導致對應區域的page cache被釋放;

3、 類似/proc/sys/vm/drop_caches、fadvice(DONTDEED)這樣的方式主動清理page cache;

4、類似文件truncate這樣的事件造成對應區域的page cache被釋放;

5、等等;

注意,使用direct-io方式進行read/write是不跟page cache打交道的,所以memcg也不會因此而charge。(當然,read/write需要一塊buffer,這個是要charge好的。)

 

對於page cache的swap情況。往往有種錯誤的認識是:pagecache是對磁盤文件的緩存,它只會被寫回到磁盤,而不會被swap出去。但這裏存在一些例外,這主要涉及tmpfs和shm,它們表面上看跟普通文件映射沒什麼兩樣,每個文件(或shmid)都有着自己的page cache,並且都可以按照文件的那一套邏輯來操作。但它們卻是完全基於內存的,並沒有外設作爲存儲介質,所以當需要回收page的時候,只能swap。

swap-out時,在page被釋放時uncharge對應memcg的res計數,memsw計數不變:

a.     Page在離開page cache後並不會馬上釋放,而是先被移動到swap cache、然後swap到交換區、最後才能釋放;

b.    交換區是有大小限制的,如果分配swap entry不成功,則page不能被回收,依然放在pagecache中;

c.     直到page被釋放,才uncharge;

swap-in時,在page重新回到page cache時charge:

a.     Page先被讀入(或預讀)swap cache,此時並沒有charge操作;

b.    隨後,需要swap-in的page會從swap cache移動到page cache,此時對應的memcg會做charge;

c.     而其他被預讀進swap cache的page,並不會引起charge,也不會被移動到page cache,直到它真正需要swap-in時;

 

NOTICE:swap cache與page cache的不同。

兩者都可能會有預讀,但是swap cache裏面的page只有當真正要使用的時候纔會charge,而page cache只要讀進cache就charge。因爲文件預讀是爲操作它的進程服務的,而swap預讀則未必,交換區裏的數據可能是離散的,屬於不同的進程。

 

2.   Anon page

另一類重要的內存時anon page,即匿名頁,簡單的說就是磁盤上沒有後備文件的內存,比如用戶態程序通過malloc申請的內存頁都是anonpage。另外,查看內存信息時,常看到的res,即常駐內存,指的也是這類anonpage。

anon的計數原則是:誰分配了page,誰就爲此而charge。主要有這麼幾種情況:

1、寫一個未建立映射的屬於匿名vma的虛擬內存時,page被分配,並建立映射;

2、寫一個待COW的page時,新page被分配,並重新建立映射。

這些待COW的page可能產生於如下場景:

a.     讀一個未建立映射的屬於匿名vma的虛擬內存時,此時的缺頁異常不會分配page,而是將相應地址臨時只讀的映射到一個全0的特殊page,等待COW,這是對讀操作的優化,只有寫操作纔會分配內存;

b.    fork後,父子進程會共享原來的anon page,並且映射被更改爲只讀,等待COW;

c.     private文件映射的page是以只讀方式映射到page cache中的page,等待COW;(比較有趣的情況,新的page是anon的,而對應的vma還是映射到文件的。)

反之,當page被釋放(一般在對它的映射完全撤銷時),對應的memcg得以uncharge。主要有這麼幾種情況:

1、進程munmap掉一段虛擬內存,則對應的已經映射的page會被減引用,可能導致引用減爲0而釋放;(比如主動munmap、exit退出程序、等。)

 

NOTICE:如果父子進程不在同一個memcg,則對於fork後那些尚未COW的anon page來說,很可能是charge在父進程所對應的memcg上的。父進程就算撤銷了映射,計數依然會算在它頭上(直到page被釋放)。而如果是因爲父進程的寫操作引發了COW,則新分配的page和老的page都要算在父進程頭上。不過子進程默認是跟父進程在同一個memcg的,除非刻意去移動它。

 

Anon page可能被page回收算法swap掉,也會導致對應memcg的res計數uncharge。

swap-out時,在page的最後一個映射被撤銷時uncharge:

a.     swap-out時,anon page會先放放置在swap cache上,然後對每一個映射它的進程進行unmap(前提是分配swap entry成功,否則不會swap-out);

b.    在最後一個映射被撤銷時進行uncharge;

c.     映射撤銷後,這個page可能還會呆在swapcache上,等待寫回交換區(不過寫不寫回已經不影響memcg的計數了);

swap-in時,在page的第一個映射建立時charge:

a.     對swap page的缺頁異常,以及由此觸發的預讀,將導致新page被分配,並放到swapcache,再從交換區讀入數據;

b.    新page被放到swap cache並不會導致對應memcg的charge;

c.     等這個新page第一次被映射的時候,對應memcg纔會charge;

 

NOTICE:對於共享的anon page,charge在第一次映射它的memcg上。如果swap-out,再被其他memcg的進程swap-in,則還是計在原來的memcg上。因爲swap-out後,原memcg的memsw計數是沒有改變的,所以也不能因爲swap-in而改變。

anon page被多個進程共享主要是fork()時父子進程共享這一種情況。

 

總的來說:

page cache裏的page,charge/uncharge是以page加入/脫離page cache爲準的;

anon page,charge/uncharge是以page的分配/釋放爲準的;

swap page,charge/uncharge是以page被使用/未使用爲準的。

 

 

3.1.2      Charge

一個page/swp_entry可能在以下情況下被charge:

a.     mem_cgroup_newpage_charge()

在發生新頁面的缺頁中斷或Copy-On-Write的時候。

b.    mem_cgroup_try_charge_swapin()

在do_swap_page()函數(在swap entry上發生缺頁異常)和swapoff系統調用中調用(swapoff->try_to_unuse()->unuse_mm()->unuse_vma()->unuse_pud_range()->unuse_pmd_range()->unuse_pte_range()->unuse_pte()->mem_cgroup_try_charge_swapin()),且該函數會伴隨着charge-commit-cancel機制。

c.     mem_cgroup_cache_charge()

在add_to_page_cache()函數中被調用,即加入page cache的時候。另外,還有shmem在swapin的時候會調用,一般來說,page cache是不會swap的,但是shmem卻是例外,它既有普通文件系統的特性,可以存放在page cache中,又跟anon page一樣只存在在內存中,所以reclaim的時候只能swap出去。

d.    mem_cgroup_prepare_migration()

在遷移前被調用,該函數會伴隨着charge-commit-cancel機制。

 

跟上一節描述的類似,charge主要在page cache和anon page的幾個使用場景發生,另外,還有一個遷移的情況。

3.1.3      Uncharge

一個page/swp_entry可能在以下情況下被uncharge:

a.     mem_cgroup_uncharge_page()

在anon page被完全unmap的時候。比如mapcount變成0。如果page是在swap cache中,則uncharge操作推遲到mem_cgroup_uncharge_swapcache()中執行。

b.    mem_cgroup_uncharge_cache_page()

當page從page cache的radix-tree上刪除時被調用,page cache小節已經說明了page從page cache中刪除的幾種可能情況。

c.     mem_cgroup_uncharge_swapcache()

在swapcache_free()函數中被調用。

d.    mem_cgroup_uncharge_swap()

當swp_entry的引用計數減爲0時被調用(在swap_entry_free()函數中調用),主要清除swap cgroup中的id記錄,同時修改memsw的計數。

e.     mem_cgroup_end_migration(old,new)

如果遷移成功,則old會被uncharge,對new的charge會被commit;如果遷移失敗,則對old的charge會被commit。

3.1.4      Charge-commit-cancel

在很多時候,我們在charge的時候不知道是否可以charge成功,或者charge是否是合法的,比如charge可能導致超過hard limit,可能發生內存不足,可能因爲競爭而charge失敗等。爲了處理這些情況,內核對charge引入了charge-commit-cancel機制,提供了三類函數:

Mem_cgroup_try_charge_XXX

Mem_cgroup_commit_charge_XXX

Mem_cgroup_cancel_charge_XXX

它們有時候被單獨使用,有時候封裝起來使用。

在try_charge的時候,不會設置flag來說明“這個page已經被charge過了”,而只是做 usage += PAGE_SIZE。

在commit的時候,函數會檢查這個page是否應該或可以被charge,如果可以charge,就補充設置一下flag說明該page已經被charge過了,否則,就取消charge(usage -= PAGE_SIZE).

在cancel的時候,只是簡單的做usage -= PAGE_SIZE。

3.2      Reclaim

3.2.1      概述

一旦統計發現內存使用超過限額,則會觸發memcg的內存回收機制。值得說明的是,經過多次代碼改動和memcg的推動,現在memcg的內存回收代碼已經完全與內核本身的內存回收代碼融合了(儘管還不夠完美),但這也給我們的代碼分析增加了難度。Memcg中大量使用的各種巧妙但複雜的數據結構是我們理清整個memcg內存回收機制的主要障礙,下面我們將對他們一一進行分析。

3.2.2      PFRA介紹

在分析memcg的內存回收之前,有必要對內核本身的內存回收機制PRFA(page frame reclaiming algorithm)有個簡單的理解。

首先看看PFRA在整個內核對內存的使用中的位置:

可見PFRA幾乎會覆蓋所有內核中可能使用的內存。那麼PFRA又包括哪些操作以及他們執行的時機又是如何呢?

這裏借用《深入理解Linux內核(第三版)》中的一張插圖,雖然現在的代碼實現跟圖中所示已有一些區別,但基本的框架和實現思路沒有改變。即頁框回收算法的執行有三種基本情形:

a.     內存緊缺回收

b.    睡眠回收

c.     週期回收

我們將會從其中最主要的幾個函數出發討論現有的memory cgroup及其涉及和影響到的內存回收機制。

3.2.3      Memcg的reclaim流程

在memory cgroup中,整個reclaim的流程如下:

即在三種情況下觸發reclaim:

a.     Do_charge時發現超過limit限制。

b.    修改limit設置。

c.     修改memsw_limit設置。

其中在代碼流程中,從函數do_try_to_free_pages開始,就是內核全局的reclaim代碼了,不同的是,在上圖中,虛線灰底的路徑是只有在全局reclaim的時候纔會走到,其他的部分則是memory cgroup內存回收的時候走的路徑。(在使用memory cgroup之後,reclaim中會經常看到兩個概念:global reclaim 和 target reclaim,即全局回收和局部/目標回收,前者的對象是所有的內存,後者是針對單個cgroup,但全局回收也是以單個memcg爲單位的)

從函數名可以看出,在全局回收時,函數名也有mem_cgroup_之類的字段,說明memory cgroup的實現已經完全嵌入到內核的內存回收代碼中,下面會分析memory cgroup是如何對全局內存回收產生影響的。

3.2.4      Soft_limit

如果你之前已經對內核的PFRA機制有一定的瞭解,那麼不得不提到soft_limit,這是memory cgroup對內核reclaim機制帶來影響的主要技術點。

Soft_limit是memory cgroup的一個控制字段,可以通過修改 /cgroup/容器名/memory.soft_limit_in_bytes 字段來設置該值(或在啓動容器時設置在配置文件中),相對於hard_limit,soft_limit一般會小於hard_limit,超過soft_limit後不會立刻觸發reclaim,而是作爲reclaim的依據之一,比如在回收內存時對超過soft_limit較多的cgroup中的內存優先回收,且回收至soft_limit之下。

1.   Lruvec

那麼內核是如何實現並維護soft_limit的?memory cgroup又是如何結合到內核的內存回收中的?我們首先看一下內存回收中最終調用的函數shrink_lruvec:

其實說shrink_lruvec是“最終調用的函數”是不準確的,在頁面回收中,shrink_lruvec後面還有很多事情要做,還要調用shrink_list – shrink_inactive_list – shrink_page_list,然後調用try_to_unmap解除映射或pageout將髒頁寫回磁盤,然後調free_hot_cold_page_list回收頁面內存。這裏還省略了很多細節,不過這些太過底層,是內核內存回收的底層固有機制,跟mem_cgroup無關,故不在本文的討論範圍內,在這裏我們只需要關心如何獲取到lruvec,然後調用shrink_lruvec即可完成內存回收。

看看lruvec的定義:

就是一個lru鏈表的集合,可能包括active或inactive等。

再看我們如何獲取這個lruvec的代碼:

如果在內核編譯選項中打開了CONFIG_MEMCG,則使用第一個函數實現,否則使用第二個,所以一旦我們在內核編譯中開啓了CONFIG_MEMCG(應該默認就是開啓的),則不管我們是否掛載cgroup文件系統來使用cgroup,內核代碼中走的都是有mem_cgroup的流程。另外,我們常常看到的“lru鏈表”的概念,在加入mem_cgroup後也發生了一些變化,page結構體中只有一個lru字段是用來將該page加入lru鏈表中的,所以一旦我們使用了CONFIG_MEMCG,則所有的page都會掛在某個mem_cgroup的某個mem_cgroup_per_zone對應的lru鏈表上,也就不存在全局lru鏈表的概念了。

我們只關心開啓CONFIG_MEMCG選項的情況,再看mem_cgroup_zoneinfo函數:

即每個mem_cgroup中有個類型爲mem_cgroup_lru_info的成員info,通過它以及node_id和zone_id,即可找到對應的mem_cgroup_per_zone,從而得到lruvec。其中該info成員的原型爲:

到此我們已經涉及到了幾個相關的數據結構,mem_cgroup_per_node、mem_cgroup_per_zone,看看他們的定義:

可見每個mem_cgroup_per_zone對應一個lruvec,也對應一個mem_cgroup,同時一個mem_cgroup_per_node對應多個mem_cgroup_per_zone。這個mem_cgroup_per_zone非常重要,在內核相關代碼中到處都能看到它的身影,簡單的說,一個mem_cgroup_per_zone維護了一個mem_cgroup在某個zone上使用的內存,它跟mem_cgroup是多對一的關係。

實際上,在內核開啓MEMCG後,對內存回收的單位lruvec就來自於各個mem_cgroup了(實際上是mem_cgroup對應的某個mem_cgroup_per_zone),因爲內存回收的接口,其參數都是zone,而對於物理內存,首先分內存節點,即node,然後每個內存節點上有多個zone,而一個zone上的內存可能被多個mem_cgroup使用,但反過來,一個mem_cgroup可能使用多個zone,甚至多個node上的內存。所以mem_cgroup結構體中的info成員可以通過node_id和zone_id找到對應的mem_cgroup_per_zone。

2.   Soft_limit_tree

重新回到soft_limit的話題,從reclaim的流程圖中可知,在函數shrink_zones中,在global_reclaim的時候,會調用mem_cgroup_soft_limit_reclaim函數。該函數的目的是回收該zone上超過soft_limit最多的mem_cgroup在該zone上mem_cgroup_per_zone對應的lru鏈表。那麼它是如何來找到該mem_cgroup_per_zone的呢?這就是mem_cgroup機制新加入的一個數據結構soft_limit_tree,通過分析soft_limit_tree的原型數據結構mem_cgroup_tree及其成員和其他相關數據結構,我們不難得出這樣的數據結構關係圖:


        即soft_limit_tree是整個樹的根節點,根據node的數量,有多個mem_cgroup_tree_per_node子節點,然後根據zone的數量,每個mem_cgroup_tree_per_node又有多個mem_cgroup_tree_per_zone的子節點,每個mem_cgroup_tree_per_zone則引出了一顆由mem_cgroup_per_zone組成的樹。結合上面回收內存的邏輯,我們在調用shrink_zones時,會遍歷一個zonelist上的所有zone,而每個zone在soft_limit_tree上就對應一個mem_cgroup_tree_per_zone,從這個zone,就可以找到所有相關的mem_cgroup_per_zone,進行相應的回收操作。

那麼內核是如何來維護這顆soft_limit_tree的呢?

在創建mem_cgroup的時候調用了兩個函數:alloc_mem_cgroup_per_zone_info和mem_cgroup_soft_limit_tree_init,我們再看看這兩個函數的實現:

從上述代碼可知,在創建一個cgroup的時候,內核就對該cgroup,在每一個node的每一個zone上分配好了mem_cgroup_per_zone結構,並初始化,不過那個時候他們並沒有在soft_limit_tree上(on_tree = false)。也就是在這個時候,初始化了lruvec小節中提到的mem_cgroup結構體中的info字段。另外,初始化了全局變量soft_limit_tree中除了mem_cgroup_per_zone之外的其他主幹部分。

根據下面的代碼流程圖:


        我們在三種情況下會更新soft_limit_tree,即charge、uncharge和move的時候(move的時候也會帶來charge和uncharge),所以在charge和uncharge時,檢查該page所在的mem_cgroup的soft_limit是否超限,如果超了,則將對應的mem_cgroup_per_zone加入到soft_limit_tree中,如果沒超而對應的mem_cgroup_per_zone又在soft_limit_tree上,則將其從樹上摘下。達到動態更新的目的。

值得注意的是,mem_cgroup_per_zone上記錄的usage_in_excess是其對應的cgroup所超過soft_limit的值,而該cgroup會對應很多個mem_cgroup_per_zone,所以在mem_cgroup_soft_limit_reclaim函數選取到某個zone上超過soft_limit最多的mem_cgroup對應的mem_cgroup_per_zone,並回收它上面的lruvec,它不一定能夠達到最好的回收效果,因爲這個超過soft_limit最多的mem_cgroup其大多數的內存消耗可能都在其他node或其他zone上,這裏回收的lruvec可能只有很少的page。但是因爲全局回收時往往會遍歷所有的node,所有的zone,所以按照這個策略,結果應該總是會傾向於回收超過soft_limit最多的mem_cgroup裏的內存的。

分析至此,我們可以看出上面加入了那麼多的數據結構和處理函數,目的都是爲了實現soft_limit的功能,而soft_limit本身的目的很簡單,就是在內存回收時識別出最應該被回收的mem_cgroup,並告訴內核回收到什麼程度。所以現在的實現方式並不能算優美。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章