nginx內存池實現原理

    Nginx以高效,節省內存著稱。到底如何高效,如何節省內存,這個得真正瞭解其設計原理才能知道,分析源碼是瞭解其原理最直接的方法。Nginx對非常多的基礎設施(紅黑樹 內存池 連接池 hash表)都重複造了輪子,我們來看看爲什麼要這麼做。
    對於c系統,最難的常常是內存管理,隨着系統複雜度的提高,各種內存問題都出來了,很難管理,對於系統的長期穩定運行構成影響。我們生產線上的nginx常年穩定運行,內存池設計非常精巧,值得學習。

工作原理

    預先分配一大塊內存,作爲內存池,小塊內存申請和釋放時,從內存池中分配。大塊內存另行分配
內存對齊:分配的內存塊地址會進行內存對齊,提高IO效率

優點:
將大量小內存的申請聚集到一塊,能夠比malloc 更快
減少內存碎片,防止內存泄漏
減少內存管理複雜度
缺點:
造成內存空間浪費,以空間換時間

分配時怎麼判斷是小塊內存還是大塊內存呢?
p->max=(size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
Pagesize爲內存一頁的大小,x86結構通常爲4k
Max爲內存池可分配大小和pagesize中較小的一個
如果需要分配的內存大於max,則認爲是較大內存,否則爲較小內存

內存對齊
#define ngx_align_ptr(p, a) \
  (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
m = ngx_align_ptr(m, NGX_ALIGNMENT);
這個宏使得到的內存地址爲NGX_ALIGNMENT的倍數。數據對齊,可以避免cpu取值時,要進行兩次IO 。
  

數據結構和基本設置

Src/core/ngx_palloc.h
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;//大塊內存鏈表,分配空間超過max時使用
ngx_pool_cleanup_t *cleanup;//釋放內存的callback
ngx_log_t *log;//日誌信息
};
Src/core/ngx_palloc.h
typedef struct {
u_char *last;//已分配內存的末尾,下一次分配,從這裏開始
u_char *end;//內存池結束位置
ngx_pool_t *next;//鏈表,指向下一塊內存池
ngx_uint_t failed;//內存池分配失敗次數
} ngx_pool_data_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next; //用鏈表組織,指向下一塊較大內存
void *alloc;//實際內存地址
};



基本操作

Src/core/ngx_palloc.c
基本操作 函數頭
創建內存池 ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log);
銷燬內存池 void ngx_destroy_pool(ngx_pool_t *pool);
重置內存池 void ngx_reset_pool(ngx_pool_t *pool);
內存申請(對齊) void * ngx_palloc(ngx_pool_t *pool, size_t size);
void * ngx_palloc(ngx_pool_t *pool, size_t size); (清零)
內存申請(不對齊) void * ngx_pnalloc(ngx_pool_t *pool, size_t size);
內存釋放 ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

創建內存池後



內存申請

內存申請ngx_palloc 如果分配較大內存,那麼會調用ngx_palloc_large,否則在內存池中分配

分配較小內存後的內存池



或者



分配較大內存後的內存池





重置內存池


重置內存池ngx_reset_pool
void
ngx_reset_pool(ngx_pool_t *pool)
{
ngx_pool_t *p;
  ngx_pool_large_t *l;
  //釋放掉所有較大內存
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
  pool->large = NULL;
  //重置所有較小內存塊
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
}

}



釋放內存池

ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
ngx_pool_large_t *l;

  for (l = pool->large; l; l = l->next) {
  //只有內存塊是較大內存塊時,才釋放掉。較小內存只在摧毀整個內存池時統一銷燬
if (p == l->alloc) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"free: %p", l->alloc);
ngx_free(l->alloc);
l->alloc = NULL;

return NGX_OK;
}
}
return NGX_DECLINED;

}




銷燬內存池

銷燬內存池步驟
調用所有cleanup函數,清理數據
釋放所有大塊內存
釋放所有內存池中的內存塊
值得關注的是cleanup函數
爲什麼要有cleanup回調函數? 因爲我們在釋放內存的時候,常常伴隨需要其他的釋放操作,比如釋放文件句柄,關閉網絡連接等。這些需要在釋放內存之前完成。

struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; //回調函數指針
void *data;//執行回調函數時,傳入的數據
ngx_pool_cleanup_t *next;//下一個回調函數結構體
};
註冊cleanup
ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
if (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
c->handler = NULL;
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;

}



使用場景

整個nginx在神馬時候建立內存池呢? 總共會有多少個內存池? 這些內存池神馬時候銷燬?
事實上,nginx會不止建立一個內存池,nginx給內存池分了不同的等級,進程級的內存池 connection級的內存池,request級別的內存池 模塊也可以有自己的內存池

當worker進程創建時,worker進程也創建了一個內存池,當新的連接建立時,爲這個連接創建一個內存池,當得到一個request時,爲這個request創建一個連接池
這樣,request處理完後,可以釋放掉request的整個內存池,連接斷開後,釋放掉連接的整個內存池

參考了網上大量相關資料,感謝各位前輩


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章