Nginx學習之路(七)NginX中的記憶體管理之---Nginx中的記憶體池
上一篇文章說到了Nginx中的記憶體對齊機制和記憶體分頁機制,今天就來說下Nginx中的記憶體池,記憶體池是一個使用非常廣泛的技術,在web伺服器的高併發情況下可能存在平凡的malloc()和free()過程,通過記憶體池的方式可以將這一過程的開銷極大程度的減少,Nginx的記憶體池的設計相比經典的sgi stl中的allocator記憶體池實現方式更加的貼合作業系統,下面就來詳細的介紹一下Nginx中的記憶體池:
首先先看記憶體池的結構體:
typedef struct { u_char *last;//記憶體池的可使用位置 u_char *end;//記憶體池的結束位置 ngx_pool_t *next;//指向下一塊記憶體池的指標 ngx_uint_t failed;//統計該記憶體池不能滿足分配請求的次數 } ngx_pool_data_t; struct ngx_pool_s { ngx_pool_data_t d;//資料塊,裡面儲存了關於這塊記憶體池的可使用位置,結束位置,以及指向下一塊記憶體池的指標等 size_t max;//資料塊的大小,即小塊記憶體的最大值 ngx_pool_t *current;//記錄當前記憶體池的地址 ngx_chain_t *chain;//chain結構體指標,類似一個連結串列,連結串列節點資料為一個buf ngx_pool_large_t *large;//分配大塊記憶體用,即超過max的記憶體請求 ngx_pool_cleanup_t *cleanup;//掛載一些記憶體池釋放的時候,同時釋放的資源。 ngx_log_t *log;//日誌指標 };
盜用人家的一張圖來說明
在呼叫ngx_create_pool(1024, 0x80d1c4c)後,建立的記憶體池的結構如下圖
這樣,記憶體池的設計結構就很明瞭了,下面詳細的說明一塊記憶體的分配過程:
Nginx的記憶體池的使用主要是在各個連線到達之後為每個連線單獨開闢一個記憶體池,在連線關閉後釋放這個記憶體池,因此,nginx記憶體池設計的記憶體分配比較細化,對於各種size記憶體的分配都做了一定處理,首先是這兩個函式:
//分配記憶體對齊NGX_ALIGNMENT的塊 void * ngx_palloc(ngx_pool_t *pool, size_t size) { #if !(NGX_DEBUG_PALLOC) if (size <= pool->max) { //分配小塊記憶體 return ngx_palloc_small(pool, size, 1); } #endif //分配大塊記憶體 return ngx_palloc_large(pool, size); } //分配記憶體大小size的塊,不做對齊 void * ngx_pnalloc(ngx_pool_t *pool, size_t size) { #if !(NGX_DEBUG_PALLOC) if (size <= pool->max) { //分配小塊記憶體 return ngx_palloc_small(pool, size, 0); } #endif //分配大塊記憶體 return ngx_palloc_large(pool, size); }
來看下小塊記憶體是怎麼分配的:
static ngx_inline void * ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align) { u_char *m; ngx_pool_t *p; p = pool->current; do { m = p->d.last; if (align) { //記憶體對齊NGX_ALIGNMENT的塊 m = ngx_align_ptr(m, NGX_ALIGNMENT); } //然後計算end值減去這個偏移指標位置的大小是否滿足索要分配的size大小, //如果滿足,則移動last指標位置,並返回所分配到的記憶體地址的起始地址; if ((size_t) (p->d.end - m) >= size) { p->d.last = m + size; return m; } //如果不滿足,則查詢下一個鏈。 p = p->d.next; } while (p); //如果遍歷完整個記憶體池連結串列均未找到合適大小的記憶體塊供分配,則執行ngx_palloc_block()來分配。 //ngx_palloc_block()函式為該記憶體池再分配一個block,該block的大小為連結串列中前面每一個block大小的值。 //一個記憶體池是由多個block連結起來的。分配成功後,將該block鏈入該poll鏈的最後, //同時,為所要分配的size大小的記憶體進行分配,並返回分配記憶體的起始地址。 return ngx_palloc_block(pool, size); }
來看下ngx_palloc_block()是怎麼分配的:
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new;
//計算新開闢的記憶體池大小,大小和之前的pool一致
psize = (size_t) (pool->d.end - (u_char *) pool);
//新開闢一塊記憶體池
//執行按NGX_POOL_ALIGNMENT對齊方式的記憶體分配,假設能夠分配成功,則繼續執行後續程式碼片段。
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
//初始化記憶體池的一些引數
new = (ngx_pool_t *) m;
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
//讓m指向該塊記憶體ngx_pool_data_t結構體之後資料區起始位置
m += sizeof(ngx_pool_data_t);
//m記憶體對齊到NGX_ALIGNMENT
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
//失敗4次以上移動current指標
for (p = pool->current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
pool->current = p->d.next;
}
}
p->d.next = new;
return m;
}
然後是大塊記憶體的分配:
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
//這是一個static的函式,說明外部函式不會隨便呼叫,而是提供給內部分配呼叫的,
//即nginx在進行記憶體分配需求時,不會自行去判斷是否是大塊記憶體還是小塊記憶體,
//而是交由記憶體分配函式去判斷,對於使用者需求來說是完全透明的。
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;
//ngx_alloc是一個簡單的封裝,直接呼叫的malloc
p = ngx_alloc(size, pool->log);
if (p == NULL) {
return NULL;
}
n = 0;
//將分配的記憶體鏈入pool的large鏈中,
//這裡指原始pool在之前已經分配過large記憶體的情況。
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) {
break;
}
}
//當原始pool中沒有large塊時,比如新建的一塊pool
//分配一塊ngx_pool_large_t結構體來管理large記憶體
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
if (large == NULL) {
ngx_free(p);
return NULL;
}
//將這塊large加入pool
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
關於Nginx的記憶體釋放和回收,這裡就直接貼淘寶Tengine團隊總結的過程了,總結的很好:
clean up資源:
可以看到所有掛載在記憶體池上的資源將形成一個迴圈連結串列,一路走來,發現連結串列這種看似簡單的資料結構卻被頻繁使用。
由圖可知,每個需要清理的資源都對應有一個頭部結構,這個結構中有一個關鍵的欄位handler,handler是一個函式指標,在掛載一個資源到記憶體池上的時候,同時也會註冊一個清理資源的函式到這個handler上。即是說,記憶體池在清理cleanup的時候,就是呼叫這個handler來清理對應的資源。
比如:我們可以將一個開打的檔案描述符作為資源掛載到記憶體池上,同時提供一個關閉檔案描述的函式註冊到handler上,那麼記憶體池在釋放的時候,就會呼叫我們提供的關閉檔案函式來處理檔案描述符資源了。
記憶體的釋放:
nginx只提供給了使用者申請記憶體的介面,卻沒有釋放記憶體的介面,那麼nginx是如何完成記憶體釋放的呢?總不能一直申請,用不釋放啊。針對這個問題,nginx利用了web server應用的特殊場景來完成;
一個web server總是不停的接受connection和request,所以nginx就將記憶體池分了不同的等級,有程序級的記憶體池、connection級的記憶體池、request級的記憶體池。
也就是說,建立好一個worker程序的時候,同時為這個worker程序建立一個記憶體池,待有新的連線到來後,就在worker程序的記憶體池上為該連線建立起一個記憶體池;連線上到來一個request後,又在連線的記憶體池上為request建立起一個記憶體池。這樣,在request被處理完後,就會釋放request的整個記憶體池,連線斷開後,就會釋放連線的記憶體池。因而,就保證了記憶體有分配也有釋放。
小結:通過記憶體的分配和釋放可以看出,nginx只是將小塊記憶體的申請聚集到一起申請,然後一起釋放。避免了頻繁申請小記憶體,降低記憶體碎片的產生等問題。