一、簡介
最新穩定版本nginx1.20.2。
為了能高效、快速的分配內存,以及減少內存碎片等,nginx實現了自己的內存池基礎組件。
主要實現文件ngx_palloc.h, ngx_palloc.c
二、數據結構
2.1 內存池主要結構
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; ????ngx_pool_large_t?????*large; ????ngx_pool_cleanup_t???*cleanup; ????ngx_log_t????????????*log; };
內存池中第一個成員是一個結構體:
使用ngx_pool_data_t結構體來表示當前內存池信息。
last :下次開始分配的地址
end: 內存池的結束地址
next: 內存池鏈表,將多個內存池連接起來
max
整個內存池的最大大小
current
指向從當前內存池開始查找可用內存
chain
buffer使用的,這里不涉及
large
當需要的內存大于內存池最大大小時,需要通過malloc直接分配,然后形成鏈表進行組織
cleanup
清理工作的回調鏈表
log
日志句柄
2.2 大內存鏈
當需要分配的內存比內存池的最大大小都大時,內存池無法滿足分配,所以直接從系統中分配,然后構成一個鏈表進行維護。
typedef?struct?ngx_pool_large_s??ngx_pool_large_t; struct?ngx_pool_large_s?{ ????ngx_pool_large_t?????*next; ????void?????????????????*alloc; };
2.3 清理任務鏈
有一個回調任務的鏈表,當內存池銷毀時,將依次遍歷此鏈表,逐一回調handler進行清理工作。
typedef?void?(*ngx_pool_cleanup_pt)(void?*data); typedef?struct?ngx_pool_cleanup_s??ngx_pool_cleanup_t; struct?ngx_pool_cleanup_s?{ ????ngx_pool_cleanup_pt???handler; ????void?????????????????*data; ????ngx_pool_cleanup_t???*next; };
三、內存結構圖
3.1 邏輯
3.2 實際
可以看出,很多節點都是從內存池中分配的,所以可以把精力都放在實際的數據上而不必在意其他細節上。
四、實現
4.1 創建內存池
/* ?*?NGX_MAX_ALLOC_FROM_POOL?should?be?(ngx_pagesize?-?1),?i.e.?4095?on?x86. ?*?On?Windows?NT?it?decreases?a?number?of?locked?pages?in?a?kernel. ?*/ #define?NGX_MAX_ALLOC_FROM_POOL??(ngx_pagesize?-?1) #define?NGX_DEFAULT_POOL_SIZE????(16?*?1024)
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); ????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?current?=?p; ????p->chain?=?NULL; ????p->large?=?NULL; ????p->cleanup?=?NULL; ????p->log?=?log; ????return?p; }
從代碼中可以看到,內存池最大不超過pagesize的大小
4.2 從內存池中分配空間
分配函數分了內存對齊和內存不對齊,但這只控制了內存池中分配空間,不控制大內存分配。
(1)分配小空間
-
內存對齊 ngx_palloc
-
內存不對齊 ngx_pnalloc
void?* ngx_palloc(ngx_pool_t?*pool,?size_t?size) { #if?!(NGX_DEBUG_PALLOC) ????if?(size?max)?{ ????????return?ngx_palloc_small(pool,?size,?1); ????} #endif ????return?ngx_palloc_large(pool,?size); }
當需要分配的空間小于max時,將使用小內存分配方式(即從內存池中分配空間),而ngx_pnalloc和ngx_palloc相比只是調用ngx_palloc_small時的最后一個參數為0。
從pool->current指向的內存池開始遍歷,尋找滿足分配大小的空間,找到則返回首地址
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); }
當現有內存池中都無法滿足分配條件時,創建新的內存池
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; ????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; }
其中,創建好新的內存池后,又做了一次遍歷,將failed計數加一,當大于4時,將跳過此內存池,下次就不從它開始查找。
即認為超過4次你都不能滿足分配,以后都不能滿足分配,不再用你了,減少遍歷個數,加快成功分配效率
(2)分配大空間
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; ????for?(large?=?pool->large;?large;?large?=?large->next)?{ ????????if?(large->alloc?==?NULL)?{ ????????????large->alloc?=?p; ????????????return?p; ????????} ????????if?(n++?>?3)?{ ????????????break; ????????} ????} ????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; }
可以看出,為了避免分配空間,遍歷large鏈查找可重用的節點,但是如果鏈表過大又可能太慢,所以只查找前三個,如果三個都沒有找到,則直接分配(而且節點也是從內存池中分配的,所以后續清理時,不需要管節點,只需要釋放申請的大內存本身)
內存對齊
void?* ngx_pmemalign(ngx_pool_t?*pool,?size_t?size,?size_t?alignment) { ????void??????????????*p; ????ngx_pool_large_t??*large; ????p?=?ngx_memalign(alignment,?size,?pool->log); ????if?(p?==?NULL)?{ ????????return?NULL; ????} ????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; }
4.3 注冊清理任務
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; }
可以看出,這里只是分配了一個節點,并沒有設置handler以及data數據,所以還得看具體的調用方進行設置,因為這里返回了分配的節點。
比如在函數ngx_create_temp_file中
ngx_int_t ngx_create_temp_file(ngx_file_t?*file,?ngx_path_t?*path,?ngx_pool_t?*pool, ????ngx_uint_t?persistent,?ngx_uint_t?clean,?ngx_uint_t?access) { ????... ????cln?=?ngx_pool_cleanup_add(pool,?sizeof(ngx_pool_cleanup_file_t)); ????if?(cln?==?NULL)?{ ????????return?NGX_ERROR; ????} ???????... ????????file->fd?=?ngx_open_tempfile(file->name.data,?persistent,?access); ... ????????if?(file->fd?!=?NGX_INVALID_FILE)?{ ????????????cln->handler?=?clean???ngx_pool_delete_file?:?ngx_pool_cleanup_file; ????????????clnf?=?cln->data; ????????????clnf->fd?=?file->fd; ????????????clnf->name?=?file->name.data; ????????????clnf->log?=?pool->log; ????????????return?NGX_OK; ????????} ... }
生成臨時文件,將fd以及文件名注冊到清理任務中,后續文件不使用了則不需要特殊處理,內存內存池釋放時將統一清理。
4.4 重置內存池
-
釋放大內存
-
重置內存中last
-
重置failed計數
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); ????????} ????} ????for?(p?=?pool;?p;?p?=?p->d.next)?{ ????????p->d.last?=?(u_char?*)?p?+?sizeof(ngx_pool_t); ????????p->d.failed?=?0; ????} ????pool->current?=?pool; ????pool->chain?=?NULL; ????pool->large?=?NULL; }
這里有個現象:
在內存池中空間不足時,將調用ngx_palloc_block創建一個新的內存池,而last指向的是m += sizeof(ngx_pool_data_t);, 因此當前新分配的內存池將比第一個內存池可用大小多了(max,current,chain,large,cleanup,log)這幾個字段大小(可能沒有那么多,因為要對齊,可能對齊后就完全一樣了),而現在重置時,p->d.last = (u_char *) p + sizeof(ngx_pool_t);每個內存池可用大小又變成一樣的。
4.5 銷毀內存池
-
回調清理任務
-
釋放大內存
-
釋放內存池本身
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); ????????} ????} ????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; ????????} ????} }
4.6 大內存釋放
通過遍歷找到要釋放的節點,將內存釋放,并且將alloc設置成NULL,則有了節點重用的情況。
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; }
4.7 分配并清空數據
void?* ngx_pcalloc(ngx_pool_t?*pool,?size_t?size) { ????void?*p; ????p?=?ngx_palloc(pool,?size); ????if?(p)?{ ????????ngx_memzero(p,?size); ????} ????return?p; }
正常分配的空間中都是垃圾數據,所以當前函數在分配空間后,將分配的空間清零。
4.8 回調文件清理
(1) 手動關閉指定fd
遍歷清理任務,找到ngx_pool_cleanup_file的handler,如果是要關閉的fd,則回調
void ngx_pool_run_cleanup_file(ngx_pool_t?*p,?ngx_fd_t?fd) { ????ngx_pool_cleanup_t???????*c; ????ngx_pool_cleanup_file_t??*cf; ????for?(c?=?p->cleanup;?c;?c?=?c->next)?{ ????????if?(c->handler?==?ngx_pool_cleanup_file)?{ ????????????cf?=?c->data; ????????????if?(cf->fd?==?fd)?{ ????????????????c->handler(cf); ????????????????c->handler?=?NULL; ????????????????return; ????????????} ????????} ????} }
(2) 關閉fd
void ngx_pool_cleanup_file(void?*data) { ????ngx_pool_cleanup_file_t??*c?=?data; ????ngx_log_debug1(NGX_LOG_DEBUG_ALLOC,?c->log,?0,?"file?cleanup:?fd:%d", ???????????????????c->fd); ????if?(ngx_close_file(c->fd)?==?NGX_FILE_ERROR)?{ ????????ngx_log_error(NGX_LOG_ALERT,?c->log,?ngx_errno, ??????????????????????ngx_close_file_n?"?"%s"?failed",?c->name); ????} }
(3) 刪除文件并關閉fd
void ngx_pool_delete_file(void?*data) { ????ngx_pool_cleanup_file_t??*c?=?data; ????ngx_err_t??err; ????ngx_log_debug2(NGX_LOG_DEBUG_ALLOC,?c->log,?0,?"file?cleanup:?fd:%d?%s", ???????????????????c->fd,?c->name); ????if?(ngx_delete_file(c->name)?==?NGX_FILE_ERROR)?{ ????????err?=?ngx_errno; ????????if?(err?!=?NGX_ENOENT)?{ ????????????ngx_log_error(NGX_LOG_CRIT,?c->log,?err, ??????????????????????????ngx_delete_file_n?"?"%s"?failed",?c->name); ????????} ????} ????if?(ngx_close_file(c->fd)?==?NGX_FILE_ERROR)?{ ????????ngx_log_error(NGX_LOG_ALERT,?c->log,?ngx_errno, ??????????????????????ngx_close_file_n?"?"%s"?failed",?c->name); ????} }