天天看點

Apache記憶體池内幕1-7

Apache記憶體池内幕(1)        分類:            Apache源代碼分析 2005-12-08 18:30 14988人閱讀 評論(33) 收藏 舉報 apache 伺服器 代碼分析 structure header reference

目錄(?)[+]

  1. 記憶體池概述
  2. 記憶體池配置設定結點

對于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中一個記憶體池的層次結構圖可以大緻如下描述:

Apache記憶體池内幕1-7

                                                                                           記憶體池的層次圖

2.2 記憶體池配置設定結點

在了解記憶體池的概念之前,我們首先了解一些記憶體池配置設定結點的概念。為了能夠友善的對配置設定的記憶體進行管理,Apache中使用了記憶體結點的概念來描述每次配置設定的記憶體塊。其結構類型則描述為apr_memnode_t,該結構定義在檔案Apr_allocator.h中,其定義如下: struct apr_memnode_t {     apr_memnode_t *next;                   apr_memnode_t **ref;                   apr_uint32_t   index;                  apr_uint32_t   free_index;             char          *first_avail;            char          *endp;               }; 該結點類型是整個Apache記憶體管理的基石,在後面的部分我們将其稱之為“ 記憶體結點類型”或者簡稱為“ 記憶體結點”或者“ 結點”。在該結構中,不同的結點之間通過next指針形成結點連結清單;另外當在結點内部的時候為了友善引用結點本身,成員變量中還引入了ref,該變量主要用來記錄目前結點的首位址,即使身在結點内部,也可以通過ref指針得到該結點并對該結點進行操作。 從上面的結構中可以看出事實上在apr_memnode_t結構内部沒有任何的“空閑空間”來容納實際配置設定的記憶體,事實上,它從來不單獨存在,總是依附于具體的配置設定的記憶體單元。通常情況下,一旦配置設定了實際的空間之後,Apache總是将該結構置于整個單元的最頂部,如圖3.1所示。

Apache記憶體池内幕1-7

圖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,其定義如下: #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則是定義了目前結點中的可用的空間的大小。

關于作者

張中慶,目前主要的研究方向是嵌入式浏覽器,移動中間件以及大規模伺服器設計。目前正在進行Apache的源代碼分析,計劃出版《Apache源代碼全景分析》上下冊。Apache系列文章為本書的草案部分,對Apache感興趣的朋友可以通過flydish1234 at sina.com.cn與之聯系!  

2.3 記憶體池配置設定子allocator

2.3.1配置設定子概述

盡管我們可以通過malloc函數直接配置設定apr_memnode_t類型的結點,不過Apache中并不推薦這種做法。事實上Apache中的大部分的記憶體的配置設定都是由記憶體配置設定子allocator完成的。它隐藏了内部的實際的配置設定細節,對外提供了幾個簡單的接口供記憶體池函數調用。記憶體配置設定子屬于内部資料結構,外部程式不能直接調用。記憶體配置設定子(以後簡稱為配置設定子)在檔案apr_pools.c中進行定義如下: struct apr_allocator_t {       apr_uint32_t        max_index;       apr_uint32_t        max_free_index;       apr_uint32_t        current_free_index; #if APR_HAS_THREADS       apr_thread_mutex_t  *mutex; #endif       apr_pool_t          *owner;       apr_memnode_t     *free[MAX_INDEX]; }; 該結構中最重要的無非就是free數組,數組的每個元素都是apr_memnode_t類型的位址,指向一個apr_memnode_t類型的結點連結清單。記憶體配置設定的時候則從實際的結點中進行配置設定,使用完畢後同時傳回給配置設定子。 不過free中的連結清單中結點的大小并不完全相同,其取決于目前連結清單在free數組中的索引。此處free數組的索引index具有兩層次的含義:第一層,該結點連結清單在數組中的實際索引,這是最表層的含義;另外,它還标記了目前連結清單中結點的大小。索引越大,結點也就越大。同一個連結清單中的所有結點大小都完全相等,結點的大小與結點所在連結清單的索引存在如下的關系: 結點大小 =  8K + 4K*(index-1) 是以如果連結清單索引為2,則該連結清單中所有的結點大小都是12K;如果索引為MAX_INDEX,即20,則結點大小應該為8K+4K*(MAX_INDEX-1)=84K,這也是Apache中能夠支援的“規則結點”的最大數目。不過這個公式僅僅适用于數組中1到MAX_INDEX的索引,對于索引0則不适合。當且僅當使用者申請的記憶體塊太大以至于超過了規則結點所能承受的84K的時候,它才會到索引為0的連結清單中去查找。該連結清單中的結點通常都大于84K,而且每個結點的大小也不完全相同。 在後面的部分,我們将索引1到MAX_INDEX所對應的連結清單統稱為“ 規則連結清單”,而每一個連結清單則分開稱之為“ 索引n連結清單”,與之對應,規則連結清單中的結點則統稱為“ 規則結點”,或者稱則為“ 索引n結點”,這是因為它們的大小有一定的規律可遵循;而索引0對應的連結清單則稱之為“ 索引0連結清單”,結點則稱之為“ 索引0結點”。 根據上面的描述,我們可以給出配置設定子的記憶體結構如圖3.2所示。

Apache記憶體池内幕1-7

圖3.2 配置設定子記憶體結構示意 理論上,配置設定子中的最大的結點大小應該為8K+4K*(MAX_INDEX-1),但實際卻未必如此,如果從來沒有配置設定過8K+4K*(MAX_INDEX-1)大小的記憶體,那麼MAX_INDEX索引對應的連結清單很可能是空的。此時在配置設定子中我們用變量max_index表示實際的最大結點。另外如果結點過大,則占用記憶體過多,此時有必要将該結點傳回給作業系統,配置設定子将max_free_index作為記憶體回收的最低門檻。如果該結點小于max_free_index,則不做任何處理,否則使用後必須進行釋放給作業系統。current_free_index則是…。除此之外,mutex使用者保證多線程通路時候的互斥,而owner則記錄了目前配置設定子所屬于的記憶體池。 針對配置設定子,Apache中提供了幾個相關的函數,函數名稱和作用簡要概述如表格3.1。 表3.1 Apache中提供了配置設定子相關函數

配置設定子操作 函數名稱 函數功能簡單描述
建立 apr_allocator_create 建立一個新的配置設定子
銷毀 apr_allocator_destroy 銷毀一個已經存在的配置設定子
空間配置設定 apr_allocator_alloc 調用配置設定子配置設定一定的空間
空間釋放 apr_allocator_free 釋放配置設定子已經配置設定的空間,将它傳回給配置設定子
其餘設定 apr_allocator_owner_set apr_allocator_owner_get 設定和擷取配置設定子所屬的記憶體池
apr_allocator_max_free_set apr_allocator_set_max_free 設定和擷取配置設定子内部的互斥變量

2.3.2配置設定子建立與銷毀

配置設定子的建立是所有的配置設定子操作的前提,正所謂“毛之不存,皮将焉附”。配置設定子建立使用函數apr_allocator_create實作: APR_DECLARE(apr_status_t) apr_allocator_create(apr_allocator_t **allocator) {     apr_allocator_t *new_allocator;     *allocator = NULL;     if ((new_allocator = malloc(SIZEOF_ALLOCATOR_T)) == NULL)         return APR_ENOMEM;     memset(new_allocator, 0, SIZEOF_ALLOCATOR_T);     new_allocator->max_free_index = APR_ALLOCATOR_MAX_FREE_UNLIMITED;     *allocator = new_allocator;     return APR_SUCCESS; } 配置設定子的建立非常的簡單,它使用的函數則是最通常的malloc,配置設定大小為SIZEOF_ALLOCATOR_T即APR_ALIGN_DEFAULT(sizeof(apr_allocator_t))大小。當然這塊配置設定的空間也包括了MAX_INDEX個指針變量數組。一旦配置設定完畢,函數将max_free_index初始化為APR_ALLOCATOR_MAX_FREE_UNLIMITED,該值實際為0,表明配置設定子對于回收空閑結點的大小并不設門檻,意味着即使結點再大,系統也不會回收。 建立後,結構中的max_inde,current_free_index都被初始化為0,這實際上是由memset函數隐式初始化的。一旦建立完畢,函數将傳回建立的配置設定子。隻不過此時傳回的配置設定子中的free數組中不包含任何的實際的記憶體結點連結清單。 對配置設定子使用的正常的下一步就應該是對結構成員進行初始化。主要的初始化工作就是設定系統資源歸還給作業系統的門檻max_free_index。在後面我們會看到,對于使用malloc配置設定的記憶體,如果其大小小于該門檻值,那麼這些資源并不釋放,而是歸還給記憶體池,當記憶體池本身被釋放的時候,這些記憶體才真正釋放給作業系統;如果記憶體的大小大于這個門檻值,那麼記憶體将直接釋放給作業系統。這個門檻值的設定由函數apr_allocator_max_free_set完成: APR_DECLARE(void) apr_allocator_max_free_set(apr_allocator_t *allocator,                                              apr_size_t in_size) {     apr_uint32_t max_free_index;     apr_uint32_t size = (APR_UINT32_TRUNC_CAST)in_size;     max_free_index = APR_ALIGN(size, BOUNDARY_SIZE) >> BOUNDARY_INDEX;     allocator->current_free_index += max_free_index;     allocator->current_free_index -= allocator->max_free_index;     allocator->max_free_index = max_free_index;     if (allocator->current_free_index > max_free_index)         allocator->current_free_index = max_free_index; } 參數中的size經過适當的對齊調整指派給配置設定子結構中的max_free_index。除了max_free_index之外,另外一個重要的成員就是current_free_index,該成員記錄目前記憶體池中實際的最大的記憶體塊大小。當然,它的值不允許超出max_free_index的範圍。 與配置設定子的建立對應的則是配置設定子的銷毀,銷毀使用的是函數apr_allocator_destroy。當配置設定子被銷毀的時候,我們需要確定下面兩方面的内容都被正确的銷毀: (1)、配置設定子本身的記憶體被釋放,這個可以直接調用free處理 (2)、由于配置設定子中内嵌的free數組都指向一個實際的結點連結清單,是以必須保證這些連結清單都被正确的釋放。在釋放連結清單的時候,通過一旦得到頭結點,就可以沿着next周遊釋放連結清單中的所有結點。 必須需要注意的是兩種釋放之前的釋放順序問題。正确的釋放順序應該是連結清單釋放最早;其次才是配置設定子本身記憶體的釋放。Apache中對應該部分是釋放代碼如下: APR_DECLARE(void) apr_allocator_destroy(apr_allocator_t *allocator) {     apr_uint32_t index;     apr_memnode_t *node, **ref;     for (index = 0; index < MAX_INDEX; index++) {         ref = &allocator->free[index];         while ((node = *ref) != NULL) {             *ref = node->next;             free(node);         }     }     free(allocator); }

目錄(?) [-]

  1. 配置設定子記憶體配置設定
  2. 配置設定子記憶體釋放
  3. 配置設定子記憶體管理流程

2.3.3配置設定子記憶體配置設定

使用配置設定子配置設定記憶體是最終的目的。Apache對外提供的使用配置設定子配置設定記憶體的函數是apr_allocator_alloc,然而實際在内部,該接口函數調用的則是allocator_alloc。 allocator_alloc函數原型聲明如下: apr_memnode_t *allocator_alloc(apr_allocator_t *allocator, apr_size_t size) 函數的參數非常簡單,allocator則是記憶體配置設定的時候調用的配置設定子,而size則是需要進行配置設定的大小。如果配置設定成功,則傳回配置設定後的apr_memnode_t結構。 {     apr_memnode_t *node, **ref;     apr_uint32_t max_index;     apr_size_t i, index;     size = APR_ALIGN(size + APR_MEMNODE_T_SIZE, BOUNDARY_SIZE);     if (size < MIN_ALLOC)         size = MIN_ALLOC;     index = (size >> BOUNDARY_INDEX) - 1;     if (index > APR_UINT32_MAX) {         return NULL;     } 函數所做的第一件事情就是按照我們前面所說的配置設定原則調整實際配置設定的空間大小:如果不滿8K,則以8K計算;否則調整為4K的整數倍。同時函數還将計算該與該結點對應的索引大小。一旦得到索引大小,也就知道了結點連結清單。至此Apache可以去尋找合适的結點進行記憶體配置設定了。 從配置設定子中配置設定記憶體必須考慮下面三種情況: (1)、如果需要配置設定的結點大小配置設定子中的“ 規則結點”能夠滿足,即index<=allocator->max_index。此時,能夠滿足配置設定的最小結點就是index索引對應的連結清單結點,但此時該索引對應的連結清單可能為空,是以函數将沿着數組往後查找直到找到第一個可用的不為空結點或者直到數組末尾。同時程式代碼中還給出了另外一種政策以及不使用的原因: NOTE: an optimization would be to check allocator->free[index] first and if no node is present, directly use allocator->free[max_index].  This seems like overkill though and could cause memory waste. 另外一種方案就是首先直接檢查allocator->free[index],一旦發現不可用,直接使用最大的索引allocator->free[max_index],不過這種政策可能導緻記憶體的浪費。Apache采用的則是“最合适”原則,按照這種原則,找到的第一個記憶體肯定是最合适的。下面的斜體代碼所作的無非如此:     if (index <= allocator->max_index) {         max_index = allocator->max_index;         ref = &allocator->free[index];         i = index;         while (*ref == NULL && i < max_index) {            ref++;            i++;         } 當循環退出的時候,意味着周遊結束,這時候可能産生兩種結果:第一,找到一個非空的連結清單,這時候我們可以進傳入連結表内部進行實際的記憶體配置設定;第二,從index開始往後的所有的連結清單都是空的,至此,循環退出的時候i=max_index。這兩種情況可以用下面的簡圖描述: 對于第一種情況,處理如下:         if ((node = *ref) != NULL) {             if ((*ref = node->next) == NULL && i >= max_index) {                 do {                     ref--;                     max_index--;                 }                 while (*ref == NULL && max_index > 0);                 allocator->max_index = max_index;             }             allocator->current_free_index += node->index;             if (allocator->current_free_index > allocator->max_free_index)                 allocator->current_free_index = allocator->max_free_index;             node->next = NULL;             node->first_avail = (char *)node + APR_MEMNODE_T_SIZE;             return node;         } (2)、如果配置設定的結點大小超過了 “規則結點”中的最大結點,函數将考慮索引0連結清單。索引0連結清單中的結點的實際大小通過成員變量index進行标記。 在通過next周遊索引0連結清單的時候,函數将需要的大小index和實際的結點的大小node->index進行比較。如果index>node->index,則明顯該結點無法滿足配置設定要求,此時必須繼續周遊。一旦找到合适的可供配置設定的結點大小,函數将調整node->first_avail指針指向實際可用的空閑空間。另外還需要調整配置設定子中的current_free_index為新的配置設定後的值。 (3)、如果在free[0]連結清單中都找不到合适的空間供配置設定,那麼此時隻能“另起爐竈”了。函數能做的事情無非就是調用malloc配置設定實際大小的空間,并初始化結點的各個變量,并傳回,代碼如下: if ((node = malloc(size)) == NULL)         return NULL; node->next = NULL; node->index = index; node->first_avail = (char *)node + APR_MEMNODE_T_SIZE; node->endp = (char *)node + size; 下面我們來看一個Apache中典型的調用配置設定子配置設定空間的情況,下面的代碼你可以在worker.c中找到: apr_allocator_t *allocator; apr_allocator_create(&allocator); apr_allocator_max_free_set(allocator, ap_max_mem_free); apr_pool_create_ex(&ptrans, NULL, NULL, allocator); apr_allocator_owner_set(allocator, ptrans); 當我順着這段代碼往下閱讀的時候,我曾經感覺到很困惑。當一個配置設定子建立初始,内部的free數組中的索引連結清單都為空,是以當我們在apr_pool_create_ex中調用node = allocator_alloc(allocator, MIN_ALLOC - APR_MEMNODE_T_SIZE)) == NULL的時候,所需要的記憶體就不可能來自索引連結清單内的結點中,而隻能就地配置設定,這些結點一旦配置設定後,它們就作為記憶體池的結點而被使用,但是配置設定後的結點卻并沒有立即與free數組進行關聯,即并沒有對free數組中的元素進行指派。這樣,如果不将結點與free數組進行“挂接”,那麼将永遠都不可能形成圖一所示連結清單結構。 那麼它們什麼時候才挂接到free數組中的呢?原來所有的挂接過程都是在結點釋放的時候才進行的。

2.3.4配置設定子記憶體釋放

正如前面所描述的,在配置設定記憶體的時候,Apache首先嘗試到現有的連結清單中去查找适合的空間,如果沒有适合的記憶體區域的話,Apache必須按照上述的配置設定原則進行實際的記憶體配置設定并使用。但是實際的記憶體塊并不會立即挂接到連結清單中去,隻有釋放的時候,這些區域才挂接到記憶體中。是以從這個角度而言,配置設定子記憶體的釋放并不是真正的将記憶體調用free釋放,而将其回收到配置設定連結清單池中。 Apache中提供的記憶體釋放函數是apr_allocator_free。不過該函數僅僅是對外提供的接口而已,在函數記憶體調用的則實際上是allocator_free。allocator_free函數的原型如下: static APR_INLINE void allocator_free(apr_allocator_t *allocator, apr_memnode_t *node) 函數中,node是需要釋放的記憶體結點,其最終歸還給配置設定子allocator。 {     apr_memnode_t *next, *freelist = NULL;     apr_uint32_t index, max_index;     apr_uint32_t max_free_index, current_free_index;     max_index = allocator->max_index;     max_free_index = allocator->max_free_index;     current_free_index = allocator->current_free_index; 由于node不僅僅可能是一個結點,而且可能是一個結點連結清單,是以如果需要完全釋放該連結清單中的結點,則必須通過結點中的next進行依次周遊,是以下面的循環就是整個釋放過程的架構結構:     do {         next = node->next;         index = node->index;         ……     } while ((node = next) != NULL); 對于每個結點,我們将根據它的索引大小(即記憶體大小)采取不同的處理政策: (1)、如果結點的大小超過了完全釋放的阙值max_free_index,那麼我們就不能将其簡單的歸還到索引連結清單中,而必須将其完全歸還給作業系統。函數将所有的這樣的需要完全釋放的結點儲存在連結清單freelist中,待所有的結點周遊完畢後,隻需要釋放freelist就可以釋放所有的必須釋放的結點,如下所示:         if (max_free_index != APR_ALLOCATOR_MAX_FREE_UNLIMITED             && index > current_free_index) {             node->next = freelist;             freelist = node;         } 如果max_free_index為APR_ALLOCATOR_MAX_FREE_UNLIMITED則意味着沒有回收門檻。任何記憶體,不管它有多大,APR都不會将其歸還給作業系統。 (2)、如果index<MAX_INDEX,則意味着該結點屬于“規則結點”的範圍。是以可以将該結點傳回到對應的“規則連結清單”中。如果需要釋放的結點的索引大小為index,則該結點将挂接于free[index]連結清單中。如果目前的free[index]為空,表明該大小的結點是第一個結點,此時還必須比較index和max_index。如果index>max_index,則必須重新更新max_index的大小,同時将該結點插傳入連結表的首部,作為首結點,代碼可以描述如下:         else if (index < MAX_INDEX) {             if ((node->next = allocator->free[index]) == NULL                 && index > max_index) {                 max_index = index;             }             allocator->free[index] = node;             current_free_index -= index;         } (3)、如果結點超過了“規則結點”的範圍,但是并沒有超出回收結點的範圍,此時我們則可以将其置于“索引0”連結清單的首部中。代碼如下:         else {             node->next = allocator->free[0];             allocator->free[0] = node;             current_free_index -= index;         } 待所有的結點處理完畢後,我們還必須調整配置設定子中的各個成員變量,包括max_index和current_free_index。同時不要忘記釋放freelist連結清單。     allocator->max_index = max_index;     allocator->current_free_index = current_free_index;     while (freelist != NULL) {         node = freelist;         freelist = node->next;         free(node);     } 當上面的工作都完成後,整個結點的釋放也就完畢了。事實上整個記憶體池中的記憶體就是通過上面的不斷地釋放而建構起來的。一旦建構了記憶體池,下一次的時候則可以直接去記憶體池中擷取了。

2.3.5配置設定子記憶體管理流程

根據上面的描述,我們現在來串起來看一些整個配置設定子工作的流程。假如存在下面一段代碼: 1.  apr_allocator_t *allocator; 2.  apr_allocator_create(&allocator); 3.  apr_allocator_max_free_set(allocator, 0);//簡單起見,不進行任何回收 4.  apr_memnode_t *memnode1 = apr_allocator_alloc(allocator, 3000); 5.  apr_allocator_free(memnode1); 6.  apr_memnode_t *memnode2 = apr_allocator_alloc(allocator, 3000); 7.  apr_allocator_free(memnode2); 8.  apr_memnode_t *memnode3 = apr_allocator_alloc(allocator, 3000); 9.  apr_allocator_free(memnode3); 當第一行執行完畢後,建立的配置設定子示意圖如下圖是以,該圖中尚未有任何的記憶體塊可供配置設定: 在第四行中,系統需要記憶體配置設定子配置設定2000位元組的空間,但此時沒有任何空間可供配置設定(index > allocator->max_index,同時allocator->free[0]==NULL),是以配置設定子将直接向作業系統索取8K的空間,剔除結構頭的大小,實際可用的記憶體大小為8k-APR_MEMNODE_T_SIZE。 當執行完第五行的時候,該記憶體将被歸還給配置設定子,同時儲存在索引1連結清單中。下圖中的虛線剔除後為釋放前的狀态,反之為釋放後的狀态。結果如下圖: 現在我們來考慮第六行和第七行的執行結果。當再次向配置設定子申請3000K的記憶體的時候,經過計算發現,該記憶體必須到索引為1連結清單中去擷取。如果索引1連結清單為NULL,則重複前面的步驟;

目錄(?) [-]

  1. 記憶體池
    1. 記憶體池概述
    2. 記憶體池的初始化

2.4 記憶體池

2.4.1記憶體池概述

在了解了記憶體配置設定子的概念之後,我們其實已經了解了Apache中記憶體配置設定的細節了。不過Apache中記憶體的層次結構關系則是由記憶體池負責組織,其資料結構apr_pool_t定義在apr_pools.c中,定義如下: struct apr_pool_t {     apr_pool_t           *parent;     apr_pool_t           *child;     apr_pool_t           *sibling;     apr_pool_t          **ref;  //用于指向記憶體池本身     cleanup_t            *cleanups;     apr_allocator_t      *allocator;     struct process_chain *subprocesses;     apr_abortfunc_t       abort_fn;     apr_hash_t           *user_data;     const char           *tag; #if !APR_POOL_DEBUG     apr_memnode_t        *active;     apr_memnode_t        *self;     char                 *self_first_avail; #else     debug_node_t         *nodes;     const char           *file_line;     apr_uint32_t          creation_flags;     unsigned int          stat_alloc;     unsigned int          stat_total_alloc;     unsigned int          stat_clear; #if APR_HAS_THREADS     apr_os_thread_t       owner;     apr_thread_mutex_t   *mutex; #endif #endif #ifdef NETWARE     apr_os_proc_t         owner_proc; #endif }; Apache中存在的記憶體池個數通常多于一個,它們之間形成樹型層次結構。每個記憶體池所存儲的内容以及其存儲周期都不一樣,比如連接配接記憶體池在整個HTTP連接配接期間存在,一旦連接配接結束,記憶體池也就被釋放;請求記憶體池則周期要相對短,它僅僅在某個請求周期記憶體存在,一旦請求結束,請求記憶體池也就釋放。不過每個記憶體池都具有一個apr_pool_t結構。 整個記憶體池層次樹通過parent、child以及sibling三個變量建構起來。parent指向目前記憶體池的父記憶體池;child指向目前記憶體池的子記憶體池;而sibing則指向目前記憶體池的兄弟記憶體池。是以整個記憶體池樹結構可以用圖3.3描述:

Apache記憶體池内幕1-7

圖3.3 記憶體池層次樹結構圖 在上面的圖中,我們隻是表示了層次結構,是以隻是用了child和sibling兩個成員,而忽略的parent的變量。從上面的圖中我們可以看出根結點具有n個孩子結點:child1,child2,child3…childn。而child1,child2,child3以及childn它們屬于同一個父親,而且處于層次樹的同一層次,是以它們通過連結清單連接配接,互為兄弟結點。同樣child10和child11都是child1的子記憶體池結點,互為兄弟結點。child21是child2的唯一的子結點。其餘結點類似。 除此之外apr_pool_t結構中最重要的成員變量無非就是active了。

Apache記憶體池内幕1-7

                                                                                             圖3.4 Apache中提供了大量的記憶體池管理函數,它們的功能和名稱歸納在表格3.2中。

記憶體池操作 函數名稱 函數功能簡單描述
初始化 apr_pool_initialize 對記憶體池使用中需要的内部變量進行初始化
銷毀 apr_pool_terminate 主要在終止記憶體池使用時銷毀内部的結構
建立

apr_pool_create_ex

apr_pool_create_ex_debug

建立一個新的記憶體池,另外還包括一個調試版本
清除

apr_pool_clear

apr_pool_clear_debug

清除記憶體池中的所有的記憶體,另外包括一個調試版本
apr_pool_destroy

2.4.2記憶體池的初始化

記憶體池的初始化是通過函數apr_pool_initialize實作的,在内部函數完成了下面幾件事情: APR_DECLARE(apr_status_t) apr_pool_initialize(void) {     apr_status_t rv;     if (apr_pools_initialized++)         return APR_SUCCESS; (1)、確定Apache中隻建立一個全局記憶體池,為此,Apache中使用apr_pools_initialized進行記錄。apr_pools_initialized初始值為0,初始化後該值更改為1。每次初始化之前都檢查該值,隻有值為0的時候才允許繼續執行初始化操作,否則直接傳回。通過這種手段可以確定隻有一個全局記憶體池存在。     if ((rv = apr_allocator_create(&global_allocator)) != APR_SUCCESS) {         apr_pools_initialized = 0;         return rv;     }     if ((rv = apr_pool_create_ex(&global_pool, NULL, NULL,                                  global_allocator)) != APR_SUCCESS) {         apr_allocator_destroy(global_allocator);         global_allocator = NULL;         apr_pools_initialized = 0;         return rv;     }     apr_pool_tag(global_pool, "apr_global_pool"); (2)、建立了全局的配置設定子global_allocator,并使用全局配置設定子global_allocator建立了全局記憶體池global_pool,該記憶體池是所有的記憶體池的祖先。所有的記憶體池都從該記憶體池繼承而來。它在整個Apache的生存周期都存在,即使重新開機機器,該記憶體池也不會釋放。除非你把Apache徹底關閉。該記憶體池在系統中命名為“apr_gloabl_pool”。     if ((rv = apr_atomic_init(global_pool)) != APR_SUCCESS) {         return rv;     } #if APR_HAS_THREADS     {         apr_thread_mutex_t *mutex;         if ((rv = apr_thread_mutex_create(&mutex,APR_THREAD_MUTEX_DEFAULT,global_pool)) != APR_SUCCESS) {             return rv;         }         apr_allocator_mutex_set(global_allocator, mutex);     } #endif     apr_allocator_owner_set(global_allocator, global_pool); (3)、如果目前的作業系統允許多線程,為了確定記憶體池結構被多線程通路的時候的線程安全性,還必須設定apr_pool_t結構内的互斥鎖變量mutex。最後的任務就是将記憶體配置設定子和記憶體池進行關聯。

2.4.3記憶體池的建立 勿庸置疑,記憶體池的建立是記憶體池的核心操作之一。記憶體池建立函數的原型如下所示: APR_DECLARE(apr_status_t) apr_pool_create_ex(apr_pool_t **newpool,                                              apr_pool_t *parent,                                              apr_abortfunc_t abort_fn,                                              apr_allocator_t *allocator) 其中,newpool是需要建立的新的記憶體池,并且建立後的記憶體池通過該參數傳回。parent則是目前建立的記憶體池的父親;abort_fn指明了當建立失敗的時候所調用的處理函數;allocator則是真正進行記憶體配置設定的配置設定子。 {     apr_pool_t *pool;     apr_memnode_t *node;     *newpool = NULL;     if (!parent)         parent = global_pool;     if (!abort_fn && parent)         abort_fn = parent->abort_fn;     if (allocator == NULL)         allocator = parent->allocator; 在建立過程中,我們沒有指定目前建立的記憶體池的父親,則将其預設為父親為根記憶體池global_pool,同時如果記憶體池關聯的abort_fn和配置設定子allocator沒有定義,那麼也直接繼承父輩的相關資訊。     if ((node = allocator_alloc(allocator,MIN_ALLOC - APR_MEMNODE_T_SIZE)) == NULL) {         if (abort_fn)             abort_fn(APR_ENOMEM);         return APR_ENOMEM;     }     node->next = node;     node->ref = &node->next;     pool = (apr_pool_t *)node->first_avail;     node->first_avail = pool->self_first_avail = (char *)pool + SIZEOF_POOL_T;     pool->allocator = allocator;     pool->active = pool->self = node;     pool->abort_fn = abort_fn;     pool->child = NULL;     pool->cleanups = NULL;     pool->free_cleanups = NULL;     pool->subprocesses = NULL;     pool->user_data = NULL;     pool->tag = NULL; 在一切就緒之後,函數将必須首先建立apr_pool_t結構。但是前面我們曾經說過Apache中對所有記憶體的配置設定都是以記憶體結點apr_memnode_t進行配置設定的,而且每次配置設定的最小單元為8K,這對于建立apr_pool_t結構也不例外。是以函數将首先調用配置設定子allocator配置設定8K的記憶體,然後将最頂端的記憶體配置設定給apr_memnode_t結構。此時接着apr_memnode_t結構下面的記憶體才能繼續配置設定給apr_pool_t,用來表示記憶體池結構,apr_pool_t結構之後才是真正可用的空間。在整個8K記憶體中,結點頭和記憶體池頭部分别占用的空間大小為APR_MEMNODE_T_SIZE和SIZEOF_POOL_T,是以使用者真正可用的空間實際上隻有(8k-APR_MEMNODE_T_SIZE-SIZEOF_POOL_T)大小了,至此我們還必須要調整apr_memnode_t中的first_avail指針和apr_pool_t結構中的self_first_avail指針指向真正可用空間。 經過兩輪配置設定之後,8K記憶體的布局如圖3.5所示:

Apache記憶體池内幕1-7

一旦完成了記憶體池結點的配置設定工作,我們必須将其挂結到記憶體池層次樹上。挂結的過程無非就是設定parent,child以及sibling的過程。     if ((pool->parent = parent) != NULL) {         if ((pool->sibling = parent->child) != NULL)             pool->sibling->ref = &pool->sibling;         parent->child = pool;         pool->ref = &parent->child;     }     else {         pool->sibling = NULL;         pool->ref = NULL;     }     *newpool = pool; 挂結的過程可以分為下面幾個步驟: (1)、将目前的結點的parent指針指向父結點,即pool->parent = parent。 (2)、設定目前結點的sibing。sibing應該指向那些與目前結點處于同一層次,并且父結點也相同的結點,新的結點總是被插入到子結點連結清單的首部,插入通過下面的兩句實作: pool->sibling = parent->child; parent->child = pool; 不過如果父結點為空,意味着該結點未有兄弟結點,故pool->sibling = NULL。 (3)、設定ref成員。在apr_pool_t中,ref用于指向 在記憶體池結點建立的過程中,我們可以看到,記憶體池建立後active仍然為空。是以目前記憶體池中能夠被使用的記憶體僅僅為8k- APR_MEMNODE_T_SIZE-SIZEOF_POOL_T大小。如果使用者從記憶體池中申請更多的記憶體的時候,很明顯,此時必須通過active去擴充該記憶體池對應的記憶體結點。這一點我們可以在記憶體池的記憶體配置設定中看出來。 2.4.4記憶體池的記憶體配置設定     從記憶體池中配置設定記憶體通過兩個函數實作:apr_pcalloc和apr_palloc,這兩個函數唯一的差別就是apr_pcalloc配置設定後的記憶體全部自動清零,而apr_palloc則省去了這一步的工作。 apr_palloc的函數原型如下所示: APR_DECLARE (void *) apr_palloc (apr_pool_t *pool, apr_size_t size) 函數中pool是需要配置設定記憶體的記憶體池,size則是需要配置設定的記憶體的大小。一旦配置設定成功則傳回配置設定記憶體的位址。       在了解記憶體池的記憶體配置設定之前,我們應該對active連結清單有所了解。顧名思義,active連結清單中儲存的都是曾經被使用或者正在被使用的apr_memnode_t記憶體結點。這些結點都是由配置設定子進行配置設定,之是以被使用,一個重要的原因就是它們有足夠空閑的空間。将這些結點儲存在active上,這樣下次需要記憶體的時候隻要首先周遊active連結清單即可,隻有在active連結清單中的結點不能夠滿足配置設定要求的時候才會重新跟配置設定子申請新的記憶體。另一方面,一旦某個結點被選中進入active連結清單,那麼它就不能在原先的配置設定子連結清單中存在。      對于每一個apr_memnode_t記憶體結點,它的實際可用空間為endp-first_avail的大小。但是正如前面所說,Apache中衡量空間通常使用索引的方法,對于所有的結點,它的空閑空間用free_index描述。為了加快查找速度,active連結清單中的所有的結點按照其空間空間的大小進行反向排序,為此空閑空間大得總是排在前面,空閑空間最小的則肯定排在最末尾。對于指定的配置設定空間,隻要将其與第一個結點的空閑空間進行比較,如果第一個空閑都不滿足,那麼此時必須向配置設定子重新申請空間,否則直接從第一個結點中配置設定空間,同時調整配置設定後的結點次序。     apr_memnode_t *active, *node;     void *mem;     apr_size_t free_index;     size = APR_ALIGN_DEFAULT(size);     active = pool->active;     if (size < (apr_size_t)(active->endp - active->first_avail)) {         mem = active->first_avail;         active->first_avail += size;         return mem;     } 配置設定首先計算需要配置設定的實際空間,這些空間都是使用對齊算法調整過的。Apache首先嘗試到active連結清單的第一個結點中去配置設定空間,正如前面所言,這個是連結清單中空閑最多的結點,如果它能夠滿足需要,Apache直接傳回size大小的空間,同時調整新的first_avail指針。不過這裡需要注意的是對于空連結清單的情況。當一個記憶體池使用apr_create_pool_ex新建立以後,它的active連結清單為空,不過此時active并不為NULL,事實上active=node,意味着active指向記憶體池所在的記憶體結點。是以這種情況下,空間的配置設定并不會失敗。     node = active->next;     if (size < (apr_size_t)(node->endp - node->first_avail)) {         list_remove(node);     }     else {         if ((node = allocator_alloc(pool->allocator, size)) == NULL) {             if (pool->abort_fn)                 pool->abort_fn(APR_ENOMEM);             return NULL;         }     } 如果active連結清單中的結點都不能滿足配置設定需求,那麼此時唯一能夠做的就是直接向配置設定子申請更多的空間。至于配置設定子如何去配置設定,是從池中擷取還是直接調用malloc配置設定,此處不再讨論。     node->free_index = 0;     mem = node->first_avail;     node->first_avail += size;     list_insert(node, active);     pool->active = node;     free_index = (APR_ALIGN(active->endp - active->first_avail + 1,                             BOUNDARY_SIZE) - BOUNDARY_SIZE) >> BOUNDARY_INDEX;     active->free_index = (APR_UINT32_TRUNC_CAST)free_index;     node = active->next;     if (free_index >= node->free_index)         return mem; 一旦擷取到配置設定的新的結點,那麼下一步就是從該結點中配置設定需要的size大小的空間,由mem指針指向該空間首位址。同時将該結點立即插入到actie連結清單中作為首結點。插入通過宏list_insert實作: #define list_insert(node, point) do {           /     node->ref = point->ref;                     /     *node->ref = node;                          /     node->next = point;                         /     point->ref = &node->next;                   / } while (0) 前面我們說過,active連結清單的第一個結點肯定是空閑空間最大的結點。盡管從池中剛配置設定的時候,node結點的空閑空間确實是最大,但是一旦配置設定了size大小之後則情況未必,是以node作為第一個結點存在可能是不合适的,為此必須進行适當的調整。 1)、如果新插入結點node的空閑空間确實比後繼結點node->next的空間大,那麼此時,毫無疑問,node是所有結點中空閑空間最大的結點,物歸其所,不需要再調整。     do {         node = node->next;     }     while (free_index < node->free_index);     list_remove(active);     list_insert(active, node); 2)、如果node的空間比node->next的空間小,那麼意味着node放錯了地方,為此必須從node->next開始往後周遊找到合适的位置,并從原位置移出,插入新位置。

2.4.5記憶體池的銷毀 由于Apache中所有的記憶體都來自記憶體池,是以當記憶體池被銷毀的時候,所有從記憶體池中配置設定的空間都将受到直接的影響——被釋放。但是不同的資料類型可能導緻不同的釋放結果,目前Apache中支援三種不同的資料類型的釋放: 1)、普通的字元串資料類型 這類資料類型是最簡單的資料類型,對其釋放可以直接調用free而不需要進行任何的多餘的操作 2)、帶有析構功能的資料類型 這類資料類型類似于C++中的對象。除了調用free釋放之外還需要進行額外的工作,比如apr_socket_t結構,它是與套接字的描述結構,除了釋放該結構之外,還必須close套接字。 3)、程序資料類型 APR中的程序資料類型用結構apr_proc_t進行描述,當然它的配置設定記憶體也來自記憶體池。通常一個apr_proc_t對應一個正在運作的程序,是以從記憶體池中釋放apr_proc_t結構的時候必然影響到正在運作的程序,如果處理釋放和程序的關系是記憶體釋放的時候必須考慮的問題。 下面我們較長的描述每一個中記憶體銷毀政策 2.4.5.1 帶有析構功能的資料類型的釋放 Apache2.0記憶體池中目前存放的資料種類非常繁多,既包括最普通的字元串,又包含各種複雜的資料類型,比如套接字、程序和線程描述符、檔案描述符、甚至還包括各種複雜的自定義資料類型。事實上這是必然的結果。Apache中倡導一切資料都盡量從記憶體池中配置設定,而實際需要的資料類型則千變萬化,是以記憶體池中如果出現下面的記憶體布局則不應該有任何的驚訝:

Apache記憶體池内幕1-7

在上面的圖示中,從記憶體池中配置設定記憶體的類型包括apr_bucket_brigade,apr_socket_t,apr_file_t等等。一個很明顯而且必須解決的問題就是如何釋放這些記憶體。當記憶體池被釋放的時候,記憶體池中的各種資料結構自然也就被釋放,這些都很容易就可以實作,比如free(apr_socket_t)、free(apr_dir_t)。不過有的時候情況并不是這麼簡單。比如對于apr_socket_t,除了釋放apr_socket_t結構之外,更重要的是必須關閉該socket。這種情況對于apr_file_t也類似,除了釋放記憶體外,還必須關閉檔案描述符。這項工作非常類似于對象的釋放,除了釋放對象本身的空間,還需要調用對象的析構函數進行資源的釋放。 是以正确的資源釋放方式必須是能夠識别記憶體池中的資料類型,在釋放的時候完成與該類型相關的資源的釋放工作。某一個資料結構除了調用free釋放它的空間之外,其餘的應該采取的釋放措施用資料結構cleanup_t描述,其定義如下: struct cleanup_t { struct cleanup_t *next; const void *data; apr_status_t (*plain_cleanup_fn)(void *data); apr_status_t (*child_cleanup_fn)(void *data); }; 該資料結構通常簡稱為清除政策資料結構。每一個結構對應一個處理政策,Apache中允許一個資料類型對應多個政策,各個處理政策之間通過next形成連結清單。data則是清除操作需要的額外的資料,由于資料類型的不确定性,是以隻能定義為void*,待真正需要的時候在進行強制類型轉換,通常情況下,該參數總是為目前操作的資料類型,因為清除動作總是與具體類型相關的。另外兩個成員則是函數指針,指向真正的清除操作函數。child_cleanup_fn用于清除該記憶體池的子記憶體池,plain_cleanup_fn則是用于清除目前的記憶體池。 為了能夠在釋放的時候調用對應的處理函數,首先必須在記憶體池中注冊指定類型的處理函數。注冊使用函數apr_pool_cleanup_register,注冊函數原型如下: APR_DECLARE(void) apr_pool_cleanup_register(apr_pool_t *p, const void *data,                       apr_status_t (*plain_cleanup_fn)(void *data),                       apr_status_t (*child_cleanup_fn)(void *data)) p是需要注冊cleanup函數的記憶體池,當p被釋放時,所有的cleanup函數将被調用。Data是額外的資料類型,通常情況下是注冊的資料類型,plain_cleanup_fn和child_cleanup_fn的含義與cleanup_t結構中對應成員相同,是以假如需要在全局記憶體池pglobal中注冊類型apr_socket_t類型變量sock的處理函數為socket_cleanup,則注冊過程如下: apr_pool_cleanup_register(pglobal,(void*)sock,socket_cleanup,NULL);     cleanup_t *c;     if (p != NULL) {         if (p->free_cleanups) {             c = p->free_cleanups;             p->free_cleanups = c->next;         } else {             c = apr_palloc(p, sizeof(cleanup_t));         }         c->data = data;         c->plain_cleanup_fn = plain_cleanup_fn;         c->child_cleanup_fn = child_cleanup_fn;         c->next = p->cleanups;         p->cleanups = c;     } 注冊過程非常簡單,無非就是将函數的參數指派給cleanup_t結構中的成員,同時将該結點插入到cleanup_t連結清單的首部。 apr_pool_cleanup_kill函數與apr_pool_cleanup_register相反,用于将指定的cleanup_t結構從連結清單中清除。 是以如果需要對一個記憶體池進行銷毀清除操作,它所要做的事情就是周遊該記憶體池對應的cleanup_t結構,并調用plain_cleanup_fn函數,該功能有靜态函數run_cleanups完成,其對應的代碼如下: static void run_cleanups(cleanup_t **cref){ cleanup_t *c = *cref; while (c) { *cref = c->next; (*c->plain_cleanup_fn)((void *)c->data); c = *cref; } } 2.4.5.2程序描述結構的釋放 盡管關于APR程序的描述我們要到後面的部分才能詳細讨論,不過在這部分,我們還是首先觸及到該内容。APR中使用apr_proc_t資料結構來描述一個程序,同時使用apr_procattr_t結構來描述程序的屬性。通常一個apr_proc_t對應系統中一個正在運作的程序。 由于Apache的幾乎所有的記憶體都來自記憶體池,apr_proc_t結構的配置設定也毫不例外,比如下面的代碼将從記憶體池p中配置設定apr_proc_t和apr_procattr_t結構: apr_proc_t   newproc; apr_pool_t   *p; apr_procattr_t *attr; rv = apr_pool_initialize(); rv = apr_pool_create(&p, NULL); rv = apr_procattr_create(&attr, p); 問題是當記憶體池p被銷毀的時候,newproc和attr的記憶體也将被銷毀。系統應該如何處理與newproc對應的運作程序。Apache中支援五種處理政策,這五種政策封裝在枚舉類型apr_kill_conditions_e中: typedef enum {     APR_KILL_NEVER,                 APR_KILL_ALWAYS,                APR_KILL_AFTER_TIMEOUT,         APR_JUST_WAIT,                  APR_KILL_ONLY_ONCE          } apr_kill_conditions_e; APR_KILL_NEVER:該政策意味着即使程序的描述結構apr_proc_t被釋放銷毀,該程序也不會退出,程序将忽略任何發送的關閉信号。 APR_KILL_ALWAYS:該政策意味着當程序的描述結構被銷毀的時候,對應的程序必須退出,通知程序退出使用信号SIGKILL實作。 APR_KILL_AFTER_TIMEOUT:該政策意味着當描述結構被銷毀的時候,程序必須退出,不過不是立即退出,而是等待3秒逾時候再退出。 APR_JUST_WAIT:該政策意味着描述結構被銷毀的時候,進城必須退出,但不是立即退出,而是持續等待,直到該程序完成為止。 APR_KILL_ONLY_ONCE:該政策意味着當結構被銷毀的時候,隻發送一個SIGTERM信号給程序,然後等待,不再發送信号。 現在我們回過頭來看一下記憶體池中的subprocesses成員。該成員定義為process_chain類型: struct process_chain {         apr_proc_t *proc;     apr_kill_conditions_e kill_how;         struct process_chain *next; }; 對于記憶體池p,任何一個程序如果需要從p中配置設定對應的描述資料結構apr_proc_t,那麼它首先必須維持一個process_chain結構,用于描述當p被銷毀的時候,如何處理該程序。Process_chain的成員很簡單,proc是程序描述,kill_how是銷毀處理政策,如果存在多個程序都從p中配置設定記憶體,那麼這些程序的process_chain通過next形成連結清單。反過來說,process_chain連結清單中描述了所有的從目前記憶體池中配置設定apr_proc_t結構的程序的銷毀政策。正因為程序結構的特殊性,是以如果某個程式中需要使用程序結構的話,那麼第一件必須考慮的事情就是程序的退出的時候處理政策,并将其儲存在subprocess連結清單中,該過程通過函數apr_pool_note_subprocess完成: APR_DECLARE(void) apr_pool_note_subprocess(apr_pool_t *pool, apr_proc_t *proc,                                            apr_kill_conditions_e how) {     struct process_chain *pc = apr_palloc(pool, sizeof(struct process_chain));     pc->proc = proc;     pc->kill_how = how;     pc->next = pool->subprocesses;     pool->subprocesses = pc; } apr_pool_note_subproces的實作非常簡單,無非就是對process_chain的成員進行指派,并插入到subprocess連結清單的首部。 比如,在URI重寫子產品中,需要将用戶端請求的URI更改為新的URI,如果使用map檔案進行映射的話,那麼根據請求的URI到map檔案中查找新的URI的過程并不是由主程序完成的,相反而是由主程序生成子程序,然後由子程序完成的,下面是精簡過的代碼: static apr_status_t rewritemap_program_child(…) {     ……     apr_proc_t *procnew;     procnew = apr_pcalloc(p, sizeof(*procnew));     rc = apr_proc_create(procnew, argv[0], (const char **)argv, NULL,procattr, p);     if (rc == APR_SUCCESS) {             apr_pool_note_subprocess(p, procnew, APR_KILL_AFTER_TIMEOUT);             ……     }     …… } 從上面的例子中可以看出,即使描述結構被删除,子程序也必須3秒後才被中止,不過3秒已經足夠完成查詢操作了。 同樣的例子可以在下面的幾個地方查找到:Ssl_engine_pphrase.c檔案的577行、Mod_mime_magic.c檔案的2164行、mod_ext_filter.c檔案的478行、Log.c的258行、Mod_cgi.c的465行等等。 現在我們來考慮記憶體池被銷毀的時候處理程序的情況,所有的處理由free_proc_chain完成,該函數通常僅僅由apr_pool_clear或者apr_pool_destroy調用,函數僅僅需要一個參數,就是process_chain連結清單,整個處理架構就是周遊process_chain連結清單,并根據處理政策處理對應的程序,描述如下: static void free_proc_chain(struct process_chain *procs) {     struct process_chain *pc;     if (!procs)         return;     for (pc = procs; pc; pc = pc->next) {         if(pc->kill_how == APR_KILL_AFTER_TIMEOUT)                處理APR_KILL_AFTER_TIMEOUT政策;         else if(pc->kill_how == APR_KILL_ALWAYS)                處理APR_KILL_ALWAYS政策;         else if(pc->kill_how == APR_KILL_NEVER)                處理APR_KILL_NEVER政策;         else if(pc->kill_how == APR_JUST_WAIT)                處理APR_JUST_WAIT政策;         else if(pc->kill_how == APR_KILL_ONLY_ONCE)                處理APR_KILL_ONLY_ONCE政策;     } } 2.4.5.3 記憶體池釋放 在了解了cleanup函數之後,我們現在來看記憶體池的銷毀細節。Apache中對記憶體池的銷毀是可以通過兩個函數和apr_pool_clear和apr_pool_destroy進行的。我們首先來看apr_pool_clear的細節: APR_DECLARE(void) apr_pool_clear(apr_pool_t *pool) {     apr_memnode_t *active; 記憶體池的銷毀不僅包括目前記憶體池,而且包括目前記憶體池的所有的子記憶體池,對于兄弟記憶體池,apr_pool_destroy并不處理。銷毀按照深度優先的原則,首先從最底層的銷毀,依次往上進行。函數中通過遞歸調用實作深度優先的政策,代碼如下: while (pool->child) apr_pool_destroy(pool->child); apr_pool_destroy的細節在下面描述。對于每一個需要銷毀的記憶體池,函數執行的操作都包括下面的幾個部分:     run_cleanups(&pool->cleanups);     pool->cleanups = NULL;     pool->free_cleanups = NULL; (1)、執行run_cleanups函數周遊與記憶體池關聯的所有的cleanup_t結構,并調用各自的cleanup函數執行清除操作。     free_proc_chain(pool->subprocesses);     pool->subprocesses = NULL;     pool->user_data = NULL; (2)、調用free_proc_chain處理使用目前記憶體池配置設定apr_proc_t結構的程序。     active = pool->active = pool->self;     active->first_avail = pool->self_first_avail;     if (active->next == active)         return;     *active->ref = NULL;     allocator_free(pool->allocator, active->next);     active->next = active;     active->ref = &active->next; (3)、處理與目前記憶體池關聯的active連結清單,主要的工作就是調用allocator_free将active連結清單中的所有的結點歸還給該記憶體池的配置設定子。 apr_pool_destroy函數與apr_pool_clear函數前部分工作非常的相似,對于給定記憶體池,它的所有的子記憶體池将被完全釋放,記住不是歸還給配置設定子,而是徹底歸還給作業系統。兩者的差別是對給定目前記憶體池節點的處理。apr_pool_clear并不會釋放記憶體中的任何記憶體,而apr_pool_destroy則正好相反:     if (pool->parent) {         if ((*pool->ref = pool->sibling) != NULL)           pool->sibling->ref = pool->ref;     }     allocator = pool->allocator;     active = pool->self;     *active->ref = NULL;     allocator_free(allocator, active);     if (apr_allocator_owner_get(allocator) == pool) {         apr_allocator_destroy(allocator);     } 如果目前記憶體池存在父記憶體池,那麼函數将自己從父記憶體池的孩子連結清單中脫離開來。然後調用apr_allocator_free将記憶體歸還給關聯配置設定子。如果被釋放的記憶體池正好是配置設定子的屬主,那麼屬于該記憶體池的所有的配置設定子也應該被完全的銷毀傳回給作業系統。是以函數将調用apr_allocator_owner_get(allocator)函數進行判斷。 在銷毀配置設定子的時候有一點需要注意的是,由于需要判斷目前配置設定子的是否屬于目前的記憶體池,而記憶體池結構在檢測之前已經被釋放,是以,在釋放記憶體池之前必須将其記錄下來以備使用。如果缺少了這一步,allocator可能造成記憶體洩漏。 現在我們看一下run_cleanups函數,該函數很簡單,無非就是周遊cleanup_t連結清單,并逐一調用它們的plain_cleanup_fn函數。 static void run_cleanups(cleanup_t **cref) {     cleanup_t *c = *cref;     while (c) {         *cref = c->next;         (*c->plain_cleanup_fn)((void *)c->data);         c = *cref;     } }

APR

繼續閱讀