分配記憶體時如何減少記憶體碎片(二)
記憶體碎片的產生:
記憶體分配有靜態分配和動態分配兩種
靜態分配在程式編譯連結時分配的大小和使用壽命就已經確定,而應用上要求作業系統可以提供給程序執行時申請和釋放任意大小記憶體的功能,這就是記憶體的動態分配。
因此動態分配將不可避免會產生記憶體碎片的問題,那麼什麼是記憶體碎片?記憶體碎片即“碎片的記憶體”描述一個系統中所有不可用的空閒記憶體,這些碎片之所以不能被使用,是因為負責動態分配記憶體的分配演算法使得這些空閒的記憶體無法使用,這一問題的發生,原因在於這些空閒記憶體以小且不連續方式出現在不同的位置。因此這個問題的或大或小取決於記憶體管理演算法的實現上。
為什麼會產生這些小且不連續的空閒記憶體碎片呢?
實際上這些空閒記憶體碎片存在的方式有兩種:a.內部碎片 b.外部碎片 。
內部碎片的產生:因為所有的記憶體分配必須起始於可被 4、8 或 16 整除(視處理器體系結構而定)的地址或者因為MMU的分頁機制的限制,決定記憶體分配演算法僅能把預定大小的記憶體塊分配給客戶。假設當某個客戶請求一個 43 位元組的記憶體塊時,因為沒有適合大小的記憶體,所以它可能會獲得 44位元組、48位元組等稍大一點的位元組,因此由所需大小四捨五入而產生的多餘空間就叫內部碎片。
外部碎片的產生: 頻繁的分配與回收物理頁面會導致大量的、連續且小的頁面塊夾雜在已分配的頁面中間,就會產生外部碎片。
如何解決記憶體碎片:
採用Slab Allocation機制:整理記憶體以便重複使用
最近的memcached預設情況下采用了名為Slab Allocator的機制分配、管理記憶體。在該機制出現以前,記憶體的分配是通過對所有記錄簡單地進行malloc和free來進行的。但是,這種方式會導致記憶體碎片,加重作業系統記憶體管理器的負擔,最壞的情況下,會導致作業系統比memcached程序本身還慢。Slab Allocator就是為解決該問題而誕生的。
下面來看看Slab Allocator的原理。下面是memcached文件中的slab allocator的目標:he primary goal of the slabs subsystem in memcached was to eliminate memory fragmentation issuestotally by using fixedsizememory chunks coming from a few predetermined size classes.
也就是說,Slab Allocator的基本原理是按照預先規定的大小,將分配的記憶體分割成特定長度的塊,以完全解決記憶體碎片問題。Slab Allocation的原理相當簡單。將分配的記憶體分割成各種尺寸的塊(chunk),並把尺寸相同的塊分成組(chunk的集合)(圖2.1)。
slab allocator還有重複使用已分配的記憶體的目的。也就是說,分配到的記憶體不會釋放,而是重複利用
Slab Allocation的主要術語
Page
分配給Slab的記憶體空間,預設是1MB。分配給Slab之後根據slab的大小切分成chunk。
Chunk
用於快取記錄的記憶體空間。
Slab Class
特定大小的chunk的組。
在Slab中快取記錄的原理
下面說明memcached如何針對客戶端傳送的資料選擇slab並快取到chunk中。memcached根據收到的資料的大小,選擇最適合資料大小的slab(圖2.2)。memcached中儲存著slab內空閒chunk的列表,根據該列表選擇chunk,然後將資料緩存於其中。
圖2.2:選擇儲存記錄的組的方法
實際上,Slab Allocator也是有利也有弊。下面介紹一下它的缺點。
Slab Allocator的缺點
Slab Allocator解決了當初的記憶體碎片問題,但新的機制也給memcached帶來了新的問題。這個問題就是,由於分配的是特定長度的記憶體,因此無法有效利用分配的記憶體。例如,將100位元組的資料快取到128位元組的chunk中,剩餘的28位元組就浪費了
對於該問題目前還沒有完美的解決方案,但在文件中記載了比較有效的解決方案。
The most efficient way to reduce the waste is to use a list of size classes that closely matches (if that's at all
possible) common sizes of objects that the clients of this particular installation of memcached are likely to
store.
就是說,如果預先知道客戶端傳送的資料的公用大小,或者僅快取大小相同的資料的情況下,只要使用適合資料大小的組的列表,就可以減少浪費。但是很遺憾,現在還不能進行任何調優,只能期待以後的版本了。但是,我們可以調節slab class的大小的差別
最佳適合與最差適合分配程式
最佳適合演算法在功能上與最先適合演算法類似,不同之處是,系統在分配一個記憶體塊時,要搜尋整個自由表,尋找最接近請求儲存量的記憶體塊。這種搜尋所花的時間要比最先適合演算法長得多,但不存在分配大小記憶體塊所需時間的差異。最佳適合演算法產生的記憶體碎片要比最先適合演算法多,因為將小而不能使用的碎片放在自由表開頭部分的排序趨勢更為強烈。由於這一消極因素,最佳適合演算法幾乎從來沒有人採用過。
最差適合演算法也很少採用。最差適合演算法的功能與最佳適合演算法相同,不同之處是,當分配一個記憶體塊時,系統在整個自由表中搜索與請求儲存量不匹配的記憶體快。這種方法比最佳適合演算法速度快,因為它產生微小而又不能使用的記憶體碎片的傾向較弱。始終選擇最大空閒記憶體塊,再將其分為小記憶體塊,這樣就能提高剩餘部分大得足以供系統使用的概率。
夥伴(buddy)分配程式與本文描述的其它分配程式不同,它不能根據需要從被管理記憶體的開頭部分建立新記憶體。它有明確的共性,就是各個記憶體塊可分可合,但不是任意的分與合。每個塊都有個朋友,或叫“夥伴”,既可與之分開,又可與之結合。夥伴分配程式把記憶體塊存放在比連結表更先進的資料結構中。這些結構常常是桶型、樹型和堆型的組合或變種。一般來說,夥伴分配程式的工作方式是難以描述的,因為這種技術隨所選資料結構的不同而各異。由於有各種各樣的具有已知特性的資料結構可供使用,所以夥伴分配程式得到廣泛應用。有些夥伴分配程式甚至用在原始碼中。夥伴分配程式編寫起來常常很複雜,其效能可能各不相同。夥伴分配程式通常在某種程度上限制記憶體碎片。
固定儲存量分配程式有點像最先空閒演算法。通常有一個以上的自由表,而且更重要的是,同一自由表中的所有記憶體塊的儲存量都相同。至少有四個指標:MSTART指向被管理記憶體的起點,MEND 指向被管理記憶體的末端,MBREAK 指向 MSTART 與 MEND 之間已用記憶體的末端,而 PFREE[n]則是指向任何空閒記憶體塊的一排指標。在開始時,PFREE 為 NULL,MBREAK 指標為MSTART。當一個分配請求到來時,系統將請求的儲存量增加到可用儲存量之一。然後,系統檢查 PFREE[ 增大後的儲存量 ] 空閒記憶體塊。因為PFREE[
增大後的儲存量 ] 為 NULL,一個具有該儲存量加上一個管理標題的記憶體塊就脫離 MBREAK,MBREAK 被更新。
這些步驟反覆進行,直至系統使一個記憶體塊空閒為止,此時管理標題包含有該記憶體塊的儲存量。當有一記憶體塊空閒時,PFREE[ 相應儲存量 ]通過標題的連結表插入項更新為指向該記憶體塊,而該記憶體塊本身則用一個指向 PFREE[ 相應儲存量 ]以前內容的指標來更新,以建立一個連結表。下一次分配請求到來時,系統將 PFREE[ 增大的請求儲存量 ]連結表的第一個記憶體塊送給系統。沒有理由搜尋連結表,因為所有連結的記憶體塊的儲存量都是相同的。
固定儲存量分配程式很容易實現,而且便於計算記憶體碎片,至少在塊儲存量的數量較少時是這樣。但這種分配程式的侷限性在於要有一個它可以分配的最大儲存量。固定儲存量分配程式速度快,並可在任何狀況下保持速度。這些分配程式可能會產生大量的內部記憶體碎片,但對某些系統而言,它們的優點會超過缺點。
減少記憶體碎片
記憶體碎片是因為在分配一個記憶體塊後,使之空閒,但不將空閒記憶體歸還給最大記憶體塊而產生的。最後這一步很關鍵。如果記憶體分配程式是有效的,就不能阻止系統分配記憶體塊並使之空閒。即使一個記憶體分配程式不能保證返回的記憶體能與最大記憶體塊相連線(這種方法可以徹底避免記憶體碎片問題),但你可以設法控制並限制記憶體碎片。所有這些作法涉及到記憶體塊的分割。每當系統減少被分割記憶體塊的數量,確保被分割記憶體塊儘可能大時,你就會有所改進。
這樣做的目的是儘可能多次反覆使用記憶體塊,而不要每次都對記憶體塊進行分割,以正好符合請求的儲存量。分割記憶體塊會產生大量的小記憶體碎片,猶如一堆散沙。以後很難把這些散沙與其餘記憶體結合起來。比較好的辦法是讓每個記憶體塊中都留有一些未用的位元組。留有多少位元組應看系統要在多大
程度上避免記憶體碎片。對小型系統來說,增加幾個位元組的內部碎片是朝正確方向邁出的一步。當系統請求1位元組記憶體時,你分配的儲存量取決於系統的工作狀態。
如果系統分配的記憶體儲存量的主要部分是 1 ~ 16 位元組,則為小記憶體也分配 16位元組是明智的。只要限制可以分配的最大記憶體塊,你就能夠獲得較大的節約效果。但是,這種方法的缺點是,系統會不斷地嘗試分配大於極限的記憶體塊,這使系統可能會停止工作。減少最大和最小記憶體塊儲存量之間記憶體儲存量的數量也是有用的。採用按對數增大的記憶體塊儲存量可以避免大量的碎片。例如,每個儲存量可能都比前一個儲存量大20%。在嵌入式系統中採用“一種儲存量符合所有需要”對於嵌入式系統中的記憶體分配程式來說可能是不切實際的。這種方法從內部碎片來看是代價極高的,但系統可以徹底避免外部碎片,達到支援的最大儲存量。
將相鄰空閒記憶體塊連線起來是一種可以顯著減少記憶體碎片的技術。如果沒有這一方法,某些分配演算法(如最先適合演算法)將根本無法工作。然而,效果是有限的,將鄰近記憶體塊連線起來只能緩解由於分配演算法引起的問題,而無法解決根本問題。而且,當記憶體塊儲存量有限時,相鄰記憶體塊連線可能很難實現。
有些記憶體分配器很先進,可以在執行時收集有關某個系統的分配習慣的統計資料,然後,按儲存量將所有的記憶體分配進行分類,例如分為小、中和大三類。系統將每次分配指向被管理記憶體的一個區域,因為該區域包括這樣的記憶體塊儲存量。較小儲存量是根據較大儲存量分配的。這種方案是最先適合演算法和一組有限的固定儲存量演算法的一種有趣的混合,但不是實時的。
有效地利用暫時的侷限性通常是很困難的,但值得一提的是,在記憶體中暫時擴充套件共處一地的分配程式更容易產生記憶體碎片。儘管其它技術可以減輕這一問題,但限制不同儲存量記憶體塊的數目仍是減少記憶體碎片的主要方法。
現代軟體環境業已實現各種避免記憶體碎片的工具。例如,專為分散式高可用性容錯系統開發的 OSE 實時作業系統可提供三種執行時記憶體分配程式:核心alloc(),它根據系統或記憶體塊池來分配;堆 malloc(),根據程式堆來分配; OSE 記憶體管理程式alloc_region,它根據記憶體管理程式記憶體來分配。
從許多方面來看,Alloc就是終極記憶體分配程式。它產生的記憶體碎片很少,速度很快,並有判定功能。你可以調整甚至去掉記憶體碎片。只是在分配一個儲存量後,使之空閒,但不再分配時,才會產生外部碎片。內部碎片會不斷產生,但對某個給定的系統和八種儲存量來說是恆定不變的。
Alloc是一種有八個自由表的固定儲存量記憶體分配程式的實現方法。系統程式設計師可以對每一種儲存量進行配置,並可決定採用更少的儲存量來進一步減少碎片。除開始時以外,分配記憶體塊和使記憶體塊空閒都是恆定時間操作。首先,系統必須對請求的儲存量四捨五入到下一個可用儲存量。就八種儲存量而言,這一目標可用三個 如果語句來實現。其次,系統總是在八個自由表的表頭插入或刪除記憶體塊。開始時,分配未使用的記憶體要多花幾個週期的時間,但速度仍然極快,而且所花時間恆定不變。
堆 malloc() 的記憶體開銷(8 ~ 16 位元組/分配)比 alloc小,所以你可以停用記憶體的專用權。malloc()分配程式平均來講是相當快的。它的內部碎片比alloc()少,但外部碎片則比alloc()多。它有一個最大分配儲存量,但對大多數系統來說,這一極限值足夠大。可選的共享所有權與低開銷使 malloc() 適用於有許多小型物件和共享物件的 C++應用程式。堆是一種具有內部堆資料結構的夥伴系統的實現方法。在 OSE 中,有 28 個不同的儲存量可供使用,每種儲存量都是前兩種儲存量之和,於是形成一個斐波那契(Fibonacci)序列。實際記憶體塊儲存量為序列數乘以
16 位元組,其中包括分配程式開銷或者 8 位元組/分配(在檔案和行資訊啟用的情況下為 16 位元組)。
當你很少需要大塊記憶體時,則OSE記憶體管理程式最適用。典型的系統要把儲存空間分配給整個系統、堆或庫。在有 MMU 的系統中,有些實現方法使用 MMU 的轉換功能來顯著降低甚至消除記憶體碎片。在其他情況下,OSE 記憶體管理程式會產生非常多的碎片。它沒有最大分配儲存量,而且是一種最先適合記憶體分配程式的實現方法。記憶體分配被四捨五入到頁面的偶數——典型值是 4 k 位元組。
本文來自:我愛研發網(52RD.com) - R&D大本營