Nginx 內(nèi)存池 ngx_pool_t
nginx 是自己實(shí)現(xiàn)了內(nèi)存池的,所以在nginx ngx_pool_t 這個(gè)結(jié)構(gòu)也隨處可見,這里主要分析一下內(nèi)存池的分配邏輯。
內(nèi)存池實(shí)現(xiàn)了包括小塊內(nèi)存、大塊內(nèi)存和清理資源幾種資源的處理,應(yīng)該來(lái)說(shuō)覆蓋了絕大數(shù)的使用場(chǎng)景了。
文章相關(guān)視頻講解:
高性能服務(wù)器為什么需要內(nèi)存池??jī)?nèi)存如何分配? 如何設(shè)計(jì)內(nèi)存 ?看視頻講解:「鏈接」
Nginx源碼分析之內(nèi)存池與線程池:「鏈接」
相關(guān)結(jié)構(gòu)定義
// 大塊內(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; // 實(shí)際分配內(nèi)存
};
// 小塊內(nèi)存池
typedef struct {
u_char *last; // 可分配內(nèi)存起始地址
u_char *end; // 可分配內(nèi)存結(jié)束地址
ngx_pool_t *next; // 指向內(nèi)存管理結(jié)構(gòu)
ngx_uint_t failed; // 內(nèi)存分配失敗次數(shù)
} ngx_pool_data_t;
// 內(nèi)存池管理結(jié)構(gòu)
typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s {
ngx_pool_data_t d; // 小塊內(nèi)存池
size_t max; // 小塊內(nèi)存最大的分配內(nèi)存,評(píng)估大內(nèi)存還是小塊內(nèi)存
ngx_pool_t *current; // 當(dāng)前開始分配的小塊內(nèi)存池
ngx_chain_t *chain; // chain
ngx_pool_large_t *large; // 大塊內(nèi)存
ngx_pool_cleanup_t *cleanup; // 待清理資源
ngx_log_t *log; // 日志對(duì)象
};
ngx_pool_t 是整個(gè)內(nèi)存池的管理結(jié)構(gòu),這種結(jié)構(gòu)對(duì)于個(gè)內(nèi)存池對(duì)象來(lái)說(shuō)可能存在多個(gè),但是對(duì)于用戶而言,第一下訪問(wèn)的始終是創(chuàng)建時(shí)返回的那個(gè)。多個(gè) ngx_pool_t 通過(guò) d.next 來(lái)進(jìn)行連接,current 指向 當(dāng)前開始分配的小塊內(nèi)存池,注意 ngx_pool_data_t 在內(nèi)存池結(jié)構(gòu)的起始處,可以進(jìn)行類型轉(zhuǎn)換訪問(wèn)到不同的成員。
實(shí)現(xiàn)
內(nèi)存對(duì)齊
#define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))
#define ngx_align_ptr(p, a)
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
參考 ngx_align 值對(duì)齊宏 分析,ngx_align_ptr 同理
創(chuàng)建內(nèi)存池
max 的最大值為 4095,當(dāng)從內(nèi)存池中申請(qǐng)的內(nèi)存大小大于 max 時(shí),不會(huì)從小塊內(nèi)存中進(jìn)行分配。
ngx_uint_t ngx_pagesize = getpagesize(); // linux 上是 4096
#define NGX_POOL_ALIGNMENT 16
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) // 4095
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); // 16 字節(jié)對(duì)齊申請(qǐng) size 大小的內(nèi)存
if (p == NULL) {
return NULL;
}
p->d.last = (u_char *) p + sizeof(ngx_pool_t); // 設(shè)置可分配內(nèi)存的起始處
p->d.end = (u_char *) p + size; // 設(shè)置可分配內(nèi)存的終止處
p->d.next = NULL;
p->d.failed = 0; // 內(nèi)存分配失敗次數(shù)
size = size - sizeof(ngx_pool_t); // 設(shè)置小塊內(nèi)存可分配的最大值(小于 4095)
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
p->current = p; // 設(shè)置起始分配內(nèi)存池
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
return p;
}
內(nèi)存池創(chuàng)建后的結(jié)構(gòu)邏輯如圖所示:

內(nèi)存申請(qǐng)
申請(qǐng)的內(nèi)存塊以 max 作為區(qū)分
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 1);
}
#endif
return ngx_palloc_large(pool, size);
}
小塊內(nèi)存申請(qǐng)
current 指向每次申請(qǐng)內(nèi)存時(shí)開始檢索分配的小塊內(nèi)存池,而 ngx_palloc_small 的參數(shù) pool 在內(nèi)存池沒有回收時(shí),是固定不變的。
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; // 從 current 處開始分配合適的內(nèi)存
do {
m = p->d.last;
if (align) { // 是否需要內(nèi)存對(duì)齊
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
// 當(dāng)前小塊內(nèi)存池的剩余容量滿足申請(qǐng)的內(nèi)存
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size;
return m; // 一旦滿足分配直接退出
}
p = p->d.next; // 不滿足的情況下尋找下一個(gè)小塊內(nèi)存池
} while (p);
return ngx_palloc_block(pool, size); // 沒有滿足分配的內(nèi)存池,再申請(qǐng)一個(gè)小塊內(nèi)存池
}
當(dāng)在小塊內(nèi)存池中找到了合適的內(nèi)存后的結(jié)構(gòu)如下:

當(dāng)沒有小塊內(nèi)存池滿足申請(qǐng)時(shí),會(huì)再申請(qǐng)一個(gè)小塊內(nèi)存池來(lái)滿足分配,在設(shè)置完 last 和 end 兩個(gè)內(nèi)存指示器后,對(duì)從 current 開始的內(nèi)存池成員 failed 進(jìn)行自增操作,并且當(dāng)這個(gè)內(nèi)存池的 failed 分配次數(shù)大于 4 時(shí),表面這個(gè)內(nèi)存分配失敗的次數(shù)太多,根據(jù)經(jīng)驗(yàn)應(yīng)該下一次分配可能還是失敗,所以直接跳過(guò)這個(gè)內(nèi)存池,移動(dòng) current。
新的內(nèi)存塊插入至內(nèi)存池鏈表的尾端。
#define NGX_ALIGNMENT sizeof(unsigned long) // 8
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); // 每一個(gè)內(nèi)存池的大小都相同
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log); // 16 字節(jié)對(duì)齊申請(qǐng)
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;
}
分配一塊內(nèi)存池后邏輯結(jié)構(gòu)如下:

大塊內(nèi)存申請(qǐng)
大塊內(nèi)存是通過(guò) large 連接的,并且都屬于 ngx_create_pool 返回的 ngx_pool_t 結(jié)構(gòu)。malloc 分配的內(nèi)存由一個(gè) ngx_pool_large_t 節(jié)點(diǎn)來(lái)掛載,而這個(gè) ngx_pool_large_t 節(jié)點(diǎn)又是從小塊內(nèi)存池中分配的。
- 為避免large鏈表長(zhǎng)度過(guò)大導(dǎo)致在遍歷尋找空閑掛載節(jié)點(diǎn)耗時(shí)過(guò)長(zhǎng),限制了遍歷的節(jié)點(diǎn)為3,如果沒有滿足要求則直接分配
- 頭插法 插入至large鏈表中,新的節(jié)點(diǎn)后面也是最先被訪問(wèn)
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); // 調(diào)用 malloc
if (p == NULL) {
return NULL;
}
n = 0;
for (large = pool->large; large; large = large->next) { // 從large 中鏈表中找到 alloc 為 NULL 的節(jié)點(diǎn),將分配的內(nèi)存掛在該節(jié)點(diǎn)上
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) { // 為了避免過(guò)多的遍歷,限制次數(shù)為 0
break;
}
}
// 當(dāng)遍歷的 ngx_pool_large_t 節(jié)點(diǎn)中 alloc 都有指向的內(nèi)存時(shí),從小塊內(nèi)存中分配一個(gè) ngx_pool_large_t 節(jié)點(diǎn)用于掛載新分配的大內(nèi)存
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; // 頭插法 插入至大塊內(nèi)存鏈表中
pool->large = large;
return p;
}
第一次大塊內(nèi)存分配后的結(jié)構(gòu)如下:

完整內(nèi)存池結(jié)構(gòu)邏輯
- 所有的內(nèi)存池結(jié)構(gòu)都通過(guò) d.next 連接
- 前兩個(gè)內(nèi)存池結(jié)構(gòu)的 current 都指向第三個(gè)內(nèi)存池結(jié)構(gòu)
- 所有的 ngx_pool_large_t 節(jié)點(diǎn)都是從小內(nèi)存池中分配的
- 所有的 ngx_pool_large_t 節(jié)點(diǎn)都是連接在首個(gè)內(nèi)存池結(jié)構(gòu)上的
- ngx_pool_large_t 節(jié)點(diǎn)的 alloc 被釋放但 ngx_pool_large_t 節(jié)點(diǎn)不回收

總結(jié)
ngx_pool_t 內(nèi)存分配方面
- 通過(guò) current 和 d.next 來(lái)訪問(wèn)其他的內(nèi)存池結(jié)構(gòu)
- 插入方式
- 小塊內(nèi)存池通過(guò)尾插法插入至內(nèi)存池鏈表的尾端
- 大塊內(nèi)存通過(guò)頭插法插入至large鏈表的首部
- 限制次數(shù)
- 小內(nèi)存分配失敗(failed)次數(shù)大于4次后就不再作為分配內(nèi)存的池子了
- 大內(nèi)存只尋找 large 鏈表中前三節(jié)點(diǎn)是否可以掛載新分配的內(nèi)存
- 內(nèi)存對(duì)齊,多處內(nèi)存對(duì)齊減少內(nèi)存跨 cache 的數(shù)量
其實(shí)總體而言這是一個(gè)比較簡(jiǎn)單的內(nèi)存池了,還是有一些內(nèi)存浪費(fèi)的地方,限制次數(shù) 可以說(shuō)明這個(gè)情況,不過(guò)這也是在簡(jiǎn)單、高效和內(nèi)存分配上的一個(gè)平衡了