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

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


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

  命令結果如下:

命令結果如下:

如剛纔所有,我們看到有些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模式申請內存。

  在SLAB中,可分配的內存塊稱之爲對象,在後面那張圖中,如kmalloc-8這個普通SLAB,裏面所有的對象都是8B大小,同理,kmalloc-16中的對象都是以16B爲大小。當你申請1B~8B的內存時,系統會從kmalloc-8中分配一個對象給你,當你申請8B~16B的內存時,系統會從kmalloc-16裏給你分配。雖然即使申請5B,分配了一個8B的對象,還有3B空閒,但這樣設計已經大大減小了內存碎片化了,保證了碎片內存不會超過50%(kmalloc-8除外)。需要注意,在kmalloc-8中申請到的對象,釋放時也會回到kmalloc-8中。

  除了減小了內存碎片化,SLAB還有一個作用,提高了系統的效率,當對象擁有者釋放一個對象後,SLAB的處理是僅僅標記對象爲空閒,並不做多少處理,而又有申請者申請相應大小的對象時,SLAB會優先分配最近釋放的對象,這樣這個對象甚至有可能還在硬件高速緩存中,有點類似管理區頁框分配器中每CPU高速緩存的做法。

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

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

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

  • 保存在外部,會從SLAB中分配一個對象用於保存新的SLAB的空閒對象鏈表。
  • 保存在內部,保存在這個SLAB所代表的連續頁框的頭部。

本地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則沒有。





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