Linux Buddy Allocator

        衆所周知,物理內存的管理對於一個操作系統性能的重要性,那麼著名的 Linux 是如何有效地管理起物理內存的呢。這裏將作一個詳盡的分析。
        內存管理最重要的兩個指標莫過於:1。減少碎片,提高利用率;2. 分配和釋放的速度要快。
        提到內存碎片,分爲外碎片和內碎片兩種。所謂的外碎片就是,當內存頻繁申請和釋放後,出現了很多空洞,但是它們不是連續的,當下次將要分配一塊內存時,雖然空閒內存總量大於所需分配的大小,但由於這些空洞不是連續的,導致無法滿足需求,這是很常見的問題;所謂內碎片就是,假設內存分配都是按照一定單位分配,比如 4K,當用戶只需要 512 字節的內存大小時,由於分配了 4K 出去,導致 4K-512 的內存沒有被利用,也無法再次分配,從而導致的內存的浪費。
        本文的重點夥伴系統就是爲解決外碎片而實現的算法。當然它的效率也是極其高的。
        常規的物理內存管理算法無非利用位圖或者鏈表來記錄空閒內存塊的狀態,當需要分配時,掃描整條空閒鏈表,然後找到一塊僅大於所需內存的塊返回,這裏僅大於是指,最好能找到一塊剛好大於所以的內存塊,那麼就意味着,需要對空閒內存塊進行排序,分配還好,每次釋放時,查找合適的位置都要進行很多運算。這時自然能想到,對空閒內存進行散列歸類,即按照一定規格大小分成多條鏈表,當進行分配和釋放時,就可以直接找到相應的鏈表了,進而提升了效率。這樣分配是提升了效率,但是釋放時依舊很慢,因爲內存釋放時,如果相鄰的內存也是空閒的,應該儘量合併成更大的內存,記錄到更大內存的鏈表中,那麼,如何能找到能合併的內存,就需要一個合適的算法了。夥伴系統就是這樣一個算法。
        首先說一下,Linux 管理物理內存,有一個重要的數據結構,就是 mem_map,它是一個 page 類型的數組,每個元素代表一個物理頁,由 32 個字節組成,記錄了所有有關該物理頁框的狀態。這樣想知道某一頁框的信息就非常的方便了。如同前面所說,夥伴系統把內存大小分類爲 11 種不同規格的容量,分別爲 2 的 n 次方個頁框的大小,這樣,當所需一塊內存時,很容易就確定,從哪種規格里查找空閒頁。然後待解決的就是快速查找能夠合併的內存塊的大小。夥伴系統規定,滿足以下條件的兩塊內存爲夥伴:
        1. 兩個塊具有相同的大小,記作 b;
        2. 它們的物理地址是連續的;
        3. 第一塊的第一個頁框的物理地址是 (2 x b x 頁大小)的倍數;

        前兩條規定好理解,也就是兩塊相同大小,並且連續的內存塊纔有可能是夥伴,因爲這樣,兩塊合起來就可以很順利地添加到更上一級的空閒鏈表中。第三條規定了,在滿足前兩個條件的情況下,還需要,第一塊的物理地址的限定,舉個例子。

 +---------------------------------------+
 | a0 | a1 | a2 | a3 | a4 | a5 | a6 | a7 |
 +---------------------------------------+
 |   b0    |    b1   |    b2   |    b3   |
 +---------------------------------------+
 |        c0         |        c1         |
 +---------------------------------------+

        假設,a0 - a7 代表物理頁,那麼滿足條件的夥伴爲 (a0, a1), (a2, a3),(a4, a5), (a6, a7); 然而 a0 和 a1 可以合併爲 b0, 以此類推,(b0, b1), (b2, b3) 也是夥伴, (c0, c1) 也是夥伴。除此之外其它兩兩之間均不爲夥伴,如 a1 和 a2 雖然滿足前兩個條件,但不滿足第三個條件,b1 和 b2 也是,當不爲夥伴的兩個塊即使空閒,且連續,在夥伴系統中它們也是不能合併的。這裏顯然,也有少許的碎片存的,但設想,其實這樣的情況是極少發生的,因爲如果 a0 在使用,a3 在使用,這一般發生在 a1 是在 a3 被分配之後才被釋放的,雖然此時 a2 也被釋放了,a1 和 a2 無法合併,但下次再分配該級別大小的內存時,就會首先找到 a1,不會再向 a3 後面的空閒內存中尋找,這樣僅有這種極少的浪費,其實也是可以被接受的。
        之所以會有第一種規定,因爲這樣,查找一個塊的夥伴時就很容易定位到它的夥伴的大小,再加上第二條,就很容易知道,是向前還是向後查找它的夥伴,如果沒有第三條,那兩面兩條也將沒有意義,假設上圖中, b2 塊被釋放時,再向前找與 b1 大小相同的塊將沒有意義,因爲如果 b1 空閒,把 b2 和 b1 進行了合併,或者沒有前兩條的約束,讓 b2 與 a3 進而合併,雖然可以合併到上一級中成爲更大的塊,由於 b2 搶了 b0 或者 a2 的夥伴,那麼 b0 空閒時,就無數與其它塊進行合併了,並且合併的塊可能也不再規整,從而無法進行散列,慢慢就形成了碎片,所以 b2 只有唯一的夥伴 b3 ,它也只會等到 b3 被釋放時,兩塊進行合併進而升級,這樣,每一塊都只將有唯一的夥伴,除非夥伴不被釋放,不然它們總是可以合併。
        整個結構如圖:

  +-------+     +------+      +------+
  |   1   |---->|  a1  |----->|  a2  |
  +-------+     +------+      +------+
  |   2   |
  +-------+     +------+
  |   4   |-----|  c1  |
  +-------+     +------+
  |  ...  |

        此時運行時的情形就很容易被分析出來了,每個級別中被掛入空閒鏈表的數據其實非常的少,在極端情況下,a0-a7 都被申請,但只釋放了 a1, a3, a5, a7,此時纔會出現空間,但在頻繁的申請和釋放的使用中,如果前面有空閒塊,會首先被滿足,所以出現空洞的實際情況比較少,當一個塊被釋放時,只需要簡單的運算就可以確定它和它的夥伴能否合併成更大的塊,以此類型,儘量地形成更大的連續空間以滿足系統的需求。
        只所以說合並變的簡單,看一下代碼就知道了。
static inline struct page *
__page_find_buddy(struct page *page, unsigned long page_idx, unsigned int order)
{
	unsigned long buddy_idx = page_idx ^ (1 << order);

	return page + (buddy_idx - page_idx);
}
        page_idx 爲頁框號,即 a0 之類的序號,order 爲級別,即 2 的 order 次方個頁框,page_idx 與 (1 << order)做異或,那麼如果 page_idx 中相應的位爲 1,相當於 page_idx - 2^order,因爲互爲夥伴的兩個塊的第一個塊,它的地址,肯定爲 (2 x b x 頁大小),b 爲 2 的 order 次方,因爲都以頁大小爲單位,所以相當於 2 x 2^order,爲 2 ^ (order + 1),那麼它的第 2 ^ order 位肯定爲 0,如果爲 1 那麼說明它是第二塊,此時應該找第一塊,所以要減去相應級別的大小,同理,如果爲 0,說明它是第 1 塊,此時應找第二塊,所以要加上同級別的大小。
        綜上所述,夥伴系統的原理就很清楚了,由於有不同級別塊大小的散列,使查找能滿足塊的空閒內存非常的迅速,再加上查找合併塊的高效算法,使合併查找非常高效。這就是夥伴系統!

發佈了67 篇原創文章 · 獲贊 813 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章