nginx的內存池相關文章已經很多了,這裏寫一下簡單原理和最近碰到的問題。
用到的幾個結構,相應說明請看註釋:
//每次能從pool分配的最大內存塊大小,ngx_pagesize在X86下一般是4096,即4k,也就是說每次能從pool分配的最大內存塊大小爲4095字節,將近4k
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
//默認pool大小:16k
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)
struct ngx_pool_large_s { /*大塊內存數據結構*/
ngx_pool_large_t *next; /*其實是一個頭插法的單鏈表,每次分配一個大塊內存都將列表節點插入到這個鏈表的表頭*/
void *alloc; /*大塊內存是直接用malloc來分配的,alloc就是用來保存分配到的內存地址*/
};
typedef struct { /*一個內存池是由多個pool節點組成的鏈,這個結構用來鏈接各個pool節點和保存pool節點可用的內存區域起止地址*/
u_char *last; /*當前內存分配結束位置,即下一段可分配內存的起始位置*/
u_char *end; /*該pool節點內存結束地址*/
ngx_pool_t *next; /*下一個pool節點地址*/
ngx_uint_t failed;/*該pool節點分配內存失敗的次數*/
} ngx_pool_data_t;
struct ngx_pool_s { /*內存池控制結構*/
ngx_pool_data_t d; /*當前pool節點信息*/
size_t max; /*一次能分配的最大內存大小*/
ngx_pool_t *current; /*用來保存當前從哪個pool上分配內存的pool指針,每次分配內存都會從current指向的pool上分配*/
ngx_chain_t *chain;
ngx_pool_large_t *large; /*大塊內存列表,*/
ngx_pool_cleanup_t *cleanup;
ngx_log_t *log;
};
先看一下創建內存池的實現代碼,不到20行的代碼,很簡單的:
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;
}
從上面的代碼可以知道max最大爲4095,也就是說每次申請的內存最大大小爲4095字節,超出則使用大塊內存(參考ngx_palloc和ngx_palloc_large的實現,這裏不講了)。 在X64下(下同),調用pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, log)後,得到的pool內存大小及佈局如下:
last和end指針指向的內存區域就是可用空間,pool頭已經佔80個字節了,所以可用空間比創建時指定的pool大小少80個字節,這裏是我覺得設計得不合理的地方,這個pool的大小應該等於用戶在創建pool時指定的大小加上pool頭大小,這樣用戶創建了多少就能用多少。
得到pool之後就可以從裏面分配內存了,先來看一下分配內存的函數實現,也是幾行代碼:
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
if (size <= pool->max) {
p = pool->current;
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);
return ngx_palloc_block(pool, size);
}
return ngx_palloc_large(pool, size);
}
比如調用p = ngx_palloc(pool, 32); last會向後移動32個字節,內存佈局如下:
假設這個pool可用空間不夠了,那麼會調用ngx_palloc_block分配一個與當前pool大小一樣的pool,並將該pool掛在內存池鏈上和將last指針調整好之後直接返回給用戶可用的內存地址,如下圖是調用q = ngx_palloc(pool, 128);後的內存佈局,左邊是可用空間不夠的pool,右邊是ngx_palloc_block分配的pool:
以上就是nginx內存池的基本原理,首先分配一個大塊內存,然後每次分配小塊內存時直接修改last指針後直接返回內存地址,非常之高效。
基本原理講完了,再來看看ngx_palloc的bug,這個bug隱藏得比較深,耗費了好幾天才搞定。這個bug是我同事KawaruNagisa發現的,他也給官網提bug並accept了,相信下個版本會得到修復的。我接觸nginx時間不是很長,這個bug正好有機會來研究研究nginx的內存池實現。先來看看ngx_palloc的幾行代碼:
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
……
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT); //內存對齊,在64位系統下是8字節對齊
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size;
return m;
}
……
}
上面的代碼中本意是先得到對齊之後的m,並和end指針對比,如果end和m之間的可用空間大於等於要分配的size,那麼就分配成功,並將last向後移動。但是還有另一種情況沒有考慮到:假設 last=28, end=30, size = 32, end-last=2,即end和last之間只有2個字節的可用空間,那麼將last 8字節對齊之後爲32,即m=32,那麼end-m=-2,-2再轉換成size_t則變成了18446744073709551614,再跟size相比,肯定爲true,接着調整last指針並返回m;而我們要分配32個字節,應該再分配一個pool並在這個pool上分配內存,顯然是bug啊。用gdb模擬一下:
所以上面代碼中if條件裏應該加上p->d.end > m ,修復後的ngx_palloc應該如下:
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
if (size <= pool->max) {
p = pool->current;
do {
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
if (p->d.end > m && (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);
}
return ngx_palloc_large(pool, size);
}
另一種避免這個Bug的方法是創建pool時(ngx_create_pool)指定的size要按NGX_ALIGNMENT字節對齊,否則比較容易出Bug。