菜鳥學習Nginx之記憶體池
從今天開始深入介紹Nginx框架。
首先來談談我對《深入理解Nginx模組開發與架構解析》看法,這本書應該是到目前為止,市面寫的最詳細,最充實的書籍(沒有之一),值得擁有。然而此書對於一個小白來說,並不太適合,此書適合有相關使用經驗或者開發經驗,適合於進一步深造的同學。如果是小白,建議先瀏覽一下網上的部落格,對Nginx各個方面有一定了解,然後在深入閱讀此書。這是僅僅是我個人經驗,畢竟我是這樣走過來的。最後建議閱讀此書的朋友,最好多閱讀幾遍,每一次都有不少的收穫。
開始我們今天的主題,任何一款軟體都離不開資料結構,良好的資料結構對於軟體的發展會起到事半功倍的效果。
一、Nginx很奇葩
Nginx這款軟體很奇葩,但同時體現出它的優秀。說它奇葩之處體現在:為了節約記憶體,不會輕易主動申請記憶體,而經常複用,例如利用一個指標最低2bit始終為0這個特性,來儲存一個欄位。但是它為了提升效能,卻又申請大塊空間,例如在共享記憶體方面,它為每個變數申請128位元組(考慮CPU二級快取)。
二、資料結構
2.1資料結構
本篇將介紹始終貫穿整個軟體的物件--ngx_pool_t記憶體池。記憶體池存在意義,不用多說,直接上資料結構定義。
typedef struct ngx_pool_large_s ngx_pool_large_t; //申請大記憶體段 struct ngx_pool_large_s { ngx_pool_large_t *next; void *alloc; /* 儲存通過malloc返回的指標 */ }; //記憶體池元資料 用於把記憶體池節點使用連結串列方式關聯起來 typedef struct { u_char *last; /* 可分配起始位置 */ u_char *end; /* 當前記憶體池塊 最後有效位置 */ ngx_pool_t *next; /* 指向下一個記憶體池塊 */ ngx_uint_t failed; /* 代表從池中申請記憶體失敗次數 */ } ngx_pool_data_t; /* 記憶體池頭,跳過記憶體池頭是資料區 */ typedef struct ngx_pool_s ngx_pool_t; struct ngx_pool_s { ngx_pool_data_t d; /* 當前記憶體池元資料 */ size_t max; /* 申請空間大於max則表示需要申請大記憶體,只在建立記憶體池時賦值 */ ngx_pool_t *current; /* current用於加速遍歷 */ ngx_chain_t *chain; /* 在http filter模組中使用 主要用於http response */ ngx_pool_large_t *large; /* 大記憶體 */ ngx_pool_cleanup_t *cleanup; /* 設定回撥 ngx_pool_cleanup_add */ ngx_log_t *log; };
2.2、記憶體池組織形態
2.3 特點
Nginx實現的記憶體池有如下特點:
- 為了滿足大記憶體需求(一個記憶體池節點,實際可用記憶體大小是固定的max,當申請的記憶體大於max則認為是大記憶體),nginx設計一個獨立連結串列(large)用於儲存大記憶體塊。大記憶體塊採用malloc/free直接申請堆記憶體,可見大記憶體適用生命週期較短場景,否則會把記憶體耗盡。大記憶體頭部ngx_pool_large_t記憶體是從當前記憶體池中申請。為什麼這樣設計呢?為了複用。
- 對於大記憶體,始終存放到記憶體池首節點中,對大記憶體頭sizeof(ngx_pool_large_t)的記憶體空間一定是來自current所指向記憶體池節點。如上圖所示,可參考後續原始碼分析中。
- 往往的應用層業務邏輯很複雜,為了方便通常在業務邏輯最後環節進行資源的回收,Nginx也考慮到此需求,所以在記憶體池中增加ngx_pool_cleanup_t結構。注意:雖然這裡名稱叫做pool_cleanup,業務層只需要設定好自己的回撥函式即可。Nginx框架在釋放記憶體池之前就會呼叫回撥函式。至於回撥函式內容就非常靈活,完全取決於當前業務邏輯。例如:關閉各種檔案控制代碼,刪除各種臨時資料檔案等。
- 資料結構 ngx_pool_data_t,是將記憶體池節點以連結串列形式進行關聯即next,每次建立新記憶體節點掛在連結串列最後zuiho。其中end-last(減法)等於可用記憶體空間。
三、相關介面
下面介紹相關程式碼,畢竟只有看懂程式碼才能深入理解其中的內涵
/**
* 建立記憶體池
*/
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);//16位元組對齊
if (p == NULL) {
return NULL;
}
/* 初始化記憶體池頭 */
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.end = (u_char *) p + size;
p->d.next = NULL;
p->d.failed = 0;
/* 按照記憶體頁大小使用 超過記憶體頁大小 則浪費記憶體空間 */
size = size - sizeof(ngx_pool_t);
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
p->current = p;
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
return p;
}
簡要說明:
- 為了保證訪問速度,採用16位元組方式對齊。
- 如果申請的記憶體大小大於一個記憶體頁大小(一般是4k),雖然能夠申請成功,但是有記憶體浪費。因此在使用記憶體時需要注意大小。
/**
* 銷燬記憶體池
*/
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
/* 銷燬記憶體池之前 進行資源的回收 主要是業務模組繫結資源,例如關閉檔案控制代碼、刪除臨時檔案等 */
for (c = pool->cleanup; c; c = c->next) {
if (c->handler) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"run cleanup: %p", c);
c->handler(c->data);//執行回撥函式
}
}
/*為了篇幅 刪除debug除錯資訊 */
//釋放大記憶體,由此可知所有的大記憶體均放在記憶體池首節點中
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
//按照連結串列逐一釋放記憶體池節點
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
/**
* 業務模組設定清理回撥
*/
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;//設定null,由呼叫者在外部設定
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
那麼如使用記憶體池呢?非常簡單,直接呼叫ngx_palloc/ngx_pnalloc,流程圖如下:
這裡需要闡明一個觀點,對於Nginx來說,所有申請的記憶體均來自記憶體池(除大記憶體),可以理解成萬物皆池化。
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC) //預設不開啟
if (size <= pool->max) {//檢查待申請的記憶體是否大於max,如果大於則表明申請大記憶體
return ngx_palloc_small(pool, size, 1);
}
#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) {
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size;
return m;
}
p = p->d.next;
} while (p);
return ngx_palloc_block(pool, size);//分配新記憶體池節點
}
/**
* 向作業系統申請分配記憶體塊
* block 代表塊
*/
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new;
//向作業系統中申請新的記憶體
psize = (size_t) (pool->d.end - (u_char *) pool);
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 += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;/* size表示業務從記憶體池中申請的空間大小 */
/* 將每一個記憶體失敗次數加1 如果失敗次數大於4次 則修改current指標 提升遍歷速度 */
for (p = pool->current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
pool->current = p->d.next;//始終條current指標
}
}
p->d.next = new;//掛連結串列 放到連結串列最後
return m;
}
/**
* 申請大記憶體 直接向作業系統申請
*/
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;
p = ngx_alloc(size, pool->log);
if (p == NULL) {
return NULL;
}
n = 0;
/* 遍歷large連結串列 遍歷三次仍然沒有找到合適位置 則建立一個新節點 */
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) {
break;
}
}
/**
* 建立新節點然後在連結串列頭插入 large頭部資訊 也是從池中分配
* 此處比較巧妙 體現萬物皆池化的特點
*/
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
if (large == NULL) {
ngx_free(p);
return NULL;
}
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
四、記憶體池生命週期
在Nginx中有三種不同生命週期的記憶體池為:程序級、連線級、請求級。
級別 |
存活時長 |
說明 |
程序級 |
伴隨整個程序,時間最長 |
ngx_cycle_t中記憶體池 |
連線級 |
伴隨tcp會話,時間居中 |
ngx_connection_t中記憶體池 |
請求級 |
伴隨http一次請求,時間最短 |
ngx_http_request_t中記憶體池 |
為什麼會出現三種級別的記憶體池呢?仔細想想可知,對於Nginx萬物皆池化,所有記憶體的申請必須通過記憶體池。
一個程序啟動肯定需要一個(一些)用於儲存全域性資料。
Nginx是用於網路通訊,自然需要維持tcp相關資料,例如:對於長連線http請求。
請求級,自然對應http請求,雖然http採用長連線方式,但是每一次http請求可能都不一樣,自然需要為每個http請求分配一個記憶體池。
五、總結
Nginx實現的記憶體池比較簡單易懂,我們在開發自己的應用程式,只要保證所有記憶體均來自記憶體池這唯一標準,那麼就不會出現記憶體問題。萬物皆池化!!下一篇,我們來看看ngx_buf_t。