Nginx(發音同 engine x)是一款輕量級的Web 伺服器/反向代理伺服器及電子郵件(IMAP/POP3)代理伺服器,并在一個BSD-like 協定下發行。由俄羅斯的程式設計師Igor Sysoev所開發,最初供俄國大型的入口網站及搜尋引擎Rambler(俄文:Рамблер)使用。
其特點是占有記憶體少,并發能力強,事實上nginx的并發能力确實在同類型的網頁伺服器中表現較好,目前中國大陸使用nginx網站使用者有:新浪、網易、 騰訊,另外知名的微網志Plurk也使用nginx,以及諸多暫不曾得知的玩意兒。
ok,本文之中有任何疏漏或不正之處,懇請批評指正。謝謝。
記憶體相關的操作主要在檔案 os/unix/ngx_alloc.{h,c} 和 core/ngx_palloc.{h,c} 中實作,ok,咱們先來看記憶體管理中幾個主要的資料結構:
再來看看大塊資料配置設定的結構體:
其它的資料結構與相關定義:
上述這些資料結構的邏輯結構圖如下:

1.1、ngx_pool_t的邏輯結構
再看一下用UML繪制的ngx_pool_t的邏輯結構圖:
在下一節,我們将會深入分析記憶體管理的主要函數。
2.1、建立記憶體池
例如,調用ngx_create_pool(1024, 0x80d1c4c)後,建立的記憶體池實體結構如下圖:
緊接着,咱們就來分析下上面代碼中所提到的:ngx_memalign()函數。
是以,nginx的記憶體池配置設定,是以16位元組為邊界對齊的。
2.1、銷毀記憶體池
接下來,咱們來看記憶體池的銷毀函數,pool指向需要銷毀的記憶體池
該函數将周遊記憶體池連結清單,所有釋放記憶體,如果注冊了clenup(也是一個連結清單結構),亦将周遊該cleanup連結清單結構依次調用clenup的handler清理。同時,還将周遊large連結清單,釋放大塊記憶體。
2.3、重置記憶體池
重置記憶體池,将記憶體池恢複到剛配置設定時的初始化狀态,注意記憶體池配置設定的初始狀态時,是不包含大塊記憶體的,是以初始狀态需要将使用的大塊記憶體釋放掉,并把記憶體池資料結構的各項指針恢複到初始狀态值。代碼片段如下:
這裡雖然重置了記憶體池,但可以看到并沒有釋放記憶體池中被使用的小塊記憶體,而隻是将其last指針指向可共配置設定的記憶體的初始位置。這樣,就省去了記憶體池的釋放和重新配置設定操作,而達到重置記憶體池的目的。上面我們主要闡述了記憶體池管理的幾個函數,接下來我們深入到如何從記憶體池中去申請使用記憶體。
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記憶體池主要有下面兩個結構來維護,他們分别維護了記憶體池的頭部和資料部。此處資料部就是供使用者配置設定小塊記憶體的地方。
有了上面的兩個結構,就可以建立一個記憶體池了,nginx用來建立一個記憶體池的接口是:
調用這個函數就可以建立一個大小為size的記憶體池了。
ngx_create_pool接口函數就是配置設定上圖這樣的一大塊記憶體,然後初始化好各個頭部字段(上圖中的彩色部分)。紅色表示的四個字段就是來自于上述的第一個結構,維護資料部分, 由圖可知:last是使用者從記憶體池配置設定新記憶體的開始位置,end是這塊記憶體池的結束位置,所有配置設定的記憶體都不能超過end。藍色表示的max字段的值等于整個資料部分的長度。使用者請求的記憶體大于max時,就認為使用者請求的是一個大記憶體,此時需要在紫色表示的large字段下面單獨配置設定;使用者請求的記憶體不大于max的話,就是小記憶體申請,直接在資料部分配置設定,此時将會移動last指針(具體見上文2.4.1節)。
3.2、配置設定小塊記憶體(size <= max)
上面建立好了一個可用的記憶體池了,也提到了小塊記憶體的配置設定問題。nginx提供給使用者使用的記憶體配置設定接口有:
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提供了接口函數:
此函數專門用來釋放某個記憶體池上的某個大塊記憶體,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隻是将小塊記憶體的申請聚集到一起申請,然後一起釋放。避免了頻繁申請小記憶體,降低記憶體碎片的産生等問題。
今閑來無事,拿着個nginx源碼在編譯器上做源碼剖析,鼓搗了一下午,至晚上不料中途停電,諸多部分未能儲存。然不想白忙活,又花費了一個晚上,終至補全,方成上文,并修訂至五日淩晨三點。同時,也參考和借鑒了dreamice、阿波等朋友們及yixiao等大牛的作品,異常感謝。讀者若有興趣,還可以看看sgi stl 的記憶體池及其管理(或者,日後自個也寫下)。