linux slub分配器浅析
不過現(xiàn)在,linux內(nèi)核中,SLAB已經(jīng)被它的簡(jiǎn)化版--SLUB所代替。最近抽時(shí)間看了一下SLUB的代碼,略記一些自己的理解。
盡管SLUB是在內(nèi)核里面實(shí)現(xiàn)的,用戶態(tài)的對(duì)象池其實(shí)也可以借鑒這樣的做法。
SLUB的總體思想還是跟SLAB類似,對(duì)象池里面的內(nèi)存都是以“大塊”為單位來進(jìn)行分配與回收的。然后每個(gè)“大塊”又按對(duì)象的大小被分割成“小塊”,使用者對(duì)于對(duì)象的分配與回收都是以“小塊”為單位來進(jìn)行的。
SLUB的結(jié)構(gòu)如下圖:
另外,kmem_cache還有以下一些參數(shù)(后面會(huì)解釋到):
int size;???????? /* 每個(gè)對(duì)象占用的空間 */
int objsize;????? /* 對(duì)象的大小 */
int offset;?????? /* 對(duì)象所占用的空間中,存放next指針的偏移 */
int refcount;???? /* 引用計(jì)數(shù) */
int inuse;??????? /* 對(duì)象除next指針外所占用的大小 */
int align;??????? /* 對(duì)齊字節(jié)數(shù) */
void (*ctor)(void *); /* 構(gòu)造函數(shù) */
unsigned long min_partial; /* kmem_cache_node中保存的最小page數(shù) */
struct kmem_cache_order_objects oo; /* 首選的page分配策略(分配多少個(gè)連續(xù)頁面,能劃分成多少個(gè)對(duì)象) */
struct kmem_cache_order_objects min; /* 次選的page分配策略 */
(另外還有一些成員,或支持了一些選項(xiàng)、或支持了DEBUG、或是為周期性的內(nèi)存回收服務(wù)。這里就不列舉了。)
?
大體結(jié)構(gòu)
kmem_cache是對(duì)象池管理器,每一個(gè)kmem_cache管理一種類型的對(duì)象。所有的管理器通過雙向鏈表由表頭slab_caches串連起來。這一點(diǎn)跟之前的SLAB是一樣的。
kmem_cache的內(nèi)存管理工作是通過成員kmem_cache_node來完成的。在NUMA環(huán)境下(非均質(zhì)存儲(chǔ)結(jié)構(gòu)),一個(gè)kmem_cache維護(hù)了一組kmem_cache_node,分別對(duì)應(yīng)每一個(gè)內(nèi)存節(jié)點(diǎn)。kmem_cache_node只負(fù)責(zé)管理對(duì)應(yīng)內(nèi)存節(jié)點(diǎn)下的內(nèi)存。(如果不是 NUMA環(huán)境,那么kmem_cache_node只有一個(gè)。)
而實(shí)際的內(nèi)存還是靠page結(jié)構(gòu)來管理的,kmem_cache_node通過partial指針串連起一組page(nr_partial代表鏈表長(zhǎng)度),它們就代表了對(duì)象池里面的內(nèi)存。page結(jié)構(gòu)不僅代表了內(nèi)存,結(jié)構(gòu)里面還有一些union變量用來記錄其對(duì)應(yīng)內(nèi)存的對(duì)象分配情況(僅當(dāng)page被加入到SLUB分配器后有效,其他情況下這些變量有另外的解釋)。
原先的SLAB則要復(fù)雜一些,SLAB里面的page僅僅是管理內(nèi)存,不維護(hù)“對(duì)象”的概念。而是由額外的SLAB控制結(jié)構(gòu)(slab)來管理對(duì)象,并通過slab結(jié)構(gòu)的一些指針數(shù)組來劃定對(duì)象的邊界。
前面說過,對(duì)象池里面的內(nèi)存是以“大塊”為單位來進(jìn)行分配與回收的,page就是這樣的大塊。page內(nèi)部被劃分成若干個(gè)小塊,每一塊用于容納一個(gè)對(duì)象。這些對(duì)象是以鏈表的形式來存儲(chǔ)的,page->freelist就是鏈表頭,只有未被分配的對(duì)象才會(huì)放在鏈表中。對(duì)象的next指針存放在其偏移量為kmem_cache->offset的位置。(見上面的圖)
而在SLAB中,“大塊”則是提供控制信息的slab結(jié)構(gòu)。page結(jié)構(gòu)只表示內(nèi)存,它僅是slab所引用的資源。
每一個(gè)page并不只代表一個(gè)頁面,而是2^order個(gè)連續(xù)的頁面,這里的order值是由kmem_cache里面的oo或min來確定的。分配頁面時(shí),首先嘗試使用oo里面的order值,分配較合適大小的連續(xù)頁面(這個(gè)值是在kmem_cache創(chuàng)建的時(shí)候計(jì)算出來的,使用這個(gè)值時(shí)需要分配一定的連續(xù)頁面,以使得內(nèi)存分割成“小塊”后剩余的邊角廢料較少)。如果分配不成功(運(yùn)行時(shí)間長(zhǎng)了,內(nèi)存碎片多了,分配大量連續(xù)頁面就不容易了),則使用min里面的order值,分配滿足對(duì)象大小的最少量的連續(xù)頁面(這個(gè)值也是創(chuàng)建kmem_cache時(shí)計(jì)算出來的)。
kmem_cache_node通過partial指針串連的一組page,這些page必須是沒被占滿的(一個(gè)page被劃分成page->objects個(gè)對(duì)象大小的空間,這些空間中有page->inuse個(gè)已經(jīng)被使用。如果page->objects==page->inuse,則page為full)。如果一個(gè)page為full,則它會(huì)被從鏈表中移除。而如果page是free的(page->inuse==0),一般情況下它也會(huì)被釋放,除非這里的nr_partial(鏈表長(zhǎng)度)小于kmem_cache里面的min_partial。(既然是池,就應(yīng)該有一定的存量,min_partial就代表最低存量。這個(gè)值也是在創(chuàng)建kmem_cache時(shí)計(jì)算出來的,對(duì)象的size較大時(shí),會(huì)得到較大的min_partial值。因?yàn)檩^大的size值在分配page時(shí)會(huì)需要更多連續(xù)頁面,而分配連續(xù)頁面不如單個(gè)的頁面容易,所以應(yīng)該多緩存一些。)
而原先的SLAB則有三個(gè)鏈表,分別維護(hù)“full”、“partial”、“free”的slab。“free”和“partial”在SLUB里面合而為一,成了前面的partial鏈表。而“full”的page就不維護(hù)了。其實(shí)也不需要維護(hù),因?yàn)閜age已經(jīng)full了,不能再滿足對(duì)象的分配,只能響應(yīng)對(duì)象的回收。而在對(duì)象回收時(shí),通過對(duì)象的地址就能得到對(duì)應(yīng)的page結(jié)構(gòu)(page結(jié)構(gòu)的地址是與內(nèi)存地址相對(duì)應(yīng)的,見《linux內(nèi)存管理淺析》)。維護(hù)full的page可以便于查看分配器的狀態(tài),所以在DEBUG模式下,kmem_cache_node里面還是會(huì)提供一個(gè)full鏈表。
分配與釋放
對(duì)象的分配與釋放并不是直接在kmem_cache_node上面操作的,而是在kmem_cache_cpu上。一個(gè)kmem_cache維護(hù)了一組kmem_cache_cpu,分別對(duì)應(yīng)系統(tǒng)中的每一個(gè)CPU。kmem_cache_cpu相當(dāng)于為每一個(gè)CPU提供了一個(gè)分配緩存,以避免CPU總是去kmem_cache_node上面做操作,而產(chǎn)生競(jìng)爭(zhēng)。并且kmem_cache_cpu能讓被它緩存的對(duì)象固定在一個(gè)CPU上,從而提高CPU的cache命中率。kmem_cache_cpu只提供了一個(gè)page的緩存。
原先的SLAB是為每個(gè)CPU提供了一個(gè)array_cache結(jié)構(gòu)來緩存對(duì)象。對(duì)象在array_cache結(jié)構(gòu)中的組織形式跟它在slab中的組織形式是不一樣的,這也就增加了復(fù)雜性。而SLUB則都是通過page結(jié)構(gòu)來組織對(duì)象的,組織形式都一樣。進(jìn)行對(duì)象分配的時(shí)候,首先嘗試在kmem_cache_cpu上去分配。如果分配不成功,再去kmem_cache_node上move一個(gè)page到kmem_cache_cpu上面來。分配不成功的原因有兩個(gè):kmem_cache_cpu上的page已經(jīng)full了、或者現(xiàn)在需要分配的node跟kmem_cache_cpu上緩存page對(duì)應(yīng)的node不相同。對(duì)于page已full的情況,page被從kmem_cache_cpu上移除掉(或者DEBUG模式下,被移動(dòng)到對(duì)應(yīng)kmem_cache_node的full鏈表上);而如果是node不匹配的情況,則kmem_cache_cpu上緩存page會(huì)先被move回到其對(duì)應(yīng)kmem_cache_node的partial鏈表上(再進(jìn)一步,如果page是free的,且partial鏈表的長(zhǎng)度已經(jīng)不小于min_partial了,則page被釋放)。
反過來,釋放對(duì)象的時(shí)候,通過對(duì)象的地址能找到它所對(duì)應(yīng)的page的地址,將對(duì)象放歸該page即可。但是里面也有一些特殊邏輯,如果page正被kmem_cache_cpu緩存,就沒有什么需要額外處理的了;否則,在將對(duì)象放歸page時(shí),需要對(duì)page加鎖(因?yàn)槠渌鸆PU也可能正在該page上分配或釋放對(duì)象)。另外,如果對(duì)象在回收之前該page是full的,則對(duì)象釋放后該page就成partial的了,它還應(yīng)該被添加到對(duì)應(yīng)的kmem_cache_node的partial鏈表中。而如果對(duì)象回收之后該page成了free的,則它應(yīng)該被釋放掉。
對(duì)象的釋放還有一個(gè)細(xì)節(jié),既然對(duì)象會(huì)放回到對(duì)應(yīng)的page上去,那如果這個(gè)page正在被其他的CPU?cache呢(其他CPU的kmem_cache_cpu正指使用這個(gè)page)?其實(shí)沒關(guān)系,kmem_cache_cpu和page各自有一個(gè)freelist指針,當(dāng)page被一個(gè)CPU cache時(shí),page的freelist上的所有對(duì)象全部移動(dòng)到kmem_cache_cpu的freelist上面去(其實(shí)就是一個(gè)指針賦值),page的freelist變成NULL。而釋放的時(shí)候是釋放到page的freelist上去。兩個(gè)freelist互不影響。但是這個(gè)地方貌似有個(gè)問題,如果一個(gè)被cache的page的freelist由于對(duì)象的釋放而變成非NULL,那么這個(gè)page就可能再被cache到其他CPU的kmem_cache_cpu上面去,于是多個(gè)kmem_cache_cpu可能cache同一個(gè)page。這將導(dǎo)致一個(gè)CPU內(nèi)部的緩存可能cache到其他CPU上的對(duì)象(因?yàn)镃PU緩存跟對(duì)象并不是對(duì)齊的),從而一個(gè)CPU上的對(duì)象寫操作可能引起另一個(gè)CPU的緩存失效。
在kmem_cache被創(chuàng)建的時(shí)候,SLUB會(huì)根據(jù)各種各樣的信息,計(jì)算出對(duì)象池的合理布局(見上面的圖)。objsize是對(duì)象本身的大小;這個(gè)大小經(jīng)過對(duì)齊處理以后就成了inuse;緊貼inuse的后面可能會(huì)存放對(duì)象的next指針(由offset來標(biāo)記),從而將對(duì)象實(shí)際占用的大小擴(kuò)大到size。
其實(shí),這里的offset并不總是指向inuse后面的位置(否則offset就可以用inuse來代替了)。offset有兩種可能的取值,一是 inuse、一是0。這里的offset指定了next指針的位置,而next是為了將對(duì)象串連在空閑鏈表中。那么,需要用到next指針的時(shí)候,對(duì)象必定是空閑的,對(duì)象里面的空間是未被使用的。于是正好,對(duì)象里的第一個(gè)字長(zhǎng)的空間就拿來當(dāng)next指針好了,此時(shí)offset就是0。但是在一些特殊情況下,對(duì)象里面的空間不能被復(fù)用作next指針,比如對(duì)象提供了構(gòu)造函數(shù)ctor,那么對(duì)象的空間是被構(gòu)造過的。此時(shí),offset就等于inuse,next指針只好存放在對(duì)象的空間之后。
關(guān)于kmem_cache_cpu
前面在講對(duì)象分配與釋放的時(shí)候,著重講的是過程。下面再細(xì)細(xì)分析一下kmem_cache_cpu的作用。
如果沒有kmem_cache_cpu,那么分配對(duì)象的過程就應(yīng)該是:
1、從對(duì)應(yīng)的kmem_cache_node的partial鏈表里面選擇一個(gè)page;
2、從選中的page的freelist鏈表里面選擇一個(gè)對(duì)象;
這就是最基本的分配過程。
但是這個(gè)過程有個(gè)不好的地方,kmem_cache_node的partial鏈表是全局的、page的freelist鏈表也是全局的:
1、第一步訪問partial鏈表的時(shí)候,需要上鎖;
2、第二步訪問page->freelist鏈表的時(shí)候,也需要上鎖;
3、對(duì)象可能剛在CPU1上被釋放,又馬上被CPU2分配走。不利于CPU cache;
引入kmem_cache_cpu就是對(duì)這一問題的優(yōu)化。每個(gè)CPU各自對(duì)應(yīng)一個(gè)kmem_cache_cpu實(shí)例,用于緩存第一步中選中的那個(gè)page。這樣一來,第一步就不需要上鎖了;而page中的對(duì)象在一段時(shí)間內(nèi)也將趨于在同一個(gè)CPU上使用,有利于CPU cache。
而kmem_cache_cpu中的freelist則是為了避免第二步的上鎖。
假設(shè)沒有kmem_cache_cpu->freelist,而page->freelist初始時(shí)有1、2、3、4,四個(gè)對(duì)象。考慮如下事件序列:
1、page被CPU1所cache,然后1、2被分配;
2、由于在CPU1上請(qǐng)求的node id與該page不匹配,page被放回kmem_cache_node的partial鏈表,那么此時(shí)page->freelist還剩3和4兩個(gè)對(duì)象;
3、page又被CPU2所cache(page在上一步已經(jīng)被放回partial鏈表了)。
此時(shí),page->freelist就有可能被CPU1和CPU2兩個(gè)CPU所訪問,當(dāng)對(duì)象1或2被釋放時(shí)(這兩個(gè)對(duì)象已經(jīng)分配給了CPU1),CPU1會(huì)訪問page->freelist;而顯然CPU2分配對(duì)象時(shí)也要會(huì)訪問page->freelist。
所以為了避免上鎖,kmem_cache_cpu要維護(hù)自己的freelist,把page->freelist下的對(duì)象都接管過來。
這樣一來,CPU1就只跟page->freelist打交道,CPU2跟kmem_cache_cpu的freelist打交道,就不需要上鎖了。
?
關(guān)于page同時(shí)被多CPU使用
前面還提到,從屬于同一個(gè)page的對(duì)象可能cache到不同CPU上,從而可能對(duì)CPU的緩存造成一定的影響。不過這似乎也是沒辦法的事情。
首先,初始狀態(tài)下,page加入到slub之后,從屬于該page的對(duì)象都是空閑的,都存在于page->freelist中;
然后,這個(gè)page可能被某個(gè)CPU的kmem_cache_cpu所cache,假設(shè)是CPU-0,那么這個(gè)kmem_cache_cpu將得到屬于該page的所有對(duì)象。 page->freelist將為空;
接下來,這個(gè)page下的一部分對(duì)象可能在CPU-0上被分配出去;
再接著,可能由于NUMA的node不匹配,這個(gè)page從CPU-0的kmem_cache_cpu上面脫離下來。這時(shí)page->freelist將保存著那些未被分配出去的對(duì)象(而其他的對(duì)象已經(jīng)在CPU-0上被分配出去了);
這時(shí),從屬于該page的一部分對(duì)象正在CPU-0上被使用著,另一部分對(duì)象存在于page->freelist中。
那么,現(xiàn)在就有兩個(gè)選擇:
1、不將這個(gè)page放回partial list,阻止其他CPU使用這個(gè)page;
2、將這個(gè)page放回partial list,允許其他CPU使用這個(gè)page;
對(duì)于第一種做法,可以避免屬于同一個(gè)page的對(duì)象被cache到不同CPU。但是這個(gè)page必須等到CPU-0再次cache它以后才能被繼續(xù)使用;或者等待CPU-0所使用的從屬于這個(gè)page的對(duì)象全都被釋放,然后這個(gè)page才能被放回partial list或者直接被釋放掉。
這樣一來,一個(gè)page盡管擁有空閑的對(duì)象,卻可能在一定時(shí)間內(nèi)處于不可用狀態(tài)(極端情況是永遠(yuǎn)不可用)。這樣實(shí)現(xiàn)的系統(tǒng)似乎不太可控……
而現(xiàn)在的slub選擇了第二種做法,將page放回partial list,于是page馬上就能被其他CPU使用起來。那么,由此引發(fā)的從屬于同一個(gè)page的對(duì)象被cache到不同CPU的問題,也就是沒辦法的事情……
?
vs slab
相比SLAB,SLUB還有一個(gè)比較有意思的特性。當(dāng)創(chuàng)建新的對(duì)象池時(shí),如果發(fā)現(xiàn)原先已經(jīng)創(chuàng)建的某個(gè)kmem_cache的size剛好等于或略大于新的size,則新的kmem_cache不會(huì)被創(chuàng)建,而是復(fù)用這個(gè)大小差不多kmem_cache。所以kmem_cache里面還維護(hù)了一個(gè)refcount(引用計(jì)數(shù)),表示它被復(fù)用的次數(shù)。另外,SLUB也去掉了SLAB中很有意思的一個(gè)特性,Coloring(著色)。
什么是著色呢?一個(gè)內(nèi)存“大塊”,在按對(duì)象大小劃分成“小塊”的時(shí)候,可能并不是那么剛好,還會(huì)空余一些邊邊角角。著色就是利用這些邊邊角角來做文章,使得“小塊”的起始地址并不總是等于“大塊”內(nèi)的0地址,而是在0地址與空余大小之間浮動(dòng)。這樣就使得同一種類型的各個(gè)對(duì)象,其地址的低幾位存在更多的變化。
為什么要這樣做呢?這是考慮到了CPU的cache。在學(xué)習(xí)操作系統(tǒng)原理的時(shí)候我們都聽說過,為提高CPU對(duì)內(nèi)存的訪存效率,CPU提供了cache。于是就有了從內(nèi)存到cache之間的映射。當(dāng)CPU指令要求訪問一個(gè)內(nèi)存地址的時(shí)候,CPU會(huì)先看看這個(gè)地址是否已經(jīng)被緩存了。
內(nèi)存到cache的映射是怎么實(shí)現(xiàn)的呢?或者說CPU怎么知道某個(gè)內(nèi)存地址有沒有被緩存呢?
一種極端的設(shè)計(jì)是“全相連映射”,任何內(nèi)存地址都可以映射到任何的cache位置上。那么CPU拿到一個(gè)地址時(shí),它可能被緩存的cache位置就太多了,需要維護(hù)一個(gè)龐大的映射關(guān)系表,并且花費(fèi)大量的查詢時(shí)間,才能確定一個(gè)地址是否被緩存。這是不太可取的。
于是,cache的映射總是會(huì)有這樣的限制,一個(gè)內(nèi)存地址只可以被映射到某些個(gè)cache位置上。而一般情況下,內(nèi)存地址的低幾位又決定了內(nèi)存被cache的位置(如:cache_location = address % cache_size)。
好了,回到SLAB的著色,著色可以使同一類型的對(duì)象其低幾位地址相同的概率減小,從而使得這些對(duì)象在cache中映射沖突的概率降低。
這有什么用呢?其實(shí)同一種類型的很多對(duì)象被放在一起使用的情況是很多的,比如數(shù)組、鏈表、vector、等等情況。當(dāng)我們?cè)诒闅v這些對(duì)象集合的時(shí)候,如果每一個(gè)對(duì)象都能被CPU緩存住,那么這段遍歷代碼的處理效率勢(shì)必會(huì)得到提升。這就是著色的意義所在。
SLUB把著色給去掉了,是因?yàn)閷?duì)內(nèi)存使用更加摳門了,盡可能的把邊邊角角減少到最小,也就干脆不要著色了。還有就是,既然kmem_cache可以被size差不多的多種對(duì)象所復(fù)用,復(fù)用得越多,著色也就越?jīng)]意義了。
轉(zhuǎn)載于:https://www.cnblogs.com/wangfengju/archive/2013/05/11/6173088.html
總結(jié)
以上是生活随笔為你收集整理的linux slub分配器浅析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第2课:关闭被黑客扫描的端口
- 下一篇: linux去掉某一字符开头的行