[筆記] 解碼Nginx:記憶體池(Memory Pool)

樑濤發表於2012-11-18

解碼Nginx:記憶體池(Memory Pool)

樑濤(@無鋒之刃)
2012-11-17 ~ 2012-11-25

nginx-1.2.5

原始碼檔案

src/core/ngx_palloc.h
src/core/ngx_palloc.c

背景

對於HTTP伺服器而言,“每秒處理請求數”是非常關鍵的效能指標。處理路徑上每一步動作的時間開銷,都將對該指標造成影響。特別地,記憶體分配/釋放作為最基礎的動作,對總處理時間施加的影響更為巨大——想想那些伴隨複雜資料結構或細碎邏輯產生的記憶體分配/釋放動作,很可能在單次處理中執行數十次,假設每次動作都呼叫malloc()/free()函式,時間開銷將頗為可觀、無法接受。由此,Nginx針對HTTP應用的業務特點,重新設計出一套記憶體管理機制,以記憶體池的形式提供給上層結構使用,從而有效地優化效能。

根據分配/釋放頻繁程度,HTTP應用的記憶體使用特徵可以劃分為兩類:

  1. 程式初始化時分配、終止時釋放的大塊記憶體,供應給配置檔案等全程不會變化的資料結構使用;
  2. 處理單次請求時反覆分配/釋放的小塊記憶體,供應給字串處理等小型結構體和臨時資料使用。

對於前者,使用malloc()/free()帶來的時間開銷並無大礙;對於後者,一旦使用完後即刻呼叫free()釋放記憶體,則會產生許多不必要的時間開銷。更理想的作法是將零碎分配得來的記憶體收集起來,在某個時間點集中釋放掉,亦即“多次分配,一次釋放”。通過設計得當的資料結構和介面,可以在確保“誰分配,誰釋放”的記憶體使用原則下,提供更好的效能。

記憶體管理模型

Nginx將記憶體管理模型組織為兩層結構的記憶體池:

  1. 第一層結構採用“向系統請求儲備記憶體塊(多次)——按需分配零碎記憶體塊(多次)——集中釋放零碎記憶體塊(單次)”的設計思路,每次分配不超過某個固定長度上限的零碎記憶體塊;
  2. 第二層結構採用簡單的“向系統請求獨立記憶體塊(多次+轉發)”的設計思路,亦即轉發分配請求給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結構體構造出單向連結串列,管理各清理回撥函式;  
  5. current欄位總是指向failed計數值不超過4的第一個ngx_pool_data_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()負責建立記憶體池,動作序列如下:

  1. 向系統請求size引數指定大小的儲備記憶體塊(按NGX_POOL_ALIGNMENT對齊,預設16位元組),邏輯上劃分為(ngx_pool_t結構體+剩餘記憶體區)兩部分;
  2. 初始化ngx_pool_data_t結構體各個欄位(參考前一節,last欄位指向初始分配地址);
  3. 計算單次分配的記憶體塊長度上限(max欄位,最大不超過size引數值或NGX_MAX_ALLOC_FROM_POOL);
  4. 初始化其它欄位(current欄位指向自己)。

ngx_destroy_pool(pool)

ngx_destroy_pool()負責銷燬記憶體池,動作序列如下:

  1. 呼叫各個清理回撥函式;
  2. 釋放各個ngx_pool_large_t結構體管理的儲備記憶體塊;
  3. 釋放各個ngx_pool_data_t結構體管理的獨立記憶體塊。

ngx_reset_pool(pool)

ngx_reset_pool()負責釋放零碎記憶體塊,動作序列如下:

  1. 釋放各個ngx_pool_large_t結構體管理的記憶體塊;
  2. 簡單地將各個ngx_pool_data_t結構體的last欄位復原成初始分配地址,而不是釋放。

ngx_palloc(pool, size)/ngx_pnalloc(pool, size)/ngx_pcalloc(pool, size)

ngx_palloc()負責分配零碎記憶體塊,動作序列如下:

  1. 若size引數大於max欄位值,轉發給ngx_palloc_large()處理(呼叫更底層的分配函式);
  2. 以current欄位所指ngx_pool_data_t結構體為起點,在單向連結串列中搜尋第一個能執行分配動作的結構體,完成分配(分配首址按NGX_ALIGNMENT對齊);
  3. 若以上動作均告失敗,轉發給ngx_palloc_block()處理(先分配新的ngx_pool_data_t結構體,再分配零碎記憶體塊)。

ngx_pnalloc()動作與ngx_palloc()類似,但不對齊分配首址。
ngx_pcalloc()將分配請求轉發給ngx_palloc()處理,並對返回的零碎記憶體塊進行清零初始化。

ngx_palloc_block(pool, size)

ngx_palloc_block()負責向系統請求新的儲備記憶體塊,最終完成分配,動作序列如下:

  1. 向系統請求指定大小的儲備記憶體塊(按NGX_POOL_ALIGNMENT對齊,預設16位元組),邏輯上劃分為(ngx_pool_data_t結構體+剩餘記憶體區)兩部分;
  2. 初始化各個欄位(包括分配首址,按NGX_ALIGNMENT對齊);
  3. 調整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負責分配不歸第一層結構管理的獨立記憶體塊,動作序列如下:

  1. 呼叫ngx_alloc()分配新的獨立記憶體塊;
  2. 搜尋第一個空閒的ngx_pool_large_t結構體,儲存並返回獨立記憶體塊首址;
  3. 若前一步失敗,從本記憶體池中分配新的ngx_pool_large_t結構體並加入單向連結串列中,儲存並返回獨立記憶體塊首址。

ngx_pmemalign()動作與ngx_palloc_large()類似,但會按alignment引數對齊獨立記憶體塊首址。

ngx_pfree(pool, p)

ngx_pfree()負責“釋放”記憶體塊,動作序列如下:

  1. 如果待“釋放”物件是獨立記憶體塊,則轉呼叫ngx_free()釋放。

實際上該函式並不真正釋放零碎記憶體塊,而是儘可能將釋放動作推遲到ngx_reset_pool()/ngx_destroy_pool()中。

ngx_pool_cleanup_add(p, size)

ngx_pool_cleanup_add()負責分配新的ngx_pool_cleanup_t結構體,以便呼叫端註冊回撥函式。動作序列如下:

  1. 從本記憶體池中分配一個ngx_pool_cleanup_t結構體;
  2. 若size引數不為0,嘗試從本記憶體池分配相應大小的一塊記憶體並作為回撥引數;
  3. 將新的結構體掛到單向連結串列中;
  4. 調整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()提供一個刪除檔案並關閉相關檔案控制程式碼的預設實現函式。

未完待續...

相關文章