背景
對於HTTP服務器而言,“每秒處理請求數”是非常關鍵的性能指標。處理路徑上每一步動作的時間開銷,都將對該指標造成影響。特別地,內存分配/釋放作為最基礎的動作,對總處理時間施加的影響更為巨大——想想那些伴隨復雜數據結構或細碎邏輯產生的內存分配/釋放動作,很可能在單次處理中執行數十次,假設每次動作都調用malloc()/free()函數,時間開銷將頗為可觀、無法接受。由此,Nginx針對HTTP應用的業務特點,重新設計出一套內存管理機制,以內存池的形式提供給上層結構使用,從而有效地優化性能。
根據分配/釋放頻繁程度,HTTP應用的內存使用特征可以劃分為兩類:
-
進程初始化時分配、終止時釋放的大塊內存,供應給配置文件等全程不會變化的數據結構使用;
-
處理單次請求時反復分配/釋放的小塊內存,供應給字符串處理等小型結構體和臨時數據使用。
對於前者,使用malloc()/free()帶來的時間開銷並無大礙;對於後者,一旦使用完後即刻調用free()釋放內存,則會產生許多不必要的時間開銷。更理想的作法是將零碎分配得來的內存收集起來,在某個時間點集中釋放掉,亦即“多次分配,一次釋放”。通過設計得當的數據結構和接口,可以在確保“誰分配,誰釋放”的內存使用原則下,提供更好的性能。
內存管理模型
Nginx將內存管理模型組織為兩層結構的內存池:
-
第一層結構采用“向系統請求儲備內存塊(多次)——按需分配零碎內存塊(多次)——集中釋放零碎內存塊(單次)”的設計思路,每次分配不超過某個固定長度上限的零碎內存塊;
-
第二層結構采用簡單的“向系統請求獨立內存塊(多次+轉發)”的設計思路,亦即轉發分配請求給malloc(),以分配第一層不負責管理的更大塊的獨立內存塊。
除此以外,還包裝了malloc()/free()以及更為底層的memalign()/posix_memalign(),用以分配不歸內存池管理的內存塊。
第一層結構
ngx_create_pool() ngx_palloc()/ngx_pnalloc() ...
| |
| \- ngx_palloc_block()
| |
\- ngx_memalign() ------\ \- ngx_memalign() ------\
| |
| |
ngx_pool_t V ngx_pool_data_t V
/--> +-------------------+ /----> +-----------------+ /-> ...
| | ngx_pool_data_t d | | | last | --\ |
| | +----------------+ | +-----------------+ | |
| | | last | --\ | | end | --+--\ |
| | +----------------+ | | +-----------------+ | | |
| | | end | --+--\ | | next | --+--+--/
| | +----------------+ | | | +-----------------+ | |
| | | next | --+--+--/ | failed | | |
| | +----------------+ | | +-----------------+ | |
| | | failed | | | | allocated | | |
| +--+----------------+ | | | area | | |
| | max | | | /-----------------/ <-/ |
| +-------------------+ | | / unallocated / |
\--- | current | | | | area | |
+-------------------+ | | | | |
| chain | --+--+-----\ | | |
+-------------------+ | | | | | |
| large | --+--+--\ | +-----------------+ <----/
+-------------------+ | | | |
/--- | cleanup | | | | |
| +-------------------+ | | | |
| | log | | | | |
| +-------------------+ | | | \-> ???
| | allocated | | | |
| | area | | | |
| /-------------------/ <-/ | \----> 第二層結構
| / unallocated / | (ngx_pool_large_t)
| | area | |
| | | |
| | | |
| | | |
| +-------------------+ <----/
|
|
| ngx_pool_cleanup_t
\--> +-------------------+
| handler |
+-------------------+
| data |
+-------------------+
/--- | next |
| +-------------------+
|
| ngx_pool_cleanup_t
\--> +-------------------+
| handler |
+-------------------+
| data |
+-------------------+
/--- | next |
| +-------------------+
|
|
\--> ...
注意點:
1. ngx_pool_t和ngx_pool_data_t結構體存放在分配出的儲備內存塊首址處,占用固定空間;
2. 內存長度大於max字段值的分配請求將轉發給第二層結構處理;
3. 使用ngx_pool_data_t結構體構造出單向鏈表,管理各儲備內存塊;
4. 使用ngx_pool_cleanup_t結構體構造出單向鏈表,管理各清理回調函數。
第二層結構
ngx_palloc()/ngx_pnalloc() ngx_palloc()/ngx_pnalloc()
| |
\- ngx_palloc_large() --\ \- ngx_palloc_large() --\
| |
ngx_pool_large_t V ngx_pool_large_t V
+-------------------+ /-----> +-------------------+ /-----> ...
| alloc | | | alloc | |
+-------------------+ | +-------------------+ |
| next | ------/ | next | ------/
+-------------------+ +-------------------+
注意點:
1. ngx_pool_large_t結構體是從ngx_pool_data_t結構管理的儲備內存塊中分配出來的,並且會適當復用。
內存池接口函數
ngx_create_pool(size, log)
ngx_create_pool()負責創建內存池,動作序列如下:
-
向系統請求size參數指定大小的儲備內存塊(按NGX_POOL_ALIGNMENT對齊,默認16字節),邏輯上劃分為(ngx_pool_t結構體+剩余內存區)兩部分;
-
初始化ngx_pool_data_t結構體各個字段(參考前一節,last字段指向初始分配地址);
-
計算單次分配的內存塊長度上限(max字段,最大不超過size參數值或NGX_MAX_ALLOC_FROM_POOL);
-
初始化其他字段(current字段指向自己)。
ngx_destroy_pool(pool)
ngx_destroy_pool()負責銷毀內存池,動作序列如下:
-
調用各個清理回調函數;
-
釋放各個ngx_pool_large_t結構體管理的儲備內存塊;
-
釋放各個ngx_pool_data_t結構體管理的獨立內存塊。
ngx_reset_pool(pool)
ngx_reset_pool()負責釋放零碎內存塊,動作序列如下:
-
釋放各個ngx_pool_large_t結構體管理的內存塊;
-
簡單地將各個ngx_pool_data_t結構體的last字段復原成初始分配地址,而不是釋放。
ngx_palloc(pool, size)/ngx_pnalloc(pool, size)/ngx_pcalloc(pool, size)
ngx_palloc()負責分配零碎內存塊,動作序列如下:
-
若size參數大於max字段值,轉發給ngx_palloc_large()處理(調用更底層的分配函數);
-
以current字段所指ngx_pool_data_t結構體為起點,在單向鏈表中搜索第一個能執行分配動作的結構體,完成分配(分配首址按NGX_ALIGNMENT對齊);
-
若以上動作均告失敗,轉發給ngx_palloc_block()處理(先分配新的ngx_pool_data_t結構體,再分配零碎內存塊)。
ngx_pnalloc()動作與ngx_palloc()類似,但不對齊分配首址。
ngx_pcalloc()將分配請求轉發給ngx_palloc()處理,並對返回的零碎內存塊進行清零初始化。
ngx_palloc_block(pool, size)
-
ngx_palloc_block()負責向系統請求新的儲備內存塊,最終完成分配,動作序列如下:
-
向系統請求指定大小的儲備內存塊(按NGX_POOL_ALIGNMENT對齊,默認16字節),邏輯上劃分為(ngx_pool_data_t結構體+剩余內存區)兩部分;
-
初始化各個字段(包括分配首址,按NGX_ALIGNMENT對齊);
-
調整current字段的指向(實際上維持著一個“... 5 5 5 4 3 2 1 0 0”的failed字段值序列),將新的ngx_pool_data_t結構體掛入單向鏈表中。
ngx_palloc_large(pool, size)/ngx_pmemalign(pool, size, alignment)
ngx_palloc_large負責分配不歸第一層結構管理的獨立內存塊,動作序列如下:
-
調用ngx_alloc()分配新的獨立內存塊;
-
搜索第一個空閒的ngx_pool_large_t結構體,保存並返回獨立內存塊首址;
-
若前一步失敗,從本內存池中分配新的ngx_pool_large_t結構體並加入單向鏈表中,保存並返回獨立內存塊首址。
ngx_pmemalign()動作與ngx_palloc_large()類似,但會按alignment參數對齊獨立內存塊首址。
ngx_pfree(pool, p)
ngx_pfree()負責“釋放”內存塊,動作序列如下:
-
如果待“釋放”對象是獨立內存塊,則轉調用ngx_free()釋放。
實際上該函數並不真正釋放零碎內存塊,而是盡可能將釋放動作推遲到ngx_reset_pool()/ngx_destroy_pool()中。
ngx_pool_cleanup_add(p, size)
ngx_pool_cleanup_add()負責分配新的ngx_pool_cleanup_t結構體,以便調用端注冊回調函數。動作序列如下:
-
從本內存池中分配一個ngx_pool_cleanup_t結構體;
-
若size參數不為0,嘗試從本內存池分配相應大小的一塊內存並作為回調參數;
-
將新的結構體掛到單向鏈表中;
-
調整cleanup字段,保持回調函數按“先進後出”的性質(類似於壓棧操作)。
ngx_pool_run_cleanup_
file(p, fd)/ngx_pool_cleanup_file(data)/ngx_pool_cleanup_file(data)
ngx_pool_run_cleanup_file()負責搜索注冊在回調函數鏈表中的、與fd參數指定文件句柄對應的回調函數並調用之。
ngx_pool_cleanup_file()提供一個關閉指定文件句柄的缺省實現函數。
ngx_pool_delete_file()提供一個刪除文件並關閉相關文件句柄的缺省實現函數。