1. 程式人生 > 其它 >SLUB和SLAB的區別【轉】

SLUB和SLAB的區別【轉】

轉自:https://blog.csdn.net/Vince_/article/details/79668199

轉載:http://www.cnblogs.com/tolimit/

  首先為什麼要說slub分配器,核心裡小記憶體分配一共有三種,SLAB/SLUB/SLOB,slub分配器是slab分配器的進化版,而slob是一種精簡的小記憶體分配演算法,主要用於嵌入式系統。慢慢的slab分配器或許會被slub取代,所以對slub的瞭解是十分有必要的。

  我們先說說slab分配器的弊端,我們知道slab分配器中每個node結點有三個連結串列,分別是空閒slab連結串列,部分空slab連結串列,已滿slab連結串列,這三個連結串列中維護著對應的slab緩衝區。我們也知道slab緩衝區的記憶體是從夥伴系統中申請過來的,我們設想一個情景,如果沒有記憶體回收機制的情況下,只要申請的slab緩衝區就會存入這三個連結串列中,並不會返回到夥伴系統裡,如果這個型別的SLAB迎來了一個分配高峰期,將會從夥伴系統中獲取很多頁面去生成許多slab緩衝區,之後這些slab緩衝區並不會自動返回到夥伴系統中,而是會新增到node結點的這三個slab連結串列中去,這樣就會有很多slab緩衝區是很少用到的。

  而slub分配器把node結點的這三個連結串列精簡為了一個連結串列,只保留了部分空slab連結串列,而SLUB中對於每個CPU來說已經不使用空閒物件連結串列,而是直接使用單個slab,並且每個CPU都維護有自己的一個部分空連結串列。在slub分配器中,對於每個node結點,也沒有了所有CPU共享的空閒物件連結串列。我們用以下圖來表示以下slab分配器和slub分配器的區別(上圖為SLAB,下圖為SLUB):

單個SLAB分配器結構

單個SLUB分配器結構

SLUB分配器

  發明SLUB分配器的主要目的就是減少slab緩衝區的個數,讓更多的空閒記憶體得到使用。首先,SLUB和SLAB一樣,都分為多種,同時也分為專用SLUB和普通SLUB。如TCP,UDP,dquot這些,它們都是專用SLAB,專屬於它們自己的模組。而後面這張圖,如kmalloc-8,kmalloc-16...還有dma-kmalloc-96,dma-kmalloc-192...在這方面與SLAB是一樣的,同樣地,也是使用一個struct kmem_cache結構來描述一個SLUB(與SLAB一樣)。並且這個struct kmem_cache與SLAB的struct kmem_cache幾乎是同一個,而且對於SLAB和SLUB,向外提供的介面是統一的(函式名、引數以及返回值一模一樣),這樣也就讓驅動和其他模組在編寫程式碼時無需操心繫統使用的是SLAB還是SLUB。這是為了同一個核心可以通過編譯選項使用SLAB或者SLUB。

  SLUB分配器中的slab緩衝區結構與SLAB分配器中的slab緩衝區的結構也有了明顯的不同,對於SLAB分配器的slab緩衝區,其結構如下:

  而在SLUB分配器的slab緩衝區結構中,已經沒有了物件描述符陣列,而freelist也拆分成了每個物件有一個指向下一個物件的指標,如下:

  雖然這兩個slab緩衝區的結構上有所不同,但其實際原理還是一樣,每次分配或釋放都會設定物件的下個空閒物件指標,讓其指向正確的位置。有疑問的同學可以看看我之前寫的linux記憶體原始碼分析 - SLAB分配器概述。在初始化一個slab緩衝區時,預設第一個空閒物件是物件0,然後物件0後面跟著的下一個空閒物件指標指向物件1,物件1的空閒物件指標指向物件2,以此類推。

  我們看看SLUB分配器的描述符,struct kmem_cache結構:

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;
    /* 標誌 */
    unsigned long flags;
    /* 每個node結點中部分空slab緩衝區數量不能低於這個值 */
    unsigned long min_partial;
    /* 分配給物件的記憶體大小(大於物件的實際大小,大小包括物件後邊的下個空閒物件指標) */
    int size;    
    /* 物件的實際大小 */
    int object_size;  
    /* 存放空閒物件指標的偏移量 */
    int offset;  
    /* cpu的可用objects數量範圍最大值 */
    int cpu_partial;   
    /* 儲存slab緩衝區需要的頁框數量的order值和objects數量的值,通過這個值可以計算出需要多少頁框,這個是預設值,初始化時會根據經驗計算這個值 */
    struct kmem_cache_order_objects oo;

    /* 儲存slab緩衝區需要的頁框數量的order值和objects數量的值,這個是最大值 */
    struct kmem_cache_order_objects max;
    /* 儲存slab緩衝區需要的頁框數量的order值和objects數量的值,這個是最小值,當預設值oo分配失敗時,會嘗試用最小值去分配連續頁框 */
    struct kmem_cache_order_objects min;
    /* 每一次分配時所使用的標誌 */
    gfp_t allocflags;   
    /* 重用計數器,當用戶請求建立新的SLUB種類時,SLUB 分配器重用已建立的相似大小的SLUB,從而減少SLUB種類的個數。 */
    int refcount;  
    /* 建立slab時的建構函式 */
    void (*ctor)(void *);
    /* 元資料的偏移量 */
    int inuse;   
    /* 對齊 */
    int align;      
    int reserved;      
    /* 快取記憶體名字 */
    const char *name;    
    /* 所有的 kmem_cache 結構都會鏈入這個連結串列,連結串列頭是 slab_caches */
    struct list_head list;    
#ifdef CONFIG_SYSFS
    /* 用於sysfs檔案系統,在/sys中會有個slub的專用目錄 */
    struct kobject kobj;   
#endif
#ifdef CONFIG_MEMCG_KMEM
    /* 這兩個主要用於memory cgroup的,先不管 */
    struct memcg_cache_params *memcg_params;
    int max_attr_size; 
#ifdef CONFIG_SYSFS
    struct kset *memcg_kset;
#endif
#endif

#ifdef CONFIG_NUMA
    /* 用於NUMA架構,該值越小,越傾向於在本結點分配物件 */
    int remote_node_defrag_ratio;
#endif
    /* 此快取記憶體的SLAB連結串列,每個NUMA結點有一個,有可能該快取記憶體有些SLAB處於其他結點上 */
    struct kmem_cache_node *node[MAX_NUMNODES];
};

  掃一下整個kmem_cache結構,知識點最重要的有4個:每CPU對應的cpu_slab結構,每個node結點對應的kmem_cache_node結構,slub重用以及struct kmem_cache_order_objects結構對應的oo,max,min這三個值。

  除去以上4個知識點,我們先簡單說說kmem_cache中的一些成員變數:

  • size:size = 物件大小 + 物件後面緊跟的下個空閒物件指標。
  • object_size:物件大小。
  • offset:物件首地址 + offset = 下個空閒物件指標地址
  • min_partial:node結點中部分空slab緩衝區數量不能小於這個值,如果小於這個值,空閒slab緩衝區則不能夠進行釋放,而是將空閒slab加入到node結點的部分空slab連結串列中。
  • cpu_partial:同min_partial類似,只是這個值表示的是空閒物件數量,而不是部分空slab數量,即CPU的空閒物件數量不能小於這個值,小於的情況下要去對應node結點的部分空連結串列中獲取若干個部分空slab。
  • name:該kmem_cache的名字。

  

  我們再來看看struct kmem_cache_cpu __percpu *cpu_slab,對於同一種kmem_cache來說,每個CPU對應有自己的struct kmem_cache_cpu結構,這個結構如下:

struct kmem_cache_cpu {
    /* 指向下一個空閒物件,用於快速找到物件 */
    void **freelist;
    /* 用於保證cmpxchg_double計算髮生在正確的CPU上,並且可作為一個鎖保證不會同時申請這個kmem_cache_cpu的物件 */
    unsigned long tid;    
    /* CPU當前所使用的slab緩衝區描述符,freelist會指向此slab的下一個空閒物件 */
    struct page *page;    
    /* CPU的部分空slab連結串列,放到CPU的部分空slab連結串列中的slab會被凍結,而放入node中的部分空slab連結串列則解凍,凍結標誌在slab緩衝區描述符中 */
    struct page *partial;
#ifdef CONFIG_SLUB_STATS
    unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};

  在此結構中主要注意有個partial部分空slab連結串列以及page指標,page指標指向當前使用的slab緩衝區描述符,核心中slab緩衝區描述符與頁描述符共用一個struct page結構。SLUB分配器與SLAB分配器有一部分不同就在此,SLAB分配器的每CPU結構中儲存的是空閒物件連結串列,而SLUB分配器的每CPU結構中儲存的是一個slab緩衝區。而對於tid,它主要用於檢查是否有併發,對於一些操作,操作前讀取其值,操作結束後再檢查其值是否與之前讀取的一致,非一致則要進行一些相應的處理,這個tid一般是遞增狀態,每分配一次物件加1。這個結構說明了一個問題,就是每個CPU有自己當前使用的slab緩衝區,CPU0不能夠使用CPU1所在使用的slab快取,CPU1也不能夠使用CPU0正在使用的slab快取。而CPU從node獲取slab緩衝區時,一般傾向於從該CPU所在的node結點上分配,如果該node結點沒有空閒的記憶體,則根據memcg以及node結點的zonelist從其他node獲取slab緩衝區。這些具體可以在程式碼中見到。

  我們再看看kmem_cache_node結構:

struct kmem_cache_node {
    /* 鎖 */
    spinlock_t list_lock;

/* SLAB使用 */
#ifdef CONFIG_SLAB
    /* 只使用了部分物件的SLAB描述符的雙向迴圈連結串列 */
    struct list_head slabs_partial;    /* partial list first, better asm code */
    /* 不包含空閒物件的SLAB描述符的雙向迴圈連結串列 */
    struct list_head slabs_full;
    /* 只包含空閒物件的SLAB描述符的雙向迴圈連結串列 */
    struct list_head slabs_free;
    /* 快取記憶體中空閒物件個數(包括slabs_partial連結串列中和slabs_free連結串列中所有的空閒物件) */
    unsigned long free_objects;
    /* 快取記憶體中空閒物件的上限 */
    unsigned int free_limit;
    /* 下一個被分配的SLAB使用的顏色 */
    unsigned int colour_next;    /* Per-node cache coloring */
    /* 指向這個結點上所有CPU共享的一個本地快取記憶體 */
    struct array_cache *shared;    /* shared per node */
    struct alien_cache **alien;    /* on other nodes */
    /* 兩次快取收縮時的間隔,降低次數,提高效能 */
    unsigned long next_reap;    
    /* 0:收縮  1:獲取一個物件 */
    int free_touched;        /* updated without locking */
#endif

/* SLUB使用 */
#ifdef CONFIG_SLUB
    unsigned long nr_partial;
    struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
    /* 該node中此kmem_cache的所有slab的數量 */
    atomic_long_t nr_slabs;
    /* 該node中此kmem_cache中所有物件的數量 */
    atomic_long_t total_objects;
    struct list_head full;
#endif
#endif

};

  這個結構中我們只需要看#ifdef CONFIG_SLUB部分,這個結構里正常情況下只有一個node結點部分空slab連結串列partial,如果在編譯核心時選擇了CONFIG_SLUB_DEBUG選項,則會有個node結點滿slab連結串列。對於SLAB分配器,SLUB分配器在這個結構也做出了相應的變化,去除了滿slab緩衝區連結串列和空閒slab緩衝區連結串列,只使用了一個部分空slab緩衝區連結串列。對於所有的CPU來說,它們可以使用這個node結點裡面部分空連結串列中儲存的那些slab緩衝區,當它們需要使用時,要先將緩衝區拿到CPU對應自己的連結串列或者當前使用中,也就是說node結點上部分空slab緩衝區同一個時間只能讓一個CPU使用。

  而關於slub重用,這裡只做一個簡單的解釋,其作用是為了減少slub的種類,比如我有個kmalloc-8型別的slub,裡面每個物件大小是8,而我某個驅動想申請自己所屬的slub,其物件大小是6,這時候系統會給驅動一個假象,讓驅動申請了自己專屬的slub,但系統實際把kmalloc-8這個型別的slub返回給了驅動,之後驅動中分配物件時實際上就是從kmalloc-8中分配物件,這就是slub重用,將相近大小的slub共用一個slub型別,雖然會造成一些內碎片,但是大大減少了slub種類過多以及減少使用了跟多的記憶體。

  最後說說struct kmem_cache_order_objects結構對應的oo,max,min這三個值,struct kmem_cache_order_objects結構實際上就是一個unsigned long,這個結構有兩個作用,儲存一個slab緩衝區佔用頁框的order值和一個slab緩衝區物件數量的值。當kmem_cache需要建立一個新的slab緩衝區時,會使用它們當中儲存的oder值去申請2的order次方個數的頁框。oo是一個預設值,在大多數情況下建立一個新的slab緩衝區時會用oo中的值來申請頁框,而min是在oo申請失敗的情況下使用,它是一個比oo更小的值,當夥伴系統拿不出oo中指定的數量的頁框,會嘗試向夥伴系統申請min中指定的頁框數量(這個slab緩衝區連續頁框數量少,物件數量也會少)。而max的值是在做slab緩衝區壓縮時使用,其作用更多的是作為一個安全值,在這個kmem_cache中所有slab緩衝區的objects數量都不會大於max中的值。所有情況都是max >= oo > min。

  現在,我們描述一下SLUB分配器是如何運作的,kmem_cache初始化後其是沒有slab緩衝區的,當其他模組需要從此kmem_cache中申請一個物件時,kmem_cache會從夥伴系統獲取連續的頁框作為一個slab緩衝區,然後通過kmem_cache中的cotr函式指標指向的建構函式構造初始化這個slab緩衝區後,將其設定為該cpu的當前使用slab緩衝區,當此slab緩衝區使用完後,外部模組在申請物件時,會把這個滿的slab緩衝區移除,再從夥伴系統獲取一段連續頁框作為一個新的空閒slab緩衝區,也是設定為該CPU當前使用的slab緩衝區。而那些滿slab緩衝區中有物件釋放時,SLUB分配器優先把這些緩衝區放入該CPU對應的部分空slab連結串列。而當一個部分空slab通過釋放物件成為了一個空閒slab緩衝區時,SLUB分配器會視情況而定將此空閒slab釋放還是加入到node結點的部分空slab連結串列中。

  我們先看看一個slub初始化結束的情況:

  初始化完成後,slub中並沒有一個slab緩衝區,只有在第一次申請時,才會從夥伴系統中獲取一段連續頁框作為一個slab緩衝區,如下:

  這時候當前CPU獲得了一個空閒slab緩衝區,並將其中的一個空閒物件分配出去,而下次申請物件時也會從該slab緩衝區中獲取物件,直到此緩衝區中物件用完為止。

  上面描述的是初始化完成後第一次申請物件的情況,現在我們描述一下執行時申請物件的情況,一種情況是當前CPU使用的slab緩衝區有多餘的空閒物件,這樣直接從這些多餘的空閒物件中分配一個出去即可,這種情況很簡單。我們著重說明CPU使用的slab緩衝區沒有多餘的空閒物件的情況,這種情況又分為CPU的部分空slab連結串列是否為空的情況,如果CPU部分空slab連結串列不為空,則CPU會將當前使用的滿slab移除,並從CPU的部分空slab連結串列中獲取一個部分空的slab緩衝區,並設定為CPU當前使用的slab緩衝區,如下圖:

  如果node的部分空連結串列和CPU的部分空連結串列都為空的情況,那就與我們第一次申請物件的情況一樣,直接從夥伴系統中獲取連續頁框用於一個slab緩衝區。

  現在我們再說說CPU當前使用的slab已滿,CPU的部分空slab連結串列為空的情況,這種情況下,會從node結點的部分空slab連結串列獲取若干個部分空slab緩衝區,將它們放入CPU的部分空slab連結串列中,獲取的slab緩衝區個數根據一個規則就是:cpu空閒的物件數量必須要大於kmem_cache中的cpu_partial的值的一半。具體如下:

  各種情況的申請物件都已經說明了,接下來我們說說釋放物件的情況,釋放物件也分很多種,我們先說說最簡單的一種釋放情況,就是部分空的slab釋放其中一個使用著的物件,釋放後這個部分空slab還是部分空slab(有些部分空slab只使用了一個物件,釋放這個物件後就變為空閒slab),這些部分空slab可能處於CPU當前使用slab,CPU部分空連結串列,node部分空連結串列中,但是它們的處理都是一樣的,直接釋放掉該物件即可,如下:

  

  另一種情況是滿slab緩衝區釋放物件後變為了部分空slab緩衝區,這種情況下系統會將此部分空slab緩衝區放入CPU的部分空連結串列中,如下:

  最後一種釋放情況就是部分空slab釋放一個物件後轉變成了空閒slab緩衝區,而對於這個空閒slab緩衝區的處理,系統首先會檢查node部分空連結串列中slab緩衝區的個數,如果node部分空連結串列中slab緩衝區數量小於kmem_cache中的min_partial,則將這個空閒slab緩衝區放入node部分空連結串列中。否則釋放此空閒slab,將其佔用頁框返回夥伴系統中。我們知道部分空slab有可能存在於3個地方,CPU當前使用的slab緩衝區,CPU部分空連結串列,node部分空連結串列,這三個地方對於這種情況下的處理都是一樣的,如下:

  這樣看來只有空閒的slab緩衝區會被放入node結點的部分空連結串列中,這只是從釋放物件的角度看是這樣的,當重新整理kmem_cache時,會將kmem_cache中所有的slab緩衝區放回到node結點的部分空連結串列(也包括當前CPU使用的slab緩衝區),這種情況node結點的部分空連結串列就會有部分空slab緩衝區了。而還有一種情況就是編譯時禁用了CPU的部分空連結串列,即CPU只有一個當前使用的slab緩衝區,這樣其他的部分空緩衝區都會儲存在node結點的部分空連結串列上,更多詳細細節請看核心原始碼中的mm/slub.c檔案。

slab緩衝區壓縮技術

  本來不想寫這一節,不過擔心以後懶得去用一篇文章去描述slab壓縮技術,這裡就簡單說一下吧。

  說是壓縮技術,其實就是把kmem_cache中所有的slab緩衝區放回到node結點的部分空連結串列中(包括所有CPU當前正在使用的slab),然後node結點的部分空連結串列中的空閒的slab緩衝區釋放掉,然後將node結點中的其他部分空slab緩衝區按照空閒物件數量進行重新排列,把空閒數量少的放在前面,空閒數量多的放在後面,這樣空閒數量少的更容易被移去cpu的部分空連結串列。其實思想就是讓那些更容易成為滿slab的部分空slab優先被使用。總結出來就是釋放空閒slab和對部分空slab排序。

  我們知道,在node結點的部分空連結串列中,slab緩衝區數量少於kmem_cache中的min_partial的值時,即使空閒slab緩衝區也不會被釋放,而是放入node結點部分空連結串列中,這樣一來之後會有一些空閒slab緩衝區無法自動釋放回夥伴系統,壓縮技術就是在系統記憶體緊急時會去釋放這些空閒的夥伴系統,然後對其他部分空的slab緩衝區重新排列。程式碼如下:

int __kmem_cache_shrink(struct kmem_cache *s)
{
    int node;
    int i;
    struct kmem_cache_node *n;
    struct page *page;
    struct page *t;
    /* 所有slab緩衝區的最大物件數量 */
    int objects = oo_objects(s->max);
    /* 申請objects個連結串列頭,每個inuse相同的slab緩衝區會放入對應的連結串列中 */
    struct list_head *slabs_by_inuse =
        kmalloc(sizeof(struct list_head) * objects, GFP_KERNEL);
    unsigned long flags;

    if (!slabs_by_inuse)
        return -ENOMEM;

    /* 重新整理這個kmem_cache中所有的slab,這個操作會將所有CPU中的slab放回到node結點的部分空連結串列中 */
    flush_all(s);
    
    /* 變數kmem_cache中的每個node結點 */
    for_each_kmem_cache_node(s, node, n) {
        /* node結點部分空連結串列為空則直接下一個結點 */
        if (!n->nr_partial)
            continue;

        /* node結點部分空連結串列不為空,初始化slabs_by_inuse連結串列中每個連結串列頭結點 */
        for (i = 0; i < objects; i++)
            INIT_LIST_HEAD(slabs_by_inuse + i);

        /* kmem_cache_node上鎖 */
        spin_lock_irqsave(&n->list_lock, flags);

        /* 遍歷node結點部分空連結串列中所有的部分空slab緩衝區 */
        list_for_each_entry_safe(page, t, &n->partial, lru) {
            /* 將node結點中所有的部分空slab緩衝區移到slabs_by_inuse中inuse連結串列中,也就是所有inuse=1的slab放入同一個連結串列,inuse=2的放入同一個連結串列 */
            list_move(&page->lru, slabs_by_inuse + page->inuse);
            /* 如果inuse == 0,則node結點的部分空slab數量-- */
            if (!page->inuse)
                n->nr_partial--;
        }

        /* 重建node結點的部分空連結串列,將slabs_by_inuse中inuse高的放在前面,inuse低的放在後面,讓inuse高的更容易得到分配機會,也就是讓inuse高的更快用完 */
        for (i = objects - 1; i > 0; i--)
            list_splice(slabs_by_inuse + i, n->partial.prev);

        spin_unlock_irqrestore(&n->list_lock, flags);

        /* 如果有空的slab緩衝區,空的slab緩衝區儲存在slabs_by_inuse + 0的連結串列位置,釋放他們 */
        list_for_each_entry_safe(page, t, slabs_by_inuse, lru)
            discard_slab(s, page);
    }
    /* 釋放objects個連結串列頭 */
    kfree(slabs_by_inuse);
    return 0;
}
【作者】張昺華 【出處】http://www.cnblogs.com/sky-heaven/ 【部落格園】 http://www.cnblogs.com/sky-heaven/ 【知乎】 http://www.zhihu.com/people/zhang-bing-hua 【我的作品---旋轉倒立擺】 http://v.youku.com/v_show/id_XODM5NDAzNjQw.html?spm=a2hzp.8253869.0.0&from=y1.7-2 【我的作品---自平衡自動循跡車】 http://v.youku.com/v_show/id_XODM5MzYyNTIw.html?spm=a2hzp.8253869.0.0&from=y1.7-2 【大餅教你學系列】https://edu.csdn.net/course/detail/10393 【新浪微博】 張昺華--sky 【twitter】 @sky2030_ 【微信公眾號】 張昺華 本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利.