Nginx内存管理详解
目錄:
1.Nginx內(nèi)存管理介紹
2.Nginx內(nèi)存池的邏輯結(jié)構(gòu)
3.Nginx內(nèi)存池的基本數(shù)據(jù)結(jié)構(gòu)
4.內(nèi)存池基本操作介紹
5.內(nèi)存池管理源碼詳解
6.內(nèi)存池使用源碼詳解
7.小結(jié)
?
?
?
1.Nginx內(nèi)存管理介紹
在C/C++語言程序設(shè)計中,通常由程序員自己管理內(nèi)存的分配和釋放,其方式通常是malloc(free)和new(delete)等API。這樣做的缺點(diǎn)在于:由于所申請內(nèi)存塊的大小不定,當(dāng)頻繁使用時會造成大量的內(nèi)存碎片從而降低性能。通常我們所使用的解決辦法就是內(nèi)存池。
什么是內(nèi)存池呢?內(nèi)存池就是在真正使用內(nèi)存之前,先申請分配一定數(shù)量的、大小相等(一般情況下)的內(nèi)存塊留作備用。當(dāng)有新的內(nèi)存需求時,就從內(nèi)存池中分出一部分內(nèi)存塊,若內(nèi)存塊不夠再繼續(xù)申請新的內(nèi)存。而不是每次需要了就調(diào)用分配內(nèi)存的系統(tǒng)API(如malloc)進(jìn)行申請,每次不需要了就調(diào)用系統(tǒng)釋放內(nèi)存的API(如free)進(jìn)行釋放。這樣做的一個顯著優(yōu)點(diǎn)是,使得內(nèi)存分配效率得到提升。因此使用內(nèi)存池的方式對程序所使用的內(nèi)存進(jìn)行統(tǒng)一的分配和回收,是當(dāng)前最流行且高效的內(nèi)存管理方法,能夠在很大程度上降低內(nèi)存管理的難度,減少程序的缺陷,提高整個程序的穩(wěn)定性。
通過減少頻繁的內(nèi)存申請和釋放可以提升效率很容易理解,那么內(nèi)存池究竟是怎么提高程序的穩(wěn)定性的呢?我們知道在C/C++語言中,并沒有提供直接可用的垃圾回收機(jī)制,因此在程序編寫中, 一個特別容易發(fā)生的錯誤就是內(nèi)存泄露,對于運(yùn)行時間短,內(nèi)存需求小的程序來說,泄露一點(diǎn)內(nèi)存除了影響程序運(yùn)行效率之外可能并不會造成多大的問題。但是類似于Ngnix這樣需要長期運(yùn)行的web服務(wù)器程序來說,內(nèi)存泄露是一件非常嚴(yán)重的災(zāi)難,這會使得程序由于內(nèi)存耗盡而崩潰,重啟之前不再能夠提供相應(yīng)的web服務(wù)。還有一種情況就是當(dāng)內(nèi)存分配與釋放的邏輯在程序中相隔較遠(yuǎn)時,很容易發(fā)生內(nèi)存被釋放兩次乃至多次的情況。使用內(nèi)存池使得我們在開發(fā)程序時,只用關(guān)心內(nèi)存的分配,而釋放就交給內(nèi)存池來完成。
那么內(nèi)存池在Nginx中究竟是怎么使用的呢?通常我們對于每個請求或者連接都會建立相應(yīng)的內(nèi)存池,建立好內(nèi)存池之后,我們可以直接從內(nèi)存池中申請所需要的內(nèi)存,而不用去管內(nèi)存的釋放,唯一需要注意的就是當(dāng)內(nèi)存池使用完成之后需要記得銷毀內(nèi)存池。此時,內(nèi)存池會調(diào)用相應(yīng)的數(shù)據(jù)清理函數(shù)(如果有的話),之后會釋放在內(nèi)存池中管理的內(nèi)存。
大家可能會問,既然申請的內(nèi)存在內(nèi)存池銷毀的時候才會被釋放,這不會存在內(nèi)存的浪費(fèi)么?畢竟使用完了不再需要的內(nèi)存為什么不立即釋放而非要等到銷毀內(nèi)存池時才釋放呢?確實(shí)存在這個問題,不過大家不用擔(dān)心。在Nginx中,對于大塊內(nèi)存可以使用ngx_pfree()函數(shù)提前釋放。并且由于Nginx是一個純粹的web服務(wù)器,而web服務(wù)器通常使用的協(xié)議是Http協(xié)議,并且在傳輸層使用的是Tcp協(xié)議,我們知道每一個tcp連接都是由生命周期的,因此基于tcp的http請求都會有一個很短暫的生命周期。對于這種擁有很短暫生命周期的請求,我們所建立的內(nèi)存池的生命周期也相應(yīng)會很短暫,因此其所占用的內(nèi)存資源很快就可以得到釋放,不會出現(xiàn)太多的資源浪費(fèi)的問題。畢竟工程就是一種折中嘛,我們需要在內(nèi)存資源浪費(fèi)和減低程序內(nèi)存管理難度、提升效率之間選擇一個合適的權(quán)衡。
說了這么多,現(xiàn)在就讓我們開始研究和學(xué)習(xí)Nginx內(nèi)存管理的機(jī)制和源碼吧。注:本文的講解都是基于nginx-1.10.3版本。
?
?
?
2.Nginx內(nèi)存池的邏輯結(jié)構(gòu)
前面提到Nginx內(nèi)存管理機(jī)制其實(shí)就是內(nèi)存池,其底層實(shí)現(xiàn)就是一個鏈表結(jié)構(gòu)。我們需要對內(nèi)存池進(jìn)行管理和分配,依賴的就是ngx_pool_t結(jié)構(gòu)體,可以認(rèn)為該結(jié)構(gòu)就是內(nèi)存池的分配管理模塊。那么內(nèi)存池的邏輯結(jié)構(gòu)究竟是什么樣呢?其實(shí)就是一個ngx_pool_t結(jié)構(gòu)體,在這個結(jié)構(gòu)體中包含了三個部分:小塊內(nèi)存形成的單鏈表,大塊內(nèi)存形成的單鏈表和數(shù)據(jù)清理函數(shù)形成的單鏈表。先給出一張整個內(nèi)存池內(nèi)部實(shí)現(xiàn)的結(jié)構(gòu)圖,方便大家理解。具體如圖2.1所示:
?
?
圖2.1 Nginx內(nèi)存池示意圖
?
? 圖2.1完整的展示了ngx_pool_t內(nèi)存池中小塊內(nèi)存、大塊內(nèi)存和資源清理函數(shù)鏈表間的關(guān)系。圖中,內(nèi)存池預(yù)先分配的剩余空閑內(nèi)存不足以滿足用戶申請的內(nèi)存需求,導(dǎo)致又分配了兩個小內(nèi)存池。其中原內(nèi)存池的failed成員已經(jīng)大于4,所以current指向了第2塊小塊內(nèi)存池,這樣當(dāng)用戶再次從小塊內(nèi)存池中請求分配內(nèi)存空間時,將會直接忽略第1塊小內(nèi)存池,從第2塊小塊內(nèi)存池開始遍歷。從這里可以看到,我們使用的內(nèi)存池確實(shí)存在當(dāng)failed成員大于4之后不能利用其空閑內(nèi)存的資源浪費(fèi)現(xiàn)象(由于current指針后移)。值得注意的是:我們的第2、3塊小塊內(nèi)存池中只包含了ngx_pool_t結(jié)構(gòu)體和數(shù)據(jù)區(qū),并不包含max、current、...、log。這是由于后續(xù)第1塊小內(nèi)存池已經(jīng)包含了這些信息,后續(xù)的小塊內(nèi)存池不必在浪費(fèi)空間存儲這些信息。我們在第6小節(jié):內(nèi)存池的使用中將會有所介紹。圖中共分配了3個大塊內(nèi)存,其中第二塊的alloc為NULL(提前調(diào)用了ngx_pfree())。圖中還掛在了兩個資源清理方法。提醒一下的是:如果在這里沒有弄清楚,沒有關(guān)系,看完了后面的部分再回過頭來理解這個示意圖就能夠很好的理解了。這里只是先給出一個概括性的Nginx內(nèi)存池邏輯結(jié)構(gòu)的介紹,先給大家留下一個大概的印象。
?
?
?
3.Nginx內(nèi)存池的基本數(shù)據(jù)結(jié)構(gòu)
本部分主要介紹內(nèi)存池中重要的數(shù)據(jù)結(jié)構(gòu),主要是ngx_pool_t,然后介紹ngx_pool_t中三個重要數(shù)據(jù)結(jié)構(gòu):ngx_pool_data_t,ngx_pool_large_t和ngx_pool_cleanup_t。
?
(1)ngx_pool_t
我們可以在Nginx的源碼的src/core/目錄下的nax_palloc.h頭文件中看到:
?
1 struct ngx_pool_s { 2 ngx_pool_data_t d; 3 size_t max; 4 ngx_pool_t *current; 5 ngx_chain_t *chain; 6 ngx_pool_large_t *large; 7 ngx_pool_cleanup_t *cleanup; 8 ngx_log_t *log; 9 };?
? 并且在src/core/ngx_core.h中:
| 1 | typedef?struct?ngx_pool_s??????? ngx_pool_t; |
?
下面將具體講解ngx_pool_t結(jié)構(gòu)體中每個成員的含義和用途:
?d:ngx_pool_data_t結(jié)構(gòu)體,描述內(nèi)存池中的小塊內(nèi)存。當(dāng)小塊內(nèi)存不足時,會再分配一個ngx_pool_t(里面含有一個新分配且未使用的小塊內(nèi)存空間和用于管理這塊內(nèi)存空間的ngx_pool_data_t結(jié)構(gòu)體)。這些小塊內(nèi)存塊之間通過d中的next成員鏈接形成的單鏈表。掛在d成員上。
?
max:評估申請內(nèi)存屬于小塊還是大塊的標(biāo)準(zhǔn),在x86上默認(rèn)是4095字節(jié)。
?
current:多個小塊內(nèi)存構(gòu)成單鏈表時,指向分配內(nèi)存時遍歷的第一個小塊內(nèi)存。
?
chain:與內(nèi)存池關(guān)系不大,略過。
?
large:ngx_pool_large_t結(jié)構(gòu)體,當(dāng)用戶申請的內(nèi)存空間大于max時,就會分配大塊內(nèi)存。而多個大塊內(nèi)存之間是通過ngx_pool_large_t中的next成員鏈接形成的單鏈表。掛在large成員上。
?
cleanup:ngx_pool_cleanup_t結(jié)構(gòu)體,所有待清理的資源(例如需要關(guān)閉或者刪除的文件)以ngx_pool_cleanup_t對象中的next成員鏈接形成單鏈表。掛在cleanup成員上。
?
log:內(nèi)存池中執(zhí)行時輸出日志的地方。
?
?
(a).ngx_pool_data_t
我們可以在Nginx的源碼的src/core/目錄下的nax_palloc.h頭文件中看到:
?
| 123456 | typedef?struct?{????u_char?????????????? *last;????u_char?????????????? *end;????ngx_pool_t?????????? *next;????ngx_uint_t??????????? failed;} ngx_pool_data_t; |
?
下面將具體講解ngx_pool_data_t結(jié)構(gòu)體中每個成員的含義和用途:
last:指向小塊內(nèi)存中未分配的空閑內(nèi)存的首地址。
?
end:指向當(dāng)前小塊內(nèi)存的尾部。
?
next:同屬于一個內(nèi)存池的多個小塊內(nèi)存之間,通過next成員鏈接形成單鏈表。
?
failed: 每當(dāng)當(dāng)前的小塊內(nèi)存由于空閑部分較少而不能滿足用戶提出的內(nèi)存申請請求時,failed成員就會加1。當(dāng)failed成員大于4后,ngx_pool_t的current成員就會移向下一個小塊內(nèi)存,在以后分配內(nèi)存時,將從下一個小塊內(nèi)存開始遍歷。
?
?
(b).ngx_pool_large_t
我們可以在Nginx的源碼的src/core/nax_palloc.h頭文件中看到:
?
| 123456 | typedef?struct?ngx_pool_large_s? ngx_pool_large_t;struct?ngx_pool_large_s {????ngx_pool_large_t???? *next;????void?????????????????*alloc;}; |
?
下面將具體講解ngx_pool_large_t結(jié)構(gòu)體中每個成員的含義和用途:
next:所有大塊內(nèi)存通過next指針鏈接在一起形成單鏈表。
?
alloc:指向分配的大塊內(nèi)存,后面我們將會看到大塊內(nèi)存底層是通過ngx_alloc分配,ngx_free釋放。釋放完了之后賦值為NULL。
?
?
(c).ngx_pool_cleanup_t
? 我們可以在Nginx的源碼的src/core/nax_palloc.h頭文件中看到:
?
| 1234567 | typedef?struct?ngx_pool_cleanup_s? ngx_pool_cleanup_t;struct?ngx_pool_cleanup_s {????ngx_pool_cleanup_pt?? handler;????void?????????????????*data;????ngx_pool_cleanup_t?? *next;}; |
?
?下面將具體講解ngx_pool_cleanup_t結(jié)構(gòu)體中每個成員的含義和用途:
handler:初始化為NULL,需要設(shè)置的清理函數(shù)。
?
| 1 | typedef?void?(*ngx_pool_cleanup_pt)(void?*data); |
?
根據(jù)上面的聲明,可以看出,ngx_pool_clean_pt是一個函數(shù)指針,有一個通用型的參數(shù)data,返回類型為void。后面我們會看到當(dāng)銷毀內(nèi)存池的時候,底層會遍歷掛在cleanup成員上的單鏈表上的各個節(jié)點(diǎn),調(diào)用各節(jié)點(diǎn)的數(shù)據(jù)清理函數(shù)完成相應(yīng)的清理操作。這是通過回調(diào)函數(shù)實(shí)現(xiàn)的。
?
data:用于向數(shù)據(jù)清理函數(shù)傳遞的參數(shù),指向待清理的數(shù)據(jù)的地址,若沒有則為NULL。我們可以通過ngx_pool_cleanup_add函數(shù)添加數(shù)據(jù)清理函數(shù),當(dāng)其中的參數(shù)size>0時,data不為NULL。
?
next:用于鏈接所有的數(shù)據(jù)清理函數(shù)形成單鏈表。由ngx_pool_cleanup_add函數(shù)設(shè)置next成員,用于將當(dāng)前ngx_pool_cleanup_t(由ngx_pool_cleanup_add函數(shù)返回)添加到cleanup鏈表中。
?
?
?
4.內(nèi)存池基本操作介紹
這一部分主要簡單講解與內(nèi)存池管理有關(guān)的基本操作(共15個)。主要包括四個部分:(a).內(nèi)存池操作 (b).基于內(nèi)存池的分配、釋放操作 (3).隨著內(nèi)存池釋放同步釋放資源的操作 (4).與內(nèi)存池?zé)o關(guān)的分配、釋放操作。在第5和第6節(jié)中,我們會對部分常用內(nèi)存池的操作進(jìn)行代碼上的詳細(xì)介紹。
?
(a).內(nèi)存池操作:
?
| 123 | ngx_pool_t *ngx_create_pool(size_t?size, ngx_log_t *log);void?ngx_destroy_pool(ngx_pool_t *pool);void?ngx_reset_pool(ngx_pool_t *pool); |
?
ngx_create_pool
創(chuàng)建內(nèi)存池,其參數(shù)size為整個內(nèi)存的大小,包括結(jié)構(gòu)管理(ngx_pool_t)和后續(xù)可分配的空閑內(nèi)存。這意味著,size必須大于等于sizeof(ngx_pool_t),通常在32位的系統(tǒng)是是40字節(jié),后面我們介紹源碼時會詳細(xì)的介紹。通常size的默認(rèn)大小為NGX_DEFAULT_POOL_SIZE(#define NGX_DEFAULT_POOL_SIZE??? (16 * 1024)),可以看到為16k。不用擔(dān)心其不夠用,因?yàn)楫?dāng)不夠用時,Nginx會對內(nèi)存池進(jìn)行內(nèi)存空間的擴(kuò)展,也就是申請一個新的內(nèi)存池(鏈表)節(jié)點(diǎn)(程序中成為一個block),然后掛在內(nèi)存池的最后面。
?
ngx_destory_pool
銷毀內(nèi)存池,它會執(zhí)行通過ngx_pool_cleanup_add函數(shù)添加的各種資源清理方法,然后釋放大塊內(nèi)存,最后把整個pool分配的內(nèi)存釋放掉。
?
ngx_reset_pool
重置內(nèi)存池,即將在內(nèi)存池中原有的內(nèi)存釋放后繼續(xù)使用。后面我們會看到,這個方法是把大塊的內(nèi)存釋放給操作系統(tǒng),而小塊的內(nèi)存則在不釋放的情況下復(fù)用。
?
?
(b).基于內(nèi)存池的分配、釋放操作
?
| 12345 | 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_int_t ngx_pfree(ngx_pool_t *pool,?void?*p); |
?
??? ?ngx_palloc
分配地址對齊的內(nèi)存。內(nèi)存對齊可以減少cpu讀取內(nèi)存的次數(shù),代價是存在一些內(nèi)存浪費(fèi)。
?
ngx_pnalloc
同ngx_palloc,區(qū)別是分配內(nèi)存時不考慮對齊。
?
ngx_pcalloc
同ngx_palloc,區(qū)別是分配完對齊的內(nèi)存后,再調(diào)用memset全部初始化為0。
?
ngx_pmemalign
按參數(shù)alignment進(jìn)行地址對齊來分配內(nèi)存。注意,這樣分配的內(nèi)存不管申請的size有多小,都不會使用小塊內(nèi)存,它們直接從進(jìn)程的堆中分配,并掛在大塊內(nèi)存組成的large單鏈表中。
?
ngx_pfree
提前釋放大塊內(nèi)存。由于其實(shí)現(xiàn)是遍歷large單鏈表,尋找ngx_pool_large_t對應(yīng)的alloc成員后調(diào)用ngx_free(alloc),實(shí)際上是直接調(diào)用free(alloc),釋放內(nèi)存給操作系統(tǒng),將ngx_pool_large_t移出鏈表并刪除。效率不高。
?
?
(c).隨著內(nèi)存池釋放同步釋放資源的操作
?
| 1234 | ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p,?size_t?size);void?ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);void?ngx_pool_cleanup_file(void?*data);void?ngx_pool_delete_file(void?*data); |
?
?ngx_pool_cleanup_add
添加一個需要在內(nèi)存釋放時同步釋放的資源。該方法會返回一個ngx_pool_cleanup_t結(jié)構(gòu)體,而我們得到該結(jié)構(gòu)體后需要設(shè)置ngx_pool_cleanup_t的handler成員為釋放資源時執(zhí)行的方法。ngx_pool_clean_add的參數(shù)size,當(dāng)它不為0時,會分配size大小的內(nèi)存,并將ngx_pool_cleanup_t的data成員指向該內(nèi)存,這樣可以利用這段內(nèi)存?zhèn)鬟f參數(shù),供資源清理函數(shù)使用。當(dāng)size為0時,data將為NULL。
?
ngx_pool_run_cleanup_file
在內(nèi)存釋放前,如果需要提前關(guān)閉文件(調(diào)用ngx_pool_cleanup_add添加的文件,同時ngx_pool_cleanup_t的handler成員被設(shè)置為ngx_pool_cleanup_file),則調(diào)用該方法。
?
ngx_pool_cleanup_file
以關(guān)閉文件來釋放資源的方法,可以設(shè)置到ngx_pool_cleanup_t的handler成員。
?
ngx_pool_delete_file
以刪除文件來釋放資源的方法,可以設(shè)置到ngx_pool_cleanup_t的handler成員。
?
?
(d).與內(nèi)存池?zé)o關(guān)的分配、釋放操作
?
| 1 | void?*ngx_alloc(size_t?size, ngx_log_t *log);void?*ngx_calloc(size_t?size, ngx_log_t *log);#define ngx_free??????????free |
這部分的聲明和定義實(shí)際上并不在src/core/ngx_palloc.h中,而是在/src/os/unix/ngx_alloc.h中。
?
ngx_alloc
從操作系統(tǒng)中分配內(nèi)存,通過調(diào)用malloc實(shí)現(xiàn)。
?
ngx_calloc
從操作系統(tǒng)中分配內(nèi)存并全部初始化為0,通過調(diào)用malloc和memset實(shí)現(xiàn)。
?
ngx_free
從上面的宏定義可以看到,其就是free函數(shù),釋放內(nèi)存到操作系統(tǒng)。
?
?
?
5.內(nèi)存池管理源碼詳解
? 本部分的源碼可以在src/core/ngx_palloc.h、src/core/ngx_palloc.c、src/os/unix/ngx_alloc.h和src/os/unix/ngx_alloc.c中找到。內(nèi)存池的管理主要包括內(nèi)存池的創(chuàng)建、銷毀以及重置操作。我們通過對源碼的分析來研究和學(xué)習(xí)Nginx的內(nèi)存管理技術(shù)。
?
(a).內(nèi)存池的創(chuàng)建
創(chuàng)建內(nèi)存池的操作主要由ngx_create_pool()函數(shù)完成,代碼如下:
?
| 1234567891011121314151617181920212223242526 | 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;} |
?
?
在這段代碼中,首先通過ngx_memalign()函數(shù)申請對齊的內(nèi)存,其大小為size個字節(jié)。如果內(nèi)存申請失敗,則返回NULL,否則對ngx_pool_t結(jié)構(gòu)體中的成員進(jìn)行初始化。在進(jìn)行初始化之前,讓我們先討論以下什么是小塊內(nèi)存?
?
?
| 12345 | /*?* NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86.?* On Windows NT it decreases a number of locked pages in a kernel.?*/#define NGX_MAX_ALLOC_FROM_POOL? (ngx_pagesize - 1) |
?
?
? 這是ngx_palloc.h中的一個注釋及宏定義,從中我們可以看到在x86系統(tǒng)4095字節(jié)是一個標(biāo)準(zhǔn)。因?yàn)閚gx_pagesize中存放的是當(dāng)前Nginx服務(wù)器運(yùn)行的系統(tǒng)中一頁內(nèi)存頁的大小,而在x86的系統(tǒng)上就是4KB。由于存在減1的關(guān)系,這意味著在x86系統(tǒng)上,小于等于4095字節(jié)的內(nèi)存被稱為小塊內(nèi)存,而大于4095字節(jié)的內(nèi)存被稱為大塊內(nèi)存。當(dāng)然這并不是絕對的,在上述源碼中,我們看到如果傳遞的參數(shù)size滿足:size - sizeof(ngx_pool_t) < NGX_MAX_ALLOC_FROM_POOL時,其max的值為size(小于NGX_MAX_ALLOC_FROM_POOL),而當(dāng)size不滿足上述不等式時,其值為NGX_MAX_ALLOC_FROM_POOL。也就是說NGX_MAX_ALLOC_FROM_POOL是一個最大的門限,申請的小塊內(nèi)存的大小應(yīng)該不超過其大小。在初始化max之后,我們將last指向分配好的空閑內(nèi)存空間的首地址,end指向內(nèi)存池的尾部。并將next初始化為NULL,failed的值初始化為0。然后再將current指向這塊內(nèi)存池的首地址,large和cleanup也被初始化為NULL,最后返回指向分配好的內(nèi)存空間的首地址。為了更加清晰地展示內(nèi)存池的創(chuàng)建過程,下面將會舉一個例子來說明。但是在這之前,我們先來分析以下ngx_memalign()函數(shù)的實(shí)現(xiàn)源碼。
?
關(guān)于ngx_memalign()的細(xì)節(jié)我們可以在src/os/unix/ngx_alloc.c中看到其源碼,前面部分是聲明,后面是定義。如下所示:
?
| 12345678910111213141516 | /*?* Linux has memalign() or posix_memalign()?* Solaris has memalign()?* FreeBSD 7.0 has posix_memalign(), besides, early version's malloc()?* aligns allocations bigger than page size at the page boundary?*/#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)void?*ngx_memalign(size_t?alignment,?size_t?size, ngx_log_t *log);#else#define ngx_memalign(alignment, size, log)? ngx_alloc(size, log)#endif |
?
?
| 123456789101112131415161718192021222324252627282930313233343536373839404142 | #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;}#endif |
?
我們還需要知道的就是在linux系統(tǒng)下,分配內(nèi)存有三個系統(tǒng)調(diào)用,如果不考慮內(nèi)存對齊,則有malloc();如果考慮內(nèi)存對齊,則有:memalign()和posix_memalign();從ngx_memalign()的具體聲明和實(shí)現(xiàn)中,我們可以看出這其實(shí)一個條件編譯。如果系統(tǒng)定義了NGX_HAVE_POSIX_MEMALIGN,則調(diào)用posix_memalign()申請對齊的內(nèi)存;如果系統(tǒng)定義了NGX_HAVE_MEMALIGN,則調(diào)用memalign()申請對齊的內(nèi)存;并且這兩種內(nèi)存對齊默認(rèn)都是基于16字節(jié)的。否則直接調(diào)用ngx_alloc(),而ngx_alloc()直接調(diào)用malloc()申請不對齊的內(nèi)存。講完了內(nèi)存池中三種申請內(nèi)存的方式之后,我們可以開始講解創(chuàng)建內(nèi)存池的實(shí)例了。
比如說我們需要創(chuàng)建一個大小為1024字節(jié)的內(nèi)存池作為一個分配模塊:
?
| 1 | ngx_pool_t *pool = ngx_create_pool (1024,??log); |
?
為了方便,我們不妨假設(shè)申請的這塊內(nèi)存的起始地址為10。執(zhí)行完創(chuàng)建內(nèi)存池的操作后,內(nèi)存中的分布情況如圖5.1所示:
?
圖5.1 創(chuàng)建內(nèi)存池內(nèi)存片段圖
?
從執(zhí)行結(jié)果可以看出:創(chuàng)建的內(nèi)存池總共占用了1024個字節(jié),起始地址為10,結(jié)束地址為1034。指向內(nèi)存池的指針為pool。last指針為50(10+40),因?yàn)槠鹗嫉刂肥?0,而ngx_pool_t結(jié)構(gòu)體所占用的內(nèi)存空間為40字節(jié),怎么計算得到的呢?其實(shí)很簡單,只需要考慮結(jié)構(gòu)體在內(nèi)存中的對齊問題即可。在x86中(x64中指針在內(nèi)存中占用8字節(jié)而不是4字節(jié))如下所示:
?
?
| 1234567891011121314151617 | typedef?struct?{????u_char?????????????? *last;//4字節(jié)????u_char?????????????? *end;//4字節(jié)????ngx_pool_t?????????? *next;//4字節(jié)????ngx_uint_t??????????? failed;//4字節(jié)} ngx_pool_data_t;struct?ngx_pool_s {????ngx_pool_data_t?????? d;//16字節(jié)????size_t????????????????max;//4字節(jié)????ngx_pool_t?????????? *current;//4字節(jié)????ngx_chain_t????????? *chain;//4字節(jié)????ngx_pool_large_t???? *large;//4字節(jié)????ngx_pool_cleanup_t?? *cleanup;//4字節(jié)????ngx_log_t??????????? *log;//4字節(jié)}; |
?
?
我們可以計算得到,在x86的系統(tǒng)中ngx_pool_t結(jié)構(gòu)體各個成員變量占用的空間為40字節(jié)。因此last的值為50。end的值為10+1024=1034。max的值為1024-40=984。current=10。可以看到:
在物理內(nèi)存中,申請到的內(nèi)存空間被分為了兩部分,前面一部分是ngx_pool_t內(nèi)存管理結(jié)構(gòu)各個成員變量所占用的空間,此處為40字節(jié)。后面部分的984字節(jié)的空閑空間才是我們可以在后續(xù)的程序中真正可以利用的,用來存放數(shù)據(jù)的。以上就是Nging內(nèi)存池創(chuàng)建的主要原理和具體實(shí)現(xiàn)。
?
?
(b).內(nèi)存池的銷毀
銷毀內(nèi)存池的工作主要由ngx_destroy_pool()函數(shù)完成。代碼如下:
?
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 | voidngx_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);????????}????}#if (NGX_DEBUG)????/*?????* we could allocate the pool->log from this pool?????* so we cannot use this log while free()ing the pool?????*/????for?(l = pool->large; l; l = l->next) {????????ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,?"free: %p", l->alloc);????}????for?(p = pool, n = pool->d.next;?/* void */; p = n, n = n->d.next) {????????ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,???????????????????????"free: %p, unused: %uz", p, p->d.end - p->d.last);????????if?(n == NULL) {????????????break;????????}????}#endif????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;????????}????}} |
?
我們可以看到,銷毀內(nèi)存池的主要步驟為:先通過遍歷掛在cleanup上數(shù)據(jù)清理函數(shù)鏈表,通過回調(diào)函數(shù)handler做相應(yīng)的數(shù)據(jù)清理;中間輸出部分只與調(diào)試程序相關(guān),可忽略。然后遍歷掛在large上的大塊內(nèi)存鏈表,調(diào)用ngx_free()函數(shù)釋放節(jié)點(diǎn)所占的大塊內(nèi)存空間;最后,遍歷掛在d->next上的小塊內(nèi)存池鏈表,釋放小塊內(nèi)存池(包括管理結(jié)構(gòu)和數(shù)據(jù)區(qū))占用的空間,在這一步中,我們首先清理了第一塊ngx_pool_t(包括了large、cleanup等成員)代表的小塊內(nèi)存池,然后再清理剩下的其他小塊內(nèi)存池。經(jīng)過以上三個過程,就可以完成數(shù)據(jù)清理、釋放整個內(nèi)存池占用的內(nèi)存空間,并銷毀內(nèi)存池。需要注意的是:由于內(nèi)存池的結(jié)構(gòu),我們必須最后清理管理結(jié)構(gòu)ngx_pool_t(第一塊小塊內(nèi)存池),因?yàn)槿绻惹謇淼谝粔Kngx_pool_t代表的內(nèi)存池的話,我們就找不到掛在large和cleanup上的單鏈表了,因?yàn)槲覀兦謇砹似鋯捂湵淼牡谝粋€節(jié)點(diǎn)。
?
?
(c).內(nèi)存池的重置
重置內(nèi)存池,就是將內(nèi)存池分配到初始分配的狀態(tài)。這是由ngx_reset_pool()函數(shù)完成的。代碼如下:
?
| 123456789101112131415161718192021 | voidngx_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;} |
?
我們可以看到,重置內(nèi)存池十分簡單。首先將掛在large上的大塊內(nèi)存鏈表上的各個節(jié)點(diǎn)釋放掉,并將pool->large賦值為NULL。之后,將所有小塊內(nèi)存池構(gòu)成的單鏈表中的所有節(jié)點(diǎn)結(jié)尾的last指針重置到剛分配時的位置。小塊內(nèi)存中存儲的數(shù)據(jù)并沒有被釋放,其在以后的內(nèi)存池使用的過程中將會被覆蓋更新。這可以減少內(nèi)存分配的次數(shù),提升內(nèi)存重用率。但會浪費(fèi)一些內(nèi)存空間。
?
?
?
6.內(nèi)存池使用源碼詳解
? 內(nèi)存池創(chuàng)建好之后,如何進(jìn)行使用呢?這些內(nèi)存使用完了之后是如何進(jìn)行回收利用的呢?下面的部分將會詳細(xì)的介紹內(nèi)存池的使用。
?
(a).從內(nèi)存池中申請內(nèi)存
在Nginx中,基于內(nèi)存池的申請方法主要有ngx_palloc、ngx_pnalloc、ngx_pcalloc和ngx_pmemalign共4種方法。而不基于內(nèi)存池,直接從操作系統(tǒng)中申請內(nèi)存的主要有ngx_alloc和ngx_calloc共兩種方法。在這一小節(jié)中,我們只講述從內(nèi)存池中申請內(nèi)存相關(guān)的4中方法。而其他的部分將會在后面的小節(jié)進(jìn)行講解。
基于內(nèi)存池的4中內(nèi)存申請方法的區(qū)別在第4章:內(nèi)存池API介紹中已經(jīng)詳細(xì)闡述了。此處不再贅述。
?
(1).ngx_palloc
下面給出源碼:
| 1234567891011 | 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);} |
?
從其實(shí)現(xiàn)中,我們可以看出,ngx_palloc()總共有兩個參數(shù),第一個是在那個內(nèi)存池上申請內(nèi)存(之前我們曾經(jīng)提到過通常為每個Http請求或者連接創(chuàng)建一個內(nèi)存池,此處需要傳遞的參數(shù)就是這些內(nèi)存池對應(yīng)的指針),另一個參數(shù)是size,表示申請內(nèi)存的大小。進(jìn)入函數(shù)后,首先是判斷申請的內(nèi)存大小和max(小塊內(nèi)存標(biāo)準(zhǔn))的關(guān)系,如果size<max,就調(diào)用ngx_palloc_small()函數(shù)申請內(nèi)存。否則調(diào)用ngx_palloc_large()函數(shù)申請內(nèi)存。下面讓我們先來看ngx_palloc_small()函數(shù)的源碼,如下所示:
?
| 123456789101112131415161718192021222324252627 | 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_align_ptr(m, 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);} |
?
從上述源碼中,我們可以看到,該函數(shù)從current指向的內(nèi)存池(小塊內(nèi)存池鏈表)中開始循環(huán)遍歷。在每一次遍歷中,我們首先獲得目前內(nèi)存池中未分配的空閑內(nèi)存的首地址last,并賦值給m,然后由于從ngx_palloc()函數(shù)中傳遞過來的align=1,因此調(diào)用ngx_align_ptr(),這是個什么呢?僅從此我們不能判斷其是函數(shù)還是宏,下面我們給出其源碼,在src/core/ngx_config.h中,如下所示:
?
| 12 | #define ngx_align_ptr(p, a)?????????????????????????????????????????????????? \????(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1)) |
?
可以看出,這是一個宏定義,該操作比較巧妙,用于計算以參數(shù)a對齊后的偏移指針p。實(shí)際上,我們最后分配的內(nèi)存空間就是從對齊后的偏移指針開始的,這可能會浪費(fèi)少數(shù)幾個字節(jié),但卻能提高讀取效率。接著分析ngx_palloc-small()函數(shù)中的源碼,在調(diào)用完宏ngx_align_ptr(m,?NGX_ALIGNMENT)后我們得到了以默認(rèn)參數(shù)16對齊的偏移指針m。此時,我們已經(jīng)擁有了對齊后的空閑內(nèi)存地址空間的首地址m和尾部地址end,我們就可以計算出該塊內(nèi)存池(一個block)剩余的空閑內(nèi)存空間大小:p->d.end - m。那么這個剩余的空閑內(nèi)存空間是否一定能滿足用戶的內(nèi)存申請請求(size個字節(jié))呢?答案是否定的。因此我們需要將從current開始的每一個小塊內(nèi)存池的剩余空閑內(nèi)存空間和size進(jìn)行比較,遍歷鏈表直到找到滿足申請大小(size個字節(jié))的小塊內(nèi)存池。如果小塊內(nèi)存池鏈表上的某塊小塊內(nèi)存能夠滿足需求,那么我們就將從Nginx的內(nèi)存池中劃分出內(nèi)存空間,并更新last的值(將last的值后移size個字節(jié)),然后返回m。
如果遍歷完整個小塊內(nèi)存池都沒有找到滿足申請大小的內(nèi)存,則程序調(diào)用ngx_palloc_block()函數(shù)。其源碼如下所示:
?
| 12345678910111213141516171819202122232425262728293031323334 | 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);????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_align_ptr(m, NGX_ALIGNMENT);????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;} |
?
既然當(dāng)前整個內(nèi)存池都不能滿足用戶內(nèi)存的申請,而我們的操作系統(tǒng)明明還有內(nèi)存可用(資源耗盡的情況除外),那我們總不能拒絕用戶的合理請求吧。ngx_palloc_block()函數(shù)就是應(yīng)對這種情況而出現(xiàn)的。該函數(shù)實(shí)現(xiàn)了對內(nèi)存池的擴(kuò)容。
需要注意的是,由于我們遍歷完了整個鏈表,因此此時的pool指針指向的是內(nèi)存池鏈表的最后一個節(jié)點(diǎn)。所以說在ngx_palloc_block()中計算的是當(dāng)前內(nèi)存池最后一個節(jié)點(diǎn)的大小psize。該大小為需要擴(kuò)展的空間大小。然后,我們調(diào)用前面提到過的ngx_memalgin()函數(shù)申請新的內(nèi)存空間,大小為psize,作為新的小塊內(nèi)存池節(jié)點(diǎn)。之后,我們將這個節(jié)點(diǎn)掛在內(nèi)存池的最后面。具體怎么實(shí)現(xiàn)的呢?我們來詳細(xì)的看一看。
首先將這個新節(jié)點(diǎn)進(jìn)行初始化,包括d->end、d->next、d->failed。然后將指向這塊內(nèi)存的首地址m后移sizeof(ngx_pool_data_t),大家可能還記得我們在創(chuàng)建內(nèi)存池ngx_pool_create()時,內(nèi)存池中空閑地址的首地址是在整個內(nèi)存池的首地址的基礎(chǔ)上后移了sizeof(ngx_pool_t),那么為什么此處創(chuàng)建新的內(nèi)存池節(jié)點(diǎn)只需要后移sizeof(ngx_pool_data_t)呢?在x86系統(tǒng)上,sizeof(ngx_pool_data_t)對應(yīng)16個字節(jié),而sizeof(ngx_pool_t)對應(yīng)40個字節(jié)。其實(shí)大家仔細(xì)想一想,我們創(chuàng)建的內(nèi)存池是小塊內(nèi)存池鏈表的第一個節(jié)點(diǎn),這個節(jié)點(diǎn)中除了包含ngx_pool_data_t結(jié)構(gòu)體之外,還需要包含large指針、cleanup指針等。而小塊內(nèi)存池后面的節(jié)點(diǎn)均沒有必要包含這些成員,因?yàn)槲覀兊膌arge鏈表和cleanup鏈表是直接且僅僅掛在小塊內(nèi)存池鏈表的第一個節(jié)點(diǎn)上的。不需要再掛到后續(xù)的其他小塊內(nèi)存池鏈表的結(jié)構(gòu)上。這么想是不是覺得比較合理呢?答案就是這樣的。但是我們之前的重置內(nèi)存池操作中,并沒有把后續(xù)的從第二個節(jié)點(diǎn)開始的小塊內(nèi)存池鏈表上的空閑內(nèi)存的起始地址初始化為(u_char *)p + sizeof (ngx_pool_data_t),而是將所有節(jié)點(diǎn)(包括第一個)的空閑內(nèi)存地址初始化為(u_char *)p + sizeof (ngx_pool_t)。這樣做會浪費(fèi)一些內(nèi)存空間,但是整個重置內(nèi)存池操作會簡單一點(diǎn)點(diǎn)。因?yàn)椴挥脜^(qū)分第一個節(jié)點(diǎn)和其他節(jié)點(diǎn)。如果區(qū)分的話,我們需要讓第一個節(jié)點(diǎn)的空閑內(nèi)存的起始地址初始化為(u_char *)p + sizeof (ngx_pool_t),將其他節(jié)點(diǎn)的空閑內(nèi)存的起始地址初始化為(u_char *)p + sizeof (ngx_pool_data_t)。我們的Nginx源碼就是這么實(shí)現(xiàn)的。大家知道就行了。因?yàn)檫@并不會影響內(nèi)存池的使用。
在完成對新的內(nèi)存池節(jié)點(diǎn)的初始化之后。我們需要將這個節(jié)點(diǎn)加入到小塊內(nèi)存池鏈表的尾部。具體怎么實(shí)現(xiàn)的呢?
首先我們找到current指針,并根據(jù)這個指針遍歷小塊內(nèi)存池鏈表,在每一個遍歷中,我們將每個節(jié)點(diǎn)的failed成員加1(這是因?yàn)槟銈冞@些節(jié)點(diǎn)不能給我分配內(nèi)存啊,不然也不會調(diào)用我,因此對你們的failed成員統(tǒng)統(tǒng)加1)。并且加1之后,進(jìn)行判斷,如果某個節(jié)點(diǎn)的failed成員的值大于4,那么就將current指向下一個節(jié)點(diǎn)(下次再分配內(nèi)存時將會自動忽略這個節(jié)點(diǎn))。
在遍歷完小塊內(nèi)存池的鏈表后,我們的pool指針已經(jīng)指向了鏈表的最后一個節(jié)點(diǎn),因此在鏈表的尾部插入一個節(jié)點(diǎn)非常簡單,p->d.next = new這個語句就能完成。之后返回這個指向這個新節(jié)點(diǎn)的空閑內(nèi)存空間的首地址。
上述就是ngx_palloc_small()函數(shù)完成的功能,內(nèi)容比較多大家可能都忘了,我們還沒有講解ngx_palloc()函數(shù)的另外一個部分:ngx_palloc_large(),這個函數(shù)是用于當(dāng)用戶申請的內(nèi)存大小大于我們的小塊內(nèi)存標(biāo)準(zhǔn)max的情況。下面我們將會看到,這種情況下,申請的內(nèi)存將被當(dāng)作是大數(shù)據(jù)塊,將會被掛在large鏈表上。先給出ngx_palloc_large()的源碼:
?
?
| 12345678910111213141516171819202122232425262728293031323334353637 | 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;????}????large->alloc = p;????large->next = pool->large;????pool->large = large;????return?p;} |
?
從上面的代碼中我們可以看出我們首先調(diào)用ngx_alloc()函數(shù)申請一塊大小為size的內(nèi)存空間,ngx_alloc()函數(shù)實(shí)際上就是簡單的封裝了以下malloc()函數(shù),后面我們會詳細(xì)的講解。這里知道它是由malloc實(shí)現(xiàn)的就好了。申請完內(nèi)存之后,開始遍歷large鏈表,找到鏈表中alloc為NULL的節(jié)點(diǎn),用alloc指向剛申請到的內(nèi)存空間并返回。注意這段循環(huán)代碼至多執(zhí)行3次,如果在3次后都沒有找到alloc為NULL的節(jié)點(diǎn),就會退出循環(huán),繼續(xù)執(zhí)行后面的代碼。限制代碼執(zhí)行的次數(shù)是為了提升內(nèi)存分配的效率,因?yàn)閘arge鏈表可能會很大。
之后,我們調(diào)用ngx_palloc_small()重新申請一塊大小為sizeof(ngx_pool_large_t)結(jié)構(gòu)體大小的內(nèi)存,建立一個新節(jié)點(diǎn)。最后我們把新建立的節(jié)點(diǎn)插入到large鏈表的頭部,返回申請的內(nèi)存空間的起始地址。為什么是插入頭部而不是插入尾部呢?這里面其實(shí)是有依據(jù)的,因?yàn)槲覀冎盀榱朔乐筶arge過大將遍歷large鏈表的次數(shù)設(shè)置為3,如果插在尾部,那么遍歷鏈表前面的三個節(jié)點(diǎn)就沒有意義了,因?yàn)槊看味伎赡軙闅v不到后面的空閑節(jié)點(diǎn),而導(dǎo)致每次都需要重新建立新節(jié)點(diǎn)。并且插入頭部,從頭部開始遍歷也會使得效率比較高。因?yàn)檫@樣遍歷到空閑的大塊內(nèi)存節(jié)點(diǎn)的概率會高很多。
?
?
(2).ngx_pnalloc
先給出其源碼:
?
| 1234567891011 | void?*ngx_pnalloc(ngx_pool_t *pool,?size_t?size){#if !(NGX_DEBUG_PALLOC)????if?(size <= pool->max) {????????return?ngx_palloc_small(pool, size, 0);????}#endif????return?ngx_palloc_large(pool, size);} |
?
我們可以看到,ngx_pnalloc()和ngx_palloc()非常相似,唯一的區(qū)別就是ngx_pnalloc()中調(diào)用的是ngx_palloc_small(pool, size, 0),而ngx_palloc()中調(diào)用的是ngx_palloc_small(pool, size, 1)。那么實(shí)際上的含義有什么區(qū)別呢?ngx_pnalloc()分配內(nèi)存時不考慮內(nèi)存數(shù)據(jù)對齊,而ngx_palloc()分配內(nèi)存時考慮內(nèi)存數(shù)據(jù)對齊。
?
?
(3).ngx_pcalloc
我們先給出其源碼,如下所示:
?
| 123456789101112 | void?*ngx_pcalloc(ngx_pool_t *pool,?size_t?size){????void?*p;????p = ngx_palloc(pool, size);????if?(p) {????????ngx_memzero(p, size);????}????return?p;} |
?
從其實(shí)現(xiàn)可以看出,ngx_pcalloc()和ngx_palloc()非常的相似,唯一的區(qū)別就是ngx_pcalloc()函數(shù)將剛申請到的內(nèi)存空間全部初始化為0。
?
?
(4).ngx_pmemalign
我們給出其源碼,如下所示:
?
| 1234567891011121314151617181920212223 | void?*ngx_pmemalign(ngx_pool_t *pool,?size_t?size,?size_t?alignment){????void??????????????*p;????ngx_pool_large_t? *large;????p = ngx_memalign(alignment, size, pool->log);????if?(p == NULL) {????????return?NULL;????}????large = ngx_palloc_small(pool,?sizeof(ngx_pool_large_t), 1);????if?(large == NULL) {????????ngx_free(p);????????return?NULL;????}????large->alloc = p;????large->next = pool->large;????pool->large = large;????return?p;} |
?
從其源碼實(shí)現(xiàn)中,我們可以看出ngx_pmemalign()函數(shù)首先調(diào)用ngx_memalign()函數(shù)來申請對齊的內(nèi)存地址空間。然后ngx_palloc_small()函數(shù)來建立一個新的大數(shù)據(jù)塊節(jié)點(diǎn)。并將ngx_pmemalign()函數(shù)申請的內(nèi)存空間直接掛在新建的大塊數(shù)據(jù)節(jié)點(diǎn)的alloc成員上。最后再將新建的大數(shù)據(jù)塊節(jié)點(diǎn)掛在大塊內(nèi)存組成的單鏈表中。
上面就是整個基于內(nèi)存池申請內(nèi)存的4種方法的源碼實(shí)現(xiàn)及其分析。下面我們會繼續(xù)講解釋放內(nèi)存和回收內(nèi)存。
ngx_pfree()函數(shù)用于提前釋放大塊內(nèi)存。
?
?
(b).釋放內(nèi)存
此處我們將介紹基于內(nèi)存池的內(nèi)存釋放操作函數(shù)ngx_pfree(),與內(nèi)存池?zé)o關(guān)的內(nèi)存釋放操作ngx_free()將在后面被講解。
在Nginx中,小塊內(nèi)存并不存在提前釋放這么一說,因?yàn)槠湔加玫膬?nèi)存較少,不太需要被提前釋放。但是對于非常大的內(nèi)存,如果它的生命周期遠(yuǎn)遠(yuǎn)短于所屬的內(nèi)存池,那么在內(nèi)存池銷毀之前提前釋放它就變得有意義了。下面先給出其源碼:
?
| 123456789101112131415161718 | ngx_int_tngx_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;} |
?
從其實(shí)現(xiàn)中可以看出,ngx_pfree()函數(shù)的實(shí)現(xiàn)十分簡單。通過遍歷large單鏈表,找到待釋放的內(nèi)存空間(alloc所指向的內(nèi)存空間),然后調(diào)用ngx_free()函數(shù)釋放內(nèi)存。后面我們會看到ngx_free()函數(shù)是free()函數(shù)的一個簡單封裝。釋放alloc所占用的空間后,將alloc設(shè)置為NULL。我們需要注意的是:ngx_pfree()函數(shù)僅僅釋放了large鏈表上每個節(jié)點(diǎn)的alloc成員所占用的空間,并沒有釋放ngx_pool_large_t結(jié)構(gòu)所占用的內(nèi)存空間。如此實(shí)現(xiàn)的意義在于:下次分配大塊內(nèi)存時,會期望復(fù)用這個ngx_pool_large_t結(jié)構(gòu)體。從這里可以想到,如果large鏈表中的元素很多,那么ngx_pfree()的遍歷耗損的性能是不小的,如果不能確定內(nèi)存確實(shí)非常大,最好不要調(diào)用ngx_pfree。
?
?
(c).隨著內(nèi)存池釋放同步釋放資源的操作
在Nginx服務(wù)器程序中,有些數(shù)據(jù)類型在回收其所占的資源時不能直接通過釋放內(nèi)存空間的方式進(jìn)行,而需要在釋放之前對數(shù)據(jù)進(jìn)行指定的數(shù)據(jù)清理操作。ngx_pool_cleanup_t結(jié)構(gòu)體的函數(shù)指針handler就是這么一個數(shù)據(jù)清理函數(shù),其data成員就指向要清理的數(shù)據(jù)的內(nèi)存地址。我們將要清理的方法和數(shù)據(jù)存放到ngx_pool_cleanup_t結(jié)構(gòu)體中,通過next成員組成內(nèi)存回收鏈表,就可以實(shí)現(xiàn)在釋放內(nèi)存前對數(shù)據(jù)進(jìn)行指定的數(shù)據(jù)清理操作。而與這些操作相關(guān)的方法有:ngx_pool_cleanup_add()、ngx_pool_run_cleanup_file()、ngx_pool_cleanup_file()和ngx_pool_delete_file()共4種。下面我們將分別講解這些操作。
?
(1).ngx_pool_cleanup_add()
這個方法的目的是為了添加一個需要在內(nèi)存池釋放時同步釋放的資源。我們依照慣例還是先給出其源碼,然后對源碼進(jìn)行分析和學(xué)習(xí)。其源碼如下所示:
?
| 1234567891011121314151617181920212223242526272829 | ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p,?size_t?size){????ngx_pool_cleanup_t? *c;????c = ngx_palloc(p,?sizeof(ngx_pool_cleanup_t));????if?(c == NULL) {????????return?NULL;????}????if?(size) {????????c->data = ngx_palloc(p, size);????????if?(c->data == NULL) {????????????return?NULL;????????}????}?else?{????????c->data = NULL;????}????c->handler = NULL;????c->next = p->cleanup;????p->cleanup = c;????ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0,?"add cleanup: %p", c);????return?c;} |
?
從其實(shí)現(xiàn)中我們可以看出,我們首先調(diào)用ngx_palloc()函數(shù)申請cleanup單鏈表中的一個新節(jié)點(diǎn)(指向ngx_pool_cleanup_t結(jié)構(gòu)體的指針),然后根據(jù)參數(shù)size是否為0決定是否需要申請存放目標(biāo)數(shù)據(jù)的內(nèi)存空間。當(dāng)size>0時,調(diào)用ngx_palloc()函數(shù)申請大小為size個字節(jié)的用于存放待清理的數(shù)據(jù)的內(nèi)存空間。這些要清理的數(shù)據(jù)存儲在ngx_pool_cleanup_t結(jié)構(gòu)體的data成員指向的內(nèi)存空間中。這樣可以利用這段內(nèi)存?zhèn)鬟f參數(shù),供清理資源的方法使用。當(dāng)size=0時,data為NULL。最后將新生成的ngx_pool_cleanup_t結(jié)構(gòu)體掛在cleanup單鏈表的頭部。返回一個指向ngx_pool_cleanup_t結(jié)構(gòu)體的指針。而我們得到后需要設(shè)置ngx_pool_cleanup_t的handler成員為釋放資源時執(zhí)行的方法。
返回的指向ngx_pool_cleanup_t結(jié)構(gòu)體的指針具體怎么使用呢?我們對ngx_pool_cleanup_t結(jié)構(gòu)體的data成員指向的內(nèi)存空間填充目標(biāo)數(shù)據(jù)時,將會為handler成員指定相應(yīng)的函數(shù)。
?
?
(2).ngx_pool_run_cleanup_file()
在內(nèi)存池釋放前,如果需要提前關(guān)閉文件,則調(diào)用該方法。下面給出其源碼,如下所示:
?
| 12345678910111213141516171819 | voidngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd){????ngx_pool_cleanup_t?????? *c;????ngx_pool_cleanup_file_t? *cf;????for?(c = p->cleanup; c; c = c->next) {????????if?(c->handler == ngx_pool_cleanup_file) {????????????cf = c->data;????????????if?(cf->fd == fd) {????????????????c->handler(cf);????????????????c->handler = NULL;????????????????return;????????????}????????}????}} |
?
再給出ngx_pool_cleanup_file結(jié)構(gòu)體的聲明和定義(在src/core/ngx_palloc.h頭文件中),如下所示:
?
| 12345 | typedef?struct?{????ngx_fd_t????????????? fd;????u_char?????????????? *name;????ngx_log_t??????????? *log;} ngx_pool_cleanup_file_t; |
?
從上述源碼中,我們可以看出,ngx_pool_run_cleanup_file()通過遍歷cleanup單鏈表,尋找單鏈表上的一個節(jié)點(diǎn),這個節(jié)點(diǎn)滿足handler(函數(shù)指針)等于ngx_pool_cleanup_file(在與函數(shù)名相關(guān)的表達(dá)式中,函數(shù)名會被編譯器隱式轉(zhuǎn)換成函數(shù)指針)。由于ngx_pool_cleanup_t結(jié)構(gòu)體的data成員經(jīng)常會指向ngx_pool_cleanup_file_t(在后面的ngx_pool_cleanup_file()函數(shù)中我們可以看到),我們將這個節(jié)點(diǎn)data指針賦值給cf(ngx_pool_cleanup_t結(jié)構(gòu)指針)。之后如果傳遞過來的參數(shù)fd與cf->fd相同的話(代表我們找到了需要提前關(guān)閉的文件描述符fd),就提前執(zhí)行ngx_pool_cleanup_file(fd),進(jìn)行文件的關(guān)閉操作。
?
?
(3).ngx_pool_cleanup_file()
該方法以關(guān)閉文件的方式來釋放資源,可以被設(shè)置為ngx_pool_cleanup_t的handler成員(函數(shù)指針)。我們給出其源碼實(shí)現(xiàn),如下所示:
?
| 12345678910111213 | voidngx_pool_cleanup_file(void?*data){????ngx_pool_cleanup_file_t? *c = data;????ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0,?"file cleanup: fd:%d",???????????????????c->fd);????if?(ngx_close_file(c->fd) == NGX_FILE_ERROR) {????????ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,??????????????????????ngx_close_file_n?" \"%s\" failed", c->name);????}} |
?
可以看出,ngx_pool_cleanup_t結(jié)構(gòu)的data成員指向ngx_pool_cleanup_file_t結(jié)構(gòu)體(前面講解ngx_pool_run_cleanup_file()提到過)。之后直接調(diào)用ngx_close_file()函數(shù)關(guān)閉對應(yīng)的文件。而ngx_close_file()底層是是通過close()函數(shù)實(shí)現(xiàn)的。
?
?
(4).ngx_pool_delete_file()
以刪除文件來釋放資源的方法,可以設(shè)置到ngx_pool_cleanup_t的handler成員。我們先給出其源碼,如下所示:
?
| 123456789101112131415161718192021222324 | voidngx_pool_delete_file(void?*data){????ngx_pool_cleanup_file_t? *c = data;????ngx_err_t? err;????ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0,?"file cleanup: fd:%d %s",???????????????????c->fd, c->name);????if?(ngx_delete_file(c->name) == NGX_FILE_ERROR) {????????err = ngx_errno;????????if?(err != NGX_ENOENT) {????????????ngx_log_error(NGX_LOG_CRIT, c->log, err,??????????????????????????ngx_delete_file_n?" \"%s\" failed", c->name);????????}????}????if?(ngx_close_file(c->fd) == NGX_FILE_ERROR) {????????ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,??????????????????????ngx_close_file_n?" \"%s\" failed", c->name);????}} |
?
可以看出,ngx_pool_cleanup_t結(jié)構(gòu)的data成員指向ngx_pool_cleanup_file_t結(jié)構(gòu)體,在程序中我們先將傳遞過來的參數(shù)data(待清理的目標(biāo)數(shù)據(jù))賦值給c,然后對c的成員name(文件名稱)調(diào)用ngx_delete_file()函數(shù),完成對文件的刪除操作,之后調(diào)用ngx_close_file()函數(shù)關(guān)閉相應(yīng)的文件流(關(guān)閉這個文件流可以阻止刪除的文件再次被訪問,并且釋放FILE結(jié)構(gòu)使得它可以被做用于其他的文件),這就是我們?yōu)槭裁丛趧h除對應(yīng)的文件后還需要關(guān)閉打開的文件流的原因。
補(bǔ)充一下:ngx_close_file和ngx_delete_file其實(shí)是一個宏定義,我們可以在src/os/unix/ngx_files.h中看到其具體實(shí)現(xiàn),如下所示:
?
| 123456 | #define ngx_close_file?????????? close#define ngx_close_file_n???????? "close()"#define ngx_delete_file(name)??? unlink((const char *) name)#define ngx_delete_file_n??????? "unlink()" |
?
可以看到,ngx_close_file其實(shí)就是close,在Nginx服務(wù)器程序編譯階段僅僅做一個簡單的替換。ngx_delete_file(name)也是一個宏定義,本質(zhì)上為unlink((const char *) name),該函數(shù)會刪除參數(shù)name指定的文件。
?
(d).與內(nèi)存池?zé)o關(guān)的資源分配、釋放操作
? 與內(nèi)存池?zé)o關(guān)的內(nèi)存分配和釋放操作主要有ngx_alloc()、ngx_calloc()和ngx_free()共3中操作方法。下面我們將繼續(xù)講解它們的具體實(shí)現(xiàn)。
?
(1).ngx_alloc()
ngx_alloc()函數(shù)直接從操作系統(tǒng)中申請內(nèi)存,其實(shí)現(xiàn)是對malloc()函數(shù)的一個簡單封裝。我們可以在src/os/unix/ngx_alloc.c中找到其源碼。如下所示:
?
| 123456789101112131415 | void?*ngx_alloc(size_t?size, ngx_log_t *log){????void??*p;????p =?malloc(size);????if?(p == NULL) {????????ngx_log_error(NGX_LOG_EMERG,?log, ngx_errno,??????????????????????"malloc(%uz) failed", size);????}????ngx_log_debug2(NGX_LOG_DEBUG_ALLOC,?log, 0,?"malloc: %p:%uz", p, size);????return?p;} |
?
可以看到,其實(shí)現(xiàn)非常簡單。僅僅是封裝了malloc()函數(shù),并做了一些日志和調(diào)試方面的處理。
?
?
(2).ngx_calloc()
ngx_calloc()和ngx_alloc()非常相似,唯一的區(qū)別是在調(diào)用malloc()函數(shù)申請完內(nèi)存之后,會調(diào)用ngx_memzero()函數(shù)將內(nèi)存全部初始化為0。ngx_memzero()就是memset()函數(shù)。
?
?
(3).ngx_free()
我們可以在src/os/unix/ngx_alloc.h中看到其源碼,如下所示:
?
| 1 | #define ngx_free????????? free |
?
可以看到Nginx程序釋放內(nèi)存的函數(shù)非常簡單,和銷毀內(nèi)存池中用的是同一個(free)。這里需要再次說明的是:對于在不同場合下從內(nèi)存池中申請的內(nèi)存空間的釋放時機(jī)是不一樣的。一般只有大數(shù)據(jù)塊才直接調(diào)用ngx_free()函數(shù)進(jìn)行釋放,其他數(shù)據(jù)空間的釋放都是在內(nèi)存池銷毀的時機(jī)完成的,不需要提前完成。
至此,Nginx與內(nèi)存相關(guān)的操作的源碼實(shí)現(xiàn)已基本講完了。大家如果想進(jìn)一步研究和學(xué)習(xí)Nginx內(nèi)存管理機(jī)制,可以從官方下載Nginx源碼,從源碼中去發(fā)現(xiàn)Nginx降低系統(tǒng)內(nèi)存開銷的方法。
?
?
?
7.小結(jié)
所有的講解都講述完了,我們來進(jìn)行總結(jié)一下。在第1節(jié)中,我們介紹了Nginx的內(nèi)存管理機(jī)制-內(nèi)存池的基本原理和使用內(nèi)存池管理Nginx服務(wù)器程序帶來的好處。為了方便大家對內(nèi)存池結(jié)構(gòu)的理解,我們在第2節(jié)中特意給出了ngx_pool_t內(nèi)存池的示意圖2.1,并簡單的闡述了這個圖的具體含義。在此基礎(chǔ)上,我們繼續(xù)在第3節(jié)中講述了與內(nèi)存池相關(guān)的重要的數(shù)據(jù)結(jié)構(gòu),主要包括ngx_pool_t、ngx_pool_data_t、ngx_pool_large_t和ngx_pool_cleanup_t。然后為了給大家一個內(nèi)存池操作方法的宏觀介紹,我們在第4節(jié)講述了內(nèi)存的主要操作方法(共15個分成4類)。之后在第5節(jié)中我們詳細(xì)介紹了內(nèi)存池的管理,主要包括內(nèi)存池的創(chuàng)建、銷毀和重置。在第6節(jié)中我們詳細(xì)介紹了內(nèi)存池的使用,主要包括從內(nèi)存池中如何申請內(nèi)存、釋放內(nèi)存和回收內(nèi)存。這兩個小結(jié)是整個Nginx內(nèi)存管理的精華部分,我們在這部分中詳細(xì)的分析Nginx的源碼實(shí)現(xiàn),從源碼的角度去講解Nginx內(nèi)存管理用到的技術(shù),方便我們在以后的程序設(shè)計中可以借鑒和學(xué)習(xí)。最后,希望這篇文章能真正幫助到大家學(xué)習(xí)Nginx。
總結(jié)
以上是生活随笔為你收集整理的Nginx内存管理详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux下 /dev/null 21
- 下一篇: Nginx虚拟目录alias和root目