NO IMAGE

核心記憶體池管理技術實現分析

一.Linux系統核心記憶體管理簡介

Linux採用“按需調頁”演算法,支援三層頁式儲存管理策略。將每個使用者程序4GB長度的虛擬記憶體劃分成固定大小的頁面。其中0至3GB是使用者態空間,由各程序獨佔;3GB到4GB是核心態空間,由所有程序共享,但只有核心態程序才能訪問。

Linux將實體記憶體也劃分成固定大小的頁面,由資料結構page管理,有多少頁面就有多少page結構,它們又作為元素組成一個陣列mem_map[]。

slab:在作業系統的運作過程中,經常會涉及到大量物件的重複生成、使用和釋放問題。物件生成演算法的改進,可以在很大程度上提高整個系統的效能。在Linux系統中所用到的物件,比較典型的例子是inode、task_struct等,都又這些特點。一般說來,這類物件的種類相對穩定,每種物件的數量卻是巨大的,並且在初始化與析構時要做大量的工作,所佔用的時間遠遠超過記憶體分配的時間。但是這些物件往往具有這樣一個性質,即他們在生成時,所包括的成員屬性值一般都賦成確定的數值,並且在使用完畢,釋放結構前,這些屬性又恢復為未使用前的狀態。因此,如果我們能夠用合適的方法使得在物件前後兩次背使用時,在同一塊記憶體,或同一類記憶體空間,且保留了基本的資料結構,就可以大大提高效率。slab演算法就是針對上述特點設計的。

slab演算法思路中最基本的一點被稱為object-caching,即物件快取。其核心做法就是保留物件初始化狀態的不變部分,這樣物件就用不著在每次使用時重新初始化(構造)及破壞(析構)。

物件導向的slab分配中有如下幾個術語:

l         緩衝區(cache):一種物件的所有例項都存在同一個快取區中。不同的物件,即使大小相同,也放在不同的快取區內。每個快取區有若干個slab,按照滿,半滿,空的順序排列。在slab分配的思想中,整個核心態記憶體塊可以看作是按照這種快取區來組織的,對每一種物件使用一種快取區,快取區的管理者記錄了這個快取區中物件的大小,性質,每個slab塊中物件的個數以及每個slab塊大小。

l         slab塊:slab塊是核心記憶體分配與頁面級分配的介面。每個slab塊的大小都是頁面大小的整數倍,有若干個物件組成。slab塊共分為三類:

完全塊:沒有空閒物件。

部分塊:只分配了部分物件空間,仍有空閒物件。

空閒塊:沒有分配物件,即整個塊內物件空間均可分配。

在申請新的物件空間時,如果緩衝區中存在部分塊,那麼首先檢視部分塊尋找空閒物件空間,若未成功再檢視空閒塊,如果還未成功則為這個物件分配一塊新的slab塊。

l         物件:將被申請的空間視為物件,使用建構函式初始化物件,然後由使用者使用物件。

 

二.記憶體池的資料結構

Linux記憶體池是在2.6版核心中加入的,主要的資料結構定義在mm/mempool.c中。

typedef struct mempool_s {

       spinlock_t lock;

       int min_nr;             /* elements陣列中的成員數量 */

       int curr_nr;            /* 當前elements陣列中空閒的成員數量 */

       void **elements;    /* 用來存放記憶體成員的二維陣列,其長度為min_nr,寬度是上述各個記憶體物件的長度,因為對於不同的物件型別,我們會建立相應的記憶體池物件,所以每個記憶體池物件例項的element寬度都是跟其記憶體物件相關的 */

 

       void *pool_data;     /* 記憶體池與核心緩衝區結合使用(上面的簡介當中提到了,Linux採用slab技術預先為每一種記憶體物件分配了快取區,每當我們申請某個型別的記憶體物件時,實際是從這種快取區獲取記憶體),這個指標一般是指向這種記憶體物件對應的快取區的指標 */

       mempool_alloc_t *alloc; /* 使用者在建立一個記憶體池物件時提供的記憶體分配函式,這個函式可以使用者自行編寫(因為對於某個記憶體物件如何獲取記憶體,其開發者完全可以自行控制),也可以採用記憶體池提供的分配函式 */

       mempool_free_t *free;   /* 記憶體釋放函式,其它同上 */

       wait_queue_head_t wait;/* 任務等待佇列 */

} mempool_t;

 

三.核心快取區和記憶體池的初始化

上面提到,記憶體池的使用是與特定型別的記憶體物件快取區相關聯的。例如,在系統rpc服務中,系統初始化時,會為rpc_buffers預先分配快取區,呼叫如下語句:

rpc_buffer_slabp = kmem_cache_create(“rpc_buffers”,

                                        RPC_BUFFER_MAXSIZE,

                                        0, SLAB_HWCACHE_ALIGN,

                                        NULL, NULL);

呼叫kmem_cache_create函式從系統快取區cache_cache中獲取長度為RPC_BUFFER_MAXSIZE的快取區大小的記憶體,作為rpc_buffer使用的快取區。而以後對rpc操作的所有資料結構記憶體都是從這塊快取區申請,這是linux的slab技術的要點,而記憶體池也是基於這段快取區進行的操作。

一旦rpc服務申請到了一個快取區rpc_buffer_slabp以後,就可以建立一個記憶體池來管理這個快取區了:

rpc_buffer_mempool = mempool_create(RPC_BUFFER_POOLSIZE,

                                       mempool_alloc_slab,

                                       mempool_free_slab,

                                       rpc_buffer_slabp);

mempool_create函式就是記憶體池建立函式,負責為一類記憶體物件構造一個記憶體池,傳遞的引數包括,記憶體池大小,定製的記憶體分配函式,定製的記憶體解構函式,這個物件的快取區指標。下面是mempool_create函式的具體實現:

/**

 * mempool_create – 建立一個記憶體池物件

 * @min_nr:       為記憶體池分配的最小記憶體成員數量

 * @alloc_fn:       使用者自定義記憶體分配函式

 * @free_fn:       使用者自定義記憶體釋放函式

 * @pool_data:     根據使用者自定義記憶體分配函式所提供的可選私有資料,一般是快取區指標

*/

mempool_t * mempool_create(int min_nr, mempool_alloc_t *alloc_fn,

                            mempool_free_t *free_fn, void *pool_data)

{

       mempool_t *pool;

       /*為記憶體池物件分配記憶體*/

       pool = kmalloc(sizeof(*pool), GFP_KERNEL);

       if (!pool)

              return NULL;

       memset(pool, 0, sizeof(*pool));

       /*根據記憶體池的最小長度為elements陣列分配記憶體*/

       pool->elements = kmalloc(min_nr * sizeof(void *), GFP_KERNEL);

       if (!pool->elements) {

              kfree(pool);

              return NULL;

       }

       spin_lock_init(&pool->lock);

       /*初始化記憶體池的相關引數*/

       pool->min_nr = min_nr;

       pool->pool_data = pool_data;

       init_waitqueue_head(&pool->wait);

       pool->alloc = alloc_fn;

       pool->free = free_fn;

 


       /*首先為記憶體池預先分配min_nr個element物件,這些物件就是為了儲存相應型別的記憶體物件的。資料結構形入:

*/

       while (pool->curr_nr < pool->min_nr) {

              void *element;

 

              element = pool->alloc(GFP_KERNEL, pool->pool_data);

              if (unlikely(!element)) {

                     free_pool(pool);

                     return NULL;

              }

              /*將剛剛申請到的記憶體掛到elements陣列的相應位置上,並修改curr_nr的值*/

              add_element(pool, element);

       }

       /*若成功建立記憶體池,則返回記憶體池物件的指標,這樣就可以利用mempool_alloc和mempool_free訪問記憶體池了。*/

       return pool;

}

 

四.記憶體池的使用

如果需要使用已經建立的記憶體池,則需要呼叫mempool_alloc從記憶體池中申請記憶體以及呼叫mempool_free將用完的記憶體還給記憶體池。

void * mempool_alloc(mempool_t *pool, int gfp_mask)

{

       void *element;

       unsigned long flags;

       DEFINE_WAIT(wait);

       int gfp_nowait = gfp_mask & ~(__GFP_WAIT | __GFP_IO);

 

repeat_alloc:

       /*這裡存在一些不明白的地方,先將使用者傳遞進來的gfp掩碼標誌去掉__GFP_WAIT 和 __GFP_IO 兩個標誌,試圖呼叫使用者自定義分配函式從快取區申請一個記憶體物件,而不是首先從記憶體池從分配,如果申請不到,再從記憶體池中分配。*/

       element = pool->alloc(gfp_nowait|__GFP_NOWARN, pool->pool_data);

       if (likely(element != NULL))

              return element;

 

       /*如果池中的成員(空閒)的數量低於滿時的一半時,需要額外從系統中申請記憶體,而不是從記憶體池中申請了。但是如果這段記憶體使用完了,則呼叫mempool_free將其存放到記憶體池中,下次使用就不再申請了。*/

       mb();

       if ((gfp_mask & __GFP_FS) && (gfp_mask != gfp_nowait) &&

                            (pool->curr_nr <= pool->min_nr/2)) {

              element = pool->alloc(gfp_mask, pool->pool_data);

              if (likely(element != NULL))

                     return element;

       }

 

       spin_lock_irqsave(&pool->lock, flags);

       /*如果當前記憶體池不為空,則從池中獲取一個記憶體物件,返回給申請者*/

       if (likely(pool->curr_nr)) {

              element = remove_element(pool);

              spin_unlock_irqrestore(&pool->lock, flags);

              return element;

       }

       spin_unlock_irqrestore(&pool->lock, flags);

 

       /* We must not sleep in the GFP_ATOMIC case */

       if (!(gfp_mask & __GFP_WAIT))

              return NULL;

       /*下面一部分應該和核心排程有關,所以暫時不看了*/

       prepare_to_wait(&pool->wait, &wait, TASK_UNINTERRUPTIBLE);

       mb();

       if (!pool->curr_nr)

              io_schedule();

       finish_wait(&pool->wait, &wait);

 

       goto repeat_alloc;

}

 

如果申請者呼叫mempool_free準備釋放記憶體,實際上是將記憶體物件重新放到記憶體池中。原始碼實現如下:

void mempool_free(void *element, mempool_t *pool)

{

       unsigned long flags;

 

       mb();

       /*如果當前記憶體池已經滿,則直接呼叫使用者記憶體釋放函式將記憶體還給系統*/

       if (pool->curr_nr < pool->min_nr) {

              spin_lock_irqsave(&pool->lock, flags);

              if (pool->curr_nr < pool->min_nr) {

                     /*如果記憶體池還有剩餘的空間,則將記憶體物件放入池中,喚醒等待佇列*/

                     add_element(pool, element);

                     spin_unlock_irqrestore(&pool->lock, flags);

                     wake_up(&pool->wait);

                     return;

              }

              spin_unlock_irqrestore(&pool->lock, flags);

       }

       pool->free(element, pool->pool_data);

}

這個函式十分簡單,沒有什麼過多的分析了。

 

五.記憶體池實現總結

通過上面的分析,我們發現Linux核心的記憶體池實現相當簡單。而C STL中,實現了二級分配機制,初始化時將記憶體池按照記憶體的大小分成數個級別(每個級別均是8位元組的整數倍,一般是8,16,24,…,128位元組),每個級別都預先分配了20塊記憶體。二級分配機制的基本思想是:如果使用者申請的記憶體大於我們預定義的級別,則直接呼叫malloc從堆中分配記憶體,而如果申請的記憶體大小在128位元組以內,則從最相近的記憶體大小中申請,例如申請的記憶體是10位元組,則可以從16位元組的組中取出一塊交給申請者,如果該組的記憶體儲量(初始是20)小於一定的值,就會根據一個演算法(成為refill演算法),再次從堆中申請一部分記憶體加入記憶體池,保證池中有一定量的記憶體可用。

而Linux的記憶體池實際上是與特定記憶體物件相關聯的,每一種記憶體物件(例如task_struct)都有其特定的大小以及初始化方法,這個與STL的分級有點相似,但是核心主要還是根據實際的物件的大小來確定池中物件的大小。

核心記憶體池初始時從快取區申請一定量的記憶體塊,需要使用時從池中順序查詢空閒記憶體塊並返回給申請者。回收時也是直接將記憶體插入池中,如果池已經滿,則直接釋放。記憶體池沒有動態增加大小的能力,如果記憶體池中的記憶體消耗殆盡,則只能直接從快取區申請記憶體,記憶體池的容量不會隨著使用量的增加而增加。