[nginx] nginx記憶體池ngx_pool_t的介紹
本文詳細介紹nginx中記憶體池的設計和實現。
nginx pool 由 小記憶體拉鍊、大記憶體拉鍊、回撥函式拉鍊組成。建立的第一個ngx_pool_s頭部,會作為整個pool的head,儲存一些拉鍊資訊。
1 建立一個ngx_pool:
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; }; typedef struct { u_char *last; u_char *end; ngx_pool_t *next; ngx_uint_t failed; } ngx_pool_data_t; 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 < 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; }
如上程式碼,ngx_create_pool建立了第一個ngx_pool_t(ngx_pool_s),pool的起始地址會做16位元組對齊。第一個pool結構如下:
第一個pool結構的頭部,儲存了整個池子的資訊。其中,last和end分別指向當前poll小記憶體的起止位置;next指向下一個pool結構,是小記憶體拉鍊;failed儲存當前pool結構申請小記憶體失敗的次數,用於判定當前pool是否可用;max是小記憶體的閾值,申請記憶體大於max認為是大記憶體;current指向當前可用的起始pool;chain目前沒用到;large儲存當前pool結構中的大記憶體拉鍊;cleanup是個回撥函式拉鍊;log用來記錄日誌。
2 ngx_pool記憶體操作
第一個ngx_pool_t創建出來之後,就可以從pool中申請記憶體。小記憶體直接從last和end之間的區域獲取;大記憶體用malloc申請,還要從小記憶體區域申請一個結構體儲存大記憶體地址,然後把這個結構體儲存在large拉鍊下。小記憶體不夠的時候,會建立一個新的ngx_pool_t。
從pool中申請記憶體,提供了以下四個函式:
void *ngx_palloc(ngx_pool_t *pool, size_t size); void *ngx_pnalloc(ngx_pool_t *pool, size_t size); void *ngx_pcalloc(ngx_pool_t *pool, size_t size); void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
ngx_palloc申請小記憶體時會做位元組對齊,而ngx_pnalloc不做,兩者都根據pool->max判斷申請小記憶體或大記憶體;ngx_pcalloc呼叫ngx_palloc,然後清空記憶體;ngx_pmemalign直接申請大記憶體,並且會做位元組對齊。
2.1 申請小記憶體
小記憶體的申請由函式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;
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;
}
小記憶體申請,從current指向的當前可用pool結構開始遍歷,對於每個pool結構,嘗試從小記憶體空間申請記憶體。當所有pool中小記憶體空間都不足的時候,建立一個新的pool,也就是ngx_palloc_block
ngx_palloc_block申請一個新的pool結構,掛在pool->d.next的尾部。同時,從新申請的pool中分配需要的記憶體,此時固定做位元組對齊;更新current指向的可用pool,由以上程式碼可以知道,current拉鍊所表示的當前可用的pool最多隻有5個。不能說current拉鍊長度最長只有5,因為後續有reset操作,可以將current恢復到第一個pool,但是整個池子的所有pool拉鍊都還在。
新申請的pool結構如下:
可以看到,和第一個pool結構不同,只有ngx_pool_data_t。ngx_pool_t是ngx pool的結構,ngx_pool_data_t是pool的小記憶體拉鍊,將ngx_pool_data_t放在ngx_pool_t的開始位置,可以方便進行型別轉換,當前的做法,可以看做是將ngx pool的結構放在了小記憶體拉鍊的第一塊中。
2.2 申請大記憶體
大記憶體申請由函式ngx_palloc_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;
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;
}
malloc申請大記憶體,從large拉鍊中找到空著的large塊,只找前四個,找到就填進去,找不到就用小記憶體申請函式申請一個large塊,把這個large塊放到large拉鍊頭部。large塊的申請,會做位元組對齊,但是大記憶體本身不做。
重用空閒的large塊,節約記憶體空間;只從前四個large塊中查詢,減少large拉鍊過長時查詢的時間。頭插法的large拉鍊,基於最近申請的大記憶體,更有可能被釋放的假設。
2.3 對大記憶體進行位元組對齊
ngx_pmemalign 和 ngx_palloc_large的區別在於兩點:對大記憶體進行位元組對齊;不找空著的large塊。依然是頭插法,放到large拉鍊的頭部。程式碼不放了。
2.4 記憶體釋放
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;
}
遍歷拉鍊,釋放large拉鍊的記憶體,這樣申請large記憶體的時候就有必要檢查是否有空餘的large了。small記憶體不能單獨釋放。
2.5 pool重置
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;
}
reset操作釋放所有large記憶體,large塊本身是在small區域申請的,所以不需要釋放;重置last指標的位置,重用small記憶體區域;current恢復到第一個pool。
這裡要注意的是,經過reset之後,所有pool結構的small區域起始位置都和第一個pool保持一致了,這和新申請pool結構的時候不同,reset後有部分small區域被浪費了。如下圖:
2.6 pool銷燬
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);
}
}
#if (NGX_DEBUG)
/*
* we could allocate the pool->log from this pool
* so we cannot use this log while free()ing the pool
*/
for (l = pool->large; l; l = l->next) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
}
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
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;
}
}
}
銷燬時,首先依次遍歷cleanup拉鍊,呼叫回撥函式釋放對應的記憶體;之後釋放large記憶體;之後依次釋放所有的pool。
2.7 cleanup拉鍊
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;
};
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;
}
cleanup塊記憶體從pool池子中申請。data記憶體可以從pool中獲得,也可以是外部申請,外部申請的data需要由回撥函式銷燬。cleanup拉鍊是頭插法,這意味著,最後進入池子的回撥函式,在銷燬時會被優先執行。
有了ngx_pool_cleanup_t,可以提供類似cpp中delete的行為,此時的回撥函式類似於解構函式。
2.8 fd相關的cleanup
記憶體池還提供了一些fd相關的cleanup拉鍊操作。其實這個和記憶體池關係不大,個人理解,因為cleanup拉鍊儲存的各種資料,預設只在池子銷燬的時候,才由回撥函式銷燬,提供fd相關函式,是為了在池子銷燬之前能夠對 fd進行一些操作,目前提供了清理指定fd的操作。相關函式如下,詳見原始碼。
typedef struct {
ngx_fd_t fd;
u_char *name;
ngx_log_t *log;
} ngx_pool_cleanup_file_t;
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);
void ngx_pool_cleanup_file(void *data);
void ngx_pool_delete_file(void *data);