Nginx源碼分析 - 基礎數據結構篇 - 內存池 ngx_palloc.c
。 https://blog.csdn.net/initphp/article/details/50588790
Nginx的內存管理是通過內存池來實現的。Nginx的內存池的設計非常的精巧,很多場景下,我們可以將Nginx的內存池實現抽象出來改造成我們開發中的內存池。
內存池
一般我們使用malloc/alloc/free等函數來分配和釋放內存。但是直接使用這些函數會有一些弊端:
1. 雖然系統自帶的ptmalloc內存分配管理器,也有自己的內存優化管理方案(申請內存塊以及將內存交還給系統都有自己的優化方案,具體可以研究一下ptmalloc的源碼),但是直接使用malloc/alloc/free,仍然會導致內存分配的性能比較低。
2. 頻繁使用這些函數分配和釋放內存,會導致內存碎片,不容易讓系統直接回收內存。典型的例子就是大併發頻繁分配和回收內存,會導致進程的內存產生碎片,並且不會立馬被系統回收。
3. 容易產生內存泄露。
使用內存池分配內存有幾點好處:
1. 提升內存分配效率。不需要每次分配內存都執行malloc/alloc等函數。
2. 讓內存的管理變得更加簡單。內存的分配都會在一塊大的內存上,回收的時候只需要回收大塊內存就能將所有的內存回收,防止了內存管理混亂和內存泄露問題。
數據結構定義
ngx_pool_t 內存池主結構
-
/**
-
* Nginx 內存池數據結構
-
*/
-
struct ngx_pool_s {
-
ngx_pool_data_t d; /* 內存池的數據區域*/
-
size_t max; /* 最大每次可分配內存 */
-
ngx_pool_t *current; /* 指向當前的內存池指針地址。ngx_pool_t鏈表上最後一個緩存池結構*/
-
ngx_chain_t *chain; /* 緩衝區鏈表 */
-
ngx_pool_large_t *large; /* 存儲大數據的鏈表 */
-
ngx_pool_cleanup_t *cleanup; /* 可自定義回調函數,清除內存塊分配的內存 */
-
ngx_log_t *log; /* 日誌 */
-
};
ngx_pool_data_t 數據區域結構
-
typedef struct {
-
u_char *last; /* 內存池中未使用內存的開始節點地址 */
-
u_char *end; /* 內存池的結束地址 */
-
ngx_pool_t *next; /* 指向下一個內存池 */
-
ngx_uint_t failed;/* 失敗次數 */
-
} ngx_pool_data_t;
ngx_pool_large_t 大數據塊結構
-
struct ngx_pool_large_s {
-
ngx_pool_large_t *next; /* 指向下一個存儲地址 通過這個地址可以知道當前塊長度 */
-
void *alloc; /* 數據塊指針地址 */
-
};
ngx_pool_cleanup_t 自定義清理回調的數據結構
-
struct ngx_pool_cleanup_s {
-
ngx_pool_cleanup_pt handler; /* 清理的回調函數 */
-
void *data; /* 指向存儲的數據地址 */
-
ngx_pool_cleanup_t *next; /* 下一個ngx_pool_cleanup_t */
-
};
數據結構圖
說明:
1. Nginx的內存池會放在ngx_pool_t的數據結構上(ngx_pool_data_t用於記錄內存塊block的可用地址空間和內存塊尾部)。當初始化分配的內存塊大小不能滿足需求的時候,Nginx就會調用ngx_palloc_block函數來分配一個新的內存塊,通過鏈表的形式連接起來。
2. 當申請的內存大於pool->max的值的時候,Nginx就會單獨分配一塊large的內存塊,會放置在pool->large的鏈表結構上。
3. pool->cleanup的鏈表結構主要存放需要通過回調函數清理的內存數據。(例如文件描述符)
具體函數實現
內存分配 ngx_alloc和ngx_calloc
ngx_alloc和ngx_calloc 主要封裝了Nginx的內存分配函數malloc。
-
/**
-
* 封裝了malloc函數,並且添加了日誌
-
*/
-
void *
-
ngx_alloc(size_t size, ngx_log_t *log)
-
{
-
void *p;
-
//分配一塊內存
-
p = malloc(size);
-
if (p == NULL) {
-
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
-
"malloc(%uz) failed", size);
-
}
-
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
-
return p;
-
}
-
/**
-
* 調用ngx_alloc方法,如果分配成,則調用ngx_memzero方法,將內存塊設置爲0
-
* #define ngx_memzero(buf, n) (void) memset(buf, 0, n)
-
*/
-
void *
-
ngx_calloc(size_t size, ngx_log_t *log)
-
{
-
void *p;
-
//調用內存分配函數
-
p = ngx_alloc(size, log);
-
if (p) {
-
//將內存塊全部設置爲0
-
ngx_memzero(p, size);
-
}
-
return p;
-
}
創建內存池ngx_create_pool
調用ngx_create_pool這個方法就可以創建一個內存池。
-
/**
-
* 創建一個內存池
-
*/
-
ngx_pool_t *
-
ngx_create_pool(size_t size, ngx_log_t *log) {
-
ngx_pool_t *p;
-
/**
-
* 相當於分配一塊內存 ngx_alloc(size, log)
-
*/
-
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
-
if (p == NULL) {
-
return NULL;
-
}
-
/**
-
* Nginx會分配一塊大內存,其中內存頭部存放ngx_pool_t本身內存池的數據結構
-
* ngx_pool_data_t p->d 存放內存池的數據部分(適合小於p->max的內存塊存儲)
-
* p->large 存放大內存塊列表
-
* p->cleanup 存放可以被回調函數清理的內存塊(該內存塊不一定會在內存池上面分配)
-
*/
-
p->d.last = (u_char *) p + sizeof(ngx_pool_t); //內存開始地址,指向ngx_pool_t結構體之後數據取起始位置
-
p->d.end = (u_char *) p + size; //內存結束地址
-
p->d.next = NULL; //下一個ngx_pool_t 內存池地址
-
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->d.next,並且只負責p->d的數據內容*/
-
p->current = p;
-
p->chain = NULL;
-
p->large = NULL;
-
p->cleanup = NULL;
-
p->log = log;
-
return p;
-
}
銷燬內存池ngx_destroy_pool
-
/**
-
* 銷燬內存池。
-
*/
-
void ngx_destroy_pool(ngx_pool_t *pool) {
-
ngx_pool_t *p, *n;
-
ngx_pool_large_t *l;
-
ngx_pool_cleanup_t *c;
-
/* 首先清理pool->cleanup鏈表 */
-
for (c = pool->cleanup; c; c = c->next) {
-
/* handler 爲一個清理的回調函數 */
-
if (c->handler) {
-
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
-
"run cleanup: %p", c);
-
c->handler(c->data);
-
}
-
}
-
/* 清理pool->large鏈表(pool->large爲單獨的大數據內存塊) */
-
for (l = pool->large; l; l = l->next) {
-
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
-
if (l->alloc) {
-
ngx_free(l->alloc);
-
}
-
}
-
#if (NGX_DEBUG)
-
/*
-
* we could allocate the pool->log from this pool
-
* so we cannot use this log while free()ing the pool
-
*/
-
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
-
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
-
"free: %p, unused: %uz", p, p->d.end - p->d.last);
-
if (n == NULL) {
-
break;
-
}
-
}
-
#endif
-
/* 對內存池的data數據區域進行釋放 */
-
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
-
ngx_free(p);
-
if (n == NULL) {
-
break;
-
}
-
}
-
}
重設內存池ngx_reset_pool
-
/**
-
* 重設內存池
-
*/
-
void ngx_reset_pool(ngx_pool_t *pool) {
-
ngx_pool_t *p;
-
ngx_pool_large_t *l;
-
/* 清理pool->large鏈表(pool->large爲單獨的大數據內存塊) */
-
for (l = pool->large; l; l = l->next) {
-
if (l->alloc) {
-
ngx_free(l->alloc);
-
}
-
}
-
pool->large = NULL;
-
/* 循環重新設置內存池data區域的 p->d.last;data區域數據並不擦除*/
-
for (p = pool; p; p = p->d.next) {
-
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
-
}
-
}
使用內存池分配一塊內存ngx_palloc和ngx_pnalloc
-
/**
-
* 內存池分配一塊內存,返回void類型指針
-
*/
-
void *
-
ngx_palloc(ngx_pool_t *pool, size_t size) {
-
u_char *m;
-
ngx_pool_t *p;
-
/* 判斷每次分配的內存大小,如果超出pool->max的限制,則需要走大數據內存分配策略 */
-
if (size <= pool->max) {
-
p = pool->current;
-
/*
-
* 循環讀取緩存池鏈p->d.next的各個的ngx_pool_t節點,
-
* 如果剩餘的空間可以容納size,則返回指針地址
-
*
-
* 這邊的循環,實際上最多隻有4次,具體可以看ngx_palloc_block函數
-
* */
-
do {
-
/* 對齊操作,會損失內存,但是提高內存使用速度 */
-
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
-
if ((size_t)(p->d.end - m) >= size) {
-
p->d.last = m + size;
-
return m;
-
}
-
p = p->d.next;
-
} while (p);
-
/* 如果沒有緩存池空間沒有可以容納大小爲size的內存塊,則需要重新申請一個緩存池pool節點 */
-
return ngx_palloc_block(pool, size);
-
}
-
/* 走大數據分配策略 ,在pool->large鏈表上分配 */
-
return ngx_palloc_large(pool, size);
-
}
-
/**
-
* 內存池分配一塊內存,返回void類型指針
-
* 不考慮對齊情況
-
*/
-
void *
-
ngx_pnalloc(ngx_pool_t *pool, size_t size) {
-
u_char *m;
-
ngx_pool_t *p;
-
/* 判斷每次分配的內存大小,如果超出pool->max的限制,則需要走大數據內存分配策略 */
-
if (size <= pool->max) {
-
p = pool->current;
-
/* 循環讀取數據區域的各個ngx_pool_t緩存池鏈,如果剩餘的空間可以容納size,則返回指針地址*/
-
do {
-
m = p->d.last; //分配的內存塊的地址
-
if ((size_t)(p->d.end - m) >= size) {
-
p->d.last = m + size;
-
return m;
-
}
-
p = p->d.next;
-
} while (p);
-
/* 如果沒有緩存池空間沒有可以容納大小爲size的內存塊,則需要重新申請一個緩存池*/
-
return ngx_palloc_block(pool, size);
-
}
-
/* 走大數據分配策略 */
-
return ngx_palloc_large(pool, size);
-
}
內存分配邏輯:
1. 分配一塊內存,如果分配的內存size小於內存池的pool->max的限制,則屬於小內存塊分配,走小內存塊分配邏輯;否則走大內存分配邏輯。
2. 小內存分配邏輯:循環讀取pool->d上的內存塊,是否有足夠的空間容納需要分配的size,如果可以容納,則直接分配內存;否則內存池需要申請新的內存塊,調用ngx_palloc_block。
3. 大內存分配邏輯:當分配的內存size大於內存池的pool->max的限制,則會直接調用ngx_palloc_large方法申請一塊獨立的內存塊,並且將內存塊掛載到pool->large的鏈表上進行統一管理。
ngx_palloc_block,內存池擴容:
-
/**
-
* 申請一個新的緩存池 ngx_pool_t
-
* 新的緩存池會掛載在主緩存池的 數據區域 (pool->d->next)
-
*/
-
static void *
-
ngx_palloc_block(ngx_pool_t *pool, size_t size) {
-
u_char *m;
-
size_t psize;
-
ngx_pool_t *p, *new, *current;
-
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;
-
/* 分配size大小的內存塊,返回m指針地址 */
-
m += sizeof(ngx_pool_data_t);
-
m = ngx_align_ptr(m, NGX_ALIGNMENT);
-
new->d.last = m + size;
-
current = pool->current;
-
/**
-
* 緩存池的pool數據結構會掛載子節點的ngx_pool_t數據結構
-
* 子節點的ngx_pool_t數據結構中只用到pool->d的結構,只保存數據
-
* 每添加一個子節點,p->d.failed就會+1,當添加超過4個子節點的時候,
-
* pool->current會指向到最新的子節點地址
-
*
-
* 這個邏輯主要是爲了防止pool上的子節點過多,導致每次ngx_palloc循環pool->d.next鏈表
-
* 將pool->current設置成最新的子節點之後,每次最大循環4次,不會去遍歷整個緩存池鏈表
-
*/
-
for (p = current; p->d.next; p = p->d.next) {
-
if (p->d.failed++ > 4) {
-
current = p->d.next;
-
}
-
}
-
p->d.next = new;
-
/* 最終這個還是沒變 */
-
pool->current = current ? current : new;
-
return m;
-
}
分配一塊大內存,掛載到pool->large鏈表上ngx_palloc_large:
-
/**
-
* 當分配的內存塊大小超出pool->max限制的時候,需要分配在pool->large上
-
*/
-
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;
-
/* 去pool->large鏈表上查詢是否有NULL的,只在鏈表上往下查詢3次,主要判斷大數據塊是否有被釋放的,如果沒有則只能跳出*/
-
for (large = pool->large; large; large = large->next) {
-
if (large->alloc == NULL) {
-
large->alloc = p;
-
return p;
-
}
-
if (n++ > 3) {
-
break;
-
}
-
}
-
/* 分配一個ngx_pool_large_t 數據結構 */
-
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
-
if (large == NULL) {
-
ngx_free(p); //如果分配失敗,刪除內存塊
-
return NULL;
-
}
-
large->alloc = p;
-
large->next = pool->large;
-
pool->large = large;
-
return p;
-
}
大內存塊的釋放ngx_pfree
內存池釋放需要走ngx_destroy_pool,獨立大內存塊的單獨釋放,可以走ngx_pfree方法。
-
/**
-
* 大內存塊釋放 pool->large
-
*/
-
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p) {
-
ngx_pool_large_t *l;
-
/* 在pool->large鏈上循環搜索,並且只釋放內容區域,不釋放ngx_pool_large_t數據結構*/
-
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機制 可以回調函數清理數據
Nginx的內存池cleanup機制,設計的非常巧妙。pool->cleanup本身是一個鏈表,每個ngx_pool_cleanup_t的數據結構上,保存着內存數據的本身cleanup->data和回調清理函數cleanup->handler。
通過cleanup的機制,我們就可以在內存池上保存例如文件句柄fd的資源。當我們調用ngx_destroy_pool方法銷燬內存池的時候,首先會來清理pool->cleanup,並且都會執行c->handler(c->data)回調函數,用於清理資源。
Nginx的這個機制,最顯著的就是讓文件描述符和需要自定義清理的數據的管理變得更加簡單。
分配一個cleanup結構:
-
/**
-
* 分配一個可以用於回調函數清理內存塊的內存
-
* 內存塊仍舊在p->d或p->large上
-
*
-
* ngx_pool_t中的cleanup字段管理着一個特殊的鏈表,該鏈表的每一項都記錄着一個特殊的需要釋放的資源。
-
* 對於這個鏈表中每個節點所包含的資源如何去釋放,是自說明的。這也就提供了非常大的靈活性。
-
* 意味着,ngx_pool_t不僅僅可以管理內存,通過這個機制,也可以管理任何需要釋放的資源,
-
* 例如,關閉文件,或者刪除文件等等的。下面我們看一下這個鏈表每個節點的類型
-
*
-
* 一般分兩種情況:
-
* 1. 文件描述符
-
* 2. 外部自定義回調函數可以來清理內存
-
*/
-
ngx_pool_cleanup_t *
-
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size) {
-
ngx_pool_cleanup_t *c;
-
/* 分配一個ngx_pool_cleanup_t */
-
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
-
if (c == NULL) {
-
return NULL;
-
}
-
/* 如果size !=0 從pool->d或pool->large分配一個內存塊 */
-
if (size) {
-
/* */
-
c->data = ngx_palloc(p, size);
-
if (c->data == NULL) {
-
return NULL;
-
}
-
} else {
-
c->data = NULL;
-
}
-
/* handler爲回調函數 */
-
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;
-
}
手動清理 p->cleanup鏈表上的數據:(內存池銷燬函數ngx_destroy_pool也會清理p->cleanup)
-
/**
-
* 清除 p->cleanup鏈表上的內存塊(主要是文件描述符)
-
* 回調函數:ngx_pool_cleanup_file
-
*/
-
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;
-
}
-
}
-
}
-
}
關閉文件的回調函數和刪除文件的回調函數。這個是文件句柄通用的回調函數,可以放置在p->cleanup->handler上。
-
/**
-
* 關閉文件回調函數
-
* ngx_pool_run_cleanup_file方法執行的時候,用了此函數作爲回調函數的,都會被清理
-
*/
-
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);
-
}
-
}
-
/**
-
* 刪除文件回調函數
-
*/
-
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);
-
}
-
}