Linux內存管理第八章 -- Slab Allocator (一)

Linux內存管理第八章 – Slab Allocator

在本章中,來介紹內核中更加通用的分配器。Linux使用的slab allocator與用在Solaris中使用的通用分配器有許多相似之處。Linux的實現基本上是在Bonwick的the first allocator論文的基礎上進行了一些改進而這些改進與他後續的論文中的描述十分相似。在深入探討slab allocator的每個部分之前先來通過一段描述快速瀏覽下slab allocator所使用的數據結構。

slab allocator背後的思想是緩存經常使用的object並保持在初始狀態供kernel使用。如果被基於object的allocator,內核將耗費很多時間在分配,初始化和釋放相同的object。slab allocator的目的就是緩存一些被釋放的object因此這些基礎的structures在多次調用期間被預留起來。

slab allocator由一組cache組成,這些cache由一個叫做cache chain的雙向循環鏈表連接在一起。在slab allocator的上下文件,一個cache就是一個管理許多像mm_struct或者fs_cache這種特殊類型的object的管理者。這些cache通過cache struct的next字段連接在一起。

每一cache維護了有多個連續物理page組成的block,這些block稱之爲slab。slab被切分成很多小塊來存放slab自身的數據結構和其管理的object。他們之前的關係如下圖:
1212
slab allocator有三個基本目標:

  • 更小塊的內存分配可以幫忙消除buddy allocator原本會造成的內部碎片問題。
  • 緩存常用的object因此係統不會在分配,初始化和銷燬object上浪費時間。在Solaris上的Benchmarks顯示使用slab allocator之後對於分配速度有很大的提升。
  • 通過將object地址與L1或者L2 cache對齊後可以更好的利用硬件緩存。

爲了幫助消除buddy allocator造成的內部碎片問題,系統有維護兩個cache集合,這些cache由細小的內存塊組成,其大小從25字節到217
其中一個cache集合供DMA設備使用。這些cache叫做size-N和size-N(DMA)m其中N是指要分配的內存大小。可以調用kmallock()來分配這些cache中的memory。這樣就解決了buddy allocator帶來的low level page中的內部碎片問題。也就是如果只分配幾個字節,如果沒有slab allocator的話,也需要分配一個page。

slab allocator的第二個任務是維護一些cache來分配常用的object。在內核中使用的許多結構體,初始化的時間甚至超過了分配時間。因此當一個新的slab被創建的時候,多個object被構造函數初始化後打包放入到slab中。如果一個object被釋放了,它仍然以初始狀態存放在slab中以以便object的分配能夠加快。

slab allocator最後一個任務是利用硬件緩存。如果object打包放入slab後還有剩餘空間,這些剩餘空間將被用來將slab着色。slab着色是一種嘗試讓在不同slab中的object在硬件cache中使用不同的行的方案。在不同的slab中將object以不同的起始偏移來進行擺放,這就好像這些object在使用CPU硬件緩存的不同行一樣來幫助保證從同一個slab分配出來的object不會相互從CPU硬件緩存中被刷掉。使用這種方案後,原本要浪費的空間被添加上了一種新的功能。下圖顯示了從buddy allocator中分配出來的一個page如何被用來存儲與L1 CPU緩存對齊的object。
1234
如果在編譯階段CONFIG_SLAB_DEBUG被打開,slab allocator提供了附加的slab debug功能。有兩個debug功能:red zoning和object poisoning。使用red zoning後,object的兩端會被放置水位標記。如果該水位不正常,分配器就知道當前object在分配的時候發生了buffer溢出的問題並立即上報。poisoning一個object是指在slab創建時和object釋放時將object填充預定義好的bit pattern。在分配的時候,該pattern會被檢查如果該pattern被修改了,allocator就知道該object在分配之前已經使用過了並標記它。

slab allocator提供了短小精悍的API,如下表:

kmem_cache_t * kmem_cache_create(const char *name, size_t size, size_t offset, unsigned long flags, void (ctor)(void, kmem_cache_t *, unsigned long),void (dtor)(void, kmem_cache_t *, unsigned long))
創建一個新的cache並將它加入到cache chain中
int kmem_cache_reap(int gfp_mask)
最多掃描REAP_SCAN個cache並選擇其中一個來回收所有的per-cpu objects然後將該cache中的slab釋放。該函數在memory很緊張時被調用
int kmem_cache_shrink(kmem_cache_t *cachep)
該函數將刪除一個cache中所有的per-cpu objects,並刪除slabs_free list中的所有slabs,然後返回釋放的page數目
void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)
從cache中分配單個object並將object返回給調用者
void kmem_cache_free(kmem_cache_t *cachep, void *objp)
釋放一個object並將它返還到cache中
void * kmalloc(size_t size, int flags)
從匿名cache中分配一塊memory
void kfree(const void *objp)
將kmalloc分配的內存塊釋放
int kmem_cache_destroy(kmem_cache_t * cachep)
銷燬cache中的所有slab中的所有object並釋放與之相關的memory然後再將該cache從cache chain中剔除

Caches

每個cache只能緩存一種類型的object,也就是cache中有多個slab,每個slab中有多個object,但每個object的類型都相同。在一個正在運行的Linux系統中,可以通過cat /proc/slabinfo 來查看當前所有可用的caches列表。/proc/slabinfo文件中給出了cache的一些基本信息。下面來看一下該文件內容的摘要:

slabinfo - version: 1.1 (SMP)

cache-name num-active-objs total-objs obj-size num-active-slabs total-slabs num-pages-per-slab limit batchcount
kmem_cache 80 80 248 5 5 1 : 252 126
urb_priv 0 0 64 0 0 1 : 252 126
tcp_bind_bucket 15 226 32 2 2 1 : 252 126
inode_cache 5714 5992 512 856 856 1 : 124 62
dentry_cache 5160 5160 128 172 172 1 : 252 126
mm_struct 240 240 160 10 10 1 : 252 126
vm_area_struct 3911 4480 96 112 112 1 : 252 126
size-64(DMA) 0 0 64 0 0 1 : 252 126
size-64 432 1357 64 23 23 1 : 252 126
size-32(DMA) 17 113 32 1 1 1 : 252 126
size-32 850 2712 32 24 24 1 : 252 126

其中每一列的含義如下:

  • cache-name:人類可讀的名稱如tcp_bind_bucket
  • num-active-objs:正在使用中的objects的數據
  • total-objs:包括沒有使用的object,object的總數量
  • obj-size:每一個object的大小,通常都非常小
  • num-active-slabs:包含有active object的slab數目
  • total-slabs:cache中所有slab的數目
  • num-pages-per-slab:在創建一個slab時要求申請的page數目。通常是1
  • limit:SMP架構中,在將pool中一半的free objects給到全局的pool之前,pool中可以擁有free objects的數目(不理解)
  • batchcount:SMP架構中,當沒有free object時,一個block中的processor可以分配的object數目(不理解)

爲了加快分配和釋放object,slab被放入到三個list:

  • slabs_full slab中的所有object都在使用
  • slabs_partial slab中有free objects,下次分配該object時,該slab是候選者
  • slabs_free slab中所有的object都沒有被分配,該slab也是要被銷燬的候選者

Cache Descriptor

struct kmem_cache_s {
/* 1) per-cpu data, touched during every alloc/free */
	struct array_cache	*array[NR_CPUS];
	unsigned int		batchcount;
	unsigned int		limit;
/* 2) touched by every alloc & free from the backend */
	struct kmem_list3	lists;
	/* NUMA: kmem_3list_t	*nodelists[MAX_NUMNODES] */
	unsigned int		objsize; //slab中object的size
	unsigned int	 	flags;	/* constant flags,這些標記位將影響slab allocator的行爲,後面會詳細描述 */
	unsigned int		num;	/* # of objs per slab,每個slab中包含object的個數 */
	unsigned int		free_limit; /* upper limit of objects in the lists */
	spinlock_t		spinlock;

/* 3) cache_grow/shrink */
	/* order of pgs per slab (2^n) */
	unsigned int		gfporder;//表示每個slab佔用page個數,每個slab消耗2^gfpflags 個pages

	/* force GFP flags, e.g. GFP_DMA */
	unsigned int		gfpflags;//slab從buddy allocator分配page時所使用的GFP flag

	size_t			colour;		/* cache colouring range, 每個object儘可能存放不同的cache line中*/
	unsigned int		colour_off;	/* colour offset,在slab中爲了對齊所佔用的byte */
	unsigned int		colour_next;	/* cache colouring,下一個要使用的colour line,如果達到colour大小則從0再開始 */
	kmem_cache_t		*slabp_cache;
	unsigned int		slab_size;
	unsigned int		dflags;		/* dynamic flags */

	/* constructor func, object的構造函數*/
	void (*ctor)(void *, kmem_cache_t *, unsigned long);

	/* de-constructor func,object的析構函數 */
	void (*dtor)(void *, kmem_cache_t *, unsigned long);

/* 4) cache creation/removal */
	const char		*name; //人類可讀的名稱
	struct list_head	next;指向cache chain中的下一個cache

/* 5) statistics */
#if STATS
	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;
	atomic_t		allochit;
	atomic_t		allocmiss;
	atomic_t		freehit;
	atomic_t		freemiss;
#endif
#if DEBUG
	int			dbghead;
	int			reallen;
#endif
};

Cache Static Flags

cache創建時的一些flag集合在cache整個生命週期中都是不變的。這些flag會影響slab的內部結構以及object在slab中如何存放。所有的這些flag存放在cache描述符的flags字段,以bitmask的形式組合使用。<linux/slab.h>中定義了全部可能用到的flag。

當前有三個基本的flag集合。第一個集合是internal flags,這些flag僅僅被slab allocator自己使用。在此集合中只有一個flag:CFGS_OFF_SLAB起決定了slab描述符如何存放。

Flag Description
CFGS_OFF_SLAB 置位表示該cache的slab manager和slab是分離的
CFGS_OPTIMEZE 該flag沒有用到

第二組flag集合是通過cache creator設置,這些flag決定了allocator如何對待slab以及slab中的object如何存放,見下表:

Flag Description
SLAB_HWCACHE_ALIGN 將object地址L1 CPU cache對齊
SLAB_MUST_HWCACHE_ALIGN 強制與L1 CPU cache對齊儘管可能會造成很多浪費或者slab debug被打開
SLAB_NO_REAP 從不回收cache中的此slab
SLAB_CACHE_DMA 從ZONE_DMA中分配slab
SLAB_STORE_USER 該flag純粹是一個debug flag,用來記錄釋放object的函數。如果一個object使用後被釋放,那麼它的poison byte將不匹配並且此時內核會呈現error message。因爲上一個使用該object的函數被記錄下來,那麼該問題將很好debug
SLAB_RECLAIM_ACCOUNT 該flag用於那些好回收object的cache,例如inode cache。一個名爲slab_reclaim_pages的變量用於記錄cache中的slab總共使用了多少個page。該計數將在後續的vm_enough_memory()中被使用來幫助決斷系統是否有真的out of memory

第三組flag會對slab和object執行一些附加的檢查項,這些檢查項只有在剛開發該cache時才顯得比較有價值。這些flag只有在CONFIG_SLAB_DEBUG宏被打開時纔有效。

Flag Description
SLAB_DEBUG_FREE 執行比較昂貴的操作來檢查free slab
SLAB_DEBUG_INITIAL 當釋放一個object時,調用構造函數檢查一下來保證object仍然是被正確地初始化
SLAB_RED_ZONE 在object的首位放置標記來追蹤內存覆蓋
SLAB_POISON 在向未分配或者未初始化的object中添加固定的樣式字節來追蹤object的狀態是否正確被改變

爲了阻止調用者使用錯誤的flag,在mm/slab.c中定義了一個CREATE_MASK,它有所有被允許的flag組合而成。當cache正在被創建時,被使用的flag將和CREATE_MASK進行對比,如果發現非法的flag將會向上報告錯誤。

Cache Dynamic Flags

dflag字段只有一個flag,DFLGS_GROWN,但是它非常重要。在kmem_cache_grow()中該flag被置位,因此kmem_cache_reap()將不會選擇該cache回收內存。當kmem_cache_reap()發現了一個DFLGS_GROWN被置位的cache,會先將該cache的DFLGS_GROWN flag清楚然後跳過該cache。

Cache Allocation Flags

allocation flags與之對應的是GFP page flags選項,用來從buddy allocator中爲slab分配memory。調用這個有時候使用SLAB_* 開頭的flags有時候又使用GFP_* flags。但是他們應該只是用SLAB_* flags。與之對應的GFP flag的含義在前一張buddy allocator中有詳細描述。SLAB_* 這些flag存在的原因是之前假設SLAB_*與現有的GFP flag可能不能完全匹配,功能上有些不同。但是實際上它們之間沒有區別。

Flag Description
SLAB_ATOMIC Equivalent to GFP_ATOMIC
SLAB_DMA Equivalent to GFP_DMA
SLAB_KERNEL Equivalent to GFP_KERNEL
SLAB_NFS Equivalent to GFP_NFS
SLAB_NOFS Equivalent to GFP_NOFS
SLAB_NOHIGHIO Equivalent to GFP_NOHIGHIO
SLAB_NOIO Equivalent to GFP_NOIO
SLAB_USER Equivalent to GFP_USER

還有一些少量的flags被傳入到object的構造函數和析構函數中:

Flag Description
SLAB_CTOR_CONSTRUCTOR 如果該標誌被置位,則傳入到cache中的構造函數既是構造函數又是析構函數
SLAB_CTOR_ATOMIC 表示構造函數不能睡眠
SLAB_CTOR_VERIFY 表示構造函數僅僅是用來驗證object有沒有被正確地初始化

Cache Colouring

爲了更好地利用硬件緩存,slab allocator在不同的slab中使用不同大小的offset來間隔object,具體的offset大小取決於slab中最後剩餘的memory大小是多少。offset的單位是BYTES_PER_WORD,但是在SLAB_HWCACHE_ALIGN被開啓時,object將要與L1_CACHE_BYTES對齊。
在cache創建時,會計算一個slab適合放置多少個object,多少字節將會被浪費。基於要浪費的memory大小,會計算出cache描述符中的下面兩個數字:

  • colour:當前cache中的slab可以使用的offset大小
  • colour_off:這是offset每個object的倍數

有了object offset之後,它們將使用對用硬件緩存中不同的lines。因此slab中的object幾乎不會出現相互覆蓋的狀況。
下面來通過一個例子來解釋下上述原理。假設一個slab中第一個object的地址s_mem,爲方便描述,s_mem = 0,這個slab中有100個字節要浪費,並且要與L1 cache執行32字節對齊。
在上述背景下,第一個被創建的slab中,它的objects從0開始擺放,第二個slab是從32字節開始擺放object,第三個slab從64字節開始擺放object,而第四個slab從96字節開始擺放object,然後第五個slab又從0開始擺放slab。通過這樣的安排,從每一個object中分配出來的object都會命中不同的CPU硬件緩存。此時變量colour的值爲3,而變量colour_off爲32。

Cache Creation

kmem_cache_create()函數的功能是創建新caches並將它們加入到cache chain中。創建cache過程中需要做的事情如下:

  • 執行一些基本的重要的檢查放置使用不當的情況
  • 如果CONFIG_SLAB_DEBUG被置位,執行debug相關的一些檢查項
  • 從名爲cache_cache的全局slab cache中分配一個kmem_cache_t結構體。
  • 將object的大小按字大小對齊
  • 計算一個slab上放置多少個object比較合適
  • 將object的大小與硬件緩存對齊
  • 計算着色偏移
  • 初始化slab cache描述符中剩餘字段
  • 將新cache加入到cache chain中。

下圖展示了與cache創建相關的調用圖。
12ssdf

Cache Reaping

當一個slab被釋放,它會被放置在slabs_free列表中供後續繼續使用。caches不會自動地收縮它們自己因此當kswapd進程發現memory很緊張了,它就會調用kmem_cache_reap()來釋放一些memory。該函數的作用是選擇一個cache並收縮這個cache的內存使用量。值得注意的是,緩存收割並不考慮node或者zone的內存壓力。這意味着在NUMA或者high memory機器中,內核可能會花費很多時間來釋放內存並不是很緊張的區域。但這對於像x86架構這樣只有一個memory bank的機器來說不是一個問題。
12333
如果系統中有很多個cache時,每次調用kmem_cache_reap()只會掃描並檢查REAP_SCANLEN個(當前定義爲10)cache。上一輪被掃描過的cache被存儲在clock_searchp記錄下來從而避免相同的cache被重複掃描。每次掃描,會對被掃中的cache做如下事情:

  • 查看SLAB_NO_REAP標誌位,如果被置位,則跳過該cache
  • 如果該cache正在增長,跳過該cache
  • 如果一個最近有增長或者正在增長,DFLGS_GROWN會被置位。如果該標誌位已經被置位,此slab會被跳過但是該flag會被清楚因此下次回收的時候,就可能將它回收。
  • 計算slabs_free鏈表中空閒slab的個數並計算出要釋放多少個page,將結果存入到變量pages中
  • 如果一個cache擁有構造函數或者擁有巨多slabs,調整pages變量,讓該cache不要被選中爲釋放的對象
  • 如果要釋放的page個數超過了REAP_PERFECT的值,那麼將釋放slabs_free鏈表中的一半空閒slab
  • 否則繼續掃描剩餘的cahce,並選擇一個cache至多釋放掉它的slabs_free中的一半。

Liunx2.6中的變化:
在Linux2.6中kmem_cache_reap()不再存在因爲當cache用戶可以做出一個更好的決定的時候仍然肆意地收縮cache。cache使用者現在可以通過set_shrinker()註冊一個"shrink cache"的callback用於智能判斷和收縮slab。這個簡單的函數通過操作一個struct shrinker,其中有一個指針指向callback。幷包含一個seek字段用戶表示重新創建一個object的困難程度,然後再將shinker放入到shrinker_list中。
在page回收的過程中,shrink_slab()函數被調用,來遍歷shrinker_list然後調用每個callback兩次。首先傳入參數0這表示該callback應該要返回它期望自己能釋放的page的個數。使用一個啓發的方式來決定是否值得調用該callback來回收pages。如果值得,該callback將被調用第二次,傳入要釋放的page的個數。
每一個進程描述符struct task中有一個reclaim_state字段。當slab allocator釋放pages時,這個字段被更新爲這次釋放的page個數。在調用shrink_slab()之前,這個字段被設置爲0然後當shrink_cache()被調用後再次讀取該字段的值來決定要釋放多少個page。

Cache Shrinking

當一個cache被選中來收縮自己,它所採取的步驟簡單而殘酷。

  • 刪除per CPU caches中的所有objects
  • 如果growing flag沒有被置位,刪除slab_free中的所有slabs

如果沒有微妙之處,Linux將什麼都不是。
1233333
提供了兩類令人迷惑的相似名稱的收縮函數。kmem_cache_shrink()從slabs_free鏈表中刪除所有的slabs並返回釋放的pages個數。這是一個被導出供slab allocator使用者使用的基本函數。
1234455ff
第二個函數爲__kmem_cache_shrink(),其釋放slabs_free鏈表中的所有slab之後再來檢查slabs_partial和slabs_full中是否爲空。該函數僅僅在slab allocator內部使用,並且在緩存銷燬過程中非常重要,因爲不管釋放了多少頁,緩存都是空的。

Cache Destroying

當一個module被卸載的時候,該模塊有責任通過調用kmem_cache_destroy()來銷燬任何cache。更重要的是一個cache可能已經被銷燬因爲擁有兩個同名的cache是不允許存在的。內核核心代碼通常不會去銷燬它的cache因爲它在系統的生命週期內始終存在。銷燬一個cache的步驟如下:

  • 從cache chain中刪除該cache
  • 同過刪除該cache中所有的slab來收縮該cache
  • 刪除任何的per CPU cache
  • 從cache_cache中刪除該cache的cache描述符。

detory

Slab,object,後續更新中…

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