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中倡導一切資料都盡量從記憶體池中配置設定,而實際需要的資料類型則千變萬化,是以記憶體池中如果出現下面的記憶體布局則不應該有任何的驚訝:
在上面的圖示中,從記憶體池中配置設定記憶體的類型包括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; } }
關于作者
張中慶,目前主要的研究方向是嵌入式浏覽器,移動中間件以及大規模伺服器設計。目前正在進行Apache的源代碼分析,計劃出版《Apache源代碼全景分析》上下冊。Apache系列文章為本書的草案部分,對Apache感興趣的朋友可以通過flydish1234 at sina.com.cn與之聯系!
如果你覺得本文不錯,請點選文後的“推薦本文”連結!!