Cgroup-memory子系統分析(2)

1.1      Oom

1.1.1      簡介

Oom的全稱是out-of-memory,是內核在處理系統內存不足而又回收無果的情況下采取的一種措施,內核會經過選擇殺死一些進程,以釋放一些內存,滿足當前內存申請的需求。所以oom是一種系統行爲,對應到memcg的oom,其原理和動機跟全局oom是一樣的,區別只在於對象的不同,全局oom的對象是整個系統中所有進程,而memcg oom只針對memcg中的進程(如果使能了hierarchy,還包括所有子memcg中的進程),這裏的對象主要是指oom時內核選擇從哪些進程中殺死一些進程,所以memcg的oom只可能殺死屬於該memcg的進程。

1.1.2      實現

跟全局oom一樣,memcg的oom也分成select_bad_process和oom_kill_process兩個過程:

a.     select_bad_process找出該memcg下最該被kill的進程(如果memcg設置了hierarchy,也會考慮子memcg下的進程);

b.    oom_kill_process殺掉選中的進程及與其共用mm的進程(殺進程的目的是釋放內存,所以當然要把mm的所有引用都幹掉);

對於實現的代碼細節,不同的版本代碼演進較快,之前memcg中會直接調用內核的select_bad_process和oom_kill_process,在最新的3.10中,memcg實現了自己的select_bad_process,即在memcg的代碼中自己來找到要殺死的進程。雖然函數調用不同,但是找到要殺死的進程的原理都是類似的,select的過程會給memcg(或及其子memcg)下的每個進程打一個分,得分最高者被選中。評分因素每個版本不盡相同,主要會考慮以下因素:

a.     進程擁有page和swap entry越多,分得越高;

b.    可以通過/proc/$pid/oom_score_adj進行一些分值干預;

c.     擁有CAP_SYS_ADMIN的root進程分值會被調低;

 

kill的過程比較簡單,簡單的說就是向要殺死的進程發送SIGKILL信號,但其中依然有一些細節:

a.     如果被選中的進程有一些子進程跟他不共用同一個mm,並且也是可以被殺死的,那麼就挑選這些子進程中badness得分最高的一個來代替父進程被殺死,這樣是爲了確保我們在釋放內存的同時失去更少的東西;

b.    上面已經說了,oom_kill的過程會殺死選中的進程及與其共用mm的進程,所以會遍歷所有用戶態進程,找到並殺死與選中進程共用同一個mm的進程;

c.     遍歷進程的過程中,會過濾掉通過/proc/$pid/oom_score_adj干預的不可被oom_kill掉的進程(目前是設置爲OOM_SCORE_ADJ_MIN的進程);

 

在oom的過程中,另外值得一說的是其中的同步過程。

oom過程會向選中的進程發送SIGKILL信號,但是距離進程處理信號、釋放空間,還是需要經歷一定時間的。如果系統負載較高,則這段時間內很可能有其他上下文也需要卻得不到page,而觸發新的oom。那麼如果大量oom在短時間內爆發,可能會大面積殺死系統中的進程,帶來一場浩劫。

所以oom過程需要同步:在給選中的進程發送SIGKILL後,會設置其TIF_MEMDIE標記。而在select被殺死進程的過程中如果發現記有TIF_MEMDIE的進程,則終止當前的oom過程,並等待上一個oom過程結束。這樣做可以避免oom時大面積的kill進程。

而在進程退出時,會先將task->mm置爲NULL,再mmput(mm)釋放掉引用計數,從而導致內存空間被釋放(如果引用計數減爲0的話)。所以,只要task->mm被置爲NULL(內存即將開始釋放),就沒人認得它是屬於哪個memcg的了,針對那個memcg的新的oom過程就可以開始。

1.1.3      關閉oom

從oom的實現可知,雖然在oom的設計中已經考慮的各種情況,並採取了各種措施來讓oom在殺死進程釋放內存的同時對系統造成最小的傷害,並且這種努力和改進一直在繼續。但系統的環境複雜多變,內核永遠無法完美的識別某個進程對系統的重要性,在實際應用中,oom常常會造成未知的影響,所以我們有時會希望可以關閉oom,而memcg也正好提供了這樣的功能。

在memcg的用戶態目錄下,memory.oom_control文件提供了這樣的接口,通過查看該文件:

其中顯示了兩個字段,oom_kill_disable表示是否使能了該memcg的oom_kill,1爲使能,0爲關閉;而under_oom則表示當前memcg是否在oom過程中。

該文件的特殊性在於其顯示的內容和寫入的內容不同,對該文件,我們只可以寫入0或1,表示關閉或開啓該memcg的oom。當寫入0關閉oom時,當某個進程遇到上面說的內存不足而又回收無果的情況下,內核會將該進程放到memcg的oom等待隊列中,休眠該進程,等到有足夠的內存可以使用時,再喚醒該進程。這樣就可以避免出現進程被意外殺死的情況,但是這麼做的話,需要確保你的進程可以接受睡眠。

對於全局的oom,並沒有提供關閉的功能,這是因爲全局的oom可能在任何情況下觸發,比如中斷上下文等,在這些情況下是不能睡眠的,否則可能造成比殺死某個進程更嚴重的後果。

1.2      Kmem

1.2.1      概述

Kmem controller是對mem controller的補充,原來的memcg只能對用戶程序申請的用戶態內存進行統計和管理,kmem只有網絡tcp部分的支持,最近幾個版本才加入了kmem controller的支持,徹底支持了對內核內存的統計和管理。但截止目前(3.12),kmem controller還是有很多不完善,還不建議在商用中使用。

因爲kmemcontroller是在memcg的基礎上加上的一個相對獨立的特性,所以本文用一種新的方式,即通過分析完整加入整個kmem controller的patchset,選取最重要的幾個部分,只要大家對整個kmem controller是如何一步一步實現的,就可以對整個kmem controller的實現及原理有了清晰的認識。

閱讀本節需要對原來的memcg實現有一定的瞭解,同時對內核的slab/slub機制有一定的瞭解,因爲kmem controller的本質就是讓每個memcg管理自己的slab/slub。(本文分析的代碼基於主線3.12)

1.2.2      實現

完整的patch在:https://lkml.org/lkml/2012/11/1/171

原始patchset一共29個,但本文只會選取其中最主要的幾個進行分析,通過這些patch將會了解整個kmemcontroller的實現步驟和原理。

一.實現kmem accounting的基本框架

Upstream commit: 510fc4e11b772fd60f2c545c64d4c55abd07ce36

[04/29] memcg: kmem accounting basic infrastructure

主要是在mem_cgroup中增加了struct res_counter kmem,同時增加了kmem統計和限制相關的cgroup文件及其處理函數,在這部分的處理上跟原來mem cgroup中的處理是完全一樣的,只是對象換成了kmem。

另外,這裏需要說明的是kmem cgroup設計的統計方法:所有統計在kmem counter上的內存同時也會統計在mem counter上,所以在設置內存限制時,如果要實現對kmem的限制,那麼只有將memory.kmem.limit_in_bytes設置成小於memory.limit_in_bytes纔是有意義的。

二.實現kmem controller的基本框架

Upstream commit: 7ae1e1d0f8ac2927ed7e3ca6d15e42d485903459

[06/29] memcg: kmem controller infrastructure

這個patch很關鍵,它實現了kmemcontroller的核心框架。

即對memcg中使用的內核內存進行統計跟蹤,只要進程在某個cgroup內(非root cgroup),而且內存申請中有__GFP_KMEMCG標誌,該patch引入的controller框架就會對申請的內存進行統計(利用上一步加入的kmem account),該框架主要實現了三個接口:

1.   memcg_kmem_newpage_charge

如果cgroup可以滿足內存申請(還沒有超過上限),則會更新相應的account,並返回true,此時page還沒有被分配。

這個patch只是加了這個接口,後續該接口會在具體的地方被調用,目前只有在夥伴算法的核心函數__alloc_pages_nodemask中被調用。需要注意的是,在有一些情況下,對kmem的申請是不會統計進來的,目前有:

a.     申請時沒有加__GFP_KMEMCG標記;

b.    申請時加了__GFP_NOFAIL標記;(因爲加了該標記即使charge失敗了內存申請也要繼續,所以這裏的策略是乾脆不統計了,該策略並不完美,後續可能會修改)

c.     在中斷上下文中;

d.    當前進程是線程組的線程(!current->mm);

e.     當前線程是內核線程(current->flags & PF_KTHREAD);

所以即使把內核線程加入cgroup中,它們使用的kmem,也是不會統計進來的。

2.   memcg_kmem_commit_charge

如果申請內存失敗,這裏會執行計數的恢復操作,如果內核申請成功了,就會綁定該page和cgroup的關係(設置該page對應的page_cgroup的mem_cgroup成員指向進程所在memcg,同時更新page_cgroup的標記,說明該page已經在某個cgroup中了);

charge和commit_charge是在一起用的,目前都只在__alloc_pages_nodemask函數中用,即先charge,成功後再申請內存,然後commit_charge。

3.   memcg_kmem_uncharge_pages

在free的時候調用,主要做計數的恢復操作。

三.實現slab/slub跟memcg的關係

Upstream commit: ba6c496ed834a37a26fc6fc87fc9aecb0fa0014d

[15/29] slab/slub: struct memcg_params

這個patch很小,但它建立了slab/slub跟memcg的關係,上面兩個patch都只是計數和控制的框架,還沒有具體的對象讓這個框架來操作,而它操作的對象正是memcg中對應的slab/slub,建立這個聯繫後,後面纔會一點點把操作對象加進來。

該patch加入了這個結構(patch本身加入的該結構體要簡單很多,這個是3.12代碼中最新的結構體),kmem_cache結構體中增加了指向該結構體的成員。參考附錄瞭解slub的基本機構,加入kmem controller後,跟原來相比的變化是:

1.    原來一類object對應一個kmem_cache,現在同一類object會有多個kmem_cache,其中原來的kmem_cache現在作爲root cache,其他的kmem_cache是每個memcg對應一個,都作爲root cache的child cache;

這裏很重要的是引入了rootcache的概念,可以簡單的理解,即在加入kmem controller之前,原來內核中所有的kmem_cache,在加入kmem controller之後,這些kmem cache都變成了root cache,而之後爲每個memcg創建的kmem_cache,都是相應的kmem_cache的child cache;

2.    這些新加入的kmem_cache也會加入到slab_caches鏈表中(同一個memcg中使用的kmem_cache也會通過memcg_cache_params->list鏈接在一起),所以相對原來,內核中kmem_cache的數量將大大增加,不過memcg中的kmem_cache只有在使能了kmem.limit_in_bytes,且執行了slab內存申請的操作,纔會創建具體的kmem_cache。

對於這些關係,我們常常會利用數據結構間的聯繫做一些查找,比如

a.     通過kmem_cache找到對應的memcg:

Kmem_cache --> 找到 memcg_cache_params --> 找到memcg。

b.    通過root cache和memcg找到對應的kmem_cache:

Root cache --> 找到 memcg_cache_params,加上memcg的id -->memcg對應的kmem_cache。

        爲了實現上面提到的一些功能,patchset中其他一些patch輔助實現了一些函數,比如創建memcg對應的kmem_cache,將對應的kmem_cache與memcg建立聯繫等,這些patch不在本文一一列出。

四.實現memcg_kmem_get_cache接口

Upstream commit: d7f25f8a2f81252d1ac134470ba1d0a287cf8fcd

[19/29] memcg: infrastructure to match an allocation to theright cache

實現了memcg_kmem_get_cache接口,即某進程在嘗試獲取某kmem_cache時,獲取到的是該進程所在cgroup的kmem_cache。其中需要注意的是:當某個cgroup中的進程第一次獲取某kmem_cache時,返回的還是root kmem_cache,同時將創建該cgroup對應的kmem_cache的任務加入work queue中異步進行,下次獲取就可以拿到cgroup對應的kmem_cache。

另外,在創建cgroup對應的kmem_cache後,需要維護該kmem_cache的數據關係,比如對應的memcg_cache_params,以及跟root kmem_cache間的指向關係等,這個操作放到kmem_cache_create_memcg中,通過memcg_register_cache函數實現。

這一步跟第三步中提到的一些,如創建memcg對應的kmem_cache,這些patch進一步爲memcg中的進程使用自己的slab/slub做了準備。

五.實現slab/slub中內存分配的memcg適配

Upstream commit: d79923fad95b0cdf7770e024677180c734cb7148

[22/29] sl[au]b: allocate objects from memcg cache

這裏正式將slab/slub中的內存管理跟memcg對接了起來。

主要有兩個地方:

1. kmalloc正常走__kmalloc時在__cache_alloc時,將傳入的kmem_cache重新通過memcg_kmem_get_cache獲取到memcg中的kmem_cache。

2. 如果kmalloc的內存過大,走kmalloc_large到夥伴系統分配內存時,在內存分配標記中增加__GFP_KMEMCG,這樣可以將分配的內存統計到相應的memcg中。

 

至此,在slab/slub的內存分配中使用了上面各個步驟添加的一系列接口,在內存分配時,如果使能了kmemcontroller,且申請內存的進程在非rootcgroup內,則在通過傳入的size獲取到對應的kmem_cache後,調用會調用接口memcg_kmem_get_cache通過該root cache獲取到對應的memcg使用的kmem_cache(如果是第一次申請該kmem_cache中的object,則返回的還是該root cache,然後將創建該memcg對應的該kmem_cache的操作放到work queue中,下次就可以直接獲取到該memcg對應的該kmem_cache);如果分配的內存過大,不走slab/slub,則會在內存分配的flag中增加__GFP_KMEMCG,直接通過夥伴系統分配內存,並將內存計數統計到memcg對應的kmem account中。(在走slab/slub時,在創建memcg對應的kmem_cache時,分配的內存也是從夥伴系統中得到,這些內存也已經統計到memcg對應的kmem account中了)所以我們就可以對memcg中使用的內核內存進行計數和控制,實現了kmem controller。

1.2.3      現狀

Kmem controller的特性是在2012年底加入upstream的,但之後該特性的開發者轉去做其他的項目,一直到現在(2013年底),kmem controller的相關代碼並沒有什麼改變和發展,實際上目前的代碼並不完善,還不能真正應用到實際項目中,因爲目前對memcg中進程使用的kmem,還只是簡單的統計,如果超過了限制的上限,對於分配kmem的請求會直接返回失敗。對於memcg使用的kmem的內存回收,還依賴全局的內存回收機制(memcg的內存回收機制還只能回收用戶態內存)。所以要實現完整的kmemcontroller,我們還需要memcg實現per memcg的kmem reclaim機制。

目前,已經有人在做這個事了,正在review中,在不久的將來,kmemcontroller應該會逐步完善。

2      附錄

2.1      Slub結構圖

1.   內核中所有的slab通過slab_caches鏈接起來;

2.   每個kmem_cache結構代表一類object對象,比如inode等;

3.   Kmem_cache_cpu存放當前cpu使用的cache對象(避免不同cpu同時取partial鏈表上對象時導致的競爭問題和加鎖導致的開銷);

4.   Kmem_cache_node存放針對不同內存節點的對象;

5.   Kmem_cache_node下的partial鏈表鏈接了可用的page,page中包含了最終使用的object;

(更多slab/slub信息請參考其他相關資料)


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