Nginx源碼分析-內(nèi)存池
Nginx的內(nèi)存池實(shí)現(xiàn)得很精巧,代碼也很簡(jiǎn)潔??偟膩?lái)說(shuō),所有的內(nèi)存池基本都一個(gè)宗旨:申請(qǐng)大塊內(nèi)存,避免“細(xì)水長(zhǎng)流”。
一、創(chuàng)建一個(gè)內(nèi)存池
nginx內(nèi)存池主要有下面兩個(gè)結(jié)構(gòu)來(lái)維護(hù),他們分別維護(hù)了內(nèi)存池的頭部和數(shù)據(jù)部。此處數(shù)據(jù)部就是供用戶(hù)分配小塊內(nèi)存的地方。
//該結(jié)構(gòu)用來(lái)維護(hù)內(nèi)存池的數(shù)據(jù)塊,供用戶(hù)分配之用。
typedef struct {
u_char *last; //當(dāng)前內(nèi)存分配結(jié)束位置,即下一段可分配內(nèi)存的起始位置
u_char *end; //內(nèi)存池結(jié)束位置
ngx_pool_t *next; //鏈接到下一個(gè)內(nèi)存池
ngx_uint_t failed; //統(tǒng)計(jì)該內(nèi)存池不能滿(mǎn)足分配請(qǐng)求的次數(shù)
} ngx_pool_data_t;
//該結(jié)構(gòu)維護(hù)整個(gè)內(nèi)存池的頭部信息。
struct ngx_pool_s {
ngx_pool_data_t d; //數(shù)據(jù)塊
size_t max; //數(shù)據(jù)塊的大小,即小塊內(nèi)存的最大值
ngx_pool_t *current; //保存當(dāng)前內(nèi)存池
ngx_chain_t *chain; //可以掛一個(gè)chain結(jié)構(gòu)
ngx_pool_large_t *large; //分配大塊內(nèi)存用,即超過(guò)max的內(nèi)存請(qǐng)求
ngx_pool_cleanup_t *cleanup; //掛載一些內(nèi)存池釋放的時(shí)候,同時(shí)釋放的資源。
ngx_log_t *log;
};
有了上面的兩個(gè)結(jié)構(gòu),就可以創(chuàng)建一個(gè)內(nèi)存池了,nginx用來(lái)創(chuàng)建一個(gè)內(nèi)存池的接口是:ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)(位于src/core/ngx_palloc.c中);調(diào)用這個(gè)函數(shù)就可以創(chuàng)建一個(gè)大小為size的內(nèi)存池了。這里,我用內(nèi)存池的結(jié)構(gòu)圖來(lái)展示,就不做具體的代碼分析了。

ngx_create_pool接口函數(shù)就是分配上圖這樣的一大塊內(nèi)存,然后初始化好各個(gè)頭部字段(上圖中的彩色部分)。紅色表示的四個(gè)字段就是來(lái)自于上述的第一個(gè)結(jié)構(gòu),維護(hù)數(shù)據(jù)部分,由圖可知:last是用戶(hù)從內(nèi)存池分配新內(nèi)存的開(kāi)始位置,end是這塊內(nèi)存池的結(jié)束位置,所有分配的內(nèi)存都不能超過(guò)end。藍(lán)色表示的max字段的值等于整個(gè)數(shù)據(jù)部分的長(zhǎng)度,用戶(hù)請(qǐng)求的內(nèi)存大于max時(shí),就認(rèn)為用戶(hù)請(qǐng)求的是一個(gè)大內(nèi)存,此時(shí)需要在紫色表示的large字段下面單獨(dú)分配;用戶(hù)請(qǐng)求的內(nèi)存不大于max的話,就是小內(nèi)存申請(qǐng),直接在數(shù)據(jù)部分分配,此時(shí)將會(huì)移動(dòng)last指針。
二、分配小塊內(nèi)存(size <= max)
上面創(chuàng)建好了一個(gè)可用的內(nèi)存池了,也提到了小塊內(nèi)存的分配問(wèn)題。nginx提供給用戶(hù)使用的內(nèi)存分配接口有:
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都是從內(nèi)存池里分配size大小內(nèi)存,至于分得的是小塊內(nèi)存還是大塊內(nèi)存,將取決于size的大小;他們的不同之處在于,palloc取得的內(nèi)存是對(duì)齊的,pnalloc則否。ngx_pcalloc是直接調(diào)用palloc分配好內(nèi)存,然后進(jìn)行一次0初始化操作。ngx_pmemalign將在分配size大小的內(nèi)存并按alignment對(duì)齊,然后掛到large字段下,當(dāng)做大塊內(nèi)存處理。下面用圖形展示一下分配小塊內(nèi)存的模型:

上圖這個(gè)內(nèi)存池模型是由上3個(gè)小內(nèi)存池構(gòu)成的,由于第一個(gè)內(nèi)存池上剩余的內(nèi)存不夠分配了,于是就創(chuàng)建了第二個(gè)新的內(nèi)存池,第三個(gè)內(nèi)存池是由于前面兩個(gè)內(nèi)存池的剩余部分都不夠分配,所以創(chuàng)建了第三個(gè)內(nèi)存池來(lái)滿(mǎn)足用戶(hù)的需求。由圖可見(jiàn):所有的小內(nèi)存池是由一個(gè)單向鏈表維護(hù)在一起的。這里還有兩個(gè)字段需要關(guān)注,failed和current字段。failed表示的是當(dāng)前這個(gè)內(nèi)存池的剩余可用內(nèi)存不能滿(mǎn)足用戶(hù)分配請(qǐng)求的次數(shù),即是說(shuō):一個(gè)分配請(qǐng)求到來(lái)后,在這個(gè)內(nèi)存池上分配不到想要的內(nèi)存,那么就failed就會(huì)增加1;這個(gè)分配請(qǐng)求將會(huì)遞交給下一個(gè)內(nèi)存池去處理,如果下一個(gè)內(nèi)存池也不能滿(mǎn)足,那么它的failed也會(huì)加1,然后將請(qǐng)求繼續(xù)往下傳遞,直到滿(mǎn)足請(qǐng)求為止(如果沒(méi)有現(xiàn)成的內(nèi)存池來(lái)滿(mǎn)足,會(huì)再創(chuàng)建一個(gè)新的內(nèi)存池)。current字段會(huì)隨著failed的增加而發(fā)生改變,如果current指向的內(nèi)存池的failed達(dá)到了4的話,current就指向下一個(gè)內(nèi)存池了。猜測(cè):4這個(gè)值應(yīng)該是作者的經(jīng)驗(yàn)值,或者是一個(gè)統(tǒng)計(jì)值。
三、大塊內(nèi)存的分配(size > max)
大塊內(nèi)存的分配請(qǐng)求不會(huì)直接在內(nèi)存池上分配內(nèi)存來(lái)滿(mǎn)足,而是直接向操作系統(tǒng)申請(qǐng)這么一塊內(nèi)存(就像直接使用malloc分配內(nèi)存一樣),然后將這塊內(nèi)存掛到內(nèi)存池頭部的large字段下。內(nèi)存池的作用在于解決小塊內(nèi)存池的頻繁申請(qǐng)問(wèn)題,對(duì)于這種大塊內(nèi)存,是可以忍受直接申請(qǐng)的。同樣,用圖形展示大塊內(nèi)存申請(qǐng)模型:

注意每塊大內(nèi)存都對(duì)應(yīng)有一個(gè)頭部結(jié)構(gòu)(next&alloc),這個(gè)頭部結(jié)構(gòu)是用來(lái)將所有大內(nèi)存串成一個(gè)鏈表用的。這個(gè)頭部結(jié)構(gòu)不是直接向操作系統(tǒng)申請(qǐng)的,而是當(dāng)做小塊內(nèi)存(頭部結(jié)構(gòu)沒(méi)幾個(gè)字節(jié))直接在內(nèi)存池里申請(qǐng)的。這樣的大塊內(nèi)存在使用完后,可能需要第一時(shí)間釋放,節(jié)省內(nèi)存空間,因此nginx提供了接口函數(shù):ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);此函數(shù)專(zhuān)門(mén)用來(lái)釋放某個(gè)內(nèi)存池上的某個(gè)大塊內(nèi)存,p就是大內(nèi)存的地址。ngx_pfree只會(huì)釋放大內(nèi)存,不會(huì)釋放其對(duì)應(yīng)的頭部結(jié)構(gòu),畢竟頭部結(jié)構(gòu)是當(dāng)做小內(nèi)存在內(nèi)存池里申請(qǐng)的;遺留下來(lái)的頭部結(jié)構(gòu)會(huì)作下一次申請(qǐng)大內(nèi)存之用。
四、cleanup資源

可以看到所有掛載在內(nèi)存池上的資源將形成一個(gè)循環(huán)鏈表,一路走來(lái),發(fā)現(xiàn)鏈表這種看似簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)卻被頻繁使用。由圖可知,每個(gè)需要清理的資源都對(duì)應(yīng)有一個(gè)頭部結(jié)構(gòu),這個(gè)結(jié)構(gòu)中有一個(gè)關(guān)鍵的字段handler,handler是一個(gè)函數(shù)指針,在掛載一個(gè)資源到內(nèi)存池上的時(shí)候,同時(shí)也會(huì)注冊(cè)一個(gè)清理資源的函數(shù)到這個(gè)handler上。即是說(shuō),內(nèi)存池在清理cleanup的時(shí)候,就是調(diào)用這個(gè)handler來(lái)清理對(duì)應(yīng)的資源。比如:我們可以將一個(gè)開(kāi)打的文件描述符作為資源掛載到內(nèi)存池上,同時(shí)提供一個(gè)關(guān)閉文件描述的函數(shù)注冊(cè)到handler上,那么內(nèi)存池在釋放的時(shí)候,就會(huì)調(diào)用我們提供的關(guān)閉文件函數(shù)來(lái)處理文件描述符資源了。
五、內(nèi)存的釋放
只提供給了用戶(hù)申請(qǐng)內(nèi)存的接口,卻沒(méi)有釋放內(nèi)存的接口,那么nginx是如何完成內(nèi)存釋放的呢?總不能一直申請(qǐng),用不釋放啊。針對(duì)這個(gè)問(wèn)題,nginx利用了web server應(yīng)用的特殊場(chǎng)景來(lái)完成;一個(gè)web server總是不停的接受connection和request,所以nginx就將內(nèi)存池分了不同的等級(jí),有進(jìn)程級(jí)的內(nèi)存池、connection級(jí)的內(nèi)存池、request級(jí)的內(nèi)存池。也就是說(shuō),創(chuàng)建好一個(gè)worker進(jìn)程的時(shí)候,同時(shí)為這個(gè)worker進(jìn)程創(chuàng)建一個(gè)內(nèi)存池,待有新的連接到來(lái)后,就在worker進(jìn)程的內(nèi)存池上為該連接創(chuàng)建起一個(gè)內(nèi)存池;連接上到來(lái)一個(gè)request后,又在連接的內(nèi)存池上為request創(chuàng)建起一個(gè)內(nèi)存池。這樣,在request被處理完后,就會(huì)釋放request的整個(gè)內(nèi)存池,連接斷開(kāi)后,就會(huì)釋放連接的內(nèi)存池。因而,就保證了內(nèi)存有分配也有釋放。
總結(jié):通過(guò)內(nèi)存的分配和釋放可以看出,nginx只是將小塊內(nèi)存的申請(qǐng)聚集到一起申請(qǐng),然后一起釋放。避免了頻繁申請(qǐng)小內(nèi)存,降低內(nèi)存碎片的產(chǎn)生等問(wèn)題
原文:http://www.tbdata.org/archives/1390
【編輯推薦】