多圖詳解Linux內(nèi)存分配器Slub
1、前言
在Linux中,伙伴系統(tǒng)(buddy system)是以頁(yè)為單位管理和分配內(nèi)存。但是現(xiàn)實(shí)的需求卻以字節(jié)為單位,假如我們需要申請(qǐng)20Bytes,總不能分配一頁(yè)吧!那豈不是嚴(yán)重浪費(fèi)內(nèi)存。那么該如何分配呢?slab分配器就應(yīng)運(yùn)而生了,專為小內(nèi)存分配而生。slab分配器分配內(nèi)存以Byte為單位。但是slab分配器并沒(méi)有脫離伙伴系統(tǒng),而是基于伙伴系統(tǒng)分配的大內(nèi)存進(jìn)一步細(xì)分成小內(nèi)存分配。
前段時(shí)間學(xué)習(xí)了下slab分配器工作原理。因?yàn)樽约罕旧硎亲鍪謾C(jī)的,發(fā)現(xiàn)現(xiàn)在好像都在使用slub分配器,想想還是再研究一下slub的工作原理。之前看了代碼,感覺(jué)挺多數(shù)據(jù)結(jié)構(gòu)和成員的。成員的意思是什么?數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系是什么?不知道你是否感覺(jué)云里霧里。既然代碼閱讀起來(lái)晦澀難懂,如果有精美的配圖,不知是否有助于閣下理解slub的來(lái)龍去脈呢?我想表達(dá)的意思就是文章圖多,圖多,圖多。我們只說(shuō)原理,盡量不看代碼。因?yàn)樗写a中包含的內(nèi)容我都會(huì)用圖來(lái)說(shuō)明。你感興趣絕對(duì)有助于你看代碼。
注:文章代碼分析基于linux-4.15.0-rc3。
2. slub數(shù)據(jù)結(jié)構(gòu)
slub的數(shù)據(jù)結(jié)構(gòu)相對(duì)于slab來(lái)說(shuō)要簡(jiǎn)單很多。并且對(duì)外接口和slab兼容。所以說(shuō),從slab的系統(tǒng)更換到slub,可以說(shuō)是易如反掌。
2.1. kmem_cache
現(xiàn)在假如從伙伴系統(tǒng)分配一頁(yè)內(nèi)存供slub分配器管理。對(duì)于slub分配器來(lái)說(shuō),就是將這段連續(xù)內(nèi)存平均分成若干大小相等的object(對(duì)象)進(jìn)行管理??墒俏覀兛偟弥烂恳粋€(gè)object的size吧!管理的內(nèi)存頁(yè)數(shù)也是需要知道的吧!不然怎么知道如何分配呢!因此需要一個(gè)數(shù)據(jù)結(jié)構(gòu)管理。那就是structkmem_cache。kmem_cache數(shù)據(jù)結(jié)構(gòu)描述如下:
struct kmem_cache {
#ifdef CONFIG_SLUB_CPU_PARTIAL
#endif
- cpu_slab:一個(gè)per cpu變量,對(duì)于每個(gè)cpu來(lái)說(shuō),相當(dāng)于一個(gè)本地內(nèi)存緩存池。當(dāng)分配內(nèi)存的時(shí)候優(yōu)先從本地cpu分配內(nèi)存以保證cache的命中率。
- flags:object分配掩碼,例如經(jīng)常使用的SLAB_HWCACHE_ALIGN標(biāo)志位,代表創(chuàng)建的kmem_cache管理的object按照硬件cache 對(duì)齊,一切都是為了速度。
- min_partial:限制struct kmem_cache_node中的partial鏈表slab的數(shù)量。雖說(shuō)是mini_partial,但是代碼的本意告訴我這個(gè)變量是kmem_cache_node中partial鏈表最大slab數(shù)量,如果大于這個(gè)mini_partial的值,那么多余的slab就會(huì)被釋放。
- size:分配的object size
- object_size:實(shí)際的object size,就是創(chuàng)建kmem_cache時(shí)候傳遞進(jìn)來(lái)的參數(shù)。和size的關(guān)系就是,size是各種地址對(duì)齊之后的大小。因此,size要大于等于object_size。
- offset:slub分配在管理object的時(shí)候采用的方法是:既然每個(gè)object在沒(méi)有分配之前不在乎每個(gè)object中存儲(chǔ)的內(nèi)容,那么完全可以在每個(gè)object中存儲(chǔ)下一個(gè)object內(nèi)存首地址,就形成了一個(gè)單鏈表。很巧妙的設(shè)計(jì)。那么這個(gè)地址數(shù)據(jù)存儲(chǔ)在object什么位置呢?offset就是存儲(chǔ)下個(gè)object地址數(shù)據(jù)相對(duì)于這個(gè)object首地址的偏移。
- cpu_partial:per cpu partial中所有slab的free object的數(shù)量的最大值,超過(guò)這個(gè)值就會(huì)將所有的slab轉(zhuǎn)移到kmem_cache_node的partial鏈表。
- oo:低16位代表一個(gè)slab中所有object的數(shù)量(oo &((1 << 16) - 1)),高16位代表一個(gè)slab管理的page數(shù)量((2^(oo 16)) pages)。
- max:看了代碼好像就是等于oo。
- min:當(dāng)按照oo大小分配內(nèi)存的時(shí)候出現(xiàn)內(nèi)存不足就會(huì)考慮min大小方式分配。min只需要可以容納一個(gè)object即可。
- allocflags:從伙伴系統(tǒng)分配內(nèi)存掩碼。
- inuse:object_size按照word對(duì)齊之后的大小。
- align:字節(jié)對(duì)齊大小。
- name:sysfs文件系統(tǒng)顯示使用。
- list:系統(tǒng)有一個(gè)slab_caches鏈表,所有的slab都會(huì)掛入此鏈表。
- node:slab節(jié)點(diǎn)。在NUMA系統(tǒng)中,每個(gè)node都有一個(gè)struct kmem_cache_node數(shù)據(jù)結(jié)構(gòu)。
2.2. kmem_cache_cpu
struct kmem_cache_cpu是對(duì)本地內(nèi)存緩存池的描述,每一個(gè)cpu對(duì)應(yīng)一個(gè)結(jié)構(gòu)體。其數(shù)據(jù)結(jié)構(gòu)如下:
struct kmem_cache_cpu {
- freelist:指向下一個(gè)可用的object。
- tid:一個(gè)神奇的數(shù)字,主要用來(lái)同步作用的。
- page:slab內(nèi)存的page指針。
- partial:本地slab partial鏈表。主要是一些部分使用object的slab。
2.3. kmem_cache_node
slab節(jié)點(diǎn)使用structkmem_cache_node結(jié)構(gòu)體描述。對(duì)于slub分配器來(lái)說(shuō),成員很少,遠(yuǎn)比slab分配器簡(jiǎn)潔。
struct kmem_cache_node {
- list_lock:自旋鎖,保護(hù)數(shù)據(jù)。
- nr_partial:slab節(jié)點(diǎn)中slab的數(shù)量。
- partial:slab節(jié)點(diǎn)的slab partial鏈表,和structkmem_cache_cpu的partial鏈表功能類似。
2.4. slub接口
了解了基本的數(shù)據(jù)結(jié)構(gòu),再來(lái)看看slub提供的API。如果你了解slub,我想這幾個(gè)接口你是再熟悉不過(guò)了。
struct kmem_cache *kmem_cache_create(const char *name,
1)kmem_cache_create是創(chuàng)建kmem_cache數(shù)據(jù)結(jié)構(gòu),參數(shù)描述如下:
2)kmem_cache_destroy作用和kmem_cache_create相反,就是銷毀創(chuàng)建的kmem_cache。
3)kmem_cache_alloc是從cachep參數(shù)指定的kmem_cache管理的內(nèi)存緩存池中分配一個(gè)對(duì)象,其中flags是分配掩碼,GFP_KERNEL是不是很熟悉的掩碼?
4)kmem_cache_free是kmem_cache_alloc的反操作
slab分配器提供的接口該如何使用呢?其實(shí)很簡(jiǎn)單,總結(jié)分成以下幾個(gè)步驟:
1.kmem_cache_create創(chuàng)建一個(gè)kmem_cache數(shù)據(jù)結(jié)構(gòu)。
- 使用kmem_cache_alloc接口分配內(nèi)存,kmem_cache_free接口釋放內(nèi)存。
- release第一步創(chuàng)建的kmem_cache數(shù)據(jù)結(jié)構(gòu)。
再來(lái)一段demo示例代碼就更好了。
- 首先使用kmem_cache_create創(chuàng)建名稱為kmem_cache_16的kmem_cache,該kmem_cache主要是描述如何管理一堆對(duì)象,其實(shí)就是slab的布局。每個(gè)對(duì)象都是16字節(jié),并且分配的對(duì)象地址按照8字節(jié)對(duì)齊,也就是說(shuō)從kmem_cache_16中分配的對(duì)象大小全是16字節(jié)。不管你要申請(qǐng)多少,反正就是16Bytes。當(dāng)然,kmem_cache_create僅僅是創(chuàng)建了一個(gè)描述slab緩存池布局的數(shù)據(jù)結(jié)構(gòu),并沒(méi)有從伙伴系統(tǒng)申請(qǐng)內(nèi)存,具體的申請(qǐng)內(nèi)存操作是在kmeme_cache_alloc中完成的。
- kmeme_cache_alloc從kmem_cache_16分配一個(gè)對(duì)象。
- 內(nèi)存使用結(jié)束記得kmem_cache_free釋放。
- 如果不需要這個(gè)kmem_cache的話,就可以調(diào)用kmem_cache_destroy進(jìn)行銷毀吧。在釋放kmem_cache之前要保證從該kmem_cache中分配的對(duì)象全部釋放了,否則無(wú)法釋放kmem_cache。
3. slub數(shù)據(jù)結(jié)構(gòu)之間關(guān)系
什么是slab緩存池呢?我的解釋是使用struct kmem_cache結(jié)構(gòu)描述的一段內(nèi)存就稱作一個(gè)slab緩存池。一個(gè)slab緩存池就像是一箱牛奶,一箱牛奶中有很多瓶牛奶,每瓶牛奶就是一個(gè)object。分配內(nèi)存的時(shí)候,就相當(dāng)于從牛奶箱中拿一瓶。總有拿完的一天。當(dāng)箱子空的時(shí)候,你就需要去超市再買一箱回來(lái)。超市就相當(dāng)于partial鏈表,超市存儲(chǔ)著很多箱牛奶。如果超市也賣完了,自然就要從廠家進(jìn)貨,然后出售給你。廠家就相當(dāng)于伙伴系統(tǒng)。
說(shuō)了這么多終于要拋出辛辛苦苦畫的美圖了。
好了,后面說(shuō)的大部分內(nèi)容請(qǐng)看這張圖。足以表明數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系了??炊诉@張圖,就可以理清數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系了。
3.1. slub管理object方法
在圖片的左上角就是一個(gè)slub緩存池中object的分布以及數(shù)據(jù)結(jié)構(gòu)和kmem_cache之間的關(guān)系。首先一個(gè)slab緩存池包含的頁(yè)數(shù)是由oo決定的。oo拆分為兩部分,低16位代表一個(gè)slab緩存池中object的數(shù)量,高16位代表包含的頁(yè)數(shù)。使用kmem_cache_create()接口創(chuàng)建kmem_cache的時(shí)候需要指出obj的size和對(duì)齊align。也就是傳入的參數(shù)。kmem_cache_create()主要是就是填充kmem_cache結(jié)構(gòu)體成員。既然從伙伴系統(tǒng)得到(2^(oo>> 16)) pages大小內(nèi)存,按照size大小進(jìn)行平分。一般來(lái)說(shuō)都不會(huì)整除,因此剩下的就是圖中灰色所示。由于每一個(gè)object的大小至少8字節(jié),當(dāng)然可以用來(lái)存儲(chǔ)下一個(gè)object的首地址。就像圖中所示的,形成單鏈表。圖中所示下個(gè)obj地址存放的位置位于每個(gè)obj首地址處,在內(nèi)核中稱作指針內(nèi)置式。同時(shí),下個(gè)obj地址存放的位置和obj首地址之間的偏移存儲(chǔ)在kmem_cache的offset成員。兩外一種方式是指針外置式,即下個(gè)obj的首地址存儲(chǔ)的位置位于obj尾部,也就是在obj尾部再分配sizeof(void*)字節(jié)大小的內(nèi)存。對(duì)于外置式則offset就等于kmem_cache的inuse成員。
3.2. per cpu freelist
針對(duì)每一個(gè)cpu都會(huì)分配一個(gè)struct kmem_cacche_cpu的結(jié)構(gòu)體??梢苑Q作是本地緩存池。當(dāng)內(nèi)存申請(qǐng)的時(shí)候,優(yōu)先從本地cpu緩存池申請(qǐng)。在分配初期,本地緩存池為空,自然要從伙伴系統(tǒng)分配一定頁(yè)數(shù)的內(nèi)存。內(nèi)核會(huì)為每一個(gè)物理頁(yè)幀創(chuàng)建一個(gè)struct page的結(jié)構(gòu)體。kmem_cacche_cpu中page就會(huì)指向正在使用的slab的頁(yè)幀。freelist成員指向第一個(gè)可用內(nèi)存obj首地址。處于正在使用的slab的struct page結(jié)構(gòu)體中的freelist會(huì)置成NULL,因?yàn)闆](méi)有其他地方使用。struct page結(jié)構(gòu)體中inuse代表已經(jīng)使用的obj數(shù)量。這地方有個(gè)很有意思的地方,在剛從伙伴系統(tǒng)分配的slab的 inuse在分配初期就置成obj的總數(shù),在分配obj的時(shí)候并不會(huì)改變。你是不是覺(jué)得很奇怪,既然表示已經(jīng)使用obj的數(shù)量,為什么一直是obj的總數(shù)呢?你想想,slab中的對(duì)象總有分配完的時(shí)候,那個(gè)時(shí)候就直接脫離kmem_cache_cpu了。此時(shí)的inuse不就名副其實(shí)了嘛!對(duì)于full slab就像圖的右下角,就像無(wú)人看管的孩子,沒(méi)有任何鏈表來(lái)管理。
3.3. per cpu partial
當(dāng)圖中右下角full slab釋放obj的時(shí)候,首先就會(huì)將slab掛入per cpu partial鏈表管理。通過(guò)struct page中next成員形成單鏈表。per cpu partial鏈表指向的第一個(gè)page中會(huì)存放一些特殊的數(shù)據(jù)。例如:pobjects存儲(chǔ)著per cpu partial鏈表中所有slab可供分配obj的總數(shù),如圖所示。當(dāng)然還有一個(gè)圖中沒(méi)有體現(xiàn)的pages成員存儲(chǔ)per cpu partial鏈表中所有slab內(nèi)存的頁(yè)數(shù)。pobjects到底有什么用呢?我們從fullslab中釋放一個(gè)obj就添加到per cpu partial鏈表,總不能無(wú)限制的添加吧!因此,每次添加的時(shí)候都會(huì)判斷當(dāng)前的pobjects是否大于kmem_cache的cpu_partial成員,如果大于,那么就會(huì)將此時(shí)per cpu partial鏈表中所有的slab移送到kmem_cache_node的partial鏈表,然后再將剛剛釋放obj的slab插入到per cpu partial鏈表。如果不大于,則更新pobjects和pages成員,并將slab插入到per cpu partial鏈表。
3.4. per node partial
per node partia鏈表類似per cpu partial,區(qū)別是node中的slab是所有cpu共享的,而per cpu是每個(gè)cpu獨(dú)占的。假如現(xiàn)在的slab布局如上圖所示。假如現(xiàn)在如紅色箭頭指向的obj將會(huì)釋放,那么就是一個(gè)empty slab,此時(shí)判斷kmem_cache_node的nr_partial是否大于kmem_cache的min_partial,如果大于則會(huì)釋放該slab的內(nèi)存。
4. slub分配內(nèi)存原理
當(dāng)調(diào)用kmem_cache_alloc()分配內(nèi)存的時(shí)候,我們可以從正在使用slab分配,也可以從percpu partial分配,同樣還可以從pernode partial分配,那么分配的順序是什么呢?我們可以用下圖表示。
首先從cpu 本地緩存池分配,如果freelist不存在,就會(huì)轉(zhuǎn)向per cpu partial分配,如果per cpu partial也沒(méi)有可用對(duì)象,繼續(xù)查看per node partial,如果很不幸也不沒(méi)有可用對(duì)象的話,就只能從伙伴系統(tǒng)分配一個(gè)slab了,并掛入per cpu freelist。我們?cè)敿?xì)看一下這幾種情況。
- kmem_cache剛剛建立,還沒(méi)有任何對(duì)象可供分配,此時(shí)只能從伙伴系統(tǒng)分配一個(gè)slab,如下圖所示。
- 如果正在使用的slab有free obj,那么就直接分配即可,這種是最簡(jiǎn)單快捷的。如下圖所示。
- 隨著正在使用的slab中obj的一個(gè)個(gè)分配出去,最終會(huì)無(wú)obj可分配,此時(shí)per cpupartial鏈表中有可用slab用于分配,那么就會(huì)從percpu partial鏈表中取下一個(gè)slab用于分配obj。如下圖所示。
- 隨著正在使用的slab中obj的一個(gè)個(gè)分配出去,最終會(huì)無(wú)obj可分配,此時(shí)per cpupartial鏈表也為空,此時(shí)發(fā)現(xiàn)per node partial鏈表中有可用slab用于分配,那么就會(huì)從per node partial鏈表中取下一個(gè)slab用于分配obj。如下圖所示。
5. slub釋放內(nèi)存原理
我們可以通過(guò)kmem_cache_free()接口釋放申請(qǐng)的obj對(duì)象。釋放對(duì)象的流程如下圖所示。
如果釋放的obj就是屬于正在使用cpu上的slab,那么直接釋放即可,非常簡(jiǎn)單;如果不是的話,首先判斷所屬slub是不是full狀態(tài),因?yàn)閒ull slab是沒(méi)媽的孩子,釋放之后就變成partial empty,急需要找個(gè)鏈表領(lǐng)養(yǎng)?。∵@個(gè)媽就是per cpu partial鏈表。如果per cpu partial鏈表管理的所有slab的free object數(shù)量超過(guò)kmem_cache的cpu_partial成員的話,就需要將per cpu partial鏈表管理的所有slab移動(dòng)到per node partial鏈表管理;如果不是full slab的話,繼續(xù)判斷釋放當(dāng)前obj后的slab是否是empty slab,如果是empty slab,那么在滿足kmem_cache_node的nr_partial大于kmem_cache的min_partial的情況下,則會(huì)釋放該slab的內(nèi)存。其他情況就直接釋放即可。
- 假設(shè)下圖左邊的情況下釋放obj,如果滿足kmem_cache_node的nr_partial大于kmem_cache的min_partial的話,釋放情況如下圖所示。
- 假設(shè)下圖左邊的情況下釋放obj,如果不滿足kmem_cache_node的nr_partial大于kmem_cache的min_partial的話,釋放情況如下圖所示。
- 假設(shè)下圖從full slab釋放obj的話,如果滿足per cpu partial管理的所有slab的free object數(shù)量大于kmem_cache的cpu_partial成員的話的話,將per cpu partial鏈表管理的所有slab移動(dòng)到per node partial鏈表管理,釋放情況如下圖所示。
- 假設(shè)下圖從full slab釋放obj的話,如果不滿足per cpu partial管理的所有slab的free object數(shù)量大于kmem_cache的cpu_partial成員的話的話,釋放情況如下圖所示。
6. kmalloc
好了,說(shuō)了這么多,估計(jì)你會(huì)感覺(jué)slab好像跟我們沒(méi)什么關(guān)系。如果作為一個(gè)驅(qū)動(dòng)開(kāi)發(fā)者,是不是感覺(jué)自己寫的driver從來(lái)沒(méi)有使用過(guò)這些接口呢?其實(shí)我們經(jīng)常使用,只不過(guò)隱藏在kmalloc的面具之下。
kmalloc的內(nèi)存分配就是基于slab分配器,在系統(tǒng)啟動(dòng)初期調(diào)用create_kmalloc_caches()創(chuàng)建多個(gè)管理不同大小對(duì)象的kmem_cache,例如:8B、16B、32B、64B、…、64MB等大小。kmem_cache的名稱以及大小使用structkmalloc_info_struct管理。所有管理不同大小對(duì)象的kmem_cache的名稱如下:
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
經(jīng)過(guò)create_kmalloc_caches()函數(shù)之后,系統(tǒng)通過(guò)create_kmalloc_cache()創(chuàng)建以上不同size的kmem_cache,并將這些kmem_cache存儲(chǔ)在kmalloc_caches全局變量中以備后續(xù)kmalloc分配內(nèi)存?,F(xiàn)在假如通過(guò)kmalloc(17, GFP_KERNEL)申請(qǐng)內(nèi)存,系統(tǒng)會(huì)從名稱“kmalloc-32”管理的slab緩存池中分配一個(gè)對(duì)象。即使浪費(fèi)了15Byte。
我們來(lái)看看kmalloc的實(shí)現(xiàn)方式。
- __builtin_constant_p是gcc工具用來(lái)判斷參數(shù)是否是一個(gè)常數(shù),畢竟有些操作對(duì)于常數(shù)來(lái)說(shuō)是可以優(yōu)化的。
- 通過(guò)kmalloc_index函數(shù)查找符合滿足分配大小的最小kmem_cache。
- 將index作為下表從kmalloc_caches數(shù)組中找到符合的kmem_cache,并從slab緩存池中分配對(duì)象。
我們?cè)倏匆幌耴malloc_index的實(shí)現(xiàn)。
作者簡(jiǎn)介:
宋牧春,linux內(nèi)核愛(ài)好者,2017年6月本科畢業(yè)于江蘇大學(xué)?,F(xiàn)就職于一家手機(jī)研發(fā)公司,任職BSP驅(qū)動(dòng)工程師,主要負(fù)責(zé)TP驅(qū)動(dòng)bringup和調(diào)試。