SLUB


內核管理頁面使用了2個算法:夥伴算法和slub算法,夥伴算法以頁爲單位管理內存,但在大多數情況下,程序需要的並不是一整頁,而是幾個、幾十個字節的小內存。於是需要另外一套系統來完成對小內存的管理,這就是slub系統。slub系統運行在夥伴系統之上,爲內核提供小內存管理的功能。

        slub把內存分組管理,每個組分別包含2^3、2^4、...2^11個字節,在4K頁大小的默認情況下,另外還有兩個特殊的組,分別是96B和192B,共11組。之所以這樣分配是因爲如果申請2^12B大小的內存,就可以使用夥伴系統提供的接口直接申請一個完整的頁面即可。

        slub就相當於零售商,它向夥伴系統“批發”內存,然後在零售出去。一下是整個slub系統的框圖:


         一切的一切源於kmalloc_caches[12]這個數組,該數組的定義如下:

struct kmem_cache kmalloc_caches[PAGE_SHIFT] __cacheline_aligned;

       每個數組元素對應一種大小的內存,可以把一個kmem_cache結構體看做是一個特定大小內存的零售商,整個slub系統中共有12個這樣的零售商,每個“零售商”只“零售”特定大小的內存,例如:有的“零售商”只"零售"8Byte大小的內存,有的只”零售“16Byte大小的內存。

       每個零售商(kmem_cache)有兩個“部門”,一個是“倉庫”:kmem_cache_node,一個“營業廳”:kmem_cache_cpu。“營業廳”裏只保留一個slab,只有在營業廳(kmem_cache_cpu)中沒有空閒內存的情況下才會從倉庫中換出其他的slab。
       所謂slab就是零售商(kmem_cache)批發的連續的整頁內存,零售商把這些整頁的內存分成許多小內存,然後分別“零售”出去,一個slab可能包含多個連續的內存頁。slab的大小和零售商有關。

相關數據結構:

        物理頁按照對象(object)大小組織成單向鏈表,對象大小時候objsize指定的。例如16字節的對象大小,每個object就是16字節,每個object包含指向下一個object的指針,該指針的位置是每個object的起始地址+offset。每個object示意圖如下:


void*指向的是下一個空閒的object的首地址,這樣object就連成了一個單鏈表。

向slub系統申請內存塊(object)時:slub系統把內存塊當成object看待

  1. slub系統剛剛創建出來,這是第一次申請。
    此時slub系統剛建立起來,營業廳(kmem_cache_cpu)和倉庫(kmem_cache_node)中沒有任何可用的slab可以使用,如下圖中1所示:

    因此只能向夥伴系統申請空閒的內存頁,並把這些頁面分成很多個object,取出其中的一個object標誌爲已被佔用,並返回給用戶,其餘的object標誌爲空閒並放在kmem_cache_cpu中保存。kmem_cache_cpu的freelist變量中保存着下一個空閒object的地址。上圖2表示申請一個新的slab,並把第一個空閒的object返回給用戶,freelist指向下一個空閒的object。

  2. slub的kmem_cache_cpu中保存的slab上有空閒的object可以使用。
    這種情況是最簡單的一種,直接把kmem_cache_cpu中保存的一個空閒object返回給用戶,並把freelist指向下一個空閒的object。


  3. slub已經連續申請了很多頁,現在kmem_cache_cpu中已經沒有空閒的object了,但kmem_cache_node的partial中有空閒的object 。所以從kmem_cache_node的partial變量中獲取有空閒object的slab,並把一個空閒的object返回給用戶。


    上圖中,kmem_cache_cpu中已經都被佔用的slab放到倉庫中,kmem_cache_node中有兩個雙鏈表,partial和full,分別盛放不滿的slab(slab中有空閒的object)和全滿的slab(slab中沒有空閒的object)。然後從partial中挑出一個不滿的slab放到kmem_cache_cpu中。

    上圖中,kmem_cache_cpu中中找出空閒的object返回給用戶。


  4. slub已經連續申請了很多頁,現在kmem_cache_cpu中保存的物理頁上已經沒有空閒的object可以使用了,而此時kmem_cache_node中沒有空閒的頁面了,只能向內存管理器(夥伴算法)申請slab。並把該slab初始化,返回第一個空閒的object。


    上圖表示,kmem_cache_node中沒有空閒的object可以使用,所以只能重新申請一個slab。



    把新申請的slab中的一個空閒object返回給用戶使用,freelist指向下一個空閒object。

向slub系統釋放內存塊(object)時,如果kmem_cache_cpu中緩存的slab就是該object所在的slab,則把該object放在空閒鏈表中即可,如果kmem_cache_cpu中緩存的slab不是該object所在的slab,然後把該object釋放到該object所在的slab中。在釋放object的時候可以分爲一下三種情況:

  1. object在釋放之前slab是full狀態的時候(slab中的object都是被佔用的),釋放該object後,這是該slab就是半滿(partail)的狀態了,這時需要把該slab添加到kmem_cache_node中的partial鏈表中。



  2. slab是partial狀態時(slab中既有object被佔用,又有空閒的),直接把該object加入到該slab的空閒隊列中即可。




  3. 該object在釋放後,slab中的object全部是空閒的,還需要把該slab釋放掉。



    這一步產生一個完全空閒的slab,需要把這個slab釋放掉。



slab思想

        摘抄《深入linux設備驅動程序內核機制》的一段話:slab分配器的基本思想是,先利用頁面分配器分配出單個或者一組連續的物理頁面,然後在此基礎上將整塊頁面分割成多個相等的小內存單元,以滿足小內存空間分配的需要。當然,爲了有效的管理這些小的內存單元並保證極高的內存使用速度和效率。


slab分配器結構

        首先看下一張圖,這是一張非常經典的圖(所以也是從別的地方截取過來的,哈哈)基本上講slab的都會上這張圖(有些圖可能會有點點不同,不過都是大同小異)

       

        這是有struct kmem_cache 和 struct slab構成的slab分配器;

        從大方向來說,每個kmem_cache都是鏈接在一起形成一個全局的雙向鏈表,由cache指向該鏈表,系統可以從Cache_chain開始掃描每個kmem_cache,來找到一個大小最合適的kmem_cache,然後從該kmem_cache中分配一個對象;

        其中每個kmem_cache(有的地方也會叫這個kmem_cache爲cache,原因是kmem_cache中的object有大有小(其實也是kmem_cache有大有小),當內存申請時,會有命中該kmem_cache的說法,和CPU中的cache命中是類似的意思,所以也會叫kmem_cache爲cache(個人理解))有三條鏈表,slabs_full 表示該鏈表中每個slab的object對象都已經分配完了;slabs_partial 表示該鏈表中的slab的object對象部分分配完了;slabs_empty  表示該鏈表中的object對象全部沒有分配出去;

        其中每個slab都是一個或者多個連續的內存頁組成,而每個slab被分成多個object對象。對象的分配和釋放都是在slab中進行的,所以slab可以在三條鏈表中移動,如果slab中的object都分配完了,則會移到full 鏈表中;如果分配了一部分object,則會移到partial鏈表中;如果所有object都釋放了,則會移動到empty鏈表中;其中當系統內存緊張的時候,slabs_empty鏈表中的slab可能會被返回給系統。

     

kmem_cache分配

        基本的概念就是這樣,下面說說簡單的代碼實現;

        首先所有的kmem_cache結構都是從cache_cache,這個內存是在系統還沒有完全初始化好就創建了(這個結構我看到了兩個版本,不過意思差不多):

  1. static kmem_cache_t cache_cache = {  
  2.          slabs_full:     LIST_HEAD_INIT(cache_cache.slabs_full),  
  3.          slabs_partial:  LIST_HEAD_INIT(cache_cache.slabs_partial),  
  4.          slabs_free:     LIST_HEAD_INIT(cache_cache.slabs_free),  
  5.          objsize:        sizeof(kmem_cache_t),  
  6.          flags:          SLAB_NO_REAP,  
  7.          spinlock:       SPIN_LOCK_UNLOCKED,  
  8.          colour_off:     L1_CACHE_BYTES,  
  9.          name:           "kmem_cache",  
  10. };  
static kmem_cache_t cache_cache = {
         slabs_full:     LIST_HEAD_INIT(cache_cache.slabs_full),
         slabs_partial:  LIST_HEAD_INIT(cache_cache.slabs_partial),
         slabs_free:     LIST_HEAD_INIT(cache_cache.slabs_free),
         objsize:        sizeof(kmem_cache_t),
         flags:          SLAB_NO_REAP,
         spinlock:       SPIN_LOCK_UNLOCKED,
         colour_off:     L1_CACHE_BYTES,
         name:           "kmem_cache",
};

系統分配

        先看下面兩個結構體:

  1. struct cache_size{  
  2.     size_t cs_size;  
  3.     struct kmem_cache *cs_cachep;  
  4. }  
  5.   
  6. struct cache_size malloc_sizes[] = {   
  7.     {.cs_size = 32},  
  8.     {.cs_size = 64},  
  9.     {.cs_size = 128},  
  10.     {.cs_size = 256},  
  11.     ................  
  12.     {.cs_size = ~0UL},  
  13. };  
 struct cache_size{
     size_t cs_size;
     struct kmem_cache *cs_cachep;
 }
 
 struct cache_size malloc_sizes[] = { 
     {.cs_size = 32},
     {.cs_size = 64},
     {.cs_size = 128},
     {.cs_size = 256},
     ................
     {.cs_size = ~0UL},
 };

        在系統初始化時,內核會調用kmem_cache_init函數對malloc_size數組進行遍歷,對數組中的每個元素都調用kmem_cache_create()函數在cache_cache中分配一個struct kmem_cache 實例,並且把kmem_cache所在的地址賦值給cache_size中的cs_cachep指針(malloc_sizes[x]->cs_cachep);

  1. void __init kmem_cache_init(void)  
  2. {  
  3.     struct cache_size *sizes = malloc_sizes;//數組  
  4.     struct cache_names *names = cache_names;//cache名稱  
  5.   
  6.     .....  
  7.   
  8.     while(sizes->cs_size != ULONG_MAX){//從32到~0UL都遍歷每個元素  
  9.         if(!sizes->cs_cachep)//表示還沒有被初始化  
  10.         {  
  11.            // kmem_cache_create就是創建一個kmem_cache  
  12. nbsp;           sizes->cs_cachep = kmem_cache_create(names->name, sizes->cs_size,  
  13.                     ARCH_KMALLOC_MINALIGN,  
  14.                     ARCH_KMALLOC_FLAGS|SLAB_PANIC,  
  15.                     NULL);  
  16.         }  
  17.         sizes++;  
  18.         names++;  
  19.     }  
  20.     .....  
  21. }  
 void __init kmem_cache_init(void)
 {
     struct cache_size *sizes = malloc_sizes;//數組
     struct cache_names *names = cache_names;//cache名稱
 
     .....
 
     while(sizes->cs_size != ULONG_MAX){//從32到~0UL都遍歷每個元素
         if(!sizes->cs_cachep)//表示還沒有被初始化
         {
            // kmem_cache_create就是創建一個kmem_cache
            sizes->cs_cachep = kmem_cache_create(names->name, sizes->cs_size,
                     ARCH_KMALLOC_MINALIGN,
                     ARCH_KMALLOC_FLAGS|SLAB_PANIC,
                     NULL);
         }
         sizes++;
         names++;
     }
     .....
 }

        初始化後的slab分配器如下圖(圖片來自於《深入linux設備程序機制》):


        從上面的圖可以看出,第一行爲malloc_sizes[]數組的所有元素,由於malloc_size[]數組中存放的是 struct cache_size結構體元素,所以cache_size->cs_size 和 cache_size->cs_cachep; 其中每個cs_cachep指向一個kmem_cache,表示該kmem_cache中slab分配的對象大小爲cs_size;這就很容易理解了;

        注意,這是slab分配器只是個空殼子,kmem_cache下面是三條空的鏈表,也就是說kmem_cache下面沒有一個slab也沒有一個page更沒有一個對象;這時候僅僅是定義了一個規則,表示該kmem_cache下的對象大小都固定爲cs_size。至於什麼時候創建slab,後面再討論;


手動分配

        所謂手動分配就是在自己程序中分配,其實就是理解下kmem_cache_create()函數:

        struct kmem_cache*  kmem_cache_create(const char*  name, size_t size,  size_t align, unsigned long flags, void(*ctor)(void*));

        參數 name 是指向字符串的指針,用來生成kmem_cache的名字,會在/proc/slabinfo中出現;生成的kmem_cache對象會用一個指針指向該name;所以要保證name在kmem_cache對象有效週期內都有效;

        參數 size  是用來指定slab分配的對象大小;

        參數 align 是表示數據對齊的,一般使用0就可以;

        參數 flags  是創建kmem_cache標識位掩碼,使用0,表示默認;

        參數 void (*ctor)(void*)  是構造函數,當slab分配一個新頁面時,會對該頁面中的每個內存對象調用該構造函數;

        返回值:從cache_cache中返回一個指向kmem_cache實例的*cachep指針;當然該kmem_cache對象也會被加入cache_chain鏈表中;


        void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t  flags);

        其中cachep就是上面kmem_cache_create()函數返回的kmem_cache對象,該函數會返回cachep下的一個空閒的內存對象;flags 則是在沒有空閒對象,需要從物理頁分配一個新頁時,才使用的到;


kmalloc函數

        下面把kmalloc函數簡單的分析下,代碼把無關的東西刪除掉了,主要是說明下kmalloc的工作原理;對照下面的圖會很容易知道該函數做了些什麼;



  1. void * kmalloc(size_t size, int flags)  
  2. {  
  3.     struct cache_size *csizep = malloc_sizes;//定義好大小的數組  
  4.     struct kmem_cache *cachep;  
  5.   
  6.     while(size > csizep->cs_size)//這是關鍵,從malloc_sizes數組(其實也是從kmem_cache鏈表)中遍歷,得到地一個大於等於size的cache  
  7.         csizep++;  
  8.   
  9.     cachep = csizep->cs_cachep;  
  10.     return kmem_cache_alloc(cachep, flags)//這裏會發現正真分配對象的還是靠kmem_cache_alloc()函數  
  11. }  
 void * kmalloc(size_t size, int flags)
 {
     struct cache_size *csizep = malloc_sizes;//定義好大小的數組
     struct kmem_cache *cachep;
 
     while(size > csizep->cs_size)//這是關鍵,從malloc_sizes數組(其實也是從kmem_cache鏈表)中遍歷,得到地一個大於等於size的cache
         csizep++;
 
     cachep = csizep->cs_cachep;
     return kmem_cache_alloc(cachep, flags)//這裏會發現正真分配對象的還是靠kmem_cache_alloc()函數
 }

slab的創建

        前面說了,kmem_cache_create()僅僅是從cache_cache中分配一個kmem_cache實例,並不會分配實際的物理頁,當然也就沒有slab了(也沒有對象)。那什麼時候會創建一個slab呢?

        只有滿足下面兩個條件時,纔會給kmem_cache分配Slab:

        (1)已發出一個分配新對象的請求;(2)kmem_cache中沒有了空閒對象;

        其實本質就是:需要得到該kmem_cache下一個對象,而kmem_cache沒有空閒對象,這時候就會給kmem_cache分配一個slab了。所以新分配的kmem_cache只有被要求分配一個對象時,纔會調用函數去申請物理頁;

        具體的分配流程:

        首先會調用kmem_cache_grow()函數給kmem_cache分配一個新的Slab。其中,該函數調用kmem_gatepages()從夥伴系統獲得一組連續的物理頁面;然後又調用kmem_cache_slabgmt()獲得一個新的Slab結構;還要調用kmem_cache_init_objs()爲新Slab中的所有對象申請構造方法(如果定義的話);最後,調用kmem_slab_link_end()把這個Slab結構插入到緩衝區中Slab鏈表的末尾。

        從slab的分配可以知道,其實所有的內存最終還是要夥伴系統來分配,這裏就可以知道,這些內存都是連續的物理頁。

        這是後面增加的(感覺非常有必要提下):在某些情況下內核模塊可能需要頻繁的分配和釋放相同的內存對象,這時候slab可以作爲內核對象的緩存,當slab對象被釋放時,slab分配器並不會把對象佔用的物理空間還給夥伴系統。這樣的好處是當內核模塊需要再次分配內存對象時,不需要那麼麻煩的向夥伴系統申請,而是可以直接在slab鏈表中分配一個合適的對象;以上是slub算法的主要原理。



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