標籤:夥伴系統 linux kernel 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 塊,此時應找第二塊,所以要加上同層級的大小。
綜上所述,夥伴系統的原理就很清楚了,由於有不同層級塊大小的散列,使尋找能滿足塊的空閑記憶體非常的迅速,再加上尋找合并塊的高效演算法,使合并尋找非常高效。這就是夥伴系統!
Linux Buddy Allocator