1. 程式人生 > >linux內存源碼分析 - SLAB分配器概述

linux內存源碼分析 - SLAB分配器概述

image 問題 src 效率 單鏈表 應該 內部 class 引用

本文為原創,轉載請註明:http://www.cnblogs.com/tolimit/

之前說了管理區頁框分配器,這裏我們簡稱為頁框分配器,在頁框分配器中主要是管理物理內存,將物理內存的頁框分配給申請者,而且我們知道也可頁框大小為4K(也可設置為4M),這時候就會有個問題,如果我只需要1KB大小的內存,頁框分配器也不得不分配一個4KB的頁框給申請者,這樣就會有3KB被白白浪費掉了。為了應對這種情況,在頁框分配器上一層又做了一層SLAB層,SLAB分配器的作用就是從頁框分配器中拿出一些頁框,專門把這些頁框拆分成一小塊一小塊的小內存,當申請者申請的是小內存時,系統就會從SLAB中獲取一小塊分配給申請者。它們的整個關系如下圖:

技術分享圖片

可以看出,SLAB分配器和頁框分配器並沒有什麽直接的聯系,對於頁框分配器來說,SLAB分配器也只是一個從它那裏申請頁框的申請者而已。

在SLAB分配器中將SLAB分為兩大類:專用SLAB和普通SLAB。專用SLAB用於特定的場合(比如TCP有自己專用的SLAB,當TCP模塊需要小內存時,會從自己的SLAB中分配),而普通SLAB就是用於常規分配的時候。我們可以使用命令查看SLAB的狀態

cat /proc/slabinfo

命令結果如下:

技術分享圖片

  如剛才所有,我們看到有些SLAB的名字比較特別,如TCP,UDP,dquot這些,它們都是專用SLAB,專屬於它們自己的模塊。而後面這張圖,如kmalloc-8

kmalloc-16...還有dma-kmalloc-96,dma-kmalloc-192...這些都是普通SLAB,當需要為一些小數據分配內存時(比如一個結構體),就會從這些普通SLAB中獲取內存。值得註意的是,對於kmalloc-8這些普通SLAB,都有一個對應的dma-kmalloc-8這種類型的普通SLAB,這種類型是專門使用了ZONE-DMA區域的內存,方便用於DMA模式申請內存。

kmem_cache結構

  雖然叫SLAB分配器,但是在SLAB分配器中,最頂層的數據結構卻不是SLAB,而是kmem_cache,我們暫且叫它SLAB緩存吧,每個SLAB緩存都有它自己的名字,就是上圖中的kmalloc-8

kmalloc-16等。總的來說,kmem_cache結構用於描述一種SLAB,並且管理著這種SLAB中所有的對象。所有的kmem_cache結構會保存在以slab_caches作為頭的鏈表中。在內核模塊中可以通過kmem_cache_create自行創建一個kmem_cache用於管理屬於自己模塊的SLAB。
  我們先看看kmem_cache結構:

/* slab分配器中的SLAB高速緩存 */
struct kmem_cache {
    /* 指向包含空閑對象的本地高速緩存,每個CPU有一個該結構,當有對象釋放時,優先放入本地CPU高速緩存中 */
    struct array_cache __percpu *cpu_cache;

/* 1) Cache tunables. Protected by slab_mutex */
    /* 要轉移進本地高速緩存或從本地高速緩存中轉移出去的對象的數量 */
    unsigned int batchcount;
    /* 本地高速緩存中空閑對象的最大數目 */
    unsigned int limit;
    /* 是否存在CPU共享高速緩存,CPU共享高速緩存指針保存在kmem_cache_node結構中 */
    unsigned int shared;

    /* 對象長度 + 填充字節 */
    unsigned int size;
    /* size的倒數,加快計算 */
    struct reciprocal_value reciprocal_buffer_size;

    
/* 2) touched by every alloc & free from the backend */
    /* 高速緩存永久屬性的標識,如果SLAB描述符放在外部(不放在SLAB中),則CFLAGS_OFF_SLAB置1 */
    unsigned int flags;        /* constant flags */
    /* 每個SLAB中對象的個數(在同一個高速緩存中slab中對象個數相同) */
    unsigned int num;        /* # of objs per slab */


/* 3) cache_grow/shrink */
    /* 一個單獨SLAB中包含的連續頁框數目的對數 */
    unsigned int gfporder;

    /* 分配頁框時傳遞給夥伴系統的一組標識 */
    gfp_t allocflags;

    /* SLAB使用的顏色個數 */
    size_t colour;            
    /* SLAB中基本對齊偏移,當新SLAB著色時,偏移量的值需要乘上這個基本對齊偏移量,理解就是1個偏移量等於多少個B大小的值 */
    unsigned int colour_off;    
    /* 空閑對象鏈表放在外部時使用,其指向的SLAB高速緩存來存儲空閑對象鏈表 */
    struct kmem_cache *freelist_cache;
    /* 空閑對象鏈表的大小 */
    unsigned int freelist_size;

    /* 構造函數,一般用於初始化這個SLAB高速緩存中的對象 */
    void (*ctor)(void *obj);


/* 4) cache creation/removal */
    /* 存放高速緩存名字 */
    const char *name;
    /* 高速緩存描述符雙向鏈表指針 */
    struct list_head list;
    int refcount;
    /* 高速緩存中對象的大小 */
    int object_size;
    int align;


/* 5) statistics */
    /* 統計 */
#ifdef CONFIG_DEBUG_SLAB
    unsigned long num_active;
    unsigned long num_allocations;
    unsigned long high_mark;
    unsigned long grown;
    unsigned long reaped;
    unsigned long errors;
    unsigned long max_freeable;
    unsigned long node_allocs;
    unsigned long node_frees;
    unsigned long node_overflow;
    atomic_t allochit;
    atomic_t allocmiss;
    atomic_t freehit;
    atomic_t freemiss;

    /* 對象間的偏移 */
    int obj_offset;
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_MEMCG_KMEM
    /* 用於分組資源限制 */
    struct memcg_cache_params *memcg_params;
#endif
    /* 結點鏈表,此高速緩存可能在不同NUMA的結點都有SLAB鏈表 */
    struct kmem_cache_node *node[MAX_NUMNODES];
};

從結構中可以看出,在這個kmem_cache中所有對象的大小是相同的(object_size),並且此kmem_cache中所有SLAB的大小也是相同的(gfporder、num)。

  在這個結構中,最重要的可能就屬struct kmem_cache_node * node[Max_NUMNODES]這個指針數組了,指向的struct kmem_cache_node中保存著slab鏈表,在NUMA架構中每個node對應數組中的一個元素,因為每個SLAB高速緩存都有可能在不同結點維護有自己的SLAB用於這個結點的分配。我們看看struct kmem_cache_node:

/* SLAB鏈表結構 */
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
    atomic_long_t nr_slabs;
    atomic_long_t total_objects;
    struct list_head full;
#endif
#endif

};

在這個結構中,最重要的就是slabs_partialslabs_fullslabs_free這三個鏈表頭。

  • slabs_partial:維護部分對象被使用了的SLAB鏈表,保存的是SLAB描述符。
  • slabs_full:維護所有對象都被使用了的SLAB鏈表,保存的是SLAB描述符。
  • slabs_free:維護所有對象都沒被使用的SLAB鏈表,保存的是SLAB描述符。

  可能到這裏大家會比較郁悶,怎麽又有SLAB鏈表,SLAB到底是什麽東西?如果看了我linux內存源碼分析 - 頁框分配器的朋友,或許可以聯系起來了。SLAB就是一組連續的頁框,它的描述符結合在頁描述符中,也就是頁描述符描述SLAB的時候,就是SLAB描述符。這三個鏈表保存的是這組頁框的首頁框的SLAB描述符。鏈表的組織形式與夥伴系統的組織頁框的形式一樣。

  剛開始創建kmem_cache完成後,這三個鏈表都為空,只有在申請對象時發現沒有可用的slab時才會創建一個新的SLAB,並加入到這三個鏈表中的一個中。也就是說kmem_cache中的SLAB數量是動態變化的,當SLAB數量太多時,kmem_cache會將一些SLAB釋放回頁框分配器中。

我們看看SLAB描述符中相關字段:

struct page {
    /* First double word block */
    /* 用於頁描述符,一組標誌(如PG_locked、PG_error),也對頁框所在的管理區和node進行編號 */
    unsigned long flags; /
    union {
        /* 用於頁描述符,當頁被插入頁高速緩存中時使用,或者當頁屬於匿名區時使用 */
        struct address_space *mapping; 
        /* 用於SLAB描述符,指向第一個對象的地址 */
        void *s_mem;            /* slab first object */
    };


    /* Second double word */
    struct {
        union {
            /* 作為不同的含義被幾種內核成分使用。例如,它在頁磁盤映像或匿名區中標識存放在頁框中的數據的位置,或者它存放一個換出頁標識符 */
            pgoff_t index;        /* Our offset within mapping. */
            /* 用於SLAB描述符,指向空閑對象鏈表 */
            void *freelist;    
            /* 當管理區頁框分配器壓力過大時,設置這個標誌就確保這個頁框專門用於釋放其他頁框時使用 */
            bool pfmemalloc; 
        };

        union {
#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) &&     defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
            /* Used for cmpxchg_double in slub */
            /* SLUB使用 */
            unsigned long counters;
#else
            /* SLUB使用 */
            unsigned counters;
#endif

            struct {

                union {
                    /* 頁框中的頁表項計數,如果沒有為-1,如果為PAGE_BUDDY_MAPCOUNT_VALUE(-128),說明此頁及其後的一共2的private次方個數頁框處於夥伴系統中,正在使用時應該是0 */
                    atomic_t _mapcount;

                    struct { /* SLUB使用 */
                        unsigned inuse:16;
                        unsigned objects:15;
                        unsigned frozen:1;
                    };
                    int units;    /* SLOB */
                };
                /* 頁框的引用計數,如果為-1,則此頁框空閑,並可分配給任一進程或內核;如果大於或等於0,則說明頁框被分配給了一個或多個進程,或用於存放內核數據。page_count()返回_count加1的值,也就是該頁的使用者數目 */
                atomic_t _count;        /* Usage count, see below. */
            };
            /* 用於SLAB時描述當前SLAB已經使用的對象 */
            unsigned int active;    /* SLAB */
        };
    };


    /* Third double word block */
    union {
        /* 包含到頁的最近最少使用(LRU)雙向鏈表的指針,用於插入夥伴系統的空閑鏈表中,只有塊中頭頁框要被插入。也用於SLAB,加入到kmem_cache中的SLAB鏈表中 */
        struct list_head lru;    


        /* SLAB使用 */
        struct {        /* slub per cpu partial pages */
            struct page *next;    /* Next partial slab */
#ifdef CONFIG_64BIT
            int pages;    /* Nr of partial slabs left */
            int pobjects;    /* Approximate # of objects */
#else
            short int pages;
            short int pobjects;
#endif
        };

        /* SLAB使用 */
        struct slab *slab_page; /* slab fields */
        struct rcu_head rcu_head;    /* Used by SLAB
                         * when destroying via RCU
                         */
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && USE_SPLIT_PMD_PTLOCKS
        pgtable_t pmd_huge_pte; /* protected by page->ptl */
#endif
    };


    /* Remainder is not double word aligned */
    union {
        /* 可用於正在使用頁的內核成分(例如: 在緩沖頁的情況下它是一個緩沖器頭指針,如果頁是空閑的,則該字段由夥伴系統使用,在給夥伴系統使用時,表明的是塊的2的次方數,只有塊的第一個頁框會使用) */
        unsigned long private;        
#if USE_SPLIT_PTE_PTLOCKS
#if ALLOC_SPLIT_PTLOCKS
        spinlock_t *ptl;
#else
        spinlock_t ptl;
#endif
#endif
        /* SLAB描述符使用,指向SLAB的高速緩存 */
        struct kmem_cache *slab_cache;    /* SL[AU]B: Pointer to slab */
        struct page *first_page;    /* Compound tail pages */
    };

#if defined(WANT_PAGE_VIRTUAL)
    /* 線性地址,如果是沒有映射的高端內存的頁框,則為空 */
    void *virtual;            /* Kernel virtual address (NULL if
                       not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
    unsigned long debug_flags;    /* Use atomic bitops on this */
#endif

#ifdef CONFIG_KMEMCHECK
    void *shadow;
#endif

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
    int _last_cpupid;
#endif
}

  在SLAB描述符中,最重要的可能就是s_mem和freelist這兩個指針。s_mem用於指向這段連續頁框中第一個對象,freelist指向空閑對象鏈表。

  空閑對象鏈表是一個由數組制成的簡單鏈表,它保存的地方有兩種情況:

  •   空閑對象鏈表是一個由數組制成的簡單鏈表,它保存的地方有兩種情況:
  • 保存在內部,保存在這個SLAB所代表的連續頁框的頭部。

  不過一般沒有什麽其他情況空閑對象鏈表都是保存在內部居多,這裏我們只討論將空閑對象鏈表保存在內部的情況,這種情況下,這個SLAB所代表的連續頁框的頭部首先放的就是空閑對象鏈表,後面接著放的是對象描述符數組(1,2個字節大小),之後緊接著就是對象所代表的內存了,如下圖:

技術分享圖片

  我們看看freelist數組是怎麽形成一個鏈表的,之前我們也說了分配時會優先分配最近釋放的對象,整個freelist跟struct page中的active有很大聯系,可以說active決定了下個分配的對象是誰,在freelist數組制作成的鏈表中,active作為下標,保存目標空閑對象的對象號,在活動過程中,動態修改這個數組中的值。我們用一幅圖可以很清楚看出freelist是如何實現:

技術分享圖片
  SLAB中的連續頁框個數與kmem_cache結構中的gfporder有關,而這個gfporder在初始化時通過對象數量、大小、freelist大小、對象描述符數組大小和著色區計算出來的。而對於對象的大小,也並不是你創建時打算使用的大小,比如,我打算創建一個kmem_cache的對象大小是10字節,而在創建過程中,系統會幫你優化和初始化這些對象,包括將你的對象保存地址放在內存對其標誌,在對象的兩邊放入一些填充區域(RED_ZONE)進行防止越界等工作。

關於SLAB著色

  看名字很難理解,其實又很好理解,我們知道內存需要處理時要先放入CPU硬件高速緩存中,而CPU硬件高速緩存與內存的映射方式有多種。在同一個kmem_cache中所有SLAB都是相同大小,都是相同連續長度的頁框組成,這樣的話在不同SLAB中相同對象號對於頁框的首地址的偏移量也相同,這樣有很可能導致不同SLAB中相同對象號的對象放入CPU硬件高速緩存時會處於同一行,當我們交替操作這兩個對象時,CPU的cache就會交替換入換出,效率就非常差。SLAB著色就是在同一個kmem_cache中對不同的SLAB添加一個偏移量,就讓相同對象號的對象不會對齊,也就不會放入硬件高速緩存的同一行中,提高了效率,如下圖:

技術分享圖片

  著色空間就是前端的空閑區域,這個區有大小都是在分配新的SLAB時計算好的,計算方法很簡單,node結點對應的kmem_cache_node中的colour_next乘上kmem_cache中的colour_off就得到了偏移量,然後colour_next++,當colour_next等於kmem_cache中的colour時,colour_next回歸到0。

偏移量 = kmem_cache.colour_off * kmem_cache.node[NODE_ID].colour_next;

    kmem_cache.node[NODE_ID].colour_next++;
    if (kmem_cache.node[NODE_ID].colour_next == kmem_cache.colour)
        kmem_cache.node[NODE_ID].colour_next = 0;

本地CPU空閑對象鏈表

  現在說說本地CPU空閑對象鏈表。這個在kmem_cache結構中用cpu_cache表示,整個數據結構是struct array_cache,它的目的是將釋放的對象加入到這個鏈表中,我們可以先看看數據結構:

struct array_cache {
    /* 可用對象數目 */
    unsigned int avail;
    /* 可擁有的最大對象數目,和kmem_cache中一樣 */
    unsigned int limit;
    /* 同kmem_cache,要轉移進本地高速緩存或從本地高速緩存中轉移出去的對象的數量 */
    unsigned int batchcount;
    /* 是否在收縮後被訪問過 */
    unsigned int touched;
    /* 偽數組,初始沒有任何數據項,之後會增加並保存釋放的對象指針 */
    void *entry[];    /*
};

因為每個CPU都有它們自己的硬件高速緩存,當此CPU上釋放對象時,可能這個對象很可能還在這個CPU的硬件高速緩存中,所以內核為每個CPU維護一個這樣的鏈表,當需要新的對象時,會優先嘗試從當前CPU的本地CPU空閑對象鏈表獲取相應大小的對象。這個本地CPU空閑對象鏈表在系統初始化完成後是一個空的鏈表,只有釋放對象時才會將對象加入這個鏈表。當然,鏈表對象個數也是有所限制,其最大值就是limit,鏈表數超過這個值時,會將batchcount個數的對象返回到所有CPU共享的空閑對象鏈表(也是這樣一個結構)中。

  註意在array_cache中有一個entry數組,裏面保存的是指向空閑對象的首地址的指針,註意這個鏈表是在kmem_cache結構中的,也就是kmalloc-8有它自己的本地CPU高速緩存鏈表,dquot也有它自己的本地CPU高速緩存鏈表,每種類型kmem_cache都有它自己的本地CPU空閑對象鏈表。

所有CPU共享的空閑對象鏈表

  原理和本地CPU空閑對象鏈表一樣,唯一的區別就是所有CPU都可以從這個鏈表中獲取對象,一個常規的對象申請流程是這樣的:系統首先會從本地CPU空閑對象鏈表中嘗試獲取一個對象用於分配;如果失敗,則嘗試來到所有CPU共享的空閑對象鏈表鏈表中嘗試獲取;如果還是失敗,就會從SLAB中分配一個;這時如果還失敗,kmem_cache會嘗試從頁框分配器中獲取一組連續的頁框建立一個新的SLAB,然後從新的SLAB中獲取一個對象。對象釋放過程也類似,首先會先將對象釋放到本地CPU空閑對象鏈表中,如果本地CPU空閑對象鏈表中對象過多,kmem_cache會將本地CPU空閑對象鏈表中的batchcount個對象移動到所有CPU共享的空閑對象鏈表鏈表中,如果所有CPU共享的空閑對象鏈表鏈表的對象也太多了,kmem_cache也會把所有CPU共享的空閑對象鏈表鏈表中batchcount個數的對象移回它們自己所屬的SLAB中,這時如果SLAB中空閑對象太多,kmem_cache會整理出一些空閑的SLAB,將這些SLAB所占用的頁框釋放回頁框分配器中。

  這個所有CPU共享的空閑對象鏈表也不是肯定會有的,kmem_cache中有個shared字段如果為1,則這個kmem_cache有這個高速緩存,如果為0則沒有。

總結

  整個框架已經說明結束了,我們用一幅圖進行整理:
技術分享圖片

linux內存源碼分析 - SLAB分配器概述