0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Linux內(nèi)核內(nèi)存管理之slab分配器

jf_0tjVfeJz ? 來(lái)源:嵌入式ARM和Linux ? 2024-02-22 09:25 ? 次閱讀

本文在行文的過(guò)程中,會(huì)多次提到cache或緩存的概念。如果沒(méi)有特殊在前面添加硬件的限定詞,就說(shuō)明cache指的是slab分配器使用的軟件緩存的意思。如果添加了硬件限定詞,則指的是處理器的硬件緩存,比如L1-DCache、L1-ICache之類的。

本節(jié)我們討論memory area,一段具有連續(xù)物理地址和任意長(zhǎng)度的內(nèi)存。

buddy算法將頁(yè)幀作為最基本的memory area。這對(duì)于申請(qǐng)較大內(nèi)存的請(qǐng)求是非常好的,但是,如果是很小的memory area請(qǐng)求,比如幾十或幾百字節(jié),我們將如何處理呢?

很明顯,申請(qǐng)一個(gè)完整頁(yè)幀存儲(chǔ)幾十個(gè)字節(jié)是非常浪費(fèi)資源的。如果引入新數(shù)據(jù)結(jié)構(gòu)描述在同一個(gè)頁(yè)幀內(nèi)如何分配memory area,會(huì)引入一個(gè)新問(wèn)題:內(nèi)部碎片。這是由于請(qǐng)求的內(nèi)存大小和分配的memory area大小不匹配造成。

早期Linux內(nèi)核就是采用這種經(jīng)典方案是,提供大小成幾何分布的memory area;換句話說(shuō),大小是2的冪次方,而不是要存儲(chǔ)數(shù)據(jù)的實(shí)際大小。這樣的好處是,無(wú)論請(qǐng)求的內(nèi)存是多少,我們都能保證內(nèi)存碎片小于50%?;谶@種方法,內(nèi)核創(chuàng)建了13個(gè)memory area列表,列表元素的大小從32 → 131072字節(jié)。這些列表還是用buddy系統(tǒng)申請(qǐng)內(nèi)存頁(yè)幀,或釋放不再包含memory area的頁(yè)幀。使用一個(gè)動(dòng)態(tài)列表追蹤每個(gè)頁(yè)幀內(nèi)的自由memory area數(shù)量。

1 slab分配器

在buddy系統(tǒng)之上運(yùn)行前述的memory area分配算法不是特別有效。在Sun Microsystems Solaris 2.4操作系統(tǒng)中首次引入的slab分配器方案給出了一種更好地算法:

存儲(chǔ)的數(shù)據(jù)類型影響memory area的分配方式(類似C++語(yǔ)言中的類的概念,也就是面向?qū)ο蟮?a target="_blank">編程思想)。例如,給用戶態(tài)進(jìn)程申請(qǐng)分配一個(gè)頁(yè)幀,內(nèi)核會(huì)調(diào)用get_zeroed_page()函數(shù),將該頁(yè)填充為0。

slab分配器擴(kuò)展了該思想,將memory area視為對(duì)象,該對(duì)象由一組數(shù)據(jù)結(jié)構(gòu)和一對(duì)函數(shù)組成,這對(duì)函數(shù)又稱為構(gòu)造函數(shù)和析構(gòu)函數(shù)。前者初始化memory area,而后者負(fù)責(zé)解除初始化。

為了避免重復(fù)初始化對(duì)象,slab分配器不會(huì)丟棄已經(jīng)申請(qǐng)但要釋放的對(duì)象,而是將其保存在內(nèi)存中。當(dāng)申請(qǐng)新對(duì)象時(shí),直接從內(nèi)存中獲取,而無(wú)需重新初始化。

內(nèi)核往往重復(fù)地申請(qǐng)相同類型的memory area(建立cache,重復(fù)利用)。比如,當(dāng)內(nèi)核創(chuàng)建一個(gè)新進(jìn)程時(shí),它會(huì)分配一些固定大小的memory area,每組固定大小的memory area組成一張表,用來(lái)保存進(jìn)程描述符、打開(kāi)的文件對(duì)象等等。當(dāng)進(jìn)程結(jié)束時(shí),這些memory area以及管理它們的表能夠被重復(fù)利用。因?yàn)檫M(jìn)程的創(chuàng)建和銷毀是很頻繁的,如果沒(méi)有slab分配器,內(nèi)核就會(huì)浪費(fèi)時(shí)間重復(fù)地分配和釋放包含相同大小memory area的頁(yè)幀;而slab分配器,將它們保存在緩存中,可以快速的重復(fù)利用。

memory area的申請(qǐng)可以按照它們的使用頻率來(lái)分類。通過(guò)創(chuàng)建一組具有合適大小的專用對(duì)象,可以有效處理預(yù)期特定大小的內(nèi)存申請(qǐng),從而避免內(nèi)部碎片。同時(shí),對(duì)于很少遇見(jiàn)的大小,可以按照幾何分布大小(例如,2的冪次方)的對(duì)象進(jìn)行處理,盡管這種方法仍然會(huì)導(dǎo)致內(nèi)部碎片的產(chǎn)生。

引入大小不是幾何分布的對(duì)象還有一個(gè)微妙的好處:內(nèi)核數(shù)據(jù)結(jié)構(gòu)的首地址往往不是以2的冪次方大小分布的物理地址上。(通俗地講,就是數(shù)據(jù)結(jié)構(gòu)的首地址不太可能正好落在2的冪次方大小的地址上)。因此,輔以硬件cache,可以產(chǎn)生更好的性能。

硬件cache性能是限制盡可能少地調(diào)用伙伴關(guān)系分配器的另一個(gè)原因(頻繁調(diào)用buddy系統(tǒng),會(huì)降低系統(tǒng)性能)。每次調(diào)用伙伴關(guān)系系統(tǒng),都會(huì)弄臟硬件cache,也就會(huì)增加平均內(nèi)存訪問(wèn)時(shí)間。內(nèi)核函數(shù)對(duì)硬件cache的影響被稱為函數(shù)占用空間;它被定義為當(dāng)該函數(shù)終止后,覆蓋的cache百分比。很明顯,越大的占用空間比會(huì)導(dǎo)致該函數(shù)之后的代碼執(zhí)行越慢,因?yàn)榇藭r(shí)的硬件cache需要重新讀取內(nèi)存,填充自己。

slab分配器將對(duì)象分組保存到cache中,每個(gè)cache保存了相同類型的對(duì)象。比如,打開(kāi)一個(gè)文件,為了保存打開(kāi)的文件這個(gè)對(duì)象,就會(huì)從slab分配器的filp緩存對(duì)象中(文件指針的意思),申請(qǐng)memory area。

包含一個(gè)cache的memory area被分成很多個(gè)slab;每個(gè)slab包含一個(gè)或多個(gè)連續(xù)的頁(yè)幀,這些頁(yè)幀用來(lái)保存已經(jīng)分配的對(duì)象和自由空閑的對(duì)象。如下圖所示:

0e2d7826-d0a6-11ee-a297-92fbcf53809c.png

內(nèi)核會(huì)周期性地掃描這些cache,釋放掉那些空slab占用的頁(yè)幀。

2 cache描述符

描述cache的數(shù)據(jù)類型是kmem_cache_t(等價(jià)于struct kmem_cache_s),各字段如下表所示。其中,忽略了收集統(tǒng)計(jì)信息和調(diào)試的幾個(gè)字段。

表8-8kmem_cache_t的各個(gè)字段

類型 名稱 描述
struct array_cache*[] array Per-CPU數(shù)組,包含指向本地的那些cache
unsigned int batchcount 與本地cache傳送的對(duì)象數(shù)量
unsigned int limit 本地cache中空閑對(duì)象的最大數(shù)量。這是可調(diào)的
struct kmem_list3 lists 見(jiàn)下表
unsigned int objsize cache中對(duì)象的大小
unsigned int flags 描述cache永久屬性的一些標(biāo)志集
unsigned int num 單個(gè)slab中的對(duì)象數(shù)量(同一cache中slab大小相同)
unsigned int free_limit 整個(gè)slab cache中自由空閑對(duì)象的上限
spinlock_t spinlock 保護(hù)cache的自旋鎖
unsigned int gfporder 單個(gè)slab中連續(xù)頁(yè)幀數(shù)量的對(duì)數(shù)
unsigned int gfpflags 申請(qǐng)分配頁(yè)幀時(shí)傳遞給伙伴關(guān)系函數(shù)的標(biāo)志組合
size_t colour slab顏色數(shù)量
unsigned int colour_off slab中基本對(duì)齊偏移量
unsigned int colour_next 用于下一個(gè)slab的顏色
kmem_cache_t * slabp_cache 指向通用slab cache的指針,該cache包含slab描述符
(如果其內(nèi)部的slab描述符已經(jīng)被使用,則為NULL)
unsigned int slab_size 單個(gè)slab的大小
unsigned int dflags 描述cache的動(dòng)態(tài)屬性的標(biāo)志集
void * ctor 指向與cache相關(guān)聯(lián)的構(gòu)造函數(shù)方法的指針
void * dtor 指向與cache相關(guān)聯(lián)的析構(gòu)函數(shù)方法的指針
const char * name 保存cache名稱的字符數(shù)組
struct list_head next cache描述符的雙向鏈表的指針

lists字段參見(jiàn)下表。

表8-9kmem_list3結(jié)構(gòu)體的各個(gè)字段

類型 名稱 描述
struct list_head slabs_partial slab描述符(包含自由和非自由對(duì)象)的雙向循環(huán)鏈表
struct list_head slabs_full slab描述符(包含非自由對(duì)象)的雙向循環(huán)鏈表
struct list_head slabs_free slab描述符(包含自由對(duì)象)的雙向循環(huán)鏈表
unsigned long free_objects cache中自由對(duì)象的數(shù)量
int free_touched slab分配器的頁(yè)回收算法使用
unsigned long next_reap slab分配器的頁(yè)回收算法使用
struct array_cache * shared 指向所有CPU共享的本地cache

3 slab描述符

cache中的每一個(gè)slab都有自己的描述符,其各字段描述,如下表所示:

類型 名稱 描述
struct list_head list 指向三個(gè)slab描述符的雙向鏈表之一。
也就是cache描述符的kmem_list3中的列表
(slabs_full、slabs_partial、slabs_free)
unsigned long colouroff 該slab中第一個(gè)對(duì)象的偏移量(跟染色有關(guān))
void * s_mem 該slab中第一個(gè)對(duì)象的地址
unsigned int inuse 該slab中當(dāng)前使用的對(duì)象數(shù)量(非自由)
unsigned int free 該slab中下一個(gè)自由對(duì)象的索引
如果沒(méi)有自由對(duì)象則等于BUFCTL_END

slab描述符有兩個(gè)存儲(chǔ)的地方:

外部slab描述符

存儲(chǔ)在該slab之外,通用緩存之一中(由cache_sizes指向)。

內(nèi)部slab描述符

存儲(chǔ)在該slab之內(nèi)內(nèi),也就是分配給slab的第一個(gè)頁(yè)幀的開(kāi)頭處。

當(dāng)對(duì)象的大小小于512MB時(shí),或者當(dāng)內(nèi)部碎片在slab內(nèi)為slab描述符和對(duì)象描述符留出足夠的空間時(shí),slab分配器選擇第二種解決方案。如果slab描述符存儲(chǔ)在slab之外,則cache描述符的flags字段中的CFLGS_OFF_SLAB標(biāo)志被設(shè)置為1;否則它將被設(shè)置為0。

下圖展示了cache和slab描述符的主要關(guān)系。已經(jīng)使用的slab,部分使用的slab和未使用的slab,它們使用不同鏈表串聯(lián)起來(lái)。

4 通用和特殊cache

cache可以分為兩類:通用和特殊。通用cache僅由slab分配器使用,而特殊cache由內(nèi)核其它部分使用。

0e49a88e-d0a6-11ee-a297-92fbcf53809c.png

通用cache包含:

第一個(gè)cache稱為kmem_cache,它的對(duì)象都是內(nèi)核中其余cache的描述符。cache_cache變量保存著這個(gè)特殊cache的描述符。

一些包含通用memory area的cache。這些內(nèi)存區(qū)域范圍是13個(gè)呈幾何分布的內(nèi)存大小。內(nèi)核中有一個(gè)表malloc_sizes(一個(gè)數(shù)組,數(shù)據(jù)類型是cache_sizes),它指向26個(gè)通用cache描述符,這些cache的大小是32、64、128、256、512、1024、2048、4096、8192、16384、32768、65536、131072字節(jié)。對(duì)于每種大小,都有兩種cache:一種適用于ISA DMA分配,另一種適用于普通內(nèi)存分配。

系統(tǒng)初始化的時(shí)候,調(diào)用函數(shù)kmem_cache_init()建立通用cache。

特殊cache都是調(diào)用kmem_cache_create()函數(shù)創(chuàng)建的。依據(jù)傳參,該函數(shù)首先檢查處理新cache的最佳方式(例如,slab描述符位于slab內(nèi)部還是外部)。然后從cache_cache通用緩存中分配新的cache描述符,并將其插入到一個(gè)cache描述符的鏈表cache_chain中(插入操作使用cache_chain_sem信號(hào)量進(jìn)行保護(hù),避免競(jìng)態(tài)條件發(fā)生)。

從cache_chain鏈表中銷毀和移除一個(gè)cache,可以調(diào)用kmem_cache_destroy()。此函數(shù)對(duì)于那些在加載時(shí)創(chuàng)建cache,卸載時(shí)銷毀cache的模塊非常有用。為了避免浪費(fèi)內(nèi)存空間,內(nèi)核必須在銷毀cache本身之前,需要銷毀所有的slab。而kmem_cache_shrink()函數(shù)正好可以通過(guò)調(diào)用slab_destroy()迭代銷毀所有slab。

不管是通用還是特殊cache,都可以讀取/proc/slabinfo獲取其名稱;該文件還指定了每個(gè)cache中自由對(duì)象、已分配對(duì)象的數(shù)量。

# name                 : tunables  
#  : slabdata   
isofs_inode_cache     72     72    656   24    4 : tunables    0    0    0 : slabdata      3      3      0
nf_conntrack         175    175    320   25    2 : tunables    0    0    0 : slabdata      7      7      0
au_finfo               0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
au_icntnr              0      0    832   39    8 : tunables    0    0    0 : slabdata      0      0      0
au_dinfo               0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
ovl_inode             69     69    688   23    4 : tunables    0    0    0 : slabdata      3      3      0
kvm_async_pf           0      0    136   30    1 : tunables    0    0    0 : slabdata      0      0      0
kvm_vcpu               0      0  17152    1    8 : tunables    0    0    0 : slabdata      0      0      0
...省略(內(nèi)核數(shù)據(jù)對(duì)象使用)
pool_workqueue      1393   1568    256   32    2 : tunables    0    0    0 : slabdata     49     49      0
radix_tree_node    13253  14896    584   28    4 : tunables    0    0    0 : slabdata    532    532      0
task_group           275    275    640   25    4 : tunables    0    0    0 : slabdata     11     11      0
vmap_area           3584   3584     64   64    1 : tunables    0    0    0 : slabdata     56     56      0
dma-kmalloc-8k         0      0   8192    4    8 : tunables    0    0    0 : slabdata      0      0      0
...省略
dma-kmalloc-8          0      0      8  512    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-192        0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-96         0      0     96   42    1 : tunables    0    0    0 : slabdata      0      0      0
...省略
kmalloc-rcl-16         0      0     16  256    1 : tunables    0    0    0 : slabdata      0      0      0
kmalloc-rcl-8          0      0      8  512    1 : tunables    0    0    0 : slabdata      0      0      0
kmalloc-8k           168    168   8192    4    8 : tunables    0    0    0 : slabdata     42     42      0
kmalloc-4k          3832   3856   4096    8    8 : tunables    0    0    0 : slabdata    482    482      0
...省略
kmalloc-16          9472   9472     16  256    1 : tunables    0    0    0 : slabdata     37     37      0
kmalloc-8          12288  12288      8  512    1 : tunables    0    0    0 : slabdata     24     24      0
kmem_cache_node     2432   2432     64   64    1 : tunables    0    0    0 : slabdata     38     38      0
kmem_cache          2239   2340    448   36    4 : tunables    0    0    0 : slabdata     65     65      0

5 slab分配器和zone分配器的關(guān)系

前面我們知道,cache的頁(yè)幀分配是在初始化時(shí)就完成的。而slab分配器創(chuàng)建新slab時(shí),則需要zone分配器獲取一組空閑且連續(xù)的物理內(nèi)存(不會(huì)分配高端內(nèi)存)。因此,需要調(diào)用kmem_getpages()函數(shù),在UMA系統(tǒng)上,實(shí)現(xiàn)大概如下所示:

void * kmem_getpages(kmem_cache_t *cachep, int flags)
{
    struct page *page;
    int i;

    flags   |= cachep->gfpflags;
    page    = alloc_pages(flags, cachep->gfporder);
    if (!page)
        return NULL;
    i = (1 << cache->gfporder);
    if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
        atomic_add(i, &slab_reclaim_pages);
    while (i--)
        SetPageSlab(page++);
    return page_address(page);
}

參數(shù)說(shuō)明:

cachep

指向需要額外頁(yè)幀的cache描述符的指針(所需頁(yè)幀數(shù)量由cachep->gfporder字段中的階數(shù)決定)。

flags

指定如何請(qǐng)求頁(yè)幀(參見(jiàn)Zone分配器)。這個(gè)標(biāo)志與cache描述符中保存的標(biāo)志組合使用。

內(nèi)存分配請(qǐng)求的大小由緩存描述符的gfporder字段指定,該字段決定了緩存中slab的大小。如果slab cache設(shè)置了SLAB_RECLAIM_ACCOUNT標(biāo)志,當(dāng)內(nèi)核檢查是否有足夠的內(nèi)存來(lái)滿足一些用戶請(qǐng)求時(shí),分配給slab的頁(yè)幀將被視為可回收頁(yè)。該函數(shù)還在分配的頁(yè)幀的頁(yè)描述符中設(shè)置PG_slab標(biāo)志。

釋放slab頁(yè)幀時(shí),調(diào)用kmem_freepages()函數(shù):

    void kmem_freepages(kmem_cache_t *cachep, void *addr)
    {
        unsigned long i = (1 << cachep->gfporder);
        struct page *page = virt_to_page(addr);

        if (current->reclaim_state)
            current->reclaim_state->reclaimed_slab += i;
        while (i--)
            ClearPageSlab(page++);
        free_pages((unsigned long) addr, cachep->gfporder);
        if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
            atomic_sub(1<gfporder, &slab_reclaim_pages);
    }

該函數(shù)釋放slab頁(yè)幀,從線性地址為addr的頁(yè)幀開(kāi)始。如果當(dāng)前進(jìn)程正在執(zhí)行內(nèi)存回收(current->reclaim_state字段不是NULL),reclaim_state->reclaimed_slab加上要釋放的頁(yè)幀數(shù),由頁(yè)幀回收算法計(jì)算在內(nèi)。此外,如果設(shè)置了SLAB_RECLAIM_ACCOUNT標(biāo)志(見(jiàn)上文),適當(dāng)?shù)臏p小slab_reclaim_pages,該變量用來(lái)記錄在內(nèi)存不足時(shí),分配給slab的頁(yè)幀有多少是空閑的。

6 分配slab給cache

剛創(chuàng)建的cache不包含slab,因此也就沒(méi)有任何空閑的對(duì)象。只有在滿足下面兩個(gè)條件時(shí)才會(huì)將slab分配給cache:

有新對(duì)象的分配請(qǐng)求時(shí)

cache沒(méi)有空閑對(duì)象時(shí)

slab分配器調(diào)用cache_grow()分配新的slab。具體的過(guò)程如下所示:

首先,調(diào)用kmem_getpages()從ZONE頁(yè)幀分配器獲取存儲(chǔ)slab所需的頁(yè)幀;

然后,調(diào)用alloc_slabmgmt()來(lái)獲取新的slab描述符。如果設(shè)置了cache描述符的CFLGS_OFF_SLAB標(biāo)志,則從cache描述符的slabp_cache指向的通用緩存中分配slab描述符;否則,將在slab的第1個(gè)頁(yè)幀中分配slab描述符。

對(duì)于給定的頁(yè)幀,內(nèi)核必須能夠確定它是否被slab分配器使用,如果是,則必須能夠快速導(dǎo)出相應(yīng)cache和slab描述符的地址。因此,cache_grow()掃描分配給新slab的頁(yè)幀的所有頁(yè)描述符,并分別使用cache和slab描述符的地址加載page描述符中l(wèi)ru字段的next和prev字段。這是正確的,因?yàn)閘ru字段僅在頁(yè)幀空閑時(shí)由buddy伙伴系統(tǒng)使用,而由slab分配器處理的頁(yè)幀設(shè)置了PG_slab標(biāo)志,對(duì)buddy伙伴系統(tǒng)而言不是空閑的。相反的問(wèn)題:對(duì)于給定的slab,哪些是實(shí)現(xiàn)它的頁(yè)幀?可以通過(guò)使用slab描述符的s_mem字段(slab第一個(gè)頁(yè)幀的起始地址)和cache描述符的gfporder字段(slab大小)來(lái)確定。

接下來(lái),cache_init_objs(),對(duì)slab所有對(duì)象調(diào)用構(gòu)造函數(shù);(也就是初始化所有對(duì)象)

最后,調(diào)用list_add_tail()將獲取的slab描述符(*slabp)添加到cache描述符(*cachep)中的空閑slab列表中,并更新空閑對(duì)象的計(jì)數(shù):

    list_add_tail(&slabp->list, &cachep->lists->slabs_free);
    cachep->lists->free_objects += cachep->num;

7 從cache中釋放slab

銷毀slab分為兩種情況:

slab cache中有太多空閑的對(duì)象;

定時(shí)器函數(shù)周期性地檢查是否有完全未使用的slab需要釋放

銷毀過(guò)程的實(shí)現(xiàn)函數(shù)是slab_destroy(),銷毀slab并將對(duì)應(yīng)的頁(yè)幀釋放回ZONE頁(yè)幀分配器:

    void slab_destroy(kmem_cache_t *cachep, slab_t *slabp)
    {
        /* 檢查cache是否具有析構(gòu)函數(shù):如果有對(duì)所有對(duì)象調(diào)用析構(gòu)函數(shù) */
        if (cachep->dtor) {
            int i;
            for (i = 0; i < cachep->num; i++) {
                // objp指向當(dāng)前正在處理的對(duì)象
                void* objp = slabp->s_mem+cachep->objsize*i;
                (cachep->dtor)(objp, cachep, 0);
            }
        }

        /* 將slab使用的所有頁(yè)幀返回給buddy系統(tǒng) */
        kmem_freepages(cachep, slabp->s_mem - slabp->colouroff);

        /* 如果slab描述符存儲(chǔ)在slab之外,需要從slab描述符的緩存中釋放 */
        if (cachep->flags & CFLGS_OFF_SLAB)
            kmem_cache_free(cachep->slabp_cache, slabp);

        /* 如果slab cache被設(shè)置了`SLAB_DESTROY_BY_RCU`標(biāo)志,
         * 意味著使用延遲執(zhí)行的方法釋放`slab`,使用call_rcu()注冊(cè)回調(diào)函數(shù)
         * 由回調(diào)函數(shù)調(diào)用kmem_freepages()。如果可能,還需要調(diào)用kmem_cache_free
         */
        if (unlikely(cachep->flags & SLAB_DESTROY_BY_RCU)) {
            struct slab_rcu *slab_rcu;
            slab_rcu = (struct slab_rcu *) slabp;
            slab_rcu->cachep = cachep;
            slab_rcu->addr = addr;
            call_rcu(&slab_rcu->head, kmem_rcu_free);
        } else {
            kmem_freepages(cachep, addr);
            if (OFF_SLAB(cachep))
                kmem_cache_free(cachep->slabp_cache, slabp);
        }
    }

8 對(duì)象描述符

每個(gè)對(duì)象也有描述符,類型是kmem_bufctl_t,這是一個(gè)unsigned short類型。對(duì)象描述符數(shù)組就存放在slab描述符的后邊。所以,對(duì)象描述符的存儲(chǔ)位置也分為兩種情況,如下圖所示:

slab外部

存儲(chǔ)在由slabp_cache指向的通用cache中。對(duì)象描述符和對(duì)象所占用的內(nèi)存大小,有存儲(chǔ)在slab中的對(duì)象數(shù)量決定(cache描述符中的num字段)。

slab內(nèi)部

存儲(chǔ)在slab之內(nèi),就在slab描述符后邊。

數(shù)組中的第一個(gè)對(duì)象描述符描述第一個(gè)對(duì)象,依次對(duì)應(yīng)。對(duì)象描述符是一個(gè)unsigned short整數(shù),只有當(dāng)對(duì)象是空閑時(shí)才有意義。它包含指向下一個(gè)空閑對(duì)象的索引,因此形成了一個(gè)空閑對(duì)象的列表。該列表中,最后一個(gè)空閑對(duì)象的索引標(biāo)記為BUFCTL_END(0xffff)。

0e5fe914-d0a6-11ee-a297-92fbcf53809c.png

9 對(duì)象的內(nèi)存對(duì)齊

slab分配器管理的對(duì)象在內(nèi)存中需要對(duì)齊,也就是說(shuō),存儲(chǔ)它們的初始物理地址是給定常數(shù)倍數(shù)的內(nèi)存單元中,通常是2的冪。這個(gè)常數(shù)稱為對(duì)齊因子。

slab分配器允許的最大對(duì)齊因子是4096(頁(yè)幀大?。_@意味著對(duì)象可以通過(guò)引用它們的物理地址或線性地址來(lái)對(duì)齊。在這兩種情況下,只有地址的低12位可能會(huì)被對(duì)齊改變。

通常,如果物理地址按照word(即計(jì)算機(jī)內(nèi)部存儲(chǔ)器總線的寬度)對(duì)齊,計(jì)算機(jī)訪問(wèn)存儲(chǔ)單元的速度會(huì)更快。因此,默認(rèn)情況下,kmem_cache_create()函數(shù)根據(jù)BYTES_PER_WORD宏指定的字長(zhǎng)來(lái)對(duì)齊對(duì)象。

對(duì)于×86處理器,宏的值為4,字長(zhǎng)為32位。在創(chuàng)建新的slab cache時(shí),可以將對(duì)象在硬件L1-cache中對(duì)齊。為了實(shí)現(xiàn)這一點(diǎn),內(nèi)核設(shè)置了SLAB_HWCACHE_ALIGNcache描述符標(biāo)志。kmem_cache_create()會(huì)按照如下方式處理請(qǐng)求:

如果對(duì)象大于cache line的一半,那么它在內(nèi)存中的對(duì)齊大小就是L1_CACHE_BYTES;換句話說(shuō),總是位于cache line的起始處。

否則,對(duì)象大小按照obj_size * n = L1_CACHE_BYTES計(jì)算出的合理值進(jìn)行對(duì)齊,總之要保證一個(gè)對(duì)象不能跨越2個(gè)cache line。

很顯然,slab分配器在這兒就是采用以空間換取時(shí)間的思想;通過(guò)人為的增加對(duì)象的大小獲得更好地緩存性能,但也會(huì)產(chǎn)生內(nèi)部碎片。

10 slab染色

我們知道,同一條cache line可以映射許多不同的內(nèi)存塊。在本章中,我們還看到了相同大小的對(duì)象最終被存儲(chǔ)在硬件cache中相同的偏移位置。在不同slab中具有相同偏移量的對(duì)象將以相對(duì)較高的概率最終映射到相同的cache line中。因此,頻繁訪問(wèn)映射到同一cache line的不同內(nèi)存位置時(shí),需要來(lái)回在硬件cache和內(nèi)存之間搬運(yùn)數(shù)據(jù),造成訪存性能降低。slab分配器通過(guò)一種稱為slab染色的策略避免這種行為:將不同的值(稱為colors)賦給不同的slab。

在分析slab著色之前,我們必須先看一下cache中對(duì)象的布局。因?yàn)閏ache在內(nèi)存中是對(duì)齊的,這意味著對(duì)象地址必須是給定值(比如aln)的倍數(shù)。但即使考慮到對(duì)齊約束,也有許多可能的方法將對(duì)象存放到slab中。如何選擇取決于對(duì)以下變量所做的決定:

num

可以存儲(chǔ)到slab中的對(duì)象數(shù)量。

osize

對(duì)象大小,包含對(duì)齊字節(jié)。

dsize

描述符大?。ò╯lab和所有對(duì)象描述符的大?。凑誧ache line對(duì)齊。如果slab和對(duì)象描述符存儲(chǔ)在slab之外,它的值等于0。

free

slab中未使用的字節(jié)(那些沒(méi)有分配給任何對(duì)象的字節(jié))。

所以,slab總長(zhǎng)可以用下面的公式計(jì)算:

slab length = (num × osize) + dsize + free

free總是小于osize,否則就可以在slab中添加一個(gè)對(duì)象了。但是,free可能大于aln。

slab分配器利用free未使用字節(jié)為slab染色。術(shù)語(yǔ)color簡(jiǎn)單地對(duì)slab進(jìn)行劃分,從而允許內(nèi)存分配器將對(duì)象分散到不同的線性地址中。通過(guò)這種方式,內(nèi)核可以從處理器的硬件cache中獲得最佳性能。

將slab染色可以將slab的第一個(gè)對(duì)象存儲(chǔ)到不同的內(nèi)存位置,同時(shí)滿足對(duì)齊約束??捎玫腸olor數(shù)量是free?aln(該值存儲(chǔ)在cache描述符的colour字段中)。因此,第1個(gè)顏色值是0,最后一個(gè)為(free?aln)?1。(一種特殊情況是,free < aln,colour設(shè)為0,所有的slab使用顏色值0,顏色的數(shù)量是一個(gè)。)

如果slab被使用顏色值col染色,第一個(gè)對(duì)象的偏移量(相對(duì)于slab初始地址)等于col × aln + dsize個(gè)字節(jié)。如下圖所示,圖中闡釋了slab內(nèi)對(duì)象的位置如何依賴slab顏色值。本質(zhì)上,染色就是將slab中未使用的部分字節(jié)從結(jié)尾處移動(dòng)到起始處。

0e767170-d0a6-11ee-a297-92fbcf53809c.png

染色只有在free足夠大時(shí)才起作用。很明顯,如果對(duì)象沒(méi)有要求對(duì)齊,或者如果slab中未使用的字節(jié)數(shù)小于對(duì)齊因子(free < aln),唯一可能的slab染色就一個(gè),顏色值為0,即第一個(gè)對(duì)象的偏移量賦值為零。

通過(guò)將當(dāng)前顏色存儲(chǔ)在cache描述符中的color_next字段中,cache_grow()函數(shù)將color_next指定的顏色分配給新的slab,然后增加該字段的值。到達(dá)colour后,它再次繞到“0”。通過(guò)這種方式,每個(gè)slab都使用與前一個(gè)不同的顏色創(chuàng)建,直到最大可用顏色。此外,cache_grow()函數(shù)從cache描述符的color_off字段獲取值aln,根據(jù)slab內(nèi)部對(duì)象的數(shù)量計(jì)算dsize,最后將值col × aln + dsize存儲(chǔ)在slab描述符的coloroff字段中。

11 空閑slab對(duì)象的本地緩存

在多核處理器系統(tǒng)中,Linux v2.6版本實(shí)現(xiàn)的slab分配器,與最初的Solaris 2.4實(shí)現(xiàn)不同。為了減少處理器之間的自旋鎖競(jìng)爭(zhēng)并更好地利用硬件緩存,slab分配器的每個(gè)緩存都包含一個(gè)CPU核的本地?cái)?shù)據(jù)結(jié)構(gòu),該數(shù)據(jù)結(jié)構(gòu)由指向被釋放對(duì)象指針組成的數(shù)組,稱為“slab本地緩存”。大多數(shù)slab對(duì)象的分配和釋放只影響本地緩存;只有當(dāng)本地緩存下溢或溢出時(shí),才會(huì)涉及到slab數(shù)據(jù)結(jié)構(gòu)。這種技術(shù)與前面的“CPU本地頁(yè)幀緩存”一節(jié)中介紹的技術(shù)非常相似。

緩存描述符的array字段是指向該指針數(shù)組,而指針指向array_cache數(shù)據(jù)結(jié)構(gòu),系統(tǒng)中的每個(gè)CPU都有一個(gè)這樣的元素。每個(gè)array_cache數(shù)據(jù)結(jié)構(gòu)都是空閑對(duì)象的本地緩存的描述符,其字段如表8-11所示。

表8-11array_cache結(jié)構(gòu)的字段

類型 名稱 描述
unsigned int avail 指向本地緩存中可用對(duì)象的指針數(shù)。該字段可以作為緩存中的第一個(gè)空閑slot。
unsigned int limit 本地緩存的大小。也就是說(shuō),本地緩存中指針的最大數(shù)量。
unsigned int batchcount 本地緩存重填或清空的塊大小。
unsigned int touched 如果本地緩存最近被使用,則標(biāo)志設(shè)置為1。

注意,本地緩存描述符不包括本地緩存本身的地址;實(shí)際上,本地緩存就放在描述符后面,代碼如下所示。當(dāng)然,本地緩存存儲(chǔ)的是指向被釋放對(duì)象的指針,而不是對(duì)象本身,對(duì)象總是放在緩存的slab中。

struct arraycache_init {
    struct array_cache cache;
    void * entries[BOOT_CPUCACHE_ENTRIES];
};

當(dāng)創(chuàng)建新的slab緩存時(shí),kmem_cache_create()函數(shù)確定本地緩存的大小(將此值存儲(chǔ)在緩存描述符的limit字段中),分配它們,并將它們的指針存儲(chǔ)在緩存描述符的array字段中。大小取決于存儲(chǔ)在slab緩存中的對(duì)象的大小,范圍從1表示非常大的對(duì)象到120表示較小的對(duì)象。此外,“batchcount”字段的初始值,即在塊中從本地緩存中添加或刪除的對(duì)象的數(shù)量,最初設(shè)置為本地緩存大小的一半。

系統(tǒng)管理員可以為每個(gè)cache調(diào)節(jié)本地緩存的大小,方法是寫batchcount字段值,該值保存在/proc/slabinfo文件中。

在多核系統(tǒng)中,存儲(chǔ)小內(nèi)存對(duì)象的slab緩存還支持一個(gè)額外的本地緩存,它的地址存儲(chǔ)在緩存描述符的lists.shared字段中。顧名思義,共享本地緩存是所有CPU共享的,它使空閑對(duì)象在本地緩存之間進(jìn)行遷移變得容易。初始值等于8倍于batchcount的值。

12 分配slab對(duì)象

新對(duì)象可以通過(guò)調(diào)用kmem_cache_alloc()函數(shù)獲得。參數(shù)cachep指向必須從中獲得新的空閑對(duì)象的緩存描述符,而參數(shù)flag表示要傳遞給ZONE頁(yè)幀分配器函數(shù)的標(biāo)志,如果緩存的所有slab都已滿時(shí),使用這些標(biāo)志創(chuàng)建新的slab。

該函數(shù)本質(zhì)上等價(jià)于下面的代碼:

    void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)
    {
        unsigned long       save_flags;
        void                *objp;
        struct array_cache  *ac;

        local_irq_save(save_flags);
        ac = cachep->array[smp_processor_id()];
        if (ac->avail) {
            ac->touched = 1;
            objp = ((void **)(ac+1))[--ac->avail];
        } else
            objp = cache_alloc_refill(cachep, flags);
        local_irq_restore(save_flags);
        return objp;
    }

該函數(shù)首先嘗試從本地緩存中檢索一個(gè)空閑對(duì)象。如果有空閑對(duì)象,avail字段包含本地緩存中指向最后一個(gè)被釋放對(duì)象的索引。因?yàn)楸镜鼐彺鏀?shù)組存儲(chǔ)在ac描述符之后,((void**)(ac+1))[——ac->avail]獲取該空閑對(duì)象的地址并減小ac->avail的值。調(diào)用cache_alloc_refill()函數(shù)來(lái)重新填充本地緩存,并在本地緩存中沒(méi)有空閑對(duì)象時(shí)獲得一個(gè)空閑對(duì)象。

cache_alloc_fill()函數(shù)基本上執(zhí)行以下步驟:

static void* cache_alloc_refill(kmem_cache_t* cachep, int flags)
{
    // ...
    check_irq_off();

    /* 1. 獲取本地緩存描述符 */
    ac = ac_data(cachep);
retry:
    batchcount = ac->batchcount;
    if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
        /* 如果最近這個(gè)緩存很少有活動(dòng),執(zhí)行部分填充。
         * 否則容易產(chǎn)生refill bouncing。
         */
        batchcount = BATCHREFILL_LIMIT;
    }
    l3 = list3_data(cachep);

    /* 2. 申請(qǐng)自旋鎖 */
    spin_lock(&cachep->spinlock);

    /* 3. 如果slab緩存包含本地共享緩存,且其中包含一些空閑對(duì)象時(shí)。
     * 則將這些空閑對(duì)象轉(zhuǎn)移給本地緩存。
     */
    if (l3->shared) {
        struct array_cache *shared_array = l3->shared;
        if (shared_array->avail) {
            if (batchcount > shared_array->avail)
                batchcount = shared_array->avail;
            shared_array->avail -= batchcount;
            ac->avail = batchcount;
            memcpy(ac_entry(ac), &ac_entry(shared_array)[shared_array->avail],
                    sizeof(void*)*batchcount);
            shared_array->touched = 1;
            goto alloc_done;
        }
    }

    /* 4 使用batchcount個(gè)指針填充本地緩存,這些指針是slab中的空閑對(duì)象的地址 */
    while (batchcount > 0) {
        struct list_head *entry;
        struct slab *slabp;

        /* 4.a 查看cache描述符中的slabs_partial和slabs_free列表,尋找合適的空閑對(duì)象:
         * (1) 如果slabs_partial還有空閑對(duì)象,則從其中申請(qǐng)空閑對(duì)象
         * (2) 否則從slabs_free申請(qǐng)空閑對(duì)象
         * (3) 如果slabs_free沒(méi)有空閑對(duì)象,則跳轉(zhuǎn)到must_grow,擴(kuò)展slab緩存
         */
        entry = l3->slabs_partial.next;
        if (entry == &l3->slabs_partial) {
            l3->free_touched = 1;
            entry = l3->slabs_free.next;
            if (entry == &l3->slabs_free)
                goto must_grow;
        }

        slabp = list_entry(entry, struct slab, list);
        check_slabp(cachep, slabp);
        check_spinlock_acquired(cachep);
        while (slabp->inuse < cachep->num && batchcount--) {
            kmem_bufctl_t next;
            STATS_INC_ALLOCED(cachep);
            STATS_INC_ACTIVE(cachep);
            STATS_SET_HIGH(cachep);

            /* 4.b 獲取空閑對(duì)象:
             * (1)獲取slab中空閑對(duì)象的指針;
             * (2)slab描述符的inuse字段+1,表明該空閑對(duì)象已經(jīng)被使用
             * (3)slab描述符的free字段+1,指向下一個(gè)空閑對(duì)象
             */
            ac_entry(ac)[ac->avail++] = slabp->s_mem + slabp->free*cachep->objsize;
            slabp->inuse++;
            next = slab_bufctl(slabp)[slabp->free];
            slabp->free = next;
        }
        check_slabp(cachep, slabp);

        /* 4.c 將已經(jīng)消耗的slab插入到cache描述符的正確列表中
         * (1) slab_fulll列表
         * (2) slab_partial列表
         */
        list_del(&slabp->list);
        if (slabp->free == BUFCTL_END)
            list_add(&slabp->list, &l3->slabs_full);
        else
            list_add(&slabp->list, &l3->slabs_partial);
    }

must_grow:
    /* 5. 此時(shí),要添加到本地緩存的指針數(shù)量存儲(chǔ)在`ac->avail`變量中。
     * 將kmem_list3結(jié)構(gòu)體的字段free_objects減去添加到本地緩存中的指針數(shù)量,
     * 即是表明這些對(duì)象不再是空閑的了。
     */
    l3->free_objects -= ac->avail;

alloc_done:
    /* 6. 釋放自旋鎖 */
    spin_unlock(&cachep->spinlock);

    /* 8. 如果沒(méi)有發(fā)生緩存重填,則調(diào)用cache_grow申請(qǐng)一個(gè)新的slab,
     *    并申請(qǐng)分配新的空閑對(duì)象
     */
    if (unlikely(!ac->avail)) {
        int x;
        x = cache_grow(cachep, flags, -1);

        /* 9. 沒(méi)有申請(qǐng)到新slab緩存,則返回NULL;
         *    否則跳轉(zhuǎn)到第一步,重新分配對(duì)象
         */
        ac = ac_data(cachep);
        if (!x && ac->avail == 0)   // 沒(méi)有可用的空閑對(duì)象,放棄
            return NULL;

        if (!ac->avail)     // 對(duì)象重填被中斷?
            goto retry;
    }

    /* 7 如果ac->avail大于0(說(shuō)明某些緩存被重填了),設(shè)置本地緩存被使用,
     *   并返回插入到本地緩存中的最后一個(gè)空閑對(duì)象的指針
     */
    ac->touched = 1;
    return ac_entry(ac)[--ac->avail];
}

13 釋放slab對(duì)象

kmem_cache_free()函數(shù)釋放先前由slab分配器分配給某個(gè)內(nèi)核函數(shù)的對(duì)象。它的參數(shù)是cachep,緩存描述符的地址,objp,要釋放的對(duì)象的地址:

    void kmem_cache_free(kmem_cache_t *cachep, void *objp)
    {
        unsigned long flags;
        struct array_cache *ac;

        local_irq_save(flags);
        ac = cachep->array[smp_processor_id()];
        if (ac->avail == ac->limit)
            cache_flusharray(cachep, ac);
        ((void**)(ac+1))[ac->avail++] = objp;
        local_irq_restore(flags);
    }

該函數(shù)首先檢查本地緩存是否有空間容納指向空閑對(duì)象的額外指針。如果是,指針被添加到本地緩存中,函數(shù)返回。否則,它首先調(diào)用cache_flusharray()來(lái)耗盡本地緩存,然后將指針添加到本地緩存。

cache_flusharray()函數(shù)的主要功能如下:

static void cache_flusharray (kmem_cache_t* cachep, struct array_cache *ac)
{
    int batchcount;
    batchcount = ac->batchcount;
    check_irq_off();

    /* 1. 申請(qǐng)自旋鎖 */
    spin_lock(&cachep->spinlock);

    /* 2. 判斷貢獻(xiàn)本地緩存是否還有空間,如果有,
     *    則從CPU本地緩存中拷貝batchcount個(gè)指針到共享緩存中;
     *    然后跳轉(zhuǎn)到第4步。
     */
    if (cachep->lists.shared) {
        struct array_cache *shared_array = cachep->lists.shared;
        int max = shared_array->limit-shared_array->avail;
        if (max) {
            if (batchcount > max)
                batchcount = max;
            memcpy(&ac_entry(shared_array)[shared_array->avail],
                    &ac_entry(ac)[0],
                    sizeof(void*)*batchcount);
            shared_array->avail += batchcount;
            goto free_done;
        }
    }

    /* 3 將當(dāng)前本地緩存中的batchcount對(duì)象返還給`slab`分配器 */
    free_block(cachep, &ac_entry(ac)[0], batchcount);
free_done:
    /* 4. 釋放自旋鎖 */
    spin_unlock(&cachep->spinlock);
    /* 5. 將移動(dòng)到共享本地緩存或slab分配器中的對(duì)象個(gè)數(shù)從本地緩存描述符中減去 */
    ac->avail -= batchcount;
    /* 6. 將本地緩存中所有合法的指針移動(dòng)到本地緩存數(shù)組的起始處。
     *    因?yàn)樵鹗继幍膶?duì)象指針已經(jīng)被我們移走了。
     */
    memmove(&ac_entry(ac)[0], &ac_entry(ac)[batchcount],
            sizeof(void*)*ac->avail);
}

free_block()函數(shù)的主要功能如下:

static void free_block(kmem_cache_t *cachep, void **objpp, int nr_objects)
{
    int i;

    check_spinlock_acquired(cachep);

    /* NUMA: move add into loop
     * 1. 增加cache描述符的空閑對(duì)象計(jì)數(shù)
     */
    cachep->lists.free_objects += nr_objects;

    for (i = 0; i < nr_objects; i++) {
        void *objp = objpp[i];
        struct slab *slabp;
        unsigned int objnr;

        /* 2. 獲取對(duì)象所在slab的描述符地址:
         *    (slab所在頁(yè)的描述符的lru字段指向相應(yīng)的slab描述符)
         */
        slabp = GET_PAGE_SLAB(virt_to_page(objp));
        // 3. 從slab cache列表中移除slab描述符
        // (cachep->lists.slabs_partial或cachep->lists.slabs_full)
        list_del(&slabp->list);
        // 4. 計(jì)算對(duì)象在slab中的索引
        objnr = (objp - slabp->s_mem) / cachep->objsize;
        check_slabp(cachep, slabp);

        // 5. 將slab中下一個(gè)空閑對(duì)象的索引存入對(duì)象描述符中
        //    下一個(gè)空閑對(duì)象是`objnr`。
        //    (最后一個(gè)釋放的對(duì)象,將是第一個(gè)待分配的對(duì)象)
        slab_bufctl(slabp)[objnr] = slabp->free;
        slabp->free = objnr;
        STATS_DEC_ACTIVE(cachep);
        // 6. 對(duì)象已經(jīng)恢復(fù)空閑
        slabp->inuse--;
        check_slabp(cachep, slabp);

        /* */
        if (slabp->inuse == 0) {
            /* 7 如果slab中所有對(duì)象都是空閑的(inuse=0),且
             *   整個(gè)slab緩存中空閑對(duì)象的數(shù)量大于cache上限,則
             *   將多余的空閑對(duì)象占用的slab頁(yè)幀釋放回`ZONE`頁(yè)幀分配器>
             *   
             *   cachep->free_limit == cachep->num+ (1+N) × cachep->batchcount
             *   此處的N是系統(tǒng)中CPU核的數(shù)量。
             */
            if (cachep->lists.free_objects > cachep->free_limit) {
                cachep->lists.free_objects -= cachep->num;
                slab_destroy(cachep, slabp);
            } else {
                /* 8. 如果slab中所有對(duì)象都是空閑的(inuse=0),但整個(gè)slab緩存
                 *    中的空閑對(duì)線小于等于cachep->free_limit,則將slab描述符
                 *    插入到cachep->lists.slabs_free列表中
                 */
                list_add(&slabp->list,
                &list3_data_ptr(cachep, objp)->slabs_free);
            }
        } else {
            /* 9. 如果inuse大于0,說(shuō)明slab部分被用,所以將slab描述符插入到
             *    cachep->lists.slabs_partial列表中。
             *    無(wú)條件的將一個(gè)slab移動(dòng)到slabs_partial列表的末尾,
             *    這也是釋放其它的最大時(shí)間。
             */
            list_add_tail(&slabp->list,
                &list3_data_ptr(cachep, objp)->slabs_partial);
        }
    }
}

14 通用對(duì)象

在前面通用和特殊緩存一節(jié)中,對(duì)內(nèi)存不頻繁的請(qǐng)求是通過(guò)一組通用緩存實(shí)現(xiàn)的,這些通用緩存對(duì)象的大小是按照幾何大小均勻分布的,從32→131072個(gè)字節(jié)。

這類對(duì)象是通過(guò)kmalloc()實(shí)現(xiàn)的,約等于下面的代碼:

    void * kmalloc(size_t size, int flags)
    {
        struct cache_sizes  *csizep = malloc_sizes;
        kmem_cache_t        *cachep;

        for (; csizep->cs_size; csizep++) {
            if (size > csizep->cs_size)
                continue;
            if (flags & __GFP_DMA)
                cachep = csizep->cs_dmacachep;
            else
                cachep = csizep->cs_cachep;
            return kmem_cache_alloc(cachep, flags);
        }
        return NULL;
    }

該函數(shù)借助malloc_sizes表鎖定最接近請(qǐng)求內(nèi)存大小的2的冪次方對(duì)應(yīng)的cache描述符表。然后調(diào)用kmem_cache_alloc()分配對(duì)象,可以傳遞給DMA內(nèi)存的緩存描述符,也可以是普通內(nèi)存的緩存描述符,取決于調(diào)用者是否傳遞了__GFP_DMA標(biāo)志。

對(duì)應(yīng)的釋放通用緩存對(duì)象的函數(shù)是kfree():

    void kfree(const void *objp)
    {
        kmem_cache_t    *c;
        unsigned long   flags;

        if (!objp)
            return;
        local_irq_save(flags);
        c = (kmem_cache_t *)(virt_to_page(objp)->lru.next);
        kmem_cache_free(c, (void *)objp);
        local_irq_restore(flags);
    }

正確的緩存描述符是通過(guò)讀取包含內(nèi)存區(qū)域的第1頁(yè)幀的描述符的lru.next字段來(lái)確定的。通過(guò)調(diào)用kmem_cache_free()釋放內(nèi)存區(qū)域。

15 內(nèi)存池

內(nèi)存池是Linux v2.6內(nèi)核的一個(gè)新特性?;旧?,內(nèi)存池允許內(nèi)核子系統(tǒng)(比如塊設(shè)備子系統(tǒng))分配一些動(dòng)態(tài)內(nèi)存,僅在內(nèi)存不足的緊急情況下使用。

首先我們不應(yīng)該將內(nèi)存池(memory pool)和預(yù)留頁(yè)幀池混淆。

實(shí)際上,這些預(yù)留頁(yè)幀只能用于滿足中斷處理程序或關(guān)鍵代碼區(qū)發(fā)出的原子內(nèi)存分配請(qǐng)求。相反,內(nèi)存池是動(dòng)態(tài)內(nèi)存的儲(chǔ)備,只能由特定的內(nèi)核組件(即內(nèi)存池的“所有者”)使用。內(nèi)核組件通常不使用這些預(yù)留的動(dòng)態(tài)內(nèi)存池;但是,如果動(dòng)態(tài)內(nèi)存變得稀缺時(shí),以至于所有正常的內(nèi)存分配請(qǐng)求都注定要失敗,內(nèi)核組件可以調(diào)用特殊的內(nèi)存池函數(shù),作為最后的手段,可以從內(nèi)存池預(yù)留獲得所需的內(nèi)存。

通常,內(nèi)存池也是基于slab分配器申請(qǐng)的,也就是說(shuō),內(nèi)存池就是預(yù)留了一些slab對(duì)象。但是,內(nèi)存池可以分配任何類型的動(dòng)態(tài)內(nèi)存,從整個(gè)的頁(yè)幀到非常小的內(nèi)存塊都可以。因此,我們一般將內(nèi)存池處理的內(nèi)存單元稱為內(nèi)存元素。

內(nèi)存池用mempool_t數(shù)據(jù)結(jié)構(gòu)表示,各個(gè)字段如下表所示:

類型 名稱 描述
spinlock_t lock 保護(hù)對(duì)象的自旋鎖
int min_nr 內(nèi)存池的最大數(shù)量
int curr_nr 內(nèi)存池的當(dāng)前數(shù)量
void ** elements 指向內(nèi)存池中內(nèi)存元素指針的數(shù)組的指針
void * pool_data 內(nèi)存池所有者的私有數(shù)據(jù)
mempool_alloc_t * alloc 分配一個(gè)內(nèi)存池元素的方法
mempool_free_t * free 釋放一個(gè)內(nèi)存池元素的方法
wait_queue_head_t wait 內(nèi)存池為空時(shí)使用的等待隊(duì)列

min_nr存儲(chǔ)內(nèi)存池中元素的初始數(shù)量。換句話說(shuō),存儲(chǔ)在此字段中的值表示內(nèi)存池的所有者肯定會(huì)從內(nèi)存分配器獲得的內(nèi)存元素的數(shù)量。curr_nr總是小于或等于min_nr,它存儲(chǔ)當(dāng)前包含在內(nèi)存池中的內(nèi)存元素的數(shù)量。內(nèi)存元素本身由一個(gè)指針數(shù)組引用,其地址存儲(chǔ)在“elements”字段中。

alloc和free方法分別與底層內(nèi)存分配器交互以獲取和釋放內(nèi)存元素。這兩種方法都可以是由擁有內(nèi)存池的內(nèi)核組件提供的自定義函數(shù)。

當(dāng)內(nèi)存元素是slab對(duì)象時(shí),alloc和free方法通常由mempool_alloc_slab()和mempool_free_slab()函數(shù)實(shí)現(xiàn),它們分別調(diào)用kmem_cache_alloc()和kmem_cache_free()函數(shù)。在這種情況下,mempool_t對(duì)象的pool_data字段存儲(chǔ)slab緩存描述符的地址。

當(dāng)內(nèi)存元素是自定義數(shù)據(jù)對(duì)象時(shí),alloc和free方法通常由kmalloc和kfree函數(shù)實(shí)現(xiàn)。分配的通用緩存對(duì)象。如下所示:

    static void *pkt_rb_alloc(int gfp_mask, void *data)
    {
        return kmalloc(sizeof(struct pkt_rb_node), gfp_mask);
    }

mempool_create()創(chuàng)建一個(gè)新的內(nèi)存池:它接收的參數(shù)是min_nr,alloc和free函數(shù)的地址,及pool_data。該函數(shù)為mempool_t對(duì)象和指向內(nèi)存元素的指針數(shù)組分配內(nèi)存,然后重復(fù)調(diào)用alloc方法來(lái)獲取min_nr內(nèi)存元素。相反,mempool_destroy()函數(shù)釋放池中的所有內(nèi)存元素,然后釋放元素?cái)?shù)組和mempool_t對(duì)象本身。

為了從內(nèi)存池中分配一個(gè)元素,內(nèi)核調(diào)用mempool_alloc()函數(shù),傳遞給它mempool_t對(duì)象的地址和內(nèi)存分配標(biāo)志。本質(zhì)上,該函數(shù)根據(jù)指定的內(nèi)存分配標(biāo)志,通過(guò)調(diào)用alloc方法,嘗試從底層內(nèi)存分配器分配內(nèi)存元素。如果分配成功,該函數(shù)返回獲得的內(nèi)存元素,而不觸及內(nèi)存池。否則,如果分配失敗,則從內(nèi)存池中取一個(gè)內(nèi)存元素。當(dāng)然,在內(nèi)存不足的情況下,太多的內(nèi)存分配請(qǐng)求會(huì)耗盡內(nèi)存池;在這種情況下,如果沒(méi)有設(shè)置__GFP_WAIT標(biāo)志,mempool_alloc()會(huì)阻塞當(dāng)前進(jìn)程,直到內(nèi)存元素被釋放到內(nèi)存池中。

相反,要將元素釋放到內(nèi)存池中,內(nèi)核調(diào)用mempool_free()函數(shù)。如果內(nèi)存池未滿(curr_min小于min_nr),則該函數(shù)將元素添加到內(nèi)存池中。否則,mempool_free()調(diào)用free方法將元素釋放到底層內(nèi)存分配器。

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 內(nèi)核
    +關(guān)注

    關(guān)注

    3

    文章

    1346

    瀏覽量

    40152
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11161

    瀏覽量

    208464
  • 分配器
    +關(guān)注

    關(guān)注

    0

    文章

    193

    瀏覽量

    25664
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4256

    瀏覽量

    62223

原文標(biāo)題:Linux內(nèi)核8.7-內(nèi)存管理之slab分配器

文章出處:【微信號(hào):嵌入式ARM和Linux,微信公眾號(hào):嵌入式ARM和Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Linux內(nèi)核內(nèi)存管理ZONE內(nèi)存分配器

    內(nèi)核中使用ZONE分配器滿足內(nèi)存分配請(qǐng)求。該分配器必須具有足夠的空閑頁(yè)幀,以便滿足各種內(nèi)存大小請(qǐng)
    的頭像 發(fā)表于 02-21 09:29 ?815次閱讀

    內(nèi)核內(nèi)存是如何進(jìn)行分配

    嵌入式LINUX驅(qū)動(dòng)學(xué)習(xí)12內(nèi)核內(nèi)存分配一、頭文件、函數(shù)及說(shuō)明:一、頭文件、函數(shù)及說(shuō)明://頭文件位置 : include/
    發(fā)表于 12-17 06:44

    關(guān)于RTT支持的內(nèi)存分配算法

    的融合。 最原始的SLAB算法是Jeff Bonwick為Solaris 操作系統(tǒng)而引入的一種高效內(nèi)核內(nèi)存分配算法。 RT-Thread的SLAB
    發(fā)表于 04-27 14:40

    關(guān)于RTT支持的內(nèi)存分配算法

    的融合。 最原始的SLAB算法是Jeff Bonwick為Solaris 操作系統(tǒng)而引入的一種高效內(nèi)核內(nèi)存分配算法。 RT-Thread的SLAB
    發(fā)表于 04-27 14:42

    VGA分配器,VGA分配器是什么意思

    VGA分配器,VGA分配器是什么意思 VGA分配器的概念:   VGA分配器是將計(jì)算機(jī)或其它VGA輸出信號(hào)分配至多個(gè)VGA顯示設(shè)備或投影顯
    發(fā)表于 03-26 09:59 ?2457次閱讀

    分配器,什么是分配器

    分配器,什么是分配器 將一路微波功率按一定比例分成n路輸出的功率元件稱為功率分配器。按輸出功率比例不同, 可分為等功率分配器和不等功率
    發(fā)表于 04-02 13:48 ?2955次閱讀
    <b class='flag-5'>分配器</b>,什么是<b class='flag-5'>分配器</b>

    linux內(nèi)存管理中的SLAB分配器詳解

    管理區(qū)頁(yè)框分配器,這里我們簡(jiǎn)稱為頁(yè)框分配器,在頁(yè)框分配器中主要是管理物理內(nèi)存,將物理
    發(fā)表于 05-17 15:01 ?2141次閱讀
    <b class='flag-5'>linux</b><b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>中的<b class='flag-5'>SLAB</b><b class='flag-5'>分配器</b>詳解

    深入剖析SLUB分配器SLAB分配器的區(qū)別

    首先為什么要說(shuō)slub分配器,內(nèi)核里小內(nèi)存分配一共有三種,SLAB/SLUB/SLOB,slub分配器
    發(fā)表于 05-17 16:05 ?1054次閱讀
    深入剖析SLUB<b class='flag-5'>分配器</b>和<b class='flag-5'>SLAB</b><b class='flag-5'>分配器</b>的區(qū)別

    Linux內(nèi)核深度解析》之內(nèi)存地址空間

    內(nèi)核空間提供了把頁(yè)劃分成小內(nèi)存分配的塊分配器,提供分配內(nèi)存的接口 kmalloc()和釋放
    的頭像 發(fā)表于 07-15 14:22 ?2223次閱讀

    bootmem分配器使用的數(shù)據(jù)結(jié)構(gòu)

    內(nèi)核初始化的過(guò)程中需要分配內(nèi)存,內(nèi)核提供了臨時(shí)的引導(dǎo)內(nèi)存分配器,在頁(yè)
    的頭像 發(fā)表于 07-22 11:18 ?1375次閱讀

    Linux引導(dǎo)內(nèi)存分配器

    早期使用的引導(dǎo)內(nèi)存分配器是 bootmem,目前正在使用 memblock 取代 bootmem。如果開(kāi)啟配置宏 CONFIG_NO_BOOTMEM,memblock 就會(huì)取代 bootmem。為了保證兼容性,bootmem 和 memblock 提供了相同的接口。
    的頭像 發(fā)表于 07-22 11:17 ?1404次閱讀

    Linux內(nèi)核伙伴分配器

    內(nèi)核初始化完畢后,使用頁(yè)分配器管理物理頁(yè),當(dāng)前使用的頁(yè)分配器是伙伴分配器,伙伴分配器的特點(diǎn)是算法
    的頭像 發(fā)表于 07-25 14:06 ?1729次閱讀

    Linux內(nèi)核分配器

    為了解決小塊內(nèi)存分配問(wèn)題,Linux 內(nèi)核提供了塊分配器,最早實(shí)現(xiàn)的塊分配器
    的頭像 發(fā)表于 07-27 09:35 ?1537次閱讀

    Linux內(nèi)核引導(dǎo)內(nèi)存分配器的原理

    Linux內(nèi)核引導(dǎo)內(nèi)存分配器使用的是伙伴系統(tǒng)算法。這種算法是一種用于動(dòng)態(tài)內(nèi)存分配的高效算法,它將
    發(fā)表于 04-03 14:52 ?363次閱讀

    Linux內(nèi)核slab性能優(yōu)化的核心思想

    今天分享一篇內(nèi)存性能優(yōu)化的文章,文章用了大量精美的圖深入淺出地分析了Linux內(nèi)核slab性能優(yōu)化的核心思想,slab
    的頭像 發(fā)表于 11-13 11:45 ?546次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b><b class='flag-5'>slab</b>性能優(yōu)化的核心思想