8.2. 后備緩存

2018-02-24 15:49 更新

8.2.?后備緩存

一個設(shè)備驅(qū)動常常以反復(fù)分配許多相同大小的對象而結(jié)束. 如果內(nèi)核已經(jīng)維護了一套相同大小對象的內(nèi)存池, 為什么不增加一些特殊的內(nèi)存池給這些高容量的對象? 實際上, 內(nèi)核確實實現(xiàn)了一個設(shè)施來創(chuàng)建這類內(nèi)存池, 它常常被稱為一個后備緩存. 設(shè)備驅(qū)動常常不展示這類的內(nèi)存行為, 它們證明使用一個后備緩存是對的, 但是, 有例外; 在 Linux 2.6 中 USB 和 SCSI 驅(qū)動使用緩存.

Linux 內(nèi)核的緩存管理者有時稱為" slab 分配器". 因此, 它的功能和類型在 <linux/slab.h> 中聲明. slab 分配器實現(xiàn)有一個 kmem_cache_t 類型的緩存; 使用一個對 kmem_cache_create 的調(diào)用來創(chuàng)建它們:


kmem_cache_t *kmem_cache_create(const char *name, size_t size,
 size_t offset,
 unsigned long flags,
 void (*constructor)(void *, kmem_cache_t *,
 unsigned long flags), void (*destructor)(void *, kmem_cache_t *, unsigned long flags)); 

這個函數(shù)創(chuàng)建一個新的可以駐留任意數(shù)目全部同樣大小的內(nèi)存區(qū)的緩存對象, 大小由 size 參數(shù)指定. name 參數(shù)和這個緩存關(guān)聯(lián)并且作為一個在追蹤問題時有用的管理信息; 通常, 它被設(shè)置為被緩存的結(jié)構(gòu)類型的名子. 這個緩存保留一個指向 name 的指針, 而不是拷貝它, 因此驅(qū)動應(yīng)當(dāng)傳遞一個指向在靜態(tài)存儲中的名子的指針(常常這個名子只是一個文字字串). 這個名子不能包含空格.

offset 是頁內(nèi)的第一個對象的偏移; 它可被用來確保一個對被分配的對象的特殊對齊, 但是你最可能會使用 0 來請求缺省值. flags 控制如何進行分配并且是下列標(biāo)志的一個位掩碼:

SLAB_NO_REAP
設(shè)置這個標(biāo)志保護緩存在系統(tǒng)查找內(nèi)存時被削減. 設(shè)置這個標(biāo)志通常是個壞主意; 重要的是避免不必要地限制內(nèi)存分配器的行動自由.

SLAB_HWCACHE_ALIGN
這個標(biāo)志需要每個數(shù)據(jù)對象被對齊到一個緩存行; 實際對齊依賴主機平臺的緩存分布. 這個選項可以是一個好的選擇, 如果在 SMP 機器上你的緩存包含頻繁存取的項. 但是, 用來獲得緩存行對齊的填充可以浪費可觀的內(nèi)存量.

SLAB_CACHE_DMA
這個標(biāo)志要求每個數(shù)據(jù)對象在 DMA 內(nèi)存區(qū)分配.

還有一套標(biāo)志用來調(diào)試緩存分配; 詳情見 mm/slab.c. 但是, 常常地, 在用來開發(fā)的系統(tǒng)中, 這些標(biāo)志通過一個內(nèi)核配置選項被全局性地設(shè)置

函數(shù)的 constructor 和 destructor 參數(shù)是可選函數(shù)( 但是可能沒有 destructor, 如果沒有 constructor ); 前者可以用來初始化新分配的對象, 后者可以用來"清理"對象在它們的內(nèi)存被作為一個整體釋放回給系統(tǒng)之前.

構(gòu)造函數(shù)和析構(gòu)函數(shù)會有用, 但是有幾個限制你必須記住. 一個構(gòu)造函數(shù)在分配一系列對象的內(nèi)存時被調(diào)用; 因為內(nèi)存可能持有幾個對象, 構(gòu)造函數(shù)可能被多次調(diào)用. 你不能假設(shè)構(gòu)造函數(shù)作為分配一個對象的一個立即的結(jié)果而被調(diào)用. 同樣地, 析構(gòu)函數(shù)可能在以后某個未知的時間中調(diào)用, 不是立刻在一個對象被釋放后. 析構(gòu)函數(shù)和構(gòu)造函數(shù)可能或不可能被允許睡眠, 根據(jù)它們是否被傳遞 SLAB_CTOR_ATOMIC 標(biāo)志(這里 CTOR 是 constructor 的縮寫).

為方便, 一個程序員可以使用相同的函數(shù)給析構(gòu)函數(shù)和構(gòu)造函數(shù); slab 分配器常常傳遞 SLAB_CTOR_CONSTRUCTOR 標(biāo)志當(dāng)被調(diào)用者是一個構(gòu)造函數(shù).

一旦一個對象的緩存被創(chuàng)建, 你可以通過調(diào)用 kmem_cache_alloc 從它分配對象.


void *kmem_cache_alloc(kmem_cache_t *cache, int flags);

這里, cache 參數(shù)是你之前已經(jīng)創(chuàng)建的緩存; flags 是你會傳遞給 kmalloc 的相同, 并且被參考如果 kmem_cache_alloc 需要出去并分配更多內(nèi)存.

為釋放一個對象, 使用 kmem_cache_free:


 void kmem_cache_free(kmem_cache_t *cache, const void *obj); 

當(dāng)驅(qū)動代碼用完這個緩存, 典型地當(dāng)模塊被卸載, 它應(yīng)當(dāng)如下釋放它的緩存:


 int kmem_cache_destroy(kmem_cache_t *cache); 

這個銷毀操作只在從這個緩存中分配的所有的對象都已返回給它時才成功. 因此, 一個模塊應(yīng)當(dāng)檢查從 kmem_cache_destroy 的返回值; 一個失敗指示某類在模塊中的內(nèi)存泄漏(因為某些對象已被丟失.)

使用后備緩存的一方面益處是內(nèi)核維護緩沖使用的統(tǒng)計. 這些統(tǒng)計可從 /proc/slabinfo 獲得.

8.2.1.?一個基于 Slab 緩存的 scull: scullc

是時候給個例子了. scullc 是一個簡化的 scull 模塊的版本, 它只實現(xiàn)空設(shè)備 -- 永久的內(nèi)存區(qū). 不象 scull, 它使用 kmalloc, scullc 使用內(nèi)存緩存. 量子的大小可在編譯時和加載時修改, 但是不是在運行時 -- 這可能需要創(chuàng)建一個新內(nèi)存區(qū), 并且我們不想處理這些不必要的細(xì)節(jié).

scullc 使用一個完整的例子, 可用來試驗 slab 分配器. 它區(qū)別于 scull 只在幾行代碼. 首先, 我們必須聲明我們自己的 slab 緩存:


/* declare one cache pointer: use it for all devices */
kmem_cache_t *scullc_cache;

slab 緩存的創(chuàng)建以這樣的方式處理( 在模塊加載時 ):


/* scullc_init: create a cache for our quanta */
scullc_cache = kmem_cache_create("scullc", scullc_quantum,
                                 0, SLAB_HWCACHE_ALIGN, NULL, NULL); /* no ctor/dtor */

if (!scullc_cache)
{
        scullc_cleanup();
        return -ENOMEM;

}

這是它如何分配內(nèi)存量子:


/* Allocate a quantum using the memory cache */
if (!dptr->data[s_pos])
{
        dptr->data[s_pos] = kmem_cache_alloc(scullc_cache, GFP_KERNEL);
        if (!dptr->data[s_pos])

                goto nomem;
        memset(dptr->data[s_pos], 0, scullc_quantum);
}

還有這些代碼行釋放內(nèi)存:


for (i = 0; i < qset; i++)
        if (dptr->data[i])
                kmem_cache_free(scullc_cache, dptr->data[i]);

最后, 在模塊卸載時, 我們不得不返回緩存給系統(tǒng):


/* scullc_cleanup: release the cache of our quanta */
if (scullc_cache)
        kmem_cache_destroy(scullc_cache);

從 scull 到 scullc 的主要不同是稍稍的速度提升以及更好的內(nèi)存使用. 因為量子從一個恰好是合適大小的內(nèi)存片的池中分配, 它們在內(nèi)存中的排列是盡可能的密集, 與 scull 量子的相反, 它帶來一個不可預(yù)測的內(nèi)存碎片.

8.2.2.?內(nèi)存池

在內(nèi)核中有不少地方內(nèi)存分配不允許失敗. 作為一個在這些情況下確保分配的方式, 內(nèi)核開發(fā)者創(chuàng)建了一個已知為內(nèi)存池(或者是 "mempool" )的抽象. 一個內(nèi)存池真實地只是一類后備緩存, 它盡力一直保持一個空閑內(nèi)存列表給緊急時使用.

一個內(nèi)存池有一個類型 mempool_t ( 在 <linux/mempool.h> 中定義); 你可以使用 mempool_create 創(chuàng)建一個:


mempool_t *mempool_create(int min_nr,
 mempool_alloc_t *alloc_fn,
 mempool_free_t *free_fn,
 void *pool_data); 

min_nr 參數(shù)是內(nèi)存池應(yīng)當(dāng)一直保留的最小數(shù)量的分配的對象. 實際的分配和釋放對象由 alloc_fn 和 free_fn 處理, 它們有這些原型:


typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);

給 mempool_create 最后的參數(shù) ( pool_data ) 被傳遞給 alloc_fn 和 free_fn.

如果需要, 你可編寫特殊用途的函數(shù)來處理 mempool 的內(nèi)存分配. 常常, 但是, 你只需要使內(nèi)核 slab 分配器為你處理這個任務(wù). 有 2 個函數(shù) ( mempool_alloc_slab 和 mempool_free_slab) 來進行在內(nèi)存池分配原型和 kmem_cache_alloc 和 kmem_cache_free 之間的感應(yīng)淬火. 因此, 設(shè)置內(nèi)存池的代碼常??磥砣绱?


cache = kmem_cache_create(. . .); 
pool = mempool_create(MY_POOL_MINIMUM,mempool_alloc_slab, mempool_free_slab, cache); 

一旦已創(chuàng)建了內(nèi)存池, 可以分配和釋放對象,使用:


void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);

當(dāng)內(nèi)存池創(chuàng)建了, 分配函數(shù)將被調(diào)用足夠的次數(shù)來創(chuàng)建一個預(yù)先分配的對象池. 因此, 對 mempool_alloc 的調(diào)用試圖從分配函數(shù)請求額外的對象; 如果那個分配失敗, 一個預(yù)先分配的對象(如果有剩下的)被返回. 當(dāng)一個對象被用 mempool_free 釋放, 它保留在池中, 如果對齊預(yù)分配的對象數(shù)目小于最小量; 否則, 它將被返回給系統(tǒng).

一個 mempool 可被重新定大小, 使用:


int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);

這個調(diào)用, 如果成功, 調(diào)整內(nèi)存池的大小至少有 new_min_nr 個對象. 如果你不再需要一個內(nèi)存池, 返回給系統(tǒng)使用:


void mempool_destroy(mempool_t *pool); 

你編寫返回所有的分配的對象, 在銷毀 mempool 之前, 否則會產(chǎn)生一個內(nèi)核 oops.

如果你考慮在你的驅(qū)動中使用一個 mempool, 請記住一件事: mempools 分配一塊內(nèi)存在一個鏈表中, 對任何真實的使用是空閑和無用的. 容易使用 mempools 消耗大量的內(nèi)存. 在幾乎每個情況下, 首選的可選項是不使用 mempool 并且代替以簡單處理分配失敗的可能性. 如果你的驅(qū)動有任何方法以不危害到系統(tǒng)完整性的方式來響應(yīng)一個分配失敗, 就這樣做. 驅(qū)動代碼中的 mempools 的使用應(yīng)當(dāng)少.

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號