如何分配內存 內存碎片處理技術

如何分配內存 內存碎片處理技術 作者:Jan Lindblad  來源:賽迪網-電子設計技術   發佈時間: 2004.10.11 8:24 內存碎片是一個很棘手的問題。如何分配內存決定着內存碎片是否會、何時會、如何會成爲一個問題。

  即使在系統中事實上仍然有許多空閒內存時,內存碎片還會最終導致出現內存用完的情況。一個不斷產生內存碎片的系統,不管產生的內存碎片多麼小,只要時間足夠長,就會將內存用完。這種情況在許多嵌入式系統中,特別是在高可用性系統中是不可接受的。有些軟件環境,如 OSE 實時操作系統已經備有避免內存碎片的良好工具,但個別程序員做出的選擇仍然會對最終結果形成影響。
  “碎片的內存”描述一個系統中所有不可用的空閒內存。這些資源之所以仍然未被使用,是因爲負責分配內存的分配器使這些內存無法使用。這一問題通常都會發生,原因在於空閒內存以小而不連續方式出現在不同的位置。由於分配方法決定內存碎片是否是一個問題,因此內存分配器在保證空閒資源可用性方面扮演着重要的角色。

 

  編譯時間與運行時間
  在許多情況下都會出現內存分配問題。程序員可以通過編譯程序和鏈接程序,爲結構、並集、數組和標量(用作局部變量、靜態變量或全局變量)方面的數據分配內存,程序員還可以在運行時間使用諸如 malloc()調用命令動態地分配內存。當用編譯程序和鏈接程序完成內存分配功能時,就不會出現內存碎片,因爲編譯程序瞭解數據壽命。掌握可供使用的數據壽命,好處在於可以使數據以後進先出的方式疊加起來。這樣就可以使內存分配程序工作效率更高,而不會出現內存碎片。一般來說,運行時間內的內存分配是不可疊加的。內存分配在時間上是獨立的,從而使得碎片問題難以解決。


圖 1,內存碎片的幾種形式。


  內存分配程序浪費內存的基本方式有三種:即額外開銷、內部碎片以及外部碎片(圖 1)。內存分配程序需要存儲一些描述其分配狀態的數據。這些存儲的信息包括任何一個空閒內存塊的位置、大小和所有權,以及其它內部狀態詳情。一般來說,一個運行時間分配程序存放這些額外信息最好的地方是它管理的內存。內存分配程序需要遵循一些基本的內存分配規則。例如,所有的內存分配必須起始於可被 4、8 或 16 整除(視處理器體系結構而定)的地址。內存分配程序把僅僅預定大小的內存塊分配給客戶,可能還有其它原因。當某個客戶請求一個 43 字節的內存塊時,它可能會獲得 44字節、48字節 甚至更多的字節。由所需大小四捨五入而產生的多餘空間就叫內部碎片。
  外部碎片的產生是當已分配內存塊之間出現未被使用的差額時,就會產生外部碎片。例如,一個應用程序分配三個連續的內存塊,然後使中間的一個內存塊空閒。內存分配程序可以重新使用中間內存塊供將來進行分配,但不太可能分配的塊正好與全部空閒內存一樣大。倘若在運行期間,內存分配程序不改變其實現法與四捨五入策略,則額外開銷和內部碎片在整個系統壽命期間保持不變。雖然額外開銷和內部碎片會浪費內存,因此是不可取的,但外部碎片纔是嵌入系統開發人員真正的敵人,造成系統失效的正是分配問題。
  定義內存碎片的方法有幾種,其中最常用的是:
 



  這一方法適用於外部碎片,但可以修改這一公式使之包括內部碎片,辦法是把內部碎片加入到分母中。內存碎片是一個介於 0 和 1 之間的分數。一個碎片爲 1(100%)的系統就是把內存全用完了。如果所有空閒內存都在一個內存塊(最大內存塊)中,碎片爲 0%。當所有空閒內存的四分之一在最大內存塊中時,碎片爲 75%。例子如下:一個系統有 5M 字節的空閒內存,當它可用來分配的最大內存塊爲 50 k 字節時,其內存碎片爲99%。這個 99%內存碎片實例來自開發嵌入式軟實時系統期間出現的一種真實情況。當這種碎片程度發生一秒後,系統就崩潰了。該系統在碎片率達到 99% 之前,已經進行了約兩週的連續現場測試。這種情況是如何發生的?爲什麼會發現得如此晚?當然,系統都經過測試,但測試很少超過兩個小時。交付前的最後壓力測試持續了一個週末。在這樣短的測試周期內未必會產生內存碎片的後果,所以就發生了內存碎片需要多長時間纔會達到臨界值,這一問題很難回答。對某些應用來說,在某些情況下,系統會在用完內存前達到一種穩定狀態。而對於另一些應用來說,系統則不會及時達到穩定狀態(圖 2)。只要消除不確定性因素和風險因素,不產生碎片的內存分配程序(圖 3)就能快速達到一種穩定狀態,從而有助於開發人員夜晚安穩睡覺。在開發數月甚至數年不再重新啓動的長期運行系統時,快速收斂到穩定狀態是一個重要因素。在比系統連續運行週期短的時間內,對系統進行適當的測試,這是必不可少的。


圖 2,這一案例研究把最先適合內存分配程序用於一個嵌入系統項目。系統在現場測試中連續運行了兩週,然後碎片率達到 99%。



圖 3,一個不產生碎片的內存分配程序一旦試驗應用程序的全部,它就能達到穩定狀態。


  很難確定哪種內存分配算法更勝一籌,因爲每種算法在不同的應用中各有所長(表 1)。最先適合內存分配算法是最常用的一種。它使用了四個指針:MSTART 指向被管理內存的始端;MEND 指向被管理內存的末尾;MBREAK 指向 MSTART 和 MEND 之間已用內存的末端; PFREE 則指向第一個空閒內存塊(如果有的話)。



  在系統開始運行時,PFREE 爲 NULL,MBREAK 指向 MSTART。當一個分配請求來到時,分配程序首先檢查 PFREE有無空閒內存塊。由於 PFREE 爲 NULL,一個具有所請求存儲量加上管理標題的內存塊就脫離 MBREAK ,然後MBREAK就更新。這一過程反覆進行,直至系統使一個內存塊空閒,管理標題包含有該存儲塊的存儲量爲止。此時,PFREE 通過頭上的鏈接表插入項被更新爲指向該內存塊,而塊本身則用一個指向舊 PFREE 內容的指針進行更新,以建立一個鏈接表。下一次出現分配請求時,系統就會搜索空閒內存塊鏈接表,尋找適合請求存儲量的第一個空閒內存塊。一旦找到合適的內存塊,它將此內存塊分成兩部分,一部分返還給系統,另一部分則送回給自由表。
  最先適合內存分配算法實現起來簡單,而且開始時很好用。但是,經過一段時間後,會出現如下的情況:當系統將內存交給自由表時,它會從自由表的開頭部分去掉大內存塊,插入剩餘的小內存塊。最先適合算法實際上成了一個排序算法,即把所有小內存碎片放在自由表的開頭部分。因此,自由表會變得很長,有幾百甚至幾千個元素。因此,內存分配變得時間很長又無法預測,大內存塊分配所花時間要比小內存塊分配來得長。另外,內存塊的無限制拆分使內存碎片程度很高。有些實現方法在使內存空閒時會將鄰近的空閒內存塊連接起來。這種方法多少有些作用,而最先適合算法與時間共處算法(time co-location)和空間共處算法(spatial co-location)不同,它在使內存塊空閒時,無法提高相鄰內存塊同時空閒的概率。

 

  最佳適合與最差適合分配程序
  最佳適合算法在功能上與最先適合算法類似,不同之處是,系統在分配一個內存塊時,要搜索整個自由表,尋找最接近請求存儲量的內存塊。這種搜索所花的時間要比最先適合算法長得多,但不存在分配大小內存塊所需時間的差異。最佳適合算法產生的內存碎片要比最先適合算法多,因爲將小而不能使用的碎片放在自由表開頭部分的排序趨勢更爲強烈。由於這一消極因素,最佳適合算法幾乎從來沒有人採用過。
  最差適合算法也很少採用。最差適合算法的功能與最佳適合算法相同,不同之處是,當分配一個內存塊時,系統在整個自由表中搜索與請求存儲量不匹配的內存快。這種方法比最佳適合算法速度快,因爲它產生微小而又不能使用的內存碎片的傾向較弱。始終選擇最大空閒內存塊,再將其分爲小內存塊,這樣就能提高剩餘部分大得足以供系統使用的概率。
  夥伴(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 字節。(T111)

轉自:http://byebyebluewater.spaces.live.com/PersonalSpace.aspx?_c11_BlogPart_n=1&_c11_BlogPart_handle=cns!6C67B5AA2503D91C!609&_c11_BlogPart_FullView=1&_c=BlogPart 
發佈了11 篇原創文章 · 獲贊 0 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章