作者:July、dreamice、阿波、yixiao。
本文出處:http://blog.csdn.net/v_JULY_v/。
引言
Nginx(發音同 engine x)是一款輕量級的Web 服務器/反向代理服務器及電子郵件(IMAP/POP3)代理服務器,並在一個BSD-like 協議下發行。由俄羅斯的程序設計師Igor Sysoev所開發,最初供俄國大型的入口網站及搜尋引擎Rambler(俄文:Рамблер)使用。
其特點是佔有內存少,併發能力強,事實上nginx的併發能力確實在同類型的網頁服務器中表現較好,目前中國大陸使用nginx網站用戶有:新浪、網易、 騰訊,另外知名的微網誌Plurk也使用nginx,以及諸多暫不曾得知的玩意兒。讀者可以到此處下載Nginx最新版本的源碼:http://nginx.org/en/download.html。同時,本文本不想給源碼太多註釋,因爲這不像講解算法,算法講解的越通俗易懂越好,而源碼剖析則不同,緣由在於不同的讀者對同一份源碼有着不同的理解,或深或淺,所以,更多的是靠讀者自己去思考與領悟。
ok,本文之中有任何疏漏或不正之處,懇請批評指正。謝謝。
Nginx源碼剖析之內存池
1、內存池結構
內存相關的操作主要在文件 os/unix/ngx_alloc.{h,c} 和 core/ngx_palloc.{h,c} 中實現,ok,咱們先來看內存管理中幾個主要的數據結構:再來看看大塊數據分配的結構體:
其它的數據結構與相關定義:
上述這些數據結構的邏輯結構圖如下(下圖最左上角部分沒有與上文的第一個數據結構內的ngx_uint_t對應起來,特此說明):
1.1、ngx_pool_t的邏輯結構
再看一下用UML繪製的ngx_pool_t的邏輯結構圖:
在下一節,我們將會深入分析內存管理的主要函數。
Nginx源碼剖析之內存管理
2、內存池操作
2.1、創建內存池
例如,調用ngx_create_pool(1024, 0x80d1c4c)後,創建的內存池物理結構如下圖:
緊接着,咱們就來分析下上面代碼中所提到的:ngx_memalign()函數。
因此,nginx的內存池分配,是以16字節爲邊界對齊的。
接下來,咱們來看內存池的銷燬函數,pool指向需要銷燬的內存池
該函數將遍歷內存池鏈表,所有釋放內存,如果註冊了clenup(也是一個鏈表結構),亦將遍歷該cleanup鏈表結構依次調用clenup的handler清理。同時,還將遍歷large鏈表,釋放大塊內存。
2.3、重置內存池
void ngx_reset_pool(ngx_pool_t *pool)重置內存池,將內存池恢復到剛分配時的初始化狀態,注意內存池分配的初始狀態時,是不包含大塊內存的,因此初始狀態需要將使用的大塊內存釋放掉,並把內存池數據結構的各項指針恢復到初始狀態值。代碼片段如下:
這裏雖然重置了內存池,但可以看到並沒有釋放內存池中被使用的小塊內存,而只是將其last指針指向可共分配的內存的初始位置。這樣,就省去了內存池的釋放和重新分配操作,而達到重置內存池的目的。
上面我們主要闡述了內存池管理的幾個函數,接下來我們深入到如何從內存池中去申請使用內存。
2.4、分配內存(重點)
2.4.1、ngx_palloc 與ngx_pnalloc函數這兩個函數的參數都爲(ngx_pool_t *pool, size_t size),且返回類型爲void*,唯一的區別是ngx_palloc從pool內存池分配以NGX_ALIGNMENT對齊的內存,而ngx_pnalloc分配適合size大小的內存,不考慮內存對齊。
我們在這裏只分析ngx_palloc,對於ngx_pnalloc其實現方式基本類似,便不再贅述。
文件:src/core/ngx_palloc.c
例如,在2.1節中創建的內存池中分配200B的內存,調用ngx_palloc(pool, 200)後,該內存池物理結構如下圖:
a、待分配內存小於max值的情況
同樣,緊接着,咱們就來分析上述代碼中的ngx_palloc_block()函數:
注意:該函數分配一塊內存後,last指針指向的是ngx_pool_data_t結構體(大小16B)之後數據區的起始位置,而創建內存池時時,last指針指向的是ngx_pool_t結構體(大小40B)之後數據區的起始位置。 結合2.8節的內存池的物理結構,更容易理解。
b、待分配內存大於max值的情況
如2.4.1節所述,如果分配的內存大小大於max值,代碼將跳到ngx_palloc_large(pool, size)位置,
ok,下面進入ngx_palloc_large(pool, size)函數的分析:
上述代碼中,調用ngx_alloc執行內存分配:
2.4.2、ngx_pcalloc與ngx_pmemalign函數
ngx_pcalloc是直接調用palloc分配好內存,然後進行一次0初始化操作。ngx_pcalloc的源碼如下:
ngx_pmemalign將在分配size大小的內存並按alignment對齊,然後掛到large字段下,當做大塊內存處理。ngx_pmemalign的源碼如下:
其餘的不再詳述。nginx提供給我們使用的內存分配接口,即上述本2.4節中這4種函數,至此,都已分析完畢。
2.5、釋放內存
需要注意的是該函數只釋放large鏈表中註冊的內存,普通內存在ngx_destroy_pool中統一釋放。2.6、註冊cleanup
2.7、文件相關
一些文件相關的操作函數如下,此處就不在詳述了。2.8、內存池的物理結構
針對本文前幾節的例子,畫出的內存池的物理結構如下圖。
從該圖也能看出2.4節的結論,即內存池第一塊內存前40字節爲ngx_pool_t結構,後續加入的內存塊前16個字節爲ngx_pool_data_t結構,這兩個結構之後便是真正可以分配內存區域。
全文總結
來自淘寶數據共享平臺blog內的一篇文章對上述Nginx源碼剖析之內存池,與內存管理總結得很好,特此引用之,作爲對上文全文的一個總結:
Nginx的內存池實現得很精巧,代碼也很簡潔。總的來說,所有的內存池基本都一個宗旨:申請大塊內存,避免“細水長流”。
3.1、創建一個內存池
nginx內存池主要有下面兩個結構來維護,他們分別維護了內存池的頭部和數據部。此處數據部就是供用戶分配小塊內存的地方。
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)(位於src/core/ngx_palloc.c中);
調用這個函數就可以創建一個大小爲size的內存池了。
ngx_create_pool接口函數就是分配上圖這樣的一大塊內存,然後初始化好各個頭部字段(上圖中的彩色部分)。紅色表示的四個字段就是來自於上述的第一個結構,維護數據部分,
由圖可知:last是用戶從內存池分配新內存的開始位置,end是這塊內存池的結束位置,所有分配的內存都不能超過end。藍色表示的max字段的值等於整個數據部分的長度。用戶請求的內存大於max時,就認爲用戶請求的是一個大內存,此時需要在紫色表示的large字段下面單獨分配;用戶請求的內存不大於max的話,就是小內存申請,直接在數據部分分配,此時將會移動last指針(具體見上文2.4.1節)。
3.2、分配小塊內存(size <= max)
上面創建好了一個可用的內存池了,也提到了小塊內存的分配問題。nginx提供給用戶使用的內存分配接口有:
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都是從內存池裏分配size大小內存,至於分得的是小塊內存還是大塊內存,將取決於size的大小;
他們的不同之處在於,palloc取得的內存是對齊的,pnalloc則否。
ngx_pcalloc是直接調用palloc分配好內存,然後進行一次0初始化操作。
ngx_pmemalign將在分配size大小的內存並按alignment對齊,然後掛到large字段下,當做大塊內存處理。下面用圖形展示一下分配小塊內存的模型:
上圖這個內存池模型是由上3個小內存池構成的,由於第一個內存池上剩餘的內存不夠分配了,於是就創建了第二個新的內存池,第三個內存池是由於前面兩個內存池的剩餘部分都不夠分配,所以創建了第三個內存池來滿足用戶的需求。
由圖可見:所有的小內存池是由一個單向鏈表維護在一起的。這裏還有兩個字段需要關注,failed和current字段。failed表示的是當前這個內存池的剩餘可用內存不能滿足用戶分配請求的次數,即是說:一個分配請求到來後,在這個內存池上分配不到想要的內存,那麼就failed就會增加1;這個分配請求將會遞交給下一個內存池去處理,如果下一個內存池也不能滿足,那麼它的failed也會加1,然後將請求繼續往下傳遞,直到滿足請求爲止(如果沒有現成的內存池來滿足,會再創建一個新的內存池)。
current字段會隨着failed的增加而發生改變,如果current指向的內存池的failed達到了4的話,current就指向下一個內存池了。猜測:4這個值應該是Nginx作者的經驗值,或者是一個統計值(詳見上文2.4.1節a部分)。
3.3、大塊內存的分配(size > max)
大塊內存的分配請求不會直接在內存池上分配內存來滿足,而是直接向操作系統申請這麼一塊內存(就像直接使用malloc分配內存一樣),
然後將這塊內存掛到內存池頭部的large字段下。內存池的作用在於解決小塊內存池的頻繁申請問題,對於這種大塊內存,是可以忍受直接申請的。
同樣,用圖形展示大塊內存申請模型:
注意每塊大內存都對應有一個頭部結構(next&alloc),這個頭部結構是用來將所有大內存串成一個鏈表用的。
這個頭部結構不是直接向操作系統申請的,而是當做小塊內存(頭部結構沒幾個字節)直接在內存池裏申請的。
這樣的大塊內存在使用完後,可能需要第一時間釋放,節省內存空間,因此nginx提供了接口函數:
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);
此函數專門用來釋放某個內存池上的某個大塊內存,p就是大內存的地址。
ngx_pfree只會釋放大內存,不會釋放其對應的頭部結構,畢竟頭部結構是當做小內存在內存池裏申請的;遺留下來的頭部結構會作下一次申請大內存之用。
3.4、cleanup資源
可以看到所有掛載在內存池上的資源將形成一個循環鏈表,一路走來,發現鏈表這種看似簡單的數據結構卻被頻繁使用。
由圖可知,每個需要清理的資源都對應有一個頭部結構,這個結構中有一個關鍵的字段handler,handler是一個函數指針,在掛載一個資源到內存池上的時候,同時也會註冊一個清理資源的函數到這個handler上。即是說,內存池在清理cleanup的時候,就是調用這個handler來清理對應的資源。
比如:我們可以將一個開打的文件描述符作爲資源掛載到內存池上,同時提供一個關閉文件描述的函數註冊到handler上,那麼內存池在釋放的時候,就會調用我們提供的關閉文件函數來處理文件描述符資源了。
3.5、內存的釋放
nginx只提供給了用戶申請內存的接口,卻沒有釋放內存的接口,那麼nginx是如何完成內存釋放的呢?總不能一直申請,用不釋放啊。針對這個問題,nginx利用了web server應用的特殊場景來完成;
一個web server總是不停的接受connection和request,所以nginx就將內存池分了不同的等級,有進程級的內存池、connection級的內存池、request級的內存池。
也就是說,創建好一個worker進程的時候,同時爲這個worker進程創建一個內存池,待有新的連接到來後,就在worker進程的內存池上爲該連接創建起一個內存池;連接上到來一個request後,又在連接的內存池上爲request創建起一個內存池。
這樣,在request被處理完後,就會釋放request的整個內存池,連接斷開後,就會釋放連接的內存池。因而,就保證了內存有分配也有釋放。
小結:通過內存的分配和釋放可以看出,nginx只是將小塊內存的申請聚集到一起申請,然後一起釋放。避免了頻繁申請小內存,降低內存碎片的產生等問題。
參考文獻