1. 程式人生 > >nginx源代碼分析之內存池實現原理

nginx源代碼分析之內存池實現原理

delete align 業務 -s 首部 ges hand 重置 mar

建議看本文檔時結合nginx源代碼。

1.1 什麽是內存池?為什麽要引入內存池?

內存池實質上是接替OS進行內存管理。應用程序申請內存時不再與OS打交道。而是從內存池中申請內存或者釋放內存到內存池。因此。內存池在實現的過程中,必定有一部分操作時從OS中申請內存。或者釋放內存到OS。例如以下圖所看到的:

技術分享

圖1

內存池的引入可有效解決兩個問題:

(1) 減少應用程序與OS之間進行頻繁內存和釋放的系統調用,進而減少程序執行期間在兩個空間的切換,提升了程序執行效率;

(2)內存池可依據應用特性組織內存管理方式。能有效減少操作系統的內存碎片。

內存池的實現方案許多,比如曾經寫過的內存池demo:

http://blog.csdn.net/houjixin/article/details/7595817

內存池的實現過程中一般包含兩個方面:(1)一套完整內存的合理組織和管理方式;(2)一套完好的接口函數對用戶(使用內存池的應用程序)提供內存操作;

1.2 Nginx內存池的實現方案分析

1.2.1 與操作系統相關的內存操作函數

在nginx中,與OS直接相關的內存操作在文件:src\os\unix文件夾下的ngx_alloc.c和ngx_alloc.h中。主要函數有:

(1)void *ngx_alloc(size_t size, ngx_log_t *log);

該函數主要通過malloc函數從OS中申請一塊內存;

(2)void *ngx_calloc(size_t size, ngx_log_t *log);

該函數首先通過ngx_alloc從OS中申請一塊內存,然後再把所申請內存置零。

(3)void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log);

該函數提供一種內存對齊的方式從OS中申請內存,該函數所返回內存塊的起始地址都是從對齊大小alignment的整數倍開始。

Nginx關於內存池相關的文件為文件夾src\core\下的 ngx_palloc.h、ngx_palloc.c,這兩個文件提供了內存池的實現。

1.2.2 關於nginx對申請內存塊的釋放問題

Nginx的應用場景比較特殊,它對內存分配的回收分為兩種管理方式,其具體描寫敘述例如以下:

  • 一般從內存池中分配出去的內存不做回收管理(通過ngx_pmemalign、ngx_palloc、ngx_pnalloc、ngx_pcalloc)。當使用完內存池之後,重置整個內存池就可以。讓全部內存池的存儲節點的可分配區直接初始化為全部可用,這一步僅僅須要調整每一個存儲節點last成員就可以。

  • 對於大塊內存釋放時,直接將其釋放給操作系統;
  • 重置內存池時將回收全部的內存池內存,自然也就回收了全部大塊內存的管理節點(結構體為ngx_pool_large_t,這些管理節點就是在內存池中進行分配的),並將全部的大塊內存全部釋放給操作系統;
  • 假設分配須要做特殊回收處理的內存。則需通過接口ngx_pool_cleanup_add來完畢申請,申請出去的每一個內存都通過內存池第一個節點的cleanup成員來管理,全部分配出去的需特殊回收內存以鏈表方式管理起來;

1.2.3 nginx內存池的結構

Nginx的內存池採用鏈表結構,每一個內存池相應3個鏈表:內存池鏈表、大塊內存鏈表和需特殊回收的已分配內存鏈表;這些個鏈表的主要差別為:

  • 內存池鏈表中每一個節點初始可使用的存儲空間大小是一樣的,而且在內存池創建時指定,大塊內存管理鏈表中,每一個分配出去的內存大小不一定一樣;
  • 大塊內存管理鏈表中,每塊分配應用的內存都大於內存池鏈表中所管理的內存塊大小;
  • 內存池鏈表的每一個內存池節點中。存儲的管理信息(結構體ngx_pool_t和待分配的內存空間連在一起,而且在待分配的內存空間之前),大塊內存的管理結構體和該結構體所管理的大內存塊不在一個連續內存空間中;
  • 大內存塊的管理結構體ngx_pool_large_s所占用的內存是在內存池鏈表中分配的。
  • 每一個已分配出去的需特殊回收的內存都由一個結構體ngx_pool_cleanup_s來描寫敘述,全部需特殊回收的內存被組織成一個鏈表。鏈表的表頭存放在內存池的第一個存儲節點結構體ngx_pool_t的cleanup成員中。
  • 需特殊回收的內存塊和其管理結構體ngx_pool_cleanup_s所占用的內存都從內存池中分配。

這兩個鏈表通過內存池鏈表中第一個節點的large成員連接起來。例如以下圖2中對大內存塊管理的描寫敘述。

技術分享

圖2 內存池結構體

1、 內存池結構體

內存池相關的結構體主要有:ngx_pool_cleanup_s、ngx_pool_large_t(ngx_pool_large_s)、ngx_pool_data_t和ngx_pool_t(即ngx_pool_s)、 ngx_pool_cleanup_file_t,例如以下所看到的:

(1)ngx_pool_cleanup_s

struct ngx_pool_cleanup_s {
   ngx_pool_cleanup_pt   handler;
   void                 *data;
   ngx_pool_cleanup_t   *next;
};


結構體ngx_pool_cleanup_s用於描寫敘述一個從內存池中分配出去的、須要特殊回收的內存塊。成員data指向這個須要特殊回收的內存塊,Handler在回收data所指向內存塊時使用,next指向下一個需特殊回收內存塊的管理結構體,這樣全部須要特殊回收內存塊的管理結構體都被組織成一個鏈表結構。

(2)ngx_pool_large_s或ngx_pool_large_t

typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
   ngx_pool_large_t     *next;
   void                 *alloc;
};


ngx_pool_large_s或ngx_pool_large_t表示大內存塊結構體,在nginx中大內存塊的管理也是採用鏈表方式,當中成員next指向下一個大內存塊,alloc指向當前結構體所管理的大內存塊。

(3)ngx_pool_data_t

typedef struct {
   u_char               *last;
   u_char               *end;
   ngx_pool_t           *next;
   ngx_uint_t            failed;
} ngx_pool_data_t;


ngx_pool_data_t用於記錄內存池中一個節點的內存塊使用情況,last表示該內存中下一次分配內存時可使用的地址。end表示當前節點內存的最大可使用地址,next表示下一個內存池節點結構體,failed表示從該節點分配內存失敗的次數,具體見上圖2中對該數據結構的描寫敘述。

(4)ngx_pool_s或者ngx_pool_t

struct ngx_pool_s {
   ngx_pool_data_t       d;
   size_t                max;
   ngx_pool_t           *current;
   ngx_chain_t          *chain;
   ngx_pool_large_t     *large;
   ngx_pool_cleanup_t   *cleanup;
   ngx_log_t            *log;
};


結構體ngx_pool_s或者ngx_pool_t用於描寫敘述一個內存池節點。內存池節點的組織方式例如以下圖所看到的(初始化時的形態。該節點中還未分為出不論什麽內存空間,因此其未分配區域為剛申請時的可用大小):

技術分享

圖3

一個內存池節點是一個連續的內存塊,在其前sizeof(ngx_pool_t)部分存儲了該節點的描寫敘述與管理信息。即結構體ngx_pool_s。該結構體之後的部分就是可用實際使用的存儲空間。

成員max表示當前內存池的可供分配內存塊大小,如圖3中未分配的區域大小,即該節點的全部大小減去ngx_pool_t結構體占領的部分之後,所剩下的能被用戶所使用的空間大小,其大小不大於“內存頁大小-1”。假設大於則改動為“內存頁大小-1”;成員current指向當前內存池鏈表中,具備分配能力的內存節點,見圖2所看到的。large指向當前內存池的大內存塊列表。log成員為日誌輸出所用。可忽略它而不影響對內存池的理解;成員cleanup指向分配出去的須要單獨回收的內存鏈表。

(5)ngx_pool_cleanup_file_t

typedefstruct {
    ngx_fd_t              fd;
    u_char               *name;
    ngx_log_t            *log;
} ngx_pool_cleanup_file_t;


Nginx內存池對打開的文件進行了特殊的管理和操作。結構體ngx_pool_cleanup_file_t就表示對打開文件的特殊操作,其成員fd表示打開的文件句柄,name表示打開的文件名稱。

1.2.4 內存的內部管理

1) 內存池鏈表擴展

【可參考】宏:#definengx_align(d, a) (((d) + (a - 1))& ~(a - 1))

用於將d向上取整為a的倍數。比如:ngx_align(7,3)即:

((7) +(3-1))&~(3-1)

=(7+2)&~2

=9&~2

= 1001&~ 0010轉換為2進制

=1001& 1101

= 1001

= 9

在用戶在內存池鏈表中申請內存時。假設內存池鏈表中的可用內存空間不夠分配,則內存池自己主動調用函數相關函數進行內存擴展。

函數ngx_palloc_block主要用於擴展內存池容量,其聲明為:

static void*ngx_palloc_block(ngx_pool_t *pool, size_t size)

對內存池pool的存儲節點鏈表新擴充一個節點,該函數的擴充算法為:

(1) 計算當前內存池的內存池鏈表的節點大小(在內存池鏈表中。每一個節點的大小都是一樣的。而且節點的管理數據結構和可分配的內存空間是連在一起的)psize。

(2) 調用ngx_memalign從操作系統的內存中申請psize大小內存塊,作為內存池鏈表的新增節點;

(3) 對新申請存儲節點的管理結構體ngx_pool_t的各成員進行初始化。可參考圖2中對該結構體的描寫敘述;

(4) 從新申請存儲節點的可分配內存空間中分配出用戶申請的內存。

(5) 將新申請的存儲節點插入到內存池的“內存池鏈表”的隊列尾部,假設當前節點的分配失敗次數小於4,則調整內存池的當期可用節點的位置移動到下一個節點;

【註意】

(1) 當用戶申請內存失敗時,內存池內部會自己主動擴充新節點並在新增節點中為用戶分配所申請的內存;

(2) 當用戶申請內存失敗(即內存池中新增了存儲節點)時。內存池鏈表匯中。從current節點到鏈表的最後一個節點的failed值全部+1;

(3) 從current遍歷到內存池隊尾,遇到failed值大於4時,則current指針移動到下一個內存池的存儲節點,知道將current指向一個failed值小於等於4的節點,例如以下圖4所看到的,當然。假設從current到隊尾的全部節點的failed值都小於等於4。則在新節點假如到內存池時current不向後移動。例如以下圖5所看到的;

技術分享

圖4 新增節點時移動current指針到下一個節點

技術分享

圖 5 新增節點current不移動

2) 大塊內存鏈表擴展

假設用戶從內存池中申請大於內存池最大存儲能力的內存時。nginx的內存池將直接從操作系統內存中申請用戶所需的大塊內存,並將新分配的內存放入到內存池的大塊內存鏈表中,該過程主要通過以下的函數完畢:

staticvoid * ngx_palloc_large(ngx_pool_t *pool, size_t size)

在該函數中,首先通過ngx_alloc從操作系統中申請一塊用戶申請大小(size參數指定)的內存塊,這塊內存將被直接返回給申請用戶使用,如有必要則在內存池中為該大內存塊申請一個小塊內存用於存儲管理用戶所申請大內存塊的數據結構ngx_pool_large_t。例如以下圖。新申請大塊內存的管理結構體ngx_pool_large_t是在內存池中存儲,用戶實際申請的大塊內存則是直接從操作系統中申請的。

技術分享
圖6 內存池擴展大塊內存

在上圖中。須要說明的是大塊內存的管理結構體ngx_pool_large_t是在當前內存池中所分配,而不一定是在內存池的第一個存儲節點中分配,這裏僅僅是為了節省空間才把這兩個管理結構體ngx_pool_large_t畫在了同一個內存池存儲節點中。

大塊內存鏈表的管理方式有以下要點:

(1) 在內存池的大塊內存鏈表中。通過結構體ngx_pool_large_t管理每一個大塊內存,多個ngx_pool_large_t節點鏈接起來形成一個大塊內存鏈表;

(2) 在大塊內存管理中,假設用戶釋放了大塊內存,則把該大塊內存的管理結構體ngx_pool_large_t中的alloc變量設為null。並不會釋放該大塊內存的管理結構體ngx_pool_large_t。而是留著等待產生新大塊內存時復用;

(3) 在申請一個新的大塊內存時,首先從頭開始遍歷由ngx_pool_large_t組成的大塊鏈表。找到某個節點的大塊內存已經被釋放。則把這個空隙管理節點利用起來。假設從頭開始連續找3個節點都沒有發現空暇的ngx_pool_large_t節點。就不再找了,而是從當前內存池中新申請一個ngx_pool_large_t,並用它管理為用戶新申請的大塊內存,然後將這個新申請的ngx_pool_large_t節點插入到大塊內存鏈表的首部!

1.2.5 對外提供的接口函數

1.2.5.1 內存申請

以下四個函數用於從內存池中分配一個內存塊,而且所回收的內存塊無需特殊處理:

void*ngx_palloc(ngx_pool_t *pool, size_t size);

void*ngx_pnalloc(ngx_pool_t *pool, size_t size);

void*ngx_pcalloc(ngx_pool_t *pool, size_t size);

void *ngx_pmemalign(ngx_pool_t*pool, size_t size, size_t alignment);

上述四個函數從內存池中分配出去的內存不做單獨回收,而是通過內存池重置來一次回收全部已分配出去的內存,當中,ngx_palloc與ngx_pnalloc差別是:從nginx的內存池申請內存池時。ngx_palloc會對新申請的內存地址進行對齊操作;ngx_pcalloc內部調用ngx_palloc從內存池中申請須要的內存,並將申請的內存空間全部置零,因此ngx_pcalloc實際上也是採用地址對齊方式申請內存。例如以下圖所看到的:

技術分享

圖7 內存地址對齊

ngx_palloc、ngx_pcalloc與ngx_pnalloc這三個函數內部處理方式相似:

(1) 假設申請的內存大小size小於等於內存池默認的最大可用內存空間大小(由結構體ngx_pool_t的成員max保存)。則從內存池中進行分配,否則通過操作系統直接分配,並通過“大塊內存管理鏈表”進行新分配內存的管理;

(2) 假設“內存池鏈表”中沒有足夠的內存可供分配,則調用前面介紹的函數ngx_create_pool對內存池進行擴充。

(3) 假設“大塊內存管理鏈表”中,則直接調用前面介紹的ngx_palloc_large函數進行大塊內存分配。

函數ngx_pmemalign主要用於通過內存池從操作系統中直接申請一大塊內存,可是申請的內存塊進行了地址對齊,而且新申請的內存塊交由內存池來管理。實質上就是將該內存塊交由大塊內存鏈表的節點結構體(ngx_pool_large_t)來管理。

通過該函數申請的大內存塊直接新分配一個ngx_pool_large_t結構體來管理。並將該結構體插入到大塊內存管理鏈表的首部。

假設遇到對回收的內存塊做特殊處理時,申請函數為:

ngx_pool_cleanup_t*ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);

該函數的內部處理方式為:

(1) 從內存池中申請一個特殊回收內存塊的管理結構體ngx_pool_cleanup_t;

(2) 從內存池中申請用戶須要大小的內存塊。

(3) 依據所申請的內存塊初始化其管理結構體。主要是將成員data指向分配給用戶“需特殊處理”的內存塊。將該管理結構體插入到特殊內存管理結構體鏈表的首部;將Handler設置為null。

用戶申請到這個回收時需特殊處理的內存塊時,就須要自己設置特殊處理函數Handler,這樣內存池在回收這塊內存時就調用用戶設置的回收函數進行處理。

1.2.5.2 內存池的操作

1) 創建內存池

內存池創建通過函數ngx_create_pool完畢,該函數聲明例如以下:

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);

它完畢創建一個內存池的動作,在該函數中指定了內存池節點的大小為Min(size - sizeof(ngx_pool_t), (ngx_pagesize - 1)),當然該大小不能大於nginx內部默認的一頁大小(ngx_pagesize- 1),否則內存池的存儲節點大小自己主動調整為一頁大小。存儲該值的變量為結構體ngx_pool_s的max成員。

因為內存池實質上是由一個個的存儲節點組成的鏈表,可是其第一個節點比較特殊,它的max成員、current成員、large成員都將被常常使用,可是第一個之後的存儲節點的這些成員基本上不會使用,在該函數內部。實際上是創建內存池的第一個存儲節點,其內部主要完畢以下業務:

(1) 依據從操作系統內存中申請參數size指定大小的內存塊作為第一個存儲節點;

(2) 該內存塊的前sizeof(ngx_pool_t)空間主要用於保存管理此存儲節點的結構體ngx_pool_t;

(3) 對結構體ngx_pool_t進行初始化。主要成員為d(ngx_pool_data_t類型),max、current等,當中:d.last為可供分配的內存地址,設置為未分配存儲空間的起始位置;d.end指向當前未分配空間的末尾。d.next用於指向下一個節點。這裏設置為null,failed用於標識分配內存失敗的次數。這裏設置為0。max設置為Min(size - sizeof(ngx_pool_t), (ngx_pagesize - 1))。current設置為當前節點的起始位置,large用於指向當前內存池的大塊內存分配鏈表,這裏設置為null,例如以下圖所看到的:

技術分享

圖8 第一個內存池存儲節點的初始化

2) 銷毀內存池

連接銷毀的接口函數聲明為:

voidngx_destroy_pool(ngx_pool_t *pool);

3) 重置內存池的函數接口為:

voidngx_reset_pool(ngx_pool_t *pool);

重置內存池主要完畢兩個功能:

l 對於大塊內存鏈表,依次遍歷並釋放每一個鏈表節點所管理的大塊內存,註意這裏並沒有釋放這些大塊內存的管理節點;

l 對於內存池的每一個存儲節點,則將全部可分配內存節點設置為未分配狀態。僅僅須要將last指針指向存儲節點的ngx_pool_t後的第一個字節就可以。註意這一步就釋放了上一步中大塊內存管理鏈表的每一個節點。

4) 釋放大塊內存

通過內存池釋放大塊內存的接口函數為:

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

在該函數中。將遍歷內存池的大塊內存列表,依次比較每一個節點所管理的內存地址,假設為傳入的地址p。則將大塊內存直接釋放到操作系統,註意該函數並未釋放大內存塊的管理結構體ngx_pool_large_t。

1.2.5.3 對文件的特殊操作

Nginx的內存池對描寫敘述打開文件的結構體內存進行了特殊管理。該動作主要通過結構體ngx_pool_cleanup_file_t來完畢。這樣在回收內存池時就會自己主動調用相應函數對打開的文件進行關閉。當然。這種內存回收時須要特殊處理的(調用相關函數關閉待回收內存中所保存的打開文件。關閉文件也是特殊處理的動作。),因此。針對文件的全部操作也都針對前面介紹的“需特殊回收的已分配內存鏈表”;相關的操作函數主要有以下三個:

voidngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);

voidngx_pool_cleanup_file(void *data);

void ngx_pool_delete_file(void*data);

函數ngx_pool_run_cleanup_file的功能為關閉連接池中保存的已打文件fd,其步驟例如以下:從連接池的第一個存儲節點中的cleanup成員中拿到“需特殊回收的已分配內存鏈表”的首地址。然後依次遍歷每一個已分配出去的“需特殊回收內存”,因為特殊回收內存塊的管理結構體為ngx_pool_cleanup_t,我們能夠通過該結構體的Handler成員變量來推斷它的處理函數是不是ngx_pool_cleanup_file(註意這是個函數,在以下有解釋其作用)假設是再取出ngx_pool_cleanup_t的data成員。此時data的類型一定是ngx_pool_cleanup_file_t(註意這是個struct),其fd成員就保存了一個打開文件的具備,假設該句柄與用戶傳入的fd一致,則將其關閉。

函數ngx_pool_cleanup_file的功能是關閉一個文件句柄,函數ngx_pool_delete_file的功能也是刪除一個文件或者解除一個文件的鏈接。

nginx源代碼分析之內存池實現原理