Apache內存池內幕

 

Apache內存池內幕(1)

對於APR中的所有的對象中,內存池對象應該是其餘對象內存分配的基礎,不僅是APR中的對象,而且對於整個Apache中的大部分對象的內存都是從內存池中進行分配的,因此我們將把內存池作爲整個APR的基礎。

2.1 內存池概述

在C語言中,內存管理的問題臭名昭著,一直是開發人員最頭疼的問題。對於小型程序而言,少許的內存問題,比如內存泄露可能還能忍受,但是對於Apache這種大負載量的服務器而言,內存的問題變得尤其重要,因爲絲毫的內存泄露以及頻繁的內存分配都可能導致服務器的效率下降甚至崩潰。
通常情況下,內存的分配和釋放通常都是mallloc和free顯式進行的。這樣做顯得單調無味,同時也可能充滿各種令人厭惡的問題。對同一塊內存的多次釋放通常會導致頁面錯誤,而一直不釋放又導致內存泄露,並且使得服務器性能大大下降。
爲了在大而且複雜的Apache中避免內在的內存管理問題,Apache的開發者創建了一套基於池概念的內存管理方案,最後這套方法移到APR中成爲通用的內存管理方案。
在這套方案中,核心概念是池的概念。Apache中的內存分配的基本結構都是資源池,包括線程池,套接字池等等。內存池通常是一塊很大的內存空間,一次性被分配成功,然後需要的時候直接去池中取,而不需要重新分配,這樣避免的頻繁的malloc操作,而且另一方面,即時內存的使用者忘記釋放內存或者根本就不想分配,那麼這些內存也不會丟失,它們仍然保存在內存池中,當內存池被銷燬的時候這些內存將自動的被銷燬。
由於Apache中的大部分資源的分配都是從內存池中分配的,因此對於大部分的Apache函數,如果其內部需要進行資源分配,那麼它的函數參數中總是會帶有一個內存池參數,該內存池參數指明分配內存來自的內存池,比如下面的兩個函數:
APR_DECLARE(apr_array_header_t *) apr_array_copy(apr_pool_t *p,const apr_array_header_t *arr);
APU_DECLARE_NONSTD(apr_status_t) apr_bucket_setaside_noop(apr_bucket *data,apr_pool_t *pool);
由於在函數的內部需要進行內存分配,因此這兩個函數的參數中都指定了一個apr_pool_t的結構,用以指名函數內存分配來自的內存池。在後面的大部分過程中我們對於該參數將不再做多餘的解釋。
Apache中的內存池並不是僅僅一個內存池,相反而是存在多個內存池,這些內存池之間形成層次結構。如果Apache中僅僅存在一個內存池的話,潛在的問題是所有的內存分配都來自這個池,而且最要命的這些內存必須在整個Apache關閉時候才被釋放,這一點顯然不是那麼合情合理,爲此Apache中根據處理階段的週期長短又引出了子內存池的概念,與之對應的是父內存池以及根內存池的概念,它們的唯一區別就是存在的週期的不同而已。比如對於HTTP連接而言,包括兩種內存池:連接內存池和請求內存池。由於一個連接可能包含多個請求,因此連接的生存週期總是比一個請求的週期長,爲此連接處理中所需要的內存則從連接內存池中分配,而請求則從請求內存池中分配。而一個請求處理完畢後請求內存池被釋放,一個連接處理後連接內存池被釋放。根內存池在整個Apache運行期間都存在。Apache中一個內存池的層次結構圖可以大致如下描述:
內存池層次結構
內存池的層次圖

2.2 內存池分配結點

在瞭解內存池的概念之前,我們首先了解一些內存池分配結點的概念。爲了能夠方便的對分配的內存進行管理,Apache中使用了內存結點的概念來描述每次分配的內存塊。其結構類型則描述爲apr_memnode_t,該結構定義在文件Apr_allocator.h中,其定義如下:
/** basic memory node structure */
struct apr_memnode_t {
    apr_memnode_t *next;               /**< next memnode */
    apr_memnode_t **ref;               /**< reference to self */
    apr_uint32_t   index;              /**< size */
    apr_uint32_t   free_index;         /**< how much free */
    char          *first_avail;        /**< pointer to first free memory */
    char          *endp;               /**< pointer to end of free memory */
};
該結點類型是整個Apache內存管理的基石,在後面的部分我們將其稱之爲“內存結點類型”或者簡稱爲“內存結點”或者“結點”。在該結構中,不同的結點之間通過next指針形成結點鏈表;另外當在結點內部的時候爲了方便引用結點本身,成員變量中還引入了ref,該變量主要用來記錄當前結點的首地址,即使身在結點內部,也可以通過ref指針得到該結點並對該結點進行操作。
從上面的結構中可以看出事實上在apr_memnode_t結構內部沒有任何的“空閒空間”來容納實際分配的內存,事實上,它從來不單獨存在,總是依附於具體的分配的內存單元。通常情況下,一旦分配了實際的空間之後,Apache總是將該結構置於整個單元的最頂部,如圖3.1所示。
 內存池分配子結點
圖3.1 內存結點示意
在上圖中,我們可能調用malloc函數分配了16K大小的空間,爲了能夠將該空間用Apache的結點進行記錄,我們將apr_memnode_t置於整個空間的頭部,此時剩下的可用空間大小應該爲16K-sizeof(apr_memnode_t),同時結構中還提供了first_avail和end_p指針分別指向這塊可用空間的首部和尾部。當這塊可用空間被不斷利用時,first_avail和end_p指針也不斷隨之移動,不過(end_p-first_avail)之間則永遠是當前的空閒空間。上圖的右邊部分演示了這種佈局。
通常情況下,其分配語句大致如下:
apr_memnode_t* node;
node=(apr_memnode_t*)malloc(size);
node->next = NULL;
node->index = index;
node->first_avail = (char *)node + APR_MEMNODE_T_SIZE;
node->endp = (char *)node + size;
Apache中對內存的分配大小並不是隨意的,隨意的分配可能會造成更多的內存碎片。爲此Apache採取的則是“規則塊”分配原則。Apache所支持的分配的最小空間是8K,如果分配的空間達不到8K的大小,則按照8K去分配;如果需要的空間超過8K,則將分配的空間往上調整爲4K的倍數。爲此我們在程序中很多地方會看到下面的宏APR_ALIGN,其定義如下:
/* APR_ALIGN() is only to be used to align on a power of 2 boundary */
#define APR_ALIGN(size, boundary) /
    (((size) + ((boundary) - 1)) & ~((boundary) - 1))
該宏所做的無非就是計算出最接近size的boundary的整數倍的整數。通常情況下size大小爲整數即可,而boundary則必須保證爲2的倍數。比如APR_ALIGN(7,4)爲8;APR_ALIGN(21,8)爲24;APR_ALIGN(21,16)則爲32。不過Apache中用的最多的還是APR_ALIGN_DEFAULT,其實際上是APR_ALIGN(size,8)。在以後的地方,我們將這種處理方式稱之爲“8對齊”或者“4K對齊”或者類似。
因此如果對於APR_ALIGN_DEFAULT(sizeof(apr_memnode_t)),其等同於APR_ALIGN(sizeof(apr_memnode_t),8)。與之對應,APR中爲了處理方便,同時也將apr_memnode_t結構的大小從sizeof(apr_memnode_t)調整爲APR_ALIGH_DEFAULT(sizeof(apr_memnode_t))。在前面的部分我們曾經描述過,對於一塊16K的內存區域,如果其用apr_memnode_t進行記錄的話,實際的可用空間大小並不是16K-sizeof(apr_memnode_t),更精確地則應該是16K-APR_ALIGN_DEFAULT(sizeof(apr_memnode_t))。
因此如果我們看到Apache中的下面的語句,我們就沒有什麼好驚訝的了。
size = APR_ALIGN(size + APR_MEMNODE_T_SIZE, 4096);
if (size <8192)
size = 8192;
在上面的代碼中我們將實際的常量都替換成實際的整數。APR_MEMNODE_T是對sizeof(apr_memnode_t)進行調整後的值。上面的語句所作的正是我們前面所說的分配策略:如果需要分配的空間累計結點頭的空間總和小於8K,則以8K進行分配,否則調整爲4K的整數倍。按照這種分配策略,如果我們要求分配的size大小爲4192,其按照最小單元分配,實際分配大小爲8192;如果我們要求分配的空間爲8192,由於其加上內存結點頭,大於8192,此時將按照最小單元分配4k,此時實際分配的空間大小爲8192+4996=12K。這樣,每個結點的空間大小都不完全一樣,爲此分配結點本身必須瞭解本結點的大小,這個可以使用index進行記錄。
不過Apache記錄內存的大小有自己的獨特的方法。如果空間爲12K,那麼Apache並不會直接將12K賦值給index變量。相反,index只是記錄當前結點大小相對於4K的倍數,計算方法如下:
index = (size >> BOUNDARY_INDEX) - 1;
這樣如果index =5,我們就可以知道該結點大小爲20K;反過來也是如此。通過這樣方法,可以節省一定的存儲空間,另一方面,也方便了程序處理。在後面的部分,我們將通過這種方法計算出來的值稱之爲“索引大小”,因此在後面的部分,我們如果需要描述內存結點大小的時候,我們直接稱之爲“索引大小爲n”或者“大小爲n”,後面不再贅述。與此相同,free_index則是定義了當前結點中的可用的空間的大小。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章