作者:CppExplore 網(wǎng)址:http://www.shnenglu.com/CppExplore/
服務(wù)器設(shè)計(jì)人員在一段時(shí)間的摸索后,都會(huì)發(fā)現(xiàn):服務(wù)器性能的關(guān)鍵在于內(nèi)存。從收包到解析,到消息內(nèi)存的申請(qǐng),到session結(jié)構(gòu)內(nèi)存的申請(qǐng)都要小心處理,盡量減少內(nèi)存數(shù)據(jù)copy,減少內(nèi)存動(dòng)態(tài)申請(qǐng),減少內(nèi)存檢索。為達(dá)到這個(gè)目的,不同的地方有不同的方法,比如常見的包解析,使用緩沖區(qū)偏移以及長度來標(biāo)識(shí)包內(nèi)字段信息;內(nèi)存使用量固定的系統(tǒng),系統(tǒng)啟動(dòng)就申請(qǐng)好所有需要的內(nèi)存,初始化好,等待使用的時(shí)候直接使用;基于license控制的系統(tǒng),根據(jù)license的數(shù)量,一次性申請(qǐng)固定數(shù)量內(nèi)存等......。本文不再總結(jié)這些特性方案,重點(diǎn)說下常見的通用的內(nèi)存池緩存技術(shù)。
內(nèi)存池可有效降低動(dòng)態(tài)申請(qǐng)內(nèi)存的次數(shù),減少與內(nèi)核態(tài)的交互,提升系統(tǒng)性能,減少內(nèi)存碎片,增加內(nèi)存空間使用率,避免內(nèi)存泄漏的可能性,這么多的優(yōu)點(diǎn),沒有理由不在系統(tǒng)中使用該技術(shù)。
為了給內(nèi)存池技術(shù)尋找基石,先從低層的內(nèi)存管理看起。
硬件層略掉不談,可回顧《操作系統(tǒng)》。
一、linux內(nèi)存管理策略
linux低層采用三層結(jié)構(gòu),實(shí)際使用中可以方便映射到兩層或者三層結(jié)構(gòu),以適用不同的硬件結(jié)構(gòu)。最下層的申請(qǐng)內(nèi)存函數(shù)get_free_page。之上有三種類型的內(nèi)存分配函數(shù)
(1)kmalloc類型。內(nèi)核進(jìn)程使用,基于slab技術(shù),用于管理小于內(nèi)存頁的內(nèi)存申請(qǐng)。思想出發(fā)點(diǎn)和應(yīng)用層面的內(nèi)存緩沖池同出一轍。但它針對(duì)內(nèi)核結(jié)構(gòu),特別處理,應(yīng)用場(chǎng)景固定,不考慮釋放。不再深入探討。
(2)vmalloc類型。內(nèi)核進(jìn)程使用。用于申請(qǐng)不連續(xù)內(nèi)存。
(3)brk/mmap類型。用戶進(jìn)程使用。malloc/free實(shí)現(xiàn)的基礎(chǔ)。
有關(guān)詳細(xì)內(nèi)容,推薦http://www.kerneltravel.net/journal/v/mem.htm。http://www.kerneltravel.net上有不少內(nèi)核相關(guān)知識(shí)。
二、malloc系統(tǒng)的內(nèi)存管理策略
malloc系統(tǒng)有自己的內(nèi)存池管理策略,malloc的時(shí)候,檢測(cè)池中是否有足夠內(nèi)存,有則直接分配,無則從內(nèi)存中調(diào)用brk/mmap函數(shù)分配,一般小于等于128k(可設(shè)置)的內(nèi)存,使用brk函數(shù),此時(shí)堆向上(有人有的硬件或系統(tǒng)向下)增長,大于128k的內(nèi)存使用mmap函數(shù)申請(qǐng),此時(shí)堆的位置任意,無固定增長方向。free的時(shí)候,檢測(cè)標(biāo)記是否是mmap申請(qǐng),是則調(diào)用unmmap歸還給操作系統(tǒng),非則檢測(cè)堆頂是否有大于128k的空間,有則通過brk歸還給操作系統(tǒng),無則標(biāo)記未使用,仍在glibc的管理下。glibc為申請(qǐng)的內(nèi)存存儲(chǔ)多余的結(jié)構(gòu)用于管理,因此即使是malloc(0),也會(huì)申請(qǐng)出內(nèi)存(一般16字節(jié),依賴于malloc的實(shí)現(xiàn)方式),在應(yīng)用程序?qū)用妫琺alloc(0)申請(qǐng)出的內(nèi)存大小是0,因?yàn)閙alloc返回的時(shí)候在實(shí)際的內(nèi)存地址上加了16個(gè)字節(jié)偏移,而c99標(biāo)準(zhǔn)則規(guī)定malloc(0)的返回行為未定義。除了內(nèi)存塊頭域,malloc系統(tǒng)還有紅黑樹結(jié)構(gòu)保存內(nèi)存塊信息,不同的實(shí)現(xiàn)又有不同的分配策略。頻繁直接調(diào)用malloc,會(huì)增加內(nèi)存碎片,增加和內(nèi)核態(tài)交互的可能性,降低系統(tǒng)性能。linux下的glibc多為Doug Lea實(shí)現(xiàn),有興趣的可以去baidu、google。
三、應(yīng)用層面的內(nèi)存池管理
跳過malloc,直接基于brk/mmap實(shí)現(xiàn)內(nèi)存池,原理上是可行的,但實(shí)際中這種實(shí)現(xiàn)要追逐內(nèi)核函數(shù)的升級(jí),增加了維護(hù)成本,另增加了移植性的困難,據(jù)說squid的內(nèi)存池是基于brk的,本人尚未閱讀squid源碼(了解磁盤緩存的最佳代碼,以后再詳細(xì)閱讀),不敢妄言。本文后面的討論的內(nèi)存池都是基于malloc(或者new)實(shí)現(xiàn)。我們可以將內(nèi)存池的實(shí)現(xiàn)分兩個(gè)類別來討論。
1、不定長內(nèi)存池。典型的實(shí)現(xiàn)有apr_pool、obstack。優(yōu)點(diǎn)是不需要為不同的數(shù)據(jù)類型創(chuàng)建不同的內(nèi)存池,缺點(diǎn)是造成分配出的內(nèi)存不能回收到池中。這是由于這種方案以session為粒度,以業(yè)務(wù)處理的層次性為設(shè)計(jì)基礎(chǔ)。
(1)apr_pool。apr全稱Apache portable Run-time libraries,Apache可移植運(yùn)行庫。可以從http://www.apache.org/網(wǎng)站上下載到。apache以高性能、穩(wěn)定性著稱,它所有模塊的內(nèi)存申請(qǐng)都由內(nèi)存池模塊apr_pool實(shí)現(xiàn)。有關(guān)apr_pool結(jié)構(gòu)、實(shí)現(xiàn)的原理,http://blog.csdn.net/tingya/(apache源碼分析類別中的apache內(nèi)存池實(shí)現(xiàn)內(nèi)幕系列)已經(jīng)有了詳細(xì)的講解,結(jié)合自己下載的源碼,已經(jīng)足夠了。本人并不推薦去看這個(gè)blog和去看詳細(xì)的代碼數(shù)據(jù)結(jié)構(gòu)以及邏輯。明白apr_pool實(shí)現(xiàn)的原理,知道如何使用就足夠了。深入細(xì)節(jié)只能是浪費(fèi)腦細(xì)胞,當(dāng)然完全憑個(gè)人興趣愛好了。
這里舉例說下簡單的使用:







































apr_pool中主要有3個(gè)對(duì)象,allocator、pool、block。pool從allocator申請(qǐng)內(nèi)存,pool銷毀的時(shí)候把內(nèi)存歸還allocator,allocator銷毀的時(shí)候把內(nèi)存歸還給系統(tǒng),allocator有一個(gè)owner成員,是一個(gè)pool對(duì)象,allocator的owner銷毀的時(shí)候,allocator被銷毀。在apr_pool中并無block這個(gè)單詞出現(xiàn),這里大家可以把從pool從申請(qǐng)的內(nèi)存稱為block,使用apr_palloc申請(qǐng)block,block只能被申請(qǐng),沒有釋放函數(shù),只能等pool銷毀的時(shí)候才能把內(nèi)存歸還給allocator,用于allocator以后的pool再次申請(qǐng)。
我給的例子中并沒有出現(xiàn)創(chuàng)建allocator的函數(shù),而是使用的默認(rèn)全局allocator。apr_pool提供了一系列函數(shù)操作allocator,可以自己調(diào)用這些函數(shù):
apr_allocator_create apr_allocator_destroy apr_allocator_alloc apr_allocator_free |
創(chuàng)建銷毀allocator |
apr_allocator_owner_set apr_allocator_owner_get |
設(shè)置獲取owner |
apr_allocator_max_free_set | 設(shè)置pool銷毀的時(shí)候內(nèi)存是否直接歸還到操作系統(tǒng)的閾值 |
apr_allocator_mutex_set apr_allocator_mutex_get |
設(shè)置獲取mutex,用于多線程 |
另外還有設(shè)置清理函數(shù)啊等等,不說了。自己去看include里的頭文件好了:apr_pool.h和apr_allocator.h兩個(gè)。源碼.c文件里,APR_DECLARE宏聲明的函數(shù)即是暴露給外部使用的函數(shù)。大家也可以仿造Loki(后文將介紹Loki)寫個(gè)頂層類重載operator new操作子,其中調(diào)用apr_palloc,使用到的數(shù)據(jù)結(jié)構(gòu)繼承該類,則自動(dòng)從pool中申請(qǐng)內(nèi)存,如要完善的地方很多,自行去研究吧。
可以看出來apr_pool的一個(gè)大缺點(diǎn)就是從池中申請(qǐng)的內(nèi)存不能歸還給內(nèi)存池,只能等pool銷毀的時(shí)候才能歸還。為了彌補(bǔ)這個(gè)缺點(diǎn),apr_pool的實(shí)際使用中,可以申請(qǐng)擁有不同生命周期的內(nèi)存池(類似與上面的例子程序中不同的大括號(hào)代表不同的生命周期,實(shí)際中,盡可以把大括號(hào)中的內(nèi)容想象成不同的線程中的......),以便盡可能快的回收不再使用的內(nèi)存。實(shí)際中apache也是這么做的。因此apr_pool比較適合用于內(nèi)存使用的生命期有明顯層次的情況。
至于擔(dān)心allocator中的內(nèi)存一旦申請(qǐng)就再也不歸還給操作系統(tǒng)(當(dāng)然最后進(jìn)程退出的時(shí)候你可以調(diào)用銷毀allocator歸還,實(shí)際中網(wǎng)絡(luò)服務(wù)程序都是一直運(yùn)行的,找不到銷毀的時(shí)機(jī))的問題,就是杞人憂天了,如果在某一時(shí)刻,系統(tǒng)占用的內(nèi)存達(dá)到頂峰,意味著以后還會(huì)有這種情況。是否能接受這個(gè)解釋,就看個(gè)人的看法和系統(tǒng)的業(yè)務(wù)需求了,不能接受,就使用其它的內(nèi)存池。個(gè)人覺得apr_pool還是很不錯(cuò)的,很多服務(wù)系統(tǒng)的應(yīng)用場(chǎng)景都適用。
(2)obstack。glibc自帶的內(nèi)存池。原理與apr_pool相同。詳細(xì)使用文檔可以參閱
http://www.gnu.org/software/libc/manual/html_node/Obstacks.html。推薦apr_pool,這個(gè)就不再多說了。
(3)AutoFreeAlloc。許式偉的專欄http://blog.csdn.net/xushiweizh/category/265099.aspx。
這個(gè)內(nèi)存池我不看好。這個(gè)也屬于一個(gè)變長的內(nèi)存池,內(nèi)存申請(qǐng)類似與apr_pool的pool/block層面,一次申請(qǐng)大內(nèi)存作為pool,用于block的申請(qǐng),同樣block不回收,等pool銷毀的時(shí)候直接歸還給操作系統(tǒng)。這個(gè)內(nèi)存池的方案,有apr_pool中block不能回收到pool的缺點(diǎn),沒有pool回收到allocator,以供下次繼續(xù)使用的優(yōu)點(diǎn),不支持多線程。適合于單線程,集中使用內(nèi)存的場(chǎng)景,意義不是很大。