內存池的設計

一【內存池概述】

內存池是一種存在於進程中,對程序運行時動態分配的內存進行管理的機制。它主要有三個功能:1減少內存碎片;2防止內存泄露;3減少因頻繁請求內存動態分配而造成系統調用過於頻繁。

第一點:內存碎片分爲內部碎片和外部碎片。內部碎片:就是系統給你分配了一個內存塊,你只使用了其中一部分,那剩餘的一部分就是內部碎片了。而外部碎片:就是系統內存空間裏面夾雜在兩個已分配內存塊之間的空閒內存。一般情況下,我們所說的內存碎片就是指外部碎片,因爲外部碎片要比內部碎片造成更大的危害。內部碎片會伴隨着內存的釋放而消失,但外部碎片則會一直存在於進程的生命期裏面,等到系統裏面的外部碎片多到一定程度時,即使系統空閒內存的總大小大於你請求的內存大小,但是由於找不到連續的合適的地址空間,請求依然失敗,進程也因此崩潰。外部碎片的問題很難完全解決,最常用的辦法就是按照某種規律來分配內存大小,比如:按照2的n次方或者4k爲單位分配,這樣就提高了每一個內存塊的重複使用的可能性,以此來降低碎片的危害,一般的內存池中就是使用該方法的。不過,在linux內核裏面則使用了夥伴算法(Buddy System)來解決碎片問題,該算法將一塊連續的內存空間劃分爲大小爲2的n次方的內存塊,然後每一個內存塊都與地址相鄰且大小一樣的內存塊結爲夥伴。當兩個夥伴塊都處於空閒狀態時,系統就會將這兩個內存塊合併成一個新的內存塊,然後系統又會檢查這個新的內存塊的夥伴塊是否處於一個空閒狀態,如果是空閒狀態,則繼續合併,直到最終塊的夥伴處於一個非空閒狀態,或者所有內存塊都被合併。

第二點:內存泄露問題一直以來是C++/C程序員頭疼的問題,以前出現這個問題時很難調試,不過現在有了valgrind之類的工具要好點。內存池則通過對動態分配內存階段性的釋放(比如:層次結構的內存池會在某個會話階段終止時或者某個連接結束時釋放內存子池),在一定程度上避免了該問題的發生。爲什麼說是“在一定程度上”,那是因爲如果對於一些內存的動態分配(比如:進程運行初期)或者沒有具備階段性釋放內存機制的內存池(非層次結構的內存池),是無法判斷該不該釋放該內存塊的。

第三點:我們知道在調用malloc庫函數時,先按照first-fit策略查找已分配的內存塊中有沒有合適的內存塊,如果找到,則將該內存塊標記爲已分配,並將地址返回。可是,當運氣不好,沒找到的時候,就要通過brk系統調用來擴展段地址獲取內存塊。這兩個步驟都需要一定的開銷,尤其是brk調用,涉及到用戶態與內核態的切換,開銷要更大一些。

如果你是開發一個用戶交互式的桌面應用,一般來說是用不到內存池的,因爲這種程序運行的時間相對不長,就算內存那塊兒出了問題導致進程崩潰,重啓下程序就可以了。但是,你要是開發一個後臺服務系統,那就要考慮通過使用內存池來提高系統的穩定性了。

二【內存池的設計】

內存池的設計差不多都是通過一組空閒列表來實現的,每組列表維護一系列大小相同的空閒塊。


如STL中的pool_allocator。STL中的pool_allocator有個特點:它的每個內存塊的管理信息(在空閒塊列表中指向下一個空閒塊的指針)直接放在所分配的內存塊中,當該內存塊被分配出去之後整個內存塊都可以被用戶使用。該機制是通過聯合體來實現的。

    typedef union obj {   
        union obj *free_list_link;   
        char client_data[1];   
    }obj;  


其它的一些內存池則是將內存塊分配兩部分:管理區和空閒區,管理區的大小是固定的,用來記錄空閒區的大小、是否被使用、指向下一個空閒塊的指針等信息,空閒區則留給用戶使用。

每個空閒列表管理2的(2+i)次方大小的空閒規則塊。當用戶申請一塊空閒塊時,會根據用戶申請的大小選擇一塊合適的空閒塊,計算公式爲:(size+(boundary-1))&~((boundary-1))。當找不到合適的內存塊,就使用malloc和new向系統申請內存空間,這時爲了減少系統調用次數,一次會多申請一些內存塊,其中一塊直接返回給申請者,剩下的就填充空閒列表。

一些網絡服務器(apache 、proftp)裏面使用了層次化的內存池,它將內存池分爲:分配子(allocator)和內存池(pool)兩部分。分配子類似於上文提到的空閒列表,只不過它的0號的空閒塊列表比較特殊,放的是比最大規則塊還要大的內存塊,這樣就提高了內存分配的靈活性,如下圖。

當用戶申請一個較大的內存塊,但是並沒有超出內存池大小的上限時,就可以將該塊放入0號列表管理。而內存池則是一個簡單的空閒列表,空閒列表中的內存塊都是由分配子分配的,而且是按照從大到小的順序鏈接的,如圖。

另外,內存池可以按照樹形結構將他們組織在一起,父子內存池可以共享同一個分配子。

爲什麼要將內存池以樹形結構組織呢?其實,這樣做說到底就是爲網絡服務器的業務邏輯服務的。我們知道一般的網絡服務器器可以提供多個虛擬的服務器,我們爲每個虛擬服務器分配一個內存池,然後又爲該服務器上的每個連接分配一個內存池,最後爲該連接中的每一個會話分配一個內存池。因爲服務器、連接、會話之間的關係是樹形結構的,所以這種層次化的內存池可以很方便的與這種業務結構對應起來,當用戶關閉某個服務器、或者斷開某個網絡連接時,只要調用父節點的內存池釋放函數,就可以將它和所屬的子池一併釋放,非常方便。

當然,如果你整個系統裏面只使用一個內存池對全局的內存進行管理也是可以的,但是這存在一些缺點:1.當你在某個會話裏面申請了一個內存塊,最後缺因爲出現了異常導致流程跳轉,使得這個內存塊沒有歸還內存池,這樣這個內存塊在進程生命期內就無法再次被利用;而層次化的內存池只要等到該會話生命期結束,會話的子池生命期結束時,它所有的內存就會被釋放(如果會話子池的分配子用的是父池,那麼內存就歸還給父池,如果分配子用的是自己的,那麼就連着分配子一起還給系統)。2.當網絡服務器運行一段時間之後,就有可能產生大量的內存塊,如果這麼多內存塊都由一個內存池進行管理,就會變的很低效。


【參考】

《stl源碼剖析》http://ishare.iask.sina.com.cn/f/10466019.html

http://blog.csdn.net/tingya/article/details/547322

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章