nginx內存池源碼解析
內存池概述
內存池是在真正使用內存之前,預先申請分配一定數量的、大小相等(一般情況下)的內存塊留作備用。當有新的內存需求時,就從內存池中分出一部分內存塊,若內存塊不夠用時,再繼續申請新的內存。
內存池的好處有減少向系統申請和釋放內存的時間開銷,解決內存頻繁分配產生的碎片,提示程序性能,減少程序員在編寫代碼中對內存的關註等
目前一些常見的內存池實現方案有STL中的內存分配區,boost中的object_pool,nginx中的ngx_pool_t,google的開源項目TCMalloc等。
為瞭自身使用的方便,Nginx封裝瞭很多有用的數據結構,比如ngx_str_t ,ngx_array_t, ngx_pool_t 等等,對於內存池,nginx設計的十分精煉,值得我們學習,本文重點給大傢介紹nginx內存池源碼,並用一個實際的代碼例子作瞭進一步的講解。
ererdr
一、nginx數據結構
// SGI STL小塊和大塊內存的分界點:128B // nginx(給HTTP服務器所有的模塊分配內存)小塊和大塊內存的分界點:4096B #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) // 內存池默認大小 #define NGX_DEFAULT_POOL_SIZE (16 * 1024) // 內存池字節對齊,SGI STL對其是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) // 將開辟的內存調整到16的整數倍 #define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))
typedef struct ngx_pool_s ngx_pool_t; 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; // 指向可用於分配空間的內存塊(failed < 4)的起始地址 ngx_chain_t *chain; // 連接所有的內存池塊 ngx_pool_large_t *large; // 大塊內存的入口指針 ngx_pool_cleanup_t *cleanup; // 內存池塊的清理操作,用戶可設置回調函數,在內存池塊釋放之前執行清理操作 ngx_log_t *log; // 日志 };
二、nginx向OS申請空間ngx_create_pool
// 根據size進行內存開辟 ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){ ngx_pool_t *p; // 根據系統平臺定義的宏以及用戶執行的size,調用不同平臺的API開辟內存池 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); // 能使用的空間 = 總空間 - 頭信息 // 指定的大小若大於一個頁面就用一個頁面,否則用指定的大小 // max = min(size, 4096),max指的是除開頭信息以外的內存塊的大小 p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; p->current = p; // 指向可用於分配空間的內存塊的起始地址 p->chain = NULL; p->large = NULL; // 小塊內存直接在內存塊開辟,大塊內存在large指向的內存開辟 p->cleanup = NULL; p->log = log; return p; }
三、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); } 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); } void* ngx_pcalloc(ngx_pool_t *pool, size_t size){ void *p; p = ngx_palloc(pool, size); // 考慮內存對齊 if (p) { ngx_memzero(p, size); // 可以初始化內存為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; // 從第一個內存塊的current指針指向的內存池進行分配 p = pool->current; do { m = p->d.last; // m指向可分配內存的起始地址 if (align) { // 把m調整為NGX_ALIGNMENT整數倍 m = ngx_align_ptr(m, NGX_ALIGNMENT); } // 內存池分配內存的核心代碼 if ((size_t) (p->d.end - m) >= size) { // 若可分配空間 >= 申請的空間 // 偏移d.last指針,記錄空閑空間的首地址 p->d.last = m + size; return m; } // 當前內存塊的空閑空間不夠分配,若有下一個內存塊則轉向下一個內存塊 // 若沒有,p會被置空,退出while p = p->d.next; } while (p); return ngx_palloc_block(pool, size); }
當前內存池的塊足夠分配:
當前內存池的塊不夠分配:
- 開辟新的內存塊,修改新內存塊頭信息的last、end、next、failed
- 前面所有內存塊的failed++
- 連接新的內存塊以及前面的內存塊
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); // 將psize對齊為NGX_POOL_ALIGNMENT的整數倍後,向OS申請空間 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; // 下一塊內存的地址為NULL new->d.failed = 0; // 當前內存塊分配空間失敗的次數 // 指向頭信息的尾部,而max,current、chain等隻在第一個內存塊有 m += sizeof(ngx_pool_data_t); m = ngx_align_ptr(m, NGX_ALIGNMENT); new->d.last = m + size; // last指向當前塊空閑空間的起始地址 // 由於每次都是從pool->current開始分配空間 // 若執行到這裡,除瞭new這個內存塊分配成功,其他的內存塊全部分配失敗 for (p = pool->current; p->d.next != NULL; p = p->d.next) { // 對所有的內存塊的failed都++,直到該內存塊分配失敗的次數大於4瞭 // 就表示該內存塊的剩餘空間很小瞭,不能再分配空間瞭 // 就修改current指針,下次從current開始分配空間,再次分配的時候可以不用遍歷前面的內存塊 if (p->d.failed++ > 4) { pool->current = p->d.next; } } p->d.next = new; // 連接可分配空間的首個內存塊 和 新開辟的內存塊 return m; }
四、大塊內存的分配與釋放
typedef struct ngx_pool_large_s ngx_pool_large_t; struct ngx_pool_large_s { ngx_pool_large_t *next; // 下一個大塊內存的起始地址 void *alloc; // 大塊內存的起始地址 }; static void * ngx_palloc_large(ngx_pool_t *pool, size_t size){ void *p; ngx_uint_t n; ngx_pool_large_t *large; // 調用的就是malloc p = ngx_alloc(size, pool->log); if (p == NULL) { return NULL; } n = 0; // for循環遍歷存儲大塊內存信息的鏈表 for (large = pool->large; large; large = large->next) { if (large->alloc == NULL) { // 當大塊內存被ngx_pfree時,alloc為NULL // 遍歷鏈表,若大塊內存的首地址為空,則把當前malloc的內存地址寫入alloc large->alloc = p; return p; } // 遍歷4次後,若還沒有找到被釋放過的大塊內存對應的信息 // 為瞭提高效率,直接在小塊內存中申請空間保存大塊內存的信息 if (n++ > 3) { break; } } // 通過指針偏移在小塊內存池上分配存放大塊內存*next和*alloc的空間 large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1); if (large == NULL) { // 如果在小塊內存上分配存儲*next和*alloc空間失敗,則無法記錄大塊內存 // 釋放大塊內存p ngx_free(p); return NULL; } large->alloc = p; // alloc指向大塊內存的首地址 large->next = pool->large; // 這兩句采用頭插法,將新內存塊的記錄信息存放於以large為頭結點的鏈表中 pool->large = large; return p; }
大塊內存的釋放
// 釋放p指向的大塊內存 ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p){ ngx_pool_large_t *l; for (l = pool->large; l; l = l->next) { // 遍歷存儲大塊內存信息的鏈表,找到p對應的大塊內存 if (p == l->alloc) { ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc); // 釋放大塊內存,但不釋放存儲信息的內存空間 ngx_free(l->alloc); // free l->alloc = NULL; // alloc置空 return NGX_OK; } } return NGX_DECLINED; }
五、關於小塊內存不釋放
就用瞭last和end兩個指著標識空閑的空間,是無法將已經使用的空間合理歸還到內存池的,隻是會重置內存池。同時還存儲瞭指向大內存塊large和清理函數cleanup的頭信息
考慮到nginx的效率,小塊內存分配高效,同時也不回收內存
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); } } // 遍歷小塊內存的鏈表,重置last、failed、current、chain、large等管理信息 for (p = pool; p; p = p->d.next) { // 由於隻有第一個內存塊有除瞭ngx_pool_data_t以外的管理信息,別的內存塊隻有ngx_pool_data_t的信息 // 不會出錯,但是會浪費空間 p->d.last = (u_char *) p + sizeof(ngx_pool_t); p->d.failed = 0; } // current指向可用於分配內存的內存塊 pool->current = pool; pool->chain = NULL; pool->large = NULL; }
nginx本質是http服務器,通常處理的是短鏈接,間接性提供服務,需要的內存不大,所以不回收內存,重置即可。
客戶端發起一個requests請求後,nginx服務器收到請求會返回response響應,若在keep-alive時間內沒有收到客戶的再次請求,nginx服務器會主動斷開連接,此時會reset內存池。下一次客戶端請求再到來時,可以復用內存池。
如果是處理長鏈接,隻要客戶端還在線,服務器的資源就無法釋放,直到系統資源耗盡。長鏈接一般使用SGI STL內存池的方式進行內存的開辟和釋放,而這種方式分配和回收空間的效率就比nginx低
六、銷毀和清空內存池
假設如下情況:
// 假設內存對齊為4B typedef struct{ char* p; char data[508]; }stData; ngx_pool_t *pool = ngx_create_pool(512, log); // 創建一個總空間為512B的nginx內存塊 stData* data_ptr = ngx_alloc(512); // 因為可用的實際內存大小為:512-sizeof(ngx_pool_t),所以屬於大內存開辟 data_ptr->p = malloc(10); // p指向外界堆內存,類似於C++對象中對用占用瞭外部資源
當回收大塊內存的時候,調用ngx_free,就會導致內存泄漏
以上內存泄漏的問題,可以通過回調函數進行內存釋放(通過函數指針實現)
typedef void (*ngx_pool_cleanup_pt)(void *data); typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t; // 以下結構體由ngx_pool_s.cleanup指向,也是存放在內存池的小塊內存 struct ngx_pool_cleanup_s { ngx_pool_cleanup_pt handler; void *data; // 指向需要釋放的資源 ngx_pool_cleanup_t *next; // 釋放資源的函數都放在一個鏈表,用next指向這個鏈表 };
nginx提供的函數接口:
// p表示內存池的入口地址,size表示p->cleanup->data指針的大小 // p->cleanup指向含有清理函數信息的結構體 // ngx_pool_cleanup_add返回 含有清理函數信息的結構體 的指針 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申請size的空間 c->data = ngx_palloc(p, size); if (c->data == NULL) { return NULL; } } else { c->data = NULL; } c->handler = NULL; // 采用頭插法,將當前結構體串在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; // 用戶設置銷毀內存池前需要調用的函數 clean_ptr->data = data_ptr->p; // 用戶設置銷毀內存池前需要釋放的內存的地址 ngx_destroy_pool(pool); // 用戶銷毀內存池
七、編譯測試內存池接口功能
void ngx_destroy_pool(ngx_pool_t *pool) { ngx_pool_t *p, *n; ngx_pool_large_t *l; ngx_pool_cleanup_t *c; // 遍歷cleanup鏈表(存放的時釋放前需要調用的函數),可釋放外部占用的資源 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; } } }
執行configure
生成Makefile文件(若報錯則表示需要apt安裝軟件)
Makefile如下:
執行make命令使用Makefile編譯源碼,在相應目錄下生成 .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) // 創建總空間為512字節的nginx內存塊 ngx_pool_t *pool = ngx_create_pool(512, NULL); if(pool == NULL){ printf("ngx_create_pool fail..."); return; } // 從小塊內存池分配的 void *p1 = ngx_palloc(pool, 128); if(p1 == NULL){ printf("ngx_palloc 128 bytes fail..."); return; } // 從大塊內存池分配的 stData *p2 = ngx_palloc(pool, 512); if(p2 == NULL){ printf("ngx_palloc 512 bytes fail..."); return; } // 占用外部堆內存 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; // 設置回調函數 c1->data = p2->ptr; // 設置資源地址 ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add(pool, sizeof(FILE*)); c2->handler = func2; c2->data = p2->pfile; // 1.調用所有的預置的清理函數 2.釋放大塊內存 3.釋放小塊內存池所有內存 ngx_destroy_pool(pool); return; }
由於ngx_pool_cleanup_add
中用頭插法將創建的清理塊鏈入pool->cleanup
,所以ngx_destroy_pool
的時候先清理文件後清理堆內存。
相關測試代碼推送到:https://github.com/BugMaker-shen/nginx_sgistl_pool
到此這篇關於nginx內存池源碼解析的文章就介紹到這瞭,更多相關nginx內存池內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!