nginx內(nèi)存池源碼解析
內(nèi)存池概述
內(nèi)存池是在真正使用內(nèi)存之前,預(yù)先申請(qǐng)分配一定數(shù)量的、大小相等(一般情況下)的內(nèi)存塊留作備用。當(dāng)有新的內(nèi)存需求時(shí),就從內(nèi)存池中分出一部分內(nèi)存塊,若內(nèi)存塊不夠用時(shí),再繼續(xù)申請(qǐng)新的內(nèi)存。
內(nèi)存池的好處有減少向系統(tǒng)申請(qǐng)和釋放內(nèi)存的時(shí)間開(kāi)銷,解決內(nèi)存頻繁分配產(chǎn)生的碎片,提示程序性能,減少程序員在編寫(xiě)代碼中對(duì)內(nèi)存的關(guān)注等
目前一些常見(jiàn)的內(nèi)存池實(shí)現(xiàn)方案有STL中的內(nèi)存分配區(qū),boost中的object_pool
,nginx中的ngx_pool_t
,google的開(kāi)源項(xiàng)目TCMalloc等。
為了自身使用的方便,Nginx封裝了很多有用的數(shù)據(jù)結(jié)構(gòu),比如ngx_str_t ,ngx_array_t, ngx_pool_t 等等,對(duì)于內(nèi)存池,nginx設(shè)計(jì)的十分精煉,值得我們學(xué)習(xí),本文重點(diǎn)給大家介紹nginx內(nèi)存池源碼,并用一個(gè)實(shí)際的代碼例子作了進(jìn)一步的講解。
一、nginx數(shù)據(jù)結(jié)構(gòu)
// SGI STL小塊和大塊內(nèi)存的分界點(diǎn):128B // nginx(給HTTP服務(wù)器所有的模塊分配內(nèi)存)小塊和大塊內(nèi)存的分界點(diǎn):4096B #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) // 內(nèi)存池默認(rèn)大小 #define NGX_DEFAULT_POOL_SIZE (16 * 1024) // 內(nèi)存池字節(jié)對(duì)齊,SGI STL對(duì)其是8B #define NGX_POOL_ALIGNMENT 16 #define NGX_MIN_POOL_SIZE ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \ NGX_POOL_ALIGNMENT) // 將開(kāi)辟的內(nèi)存調(diào)整到16的整數(shù)倍 #define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))
typedef struct ngx_pool_s ngx_pool_t; typedef struct { u_char *last; // 指向可用內(nèi)存的起始地址 u_char *end; // 指向可用內(nèi)存的末尾地址 ngx_pool_t *next; // 指向下一個(gè)內(nèi)存塊 ngx_uint_t failed; // 當(dāng)前內(nèi)存塊分配空間失敗的次數(shù) } ngx_pool_data_t; // 內(nèi)存池塊的類型 struct ngx_pool_s { ngx_pool_data_t d; // 內(nèi)存池塊頭信息 size_t max; ngx_pool_t *current; // 指向可用于分配空間的內(nèi)存塊(failed < 4)的起始地址 ngx_chain_t *chain; // 連接所有的內(nèi)存池塊 ngx_pool_large_t *large; // 大塊內(nèi)存的入口指針 ngx_pool_cleanup_t *cleanup; // 內(nèi)存池塊的清理操作,用戶可設(shè)置回調(diào)函數(shù),在內(nèi)存池塊釋放之前執(zhí)行清理操作 ngx_log_t *log; // 日志 };
二、nginx向OS申請(qǐng)空間ngx_create_pool
// 根據(jù)size進(jìn)行內(nèi)存開(kāi)辟 ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){ ngx_pool_t *p; // 根據(jù)系統(tǒng)平臺(tái)定義的宏以及用戶執(zhí)行的size,調(diào)用不同平臺(tái)的API開(kāi)辟內(nèi)存池 p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log); if (p == NULL) { return NULL; } p->d.last = (u_char *) p + sizeof(ngx_pool_t); // 指向可用內(nèi)存的起始地址 p->d.end = (u_char *) p + size; // 指向可用內(nèi)存的末尾地址 p->d.next = NULL; // 指向下一個(gè)內(nèi)存塊,當(dāng)前剛申請(qǐng)內(nèi)存塊,所以置空 p->d.failed = 0; // 內(nèi)存塊是否開(kāi)辟成功 size = size - sizeof(ngx_pool_t); // 能使用的空間 = 總空間 - 頭信息 // 指定的大小若大于一個(gè)頁(yè)面就用一個(gè)頁(yè)面,否則用指定的大小 // max = min(size, 4096),max指的是除開(kāi)頭信息以外的內(nèi)存塊的大小 p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; p->current = p; // 指向可用于分配空間的內(nèi)存塊的起始地址 p->chain = NULL; p->large = NULL; // 小塊內(nèi)存直接在內(nèi)存塊開(kāi)辟,大塊內(nèi)存在large指向的內(nèi)存開(kāi)辟 p->cleanup = NULL; p->log = log; return p; }
三、nginx向內(nèi)存池申請(qǐng)空間
void * ngx_palloc(ngx_pool_t *pool, size_t size) { #if !(NGX_DEBUG_PALLOC) if (size <= pool->max) { // 當(dāng)前分配的空間小于max,小塊內(nèi)存的分配 return ngx_palloc_small(pool, size, 1); // 考慮內(nèi)存對(duì)齊 } #endif return ngx_palloc_large(pool, 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); // 不考慮內(nèi)存對(duì)齊 } #endif return ngx_palloc_large(pool, size); } void* ngx_pcalloc(ngx_pool_t *pool, size_t size){ void *p; p = ngx_palloc(pool, size); // 考慮內(nèi)存對(duì)齊 if (p) { ngx_memzero(p, size); // 可以初始化內(nèi)存為0 } return p; }
ngx_palloc_small
分配效率高,只做了指針的偏移
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; // 從第一個(gè)內(nèi)存塊的current指針指向的內(nèi)存池進(jìn)行分配 p = pool->current; do { m = p->d.last; // m指向可分配內(nèi)存的起始地址 if (align) { // 把m調(diào)整為NGX_ALIGNMENT整數(shù)倍 m = ngx_align_ptr(m, NGX_ALIGNMENT); } // 內(nèi)存池分配內(nèi)存的核心代碼 if ((size_t) (p->d.end - m) >= size) { // 若可分配空間 >= 申請(qǐng)的空間 // 偏移d.last指針,記錄空閑空間的首地址 p->d.last = m + size; return m; } // 當(dāng)前內(nèi)存塊的空閑空間不夠分配,若有下一個(gè)內(nèi)存塊則轉(zhuǎn)向下一個(gè)內(nèi)存塊 // 若沒(méi)有,p會(huì)被置空,退出while p = p->d.next; } while (p); return ngx_palloc_block(pool, size); }
當(dāng)前內(nèi)存池的塊足夠分配:
當(dāng)前內(nèi)存池的塊不夠分配:
- 開(kāi)辟新的內(nèi)存塊,修改新內(nèi)存塊頭信息的last、end、next、failed
- 前面所有內(nèi)存塊的failed++
- 連接新的內(nèi)存塊以及前面的內(nèi)存塊
static void * ngx_palloc_block(ngx_pool_t *pool, size_t size){ u_char *m; size_t psize; ngx_pool_t *p, *new; // 開(kāi)辟與上一個(gè)內(nèi)存塊大小相同的內(nèi)存塊 psize = (size_t) (pool->d.end - (u_char *) pool); // 將psize對(duì)齊為NGX_POOL_ALIGNMENT的整數(shù)倍后,向OS申請(qǐng)空間 m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log); if (m == NULL) { return NULL; } new = (ngx_pool_t *) m; // 指向新開(kāi)辟內(nèi)存塊的起始地址 new->d.end = m + psize; // 指向新開(kāi)辟內(nèi)存塊的末尾地址 new->d.next = NULL; // 下一塊內(nèi)存的地址為NULL new->d.failed = 0; // 當(dāng)前內(nèi)存塊分配空間失敗的次數(shù) // 指向頭信息的尾部,而max,current、chain等只在第一個(gè)內(nèi)存塊有 m += sizeof(ngx_pool_data_t); m = ngx_align_ptr(m, NGX_ALIGNMENT); new->d.last = m + size; // last指向當(dāng)前塊空閑空間的起始地址 // 由于每次都是從pool->current開(kāi)始分配空間 // 若執(zhí)行到這里,除了new這個(gè)內(nèi)存塊分配成功,其他的內(nèi)存塊全部分配失敗 for (p = pool->current; p->d.next != NULL; p = p->d.next) { // 對(duì)所有的內(nèi)存塊的failed都++,直到該內(nèi)存塊分配失敗的次數(shù)大于4了 // 就表示該內(nèi)存塊的剩余空間很小了,不能再分配空間了 // 就修改current指針,下次從current開(kāi)始分配空間,再次分配的時(shí)候可以不用遍歷前面的內(nèi)存塊 if (p->d.failed++ > 4) { pool->current = p->d.next; } } p->d.next = new; // 連接可分配空間的首個(gè)內(nèi)存塊 和 新開(kāi)辟的內(nèi)存塊 return m; }
四、大塊內(nèi)存的分配與釋放
typedef struct ngx_pool_large_s ngx_pool_large_t; struct ngx_pool_large_s { ngx_pool_large_t *next; // 下一個(gè)大塊內(nèi)存的起始地址 void *alloc; // 大塊內(nèi)存的起始地址 }; static void * ngx_palloc_large(ngx_pool_t *pool, size_t size){ void *p; ngx_uint_t n; ngx_pool_large_t *large; // 調(diào)用的就是malloc p = ngx_alloc(size, pool->log); if (p == NULL) { return NULL; } n = 0; // for循環(huán)遍歷存儲(chǔ)大塊內(nèi)存信息的鏈表 for (large = pool->large; large; large = large->next) { if (large->alloc == NULL) { // 當(dāng)大塊內(nèi)存被ngx_pfree時(shí),alloc為NULL // 遍歷鏈表,若大塊內(nèi)存的首地址為空,則把當(dāng)前malloc的內(nèi)存地址寫(xiě)入alloc large->alloc = p; return p; } // 遍歷4次后,若還沒(méi)有找到被釋放過(guò)的大塊內(nèi)存對(duì)應(yīng)的信息 // 為了提高效率,直接在小塊內(nèi)存中申請(qǐng)空間保存大塊內(nèi)存的信息 if (n++ > 3) { break; } } // 通過(guò)指針偏移在小塊內(nèi)存池上分配存放大塊內(nèi)存*next和*alloc的空間 large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1); if (large == NULL) { // 如果在小塊內(nèi)存上分配存儲(chǔ)*next和*alloc空間失敗,則無(wú)法記錄大塊內(nèi)存 // 釋放大塊內(nèi)存p ngx_free(p); return NULL; } large->alloc = p; // alloc指向大塊內(nèi)存的首地址 large->next = pool->large; // 這兩句采用頭插法,將新內(nèi)存塊的記錄信息存放于以large為頭結(jié)點(diǎn)的鏈表中 pool->large = large; return p; }
大塊內(nèi)存的釋放
// 釋放p指向的大塊內(nèi)存 ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p){ ngx_pool_large_t *l; for (l = pool->large; l; l = l->next) { // 遍歷存儲(chǔ)大塊內(nèi)存信息的鏈表,找到p對(duì)應(yīng)的大塊內(nèi)存 if (p == l->alloc) { ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc); // 釋放大塊內(nèi)存,但不釋放存儲(chǔ)信息的內(nèi)存空間 ngx_free(l->alloc); // free l->alloc = NULL; // alloc置空 return NGX_OK; } } return NGX_DECLINED; }
五、關(guān)于小塊內(nèi)存不釋放
就用了last和end兩個(gè)指著標(biāo)識(shí)空閑的空間,是無(wú)法將已經(jīng)使用的空間合理歸還到內(nèi)存池的,只是會(huì)重置內(nèi)存池。同時(shí)還存儲(chǔ)了指向大內(nèi)存塊large和清理函數(shù)cleanup的頭信息
考慮到nginx的效率,小塊內(nèi)存分配高效,同時(shí)也不回收內(nèi)存
void ngx_reset_pool(ngx_pool_t *pool){ ngx_pool_t *p; ngx_pool_large_t *l; // 由于需要重置小塊內(nèi)存,而大塊內(nèi)存的控制信息在小塊內(nèi)存中保存 // 所以需要先釋放大塊內(nèi)存,在重置小塊內(nèi)存 for (l = pool->large; l; l = l->next) { if (l->alloc) { ngx_free(l->alloc); } } // 遍歷小塊內(nèi)存的鏈表,重置last、failed、current、chain、large等管理信息 for (p = pool; p; p = p->d.next) { // 由于只有第一個(gè)內(nèi)存塊有除了ngx_pool_data_t以外的管理信息,別的內(nèi)存塊只有ngx_pool_data_t的信息 // 不會(huì)出錯(cuò),但是會(huì)浪費(fèi)空間 p->d.last = (u_char *) p + sizeof(ngx_pool_t); p->d.failed = 0; } // current指向可用于分配內(nèi)存的內(nèi)存塊 pool->current = pool; pool->chain = NULL; pool->large = NULL; }
nginx本質(zhì)是http服務(wù)器,通常處理的是短鏈接,間接性提供服務(wù),需要的內(nèi)存不大,所以不回收內(nèi)存,重置即可。
客戶端發(fā)起一個(gè)requests請(qǐng)求后,nginx服務(wù)器收到請(qǐng)求會(huì)返回response響應(yīng),若在keep-alive時(shí)間內(nèi)沒(méi)有收到客戶的再次請(qǐng)求,nginx服務(wù)器會(huì)主動(dòng)斷開(kāi)連接,此時(shí)會(huì)reset內(nèi)存池。下一次客戶端請(qǐng)求再到來(lái)時(shí),可以復(fù)用內(nèi)存池。
如果是處理長(zhǎng)鏈接,只要客戶端還在線,服務(wù)器的資源就無(wú)法釋放,直到系統(tǒng)資源耗盡。長(zhǎng)鏈接一般使用SGI STL內(nèi)存池的方式進(jìn)行內(nèi)存的開(kāi)辟和釋放,而這種方式分配和回收空間的效率就比nginx低
六、銷毀和清空內(nèi)存池
假設(shè)如下情況:
// 假設(shè)內(nèi)存對(duì)齊為4B typedef struct{ char* p; char data[508]; }stData; ngx_pool_t *pool = ngx_create_pool(512, log); // 創(chuàng)建一個(gè)總空間為512B的nginx內(nèi)存塊 stData* data_ptr = ngx_alloc(512); // 因?yàn)榭捎玫膶?shí)際內(nèi)存大小為:512-sizeof(ngx_pool_t),所以屬于大內(nèi)存開(kāi)辟 data_ptr->p = malloc(10); // p指向外界堆內(nèi)存,類似于C++對(duì)象中對(duì)用占用了外部資源
當(dāng)回收大塊內(nèi)存的時(shí)候,調(diào)用ngx_free,就會(huì)導(dǎo)致內(nèi)存泄漏
以上內(nèi)存泄漏的問(wèn)題,可以通過(guò)回調(diào)函數(shù)進(jìn)行內(nèi)存釋放(通過(guò)函數(shù)指針實(shí)現(xiàn))
typedef void (*ngx_pool_cleanup_pt)(void *data); typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t; // 以下結(jié)構(gòu)體由ngx_pool_s.cleanup指向,也是存放在內(nèi)存池的小塊內(nèi)存 struct ngx_pool_cleanup_s { ngx_pool_cleanup_pt handler; void *data; // 指向需要釋放的資源 ngx_pool_cleanup_t *next; // 釋放資源的函數(shù)都放在一個(gè)鏈表,用next指向這個(gè)鏈表 };
nginx提供的函數(shù)接口:
// p表示內(nèi)存池的入口地址,size表示p->cleanup->data指針的大小 // p->cleanup指向含有清理函數(shù)信息的結(jié)構(gòu)體 // ngx_pool_cleanup_add返回 含有清理函數(shù)信息的結(jié)構(gòu)體 的指針 ngx_pool_cleanup_t* ngx_pool_cleanup_add(ngx_pool_t *p, size_t size){ ngx_pool_cleanup_t *c; // 開(kāi)辟清理函數(shù)的結(jié)構(gòu)體,實(shí)際上也是存放在內(nèi)存池的小塊內(nèi)存 c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t)); if (c == NULL) { return NULL; } if (size) { // 為c->data申請(qǐng)size的空間 c->data = ngx_palloc(p, size); if (c->data == NULL) { return NULL; } } else { c->data = NULL; } c->handler = NULL; // 采用頭插法,將當(dāng)前結(jié)構(gòu)體串在pool->cleanup后 c->next = p->cleanup; p->cleanup = c; ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c); return c; }
使用方式:
void release(void* p){ free(p); } ngx_pool_cleanup_t* clean_ptr = ngx_clean_cleanup_add(pool, sizeof(char*)); clean_ptr->handler = &release; // 用戶設(shè)置銷毀內(nèi)存池前需要調(diào)用的函數(shù) clean_ptr->data = data_ptr->p; // 用戶設(shè)置銷毀內(nèi)存池前需要釋放的內(nèi)存的地址 ngx_destroy_pool(pool); // 用戶銷毀內(nèi)存池
七、編譯測(cè)試內(nèi)存池接口功能
void ngx_destroy_pool(ngx_pool_t *pool) { ngx_pool_t *p, *n; ngx_pool_large_t *l; ngx_pool_cleanup_t *c; // 遍歷cleanup鏈表(存放的時(shí)釋放前需要調(diào)用的函數(shù)),可釋放外部占用的資源 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); } } // 釋放大塊內(nèi)存 for (l = pool->large; l; l = l->next) { if (l->alloc) { ngx_free(l->alloc); } } // 釋放小塊內(nèi)存池 for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) { ngx_free(p); if (n == NULL) { break; } } }
執(zhí)行configure
生成Makefile文件(若報(bào)錯(cuò)則表示需要apt安裝軟件)
Makefile如下:
執(zhí)行make命令使用Makefile編譯源碼,在相應(yīng)目錄下生成 .o
文件
#include <ngx_config.h> #include <nginx.h> #include <ngx_core.h> #include <ngx_palloc.h> #include <stdio.h> #include <stdlib.h> #include <string.h> void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...){ } typedef struct Data stData; struct Data{ char *ptr; FILE *pfile; }; void func1(char *p){ printf("free ptr mem!\n"); free(p); } void func2(FILE *pf){ printf("close file!\n"); fclose(pf); } void main(){ // max = 512 - sizeof(ngx_pool_t) // 創(chuàng)建總空間為512字節(jié)的nginx內(nèi)存塊 ngx_pool_t *pool = ngx_create_pool(512, NULL); if(pool == NULL){ printf("ngx_create_pool fail..."); return; } // 從小塊內(nèi)存池分配的 void *p1 = ngx_palloc(pool, 128); if(p1 == NULL){ printf("ngx_palloc 128 bytes fail..."); return; } // 從大塊內(nèi)存池分配的 stData *p2 = ngx_palloc(pool, 512); if(p2 == NULL){ printf("ngx_palloc 512 bytes fail..."); return; } // 占用外部堆內(nèi)存 p2->ptr = malloc(12); strcpy(p2->ptr, "hello world"); // 文件描述符 p2->pfile = fopen("data.txt", "w"); ngx_pool_cleanup_t *c1 = ngx_pool_cleanup_add(pool, sizeof(char*)); c1->handler = func1; // 設(shè)置回調(diào)函數(shù) c1->data = p2->ptr; // 設(shè)置資源地址 ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add(pool, sizeof(FILE*)); c2->handler = func2; c2->data = p2->pfile; // 1.調(diào)用所有的預(yù)置的清理函數(shù) 2.釋放大塊內(nèi)存 3.釋放小塊內(nèi)存池所有內(nèi)存 ngx_destroy_pool(pool); return; }
由于ngx_pool_cleanup_add
中用頭插法將創(chuàng)建的清理塊鏈入pool->cleanup
,所以ngx_destroy_pool
的時(shí)候先清理文件后清理堆內(nèi)存。
相關(guān)測(cè)試代碼推送到:https://github.com/BugMaker-shen/nginx_sgistl_pool
到此這篇關(guān)于nginx內(nèi)存池源碼解析的文章就介紹到這了,更多相關(guān)nginx內(nèi)存池內(nèi)容請(qǐng)搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!
版權(quán)聲明:本站文章來(lái)源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來(lái)源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來(lái)源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來(lái),僅供學(xué)習(xí)參考,不代表本站立場(chǎng),如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。