[nginx 源碼走讀] 內存池
nginx 內存池(源碼)通過大小內存塊的鏈式管理邏輯大致如下圖(部分內存對齊的細節沒有添加進去):
內存池數據結構
小內存塊
小內存塊是通過鏈表進行管理,內存分配過程,涉及到結點上空閒內存匹配是鏈表的遍歷,複雜度是 ,爲了提高效率,增加了failed
分配內存失敗次數統計(具體邏輯在分配函數裏)
typedef struct {
u_char *last;
u_char *end;
ngx_pool_t *next;
ngx_uint_t failed;
} ngx_pool_data_t;
大內存塊
大內存塊沒有複雜的空閒空間管理邏輯,都是直接分配單獨的結點,需要銷燬時直接釋放。
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};
內存文件
struct ngx_chain_s {
ngx_buf_t *buf;
ngx_chain_t *next;
};
內存池
nginx 內存池,主要通過大小空閒內存塊兩個鏈表進行維護, 內存池主要是對小塊內存(max
)進行邏輯管理達到重複利用。
- 小內存分配,在小內存塊
ngx_pool_data_t
鏈表進行分配。 - 大內存分配,在大內存塊
ngx_pool_large_t
鏈表分配。
可能因爲大塊內存長度比較大,重複利用率比較低,而且佔用空間比較大,不宜長期留存在物理內存空間上,所以作者不對大塊內存進行復雜大內存空間管理。
typedef struct ngx_pool_s ngx_pool_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_int_t
ngx_os_init(ngx_log_t *log) {
ngx_pagesize = getpagesize();
}
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
#define NGX_POOL_ALIGNMENT 16
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log) {
ngx_pool_t *p;
// 分配 16 字節對齊的內存。
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;
// 小塊內存大小,空閒內存最大小於 page size。
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;
}
內存對齊申請空間
內存對齊,涉及到 cpu 工作效率,是高性能系統不可缺少的一環,有空可以深入研究。
#if (NGX_HAVE_POSIX_MEMALIGN)
void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) {
void *p;
int err;
err = posix_memalign(&p, alignment, size);
if (err) {
ngx_log_error(NGX_LOG_EMERG, log, err,
"posix_memalign(%uz, %uz) failed", alignment, size);
p = NULL;
}
ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
"posix_memalign: %p:%uz @%uz", p, size, alignment);
return p;
}
#elif (NGX_HAVE_MEMALIGN)
void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) {
void *p;
p = memalign(alignment, size);
if (p == NULL) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"memalign(%uz, %uz) failed", alignment, size);
}
ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
"memalign: %p:%uz @%uz", p, size, alignment);
return p;
}
#else
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
#endif
#ifndef NGX_ALIGNMENT
#define NGX_ALIGNMENT sizeof(unsigned long) /* platform word */
#endif
釋放內存池
除了對大小內存塊數據進行釋放,還增加了回調操作的設計,方便開發者進行部分具體的業務處理。
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;
}
}
}
分配內存
如果分配的內存在小內存塊空間範圍內,就通過小內存塊空閒鏈表中分配,否則直接分配到大內存塊鏈表中。
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);
}
pool->max
查看 ngx_create_pool
的實現:
size = size - sizeof(ngx_pool_t);
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
分配小內存
滿足條件 size <= pool->max
的小內存的空間分配,遍歷小內存塊鏈表,從已分配的空間中查找合適的空閒空間進行分配,否則再創建新的小內存塊進行匹配。
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_ALIGNMENT對齊的偏移位置指針。
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
// 如果有足夠空間,就返回分配的空間,空閒內存減少 size 大小
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);
// 分配 16字節對齊的空間。
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_ALIGNMENT對齊的偏移位置指針
m = ngx_align_ptr(m, NGX_ALIGNMENT);
// 分配 size 大小的空閒空間出去
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;
}
申請大塊內存
大塊內存已分配的大塊數據,除了內存塊頭部信息是可以重複利用的,數據不會重複利用,不用將被 ngx_pfree 釋放掉。
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;
}
// 新結點插入到表頭,有點像 lru,將活躍數據放到前面去。
large->alloc = p;
large->next = pool->large;
pool->large = large;
return 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) {
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;
}
重置內存池
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;
}
問題
nginx 的內存池實現足夠精簡高效,但是依然有些問題不能兼顧到:
- 鏈表管理:
鏈表的查找遍歷時間複雜度是 。ngx_pfree
效率不高。 - 小內存塊鏈表,current 問題:
當遇到密集地分配比較大的小內存場景時,導致已分配結點,分配失敗,failed 次數增加。current 指向新的結點,由於是單向鏈表,前面的結點其實還有足夠的空閒空間分配給其它小內存的,導致空閒空間利用率不高。 - 大內存塊鏈表,重複利用已分配的信息頭問題:
遍歷粒度很小,是否會產生大量內存碎片。 - 小內存回收問題:
內存池只對大內存塊進行內存回收,並沒有小內存塊的內存回收管理。只有ngx_reset_pool
,ngx_destroy_pool
是對其進行銷燬處理的。
所以綜合以上問題,這個內存池只適合於輕量級的內存管理。
測試
nginx 代碼耦合不是很大,可以扣出來調試跟蹤一下工作流程。(源碼)
int main() {
ngx_pool_t *pool = ngx_create_pool(2 * 1024);
void *p = ngx_palloc(pool, 256);
void *p2 = ngx_palloc(pool, 1024);
void *p3 = ngx_palloc(pool, 1024);
void *p4 = ngx_palloc(pool, 256);
void *p5 = ngx_palloc(pool, 1024);
void *p6 = ngx_palloc(pool, 1024);
void *p7 = ngx_palloc(pool, 4 * 1024);
ngx_pool_cleanup_t *c = (ngx_pool_cleanup_t *)ngx_pool_cleanup_add(pool, 0);
memcpy(p, "hello world!", strlen("hello world!") + 1);
c->handler = test_cleanup;
c->data = p;
ngx_destroy_pool(pool);
return 0;
}
參考
Nginx 源碼分析-- 內存池(pool)的分析 三
nginx源碼分析–內存對齊處理
利用cpu緩存實現高性能程序
ngx_align_ptr