Linux內存管理伙伴算法
伙伴算法
Linux內核內存管理的任務包括:
-
遵從CPU的MMU(Memory Management Unit)機制
-
合理、有效、快速地管理內存
-
實現內存保護機制
-
實現虛擬內存
-
共享
-
重定位
Linux內核通過伙伴算法來管理物理內存。伙伴系統(Buddy System)在理論上是非常簡單的內存分配算法。它的用途主要是盡可能減少外部碎片,同時允許快速分配與回收物理頁面。為了減少外部碎片,連續的空閑頁面,根據空閑塊(由連續的空閑頁面組成)大小,組織成不同的鏈表(或者orders)。這樣所有的2個頁面大小的空閑塊在一個鏈表中,4個頁面大小的空閑塊在另外一個鏈表中,以此類推。
注意,不同大小的塊在空間上,不會有重疊。當一個需求為4個連續頁面時,檢查是否有4個頁面的空閑塊而快速滿足請求。若該鏈表上(每個結點都是大小為4頁面的塊)有空閑的塊,則分配給用戶,否則向下一個級別(order)的鏈表中查找。若存在(8頁面的)空閑塊(現處於另外一個級別的鏈表上),則將該頁面塊分裂為兩個4頁面的塊,一塊分配給請求者,另外一塊加入到4頁面的塊鏈表中。這樣可以避免分裂大的空閑塊,而此時有可以滿足需求的小頁面塊,從而減少外面碎片。
伙伴算法舉例
假設我們的系統內存只有16個頁面RAM。因為RAM只有16個頁面,我們只需用四個級別(orders)的伙伴位圖(因為最大連續內存大小為16個頁面),如下圖所示。
在order(0),第一個bit表示開始的2個頁面,第二個bit表示接下來的2個頁面,以此類推。因為頁面4已分配,而頁面5空閑,故第三個bit為1。
同樣在order(1)中,bit3是1的原因是一個伙伴完全空閑(頁面8和9),和它對應的伙伴(頁面10和11)卻並非如此,故以后回收頁面時,可以合並。
分配過程
當我們需要order(1)的空閑頁面塊時,則執行以下步驟:
1、初始空閑鏈表為:
order(0): 5, 10
order(1): 8 [8,9]
order(2): 12 [12,13,14,15]
order(3):
2、從上面空閑鏈表中,我們可以看出,order(1)鏈表上,有一個空閑的頁面塊,把它分配給用戶,並從該鏈表中刪除。
3、當我們再需要一個order(1)的塊時,同樣我們從order(1)空閑鏈表上開始掃描。
4、若在order(1)上沒有空閑頁面塊,那么我們就到更高的級別(order)上找,order(2)。
5、此時有一個空閑頁面塊,該塊是從頁面12開始。該頁面塊被分割成兩個稍微小一些order(1)的頁面塊,[12,13]和[14,15]。[14,15]頁面塊加到order(1)空閑鏈表中,同時[12,13]頁面塊返回給用戶。
6、最終空閑鏈表為:
order(0): 5, 10
order(1): 14 [14,15]
order(2):
order(3):
回收過程
當我們回收頁面11(order 0)時,則執行以下步驟:
1、找到在order(0)伙伴位圖中代表頁面11的位,計算使用下面公示:
index = page_idx >> (order + 1)
= 11 >> (0 + 1)
= 5
2、檢查上面一步計算位圖中相應bit的值。若該bit值為1,則和我們臨近的,有一個空閑伙伴。Bit5的值為1(注意是從bit0開始的,Bit5即為第6bit),因為它的伙伴頁面10是空閑的。
3、現在我們重新設置該bit的值為0,因為此時兩個伙伴(頁面10和頁面11)完全空閑。
4、我們將頁面10,從order(0)空閑鏈表中摘除。
5、此時,我們對2個空閑頁面(頁面10和11,order(1))進行進一步操作。
6、新的空閑頁面是從頁面10開始的,於是我們在order(1)的伙伴位圖中找到它的索引,看是否有空閑的伙伴,以進一步進行合並操作。使用第一步中的計算公司,我們得到bit 2(第3位)。
7、Bit 2(order(1)位圖)同樣也是1,因為它的伙伴頁面塊(頁面8和9)是空閑的。
8、重新設置bit2(order(1)位圖)的值,然后在order(1)鏈表中刪除該空閑頁面塊。
9、現在我們合並成了4頁面大小(從頁面8開始)的空閑塊,從而進入另外的級別。在order(2)中找到伙伴位圖對應的bit值,是bit1,且值為1,需進一步合並(原因同上)。
10、從oder(2)鏈表中摘除空閑頁面塊(從頁面12開始),進而將該頁面塊和前面合並得到的頁面塊進一步合並。現在我們得到從頁面8開始,大小為8個頁面的空閑頁面塊。
11、我們進入另外一個級別,order(3)。它的位索引為0,它的值同樣為0。這意味着對應的伙伴不是全部空閑的,所以沒有再進一步合並的可能。我們僅設置該bit為1,然后將合並得到的空閑頁面塊放入order(3)空閑鏈表中。
12、最終我們得到大小為8個頁面的空閑塊:
內存管理(二)伙伴算法
通常情況下,一個高級操作系統必須要給進程提供基本的、能夠在任意時刻申請和釋放任意大小內存的功能,就像malloc 函數那樣,然而,實現malloc 函數並不簡單,由於進程申請內存的大小是任意的,如果操作系統對malloc 函數的實現方法不對,將直接導致一個不可避免的問題,那就是內存碎片。
內存碎片就是內存被分割成很小很小的一些塊,這些塊雖然是空閑的,但是卻小到無法使用。隨着申請和釋放次數的增加,內存將變得越來越不連續。最后,整個內存將只剩下碎片,即使有足夠的空閑頁框可以滿足請求,但要分配一個大塊的連續頁框就可能無法滿足,所以減少內存浪費的核心就是盡量避免產生內存碎片。
針對這樣的問題,有很多行之有效的解決方法,其中伙伴算法被證明是非常行之有效的一套內存管理方法,因此也被相當多的操作系統所采用。
伙伴算法,簡而言之,就是將內存分成若干塊,然后盡可能以最適合的方式滿足程序內存需求的一種內存管理算法,伙伴算法的一大優勢是它能夠完全避免外部碎片的產生。什么是外部碎片以及內部碎片,前面博文slab分配器后面已有介紹。申請時,伙伴算法會給程序分配一個較大的內存空間,即保證所有大塊內存都能得到滿足。很明顯分配比需求還大的內存空間,會產生內部碎片。所以伙伴算法雖然能夠完全避免外部碎片的產生,但這恰恰是以產生內部碎片為代價的。
Linux 便是采用這著名的伙伴系統算法來解決外部碎片的問題。把所有的空閑頁框分組為 11 塊鏈表,每一塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512 和 1024 個連續的頁框。對1024 個頁框的最大請求對應着 4MB 大小的連續RAM 塊。每一塊的第一個頁框的物理地址是該塊大小的整數倍。例如,大小為 16個頁框的塊,其起始地址是 16 * 2^12 (2^12 = 4096,這是一個常規頁的大小)的倍數。
下面通過一個簡單的例子來說明該算法的工作原理:
假設要請求一個256(129~256)個頁框的塊。算法先在256個頁框的鏈表中檢查是否有一個空閑塊。如果沒有這樣的塊,算法會查找下一個更大的頁塊,也就是,在512個頁框的鏈表中找一個空閑塊。如果存在這樣的塊,內核就把512的頁框分成兩等分,一般用作滿足需求,另一半則插入到256個頁框的鏈表中。如果在512個頁框的塊鏈表中也沒找到空閑塊,就繼續找更大的塊——1024個頁框的塊。如果這樣的塊存在,內核就把1024個頁框塊的256個頁框用作請求,然后剩余的768個頁框中拿512個插入到512個頁框的鏈表中,再把最后的256個插入到256個頁框的鏈表中。如果1024個頁框的鏈表還是空的,算法就放棄並發出錯誤信號。
簡而言之,就是在分配內存時,首先從空閑的內存中搜索比申請的內存大的最小的內存塊。如果這樣的內存塊存在,則將這塊內存標記為“已用”,同時將該內存分配給應用程序。如果這樣的內存不存在,則操作系統將尋找更大塊的空閑內存,然后將這塊內存平分成兩部分,一部分返回給程序使用,另一部分作為空閑的內存塊等待下一次被分配。
以上過程的逆過程就是頁框塊的釋放過程,也是該算法名字的由來。內核試圖把大小為 b 的一對空閑伙伴塊合並為一個大小為 2b 的單獨塊。滿足以下條件的兩個塊稱為伙伴:
- 兩個快具有相同的大小,記作 b
- 它們的物理地址是連續的
- 第一塊的第一個頁框的物理地址是 2 * b * 2^12 的倍數
該算法是迭代的,如果它成功合並所釋放的塊,它會試圖合並 2b 的塊,以再次試圖形成更大的塊。
假設要釋放一個256個頁框的塊,算法就把其插入到256個頁框的鏈表中,然后檢查與該內存相鄰的內存,如果存在同樣大小為256個頁框的並且空閑的內存,就將這兩塊內存合並成512個頁框,然后插入到512個頁框的鏈表中,如果不存在,就沒有后面的合並操作。然后再進一步檢查,如果合並后的512個頁框的內存存在大小為512個頁框的相鄰且空閑的內存,則將兩者合並,然后插入到1024個頁框的鏈表中。
簡而言之,就是當程序釋放內存時,操作系統首先將該內存回收,然后檢查與該內存相鄰的內存是否是同樣大小並且同樣處於空閑的狀態,如果是,則將這兩塊內存合並,然后程序遞歸進行同樣的檢查。
下面通過一個例子,來深入地理解一下伙伴算法的真正內涵(下面這個例子並不嚴格表示Linux 內核中的實現,是闡述伙伴算法的實現思想):
假設系統中有 1MB 大小的內存需要動態管理,按照伙伴算法的要求:需要將這1M大小的內存進行划分。這里,我們將這1M的內存分為 64K、64K、128K、256K、和512K 共五個部分,如下圖 a 所示
1.此時,如果有一個程序A想要申請一塊45K大小的內存,則系統會將第一塊64K的內存塊分配給該程序(產生內部碎片為代價),如圖b所示;
2.然后程序B向系統申請一塊68K大小的內存,系統會將128K內存分配給該程序,如圖c所示;
3.接下來,程序C要申請一塊大小為35K的內存。系統將空閑的64K內存分配給該程序,如圖d所示;
4.之后程序D需要一塊大小為90K的內存。當程序提出申請時,系統本該分配給程序D一塊128K大小的內存,但此時內存中已經沒有空閑的128K內存塊了,於是根據伙伴算法的原理,系統會將256K大小的內存塊平分,將其中一塊分配給程序D,另一塊作為空閑內存塊保留,等待以后使用,如圖e所示;
5.緊接着,程序C釋放了它申請的64K內存。在內存釋放的同時,系統還負責檢查與之相鄰並且同樣大小的內存是否也空閑,由於此時程序A並沒有釋放它的內存,所以系統只會將程序C的64K內存回收,如圖f所示;
6.然后程序A也釋放掉由它申請的64K內存,系統隨機發現與之相鄰且大小相同的一段內存塊恰好也處於空閑狀態。於是,將兩者合並成128K內存,如圖g所示;
7.之后程序B釋放掉它的128k,系統也將這塊內存與相鄰的128K內存合並成256K的空閑內存,如圖h所示;
8.最后程序D也釋放掉它的內存,經過三次合並后,系統得到了一塊1024K的完整內存,如圖i所示。
有了前面的了解,我們通過Linux 內核源碼(mmzone.h)來看看伙伴算法是如何實現的:
伙伴算法管理結構
- #define MAX_ORDER 11
- struct zone {
- ……
- struct free_area free_area[MAX_ORDER];
- ……
- }
- struct free_area {
- struct list_head free_list[MIGRATE_TYPES];
- unsigned long nr_free;//該組類別塊空閑的個數
- };
前面說到伙伴算法把所有的空閑頁框分組為11塊鏈表,內存分配的最大長度便是2^10頁面。
上面兩個結構體向我們揭示了伙伴算法管理結構。zone結構中的free_area數組,大小為11,分別存放着這11個組,free_area結構體里面又標注了該組別空閑內存塊的情況。
將所有空閑頁框分為11個組,然后同等大小的串成一個鏈表對應到free_area數組中。這樣能很好的管理這些不同大小頁面的塊。
內存管理算法--Buddy伙伴算法【轉】
Buddy算法的優缺點:
1)盡管伙伴內存算法在內存碎片問題上已經做的相當出色,但是該算法中,一個很小的塊往往會阻礙一個大塊的合並,一個系統中,對內存塊的分配,大小是隨機的,一片內存中僅一個小的內存塊沒有釋放,旁邊兩個大的就不能合並。
2)算法中有一定的浪費現象,伙伴算法是按2的冪次方大小進行分配內存塊,當然這樣做是有原因的,即為了避免把大的內存塊拆的太碎,更重要的是使分配和釋放過程迅速。但是他也帶來了不利的一面,如果所需內存大小不是2的冪次方,就會有部分頁面浪費。有時還很嚴重。比如原來是1024個塊,申請了16個塊,再申請600個塊就申請不到了,因為已經被分割了。
3)另外拆分和合並涉及到 較多的鏈表和位圖操作,開銷還是比較大的。
Buddy(伙伴的定義):
這里給出伙伴的概念,滿足以下三個條件的稱為伙伴:
1)兩個塊大小相同;
2)兩個塊地址連續;
3)兩個塊必須是同一個大塊中分離出來的;
Buddy算法的分配原理:
假如系統需要4(2*2)個頁面大小的內存塊,該算法就到free_area[2]中查找,如果鏈表中有空閑塊,就直接從中摘下並分配出去。如果沒有,算法將順着數組向上查找free_area[3],如果free_area[3]中有空閑塊,則將其從鏈表中摘下,分成等大小的兩部分,前四個頁面作為一個塊插入free_area[2],后4個頁面分配出去,free_area[3]中也沒有,就再向上查找,如果free_area[4]中有,就將這16(2*2*2*2)個頁面等分成兩份,前一半掛如free_area[3]的鏈表頭部,后一半的8個頁等分成兩等分,前一半掛free_area[2]
的鏈表中,后一半分配出去。假如free_area[4]也沒有,則重復上面的過程,知道到達free_area數組的最后,如果還沒有則放棄分配。

Buddy算法的釋放原理:
內存的釋放是分配的逆過程,也可以看作是伙伴的合並過程。當釋放一個塊時,先在其對應的鏈表中考查是否有伙伴存在,如果沒有伙伴塊,就直接把要釋放的塊掛入鏈表頭;如果有,則從鏈表中摘下伙伴,合並成一個大塊,然后繼續考察合並后的塊在更大一級鏈表中是否有伙伴存在,直到不能合並或者已經合並到了最大的塊(2*2*2*2*2*2*2*2*2個頁面)。
整個過程中,位圖扮演了重要的角色,如圖2所示,位圖的某一位對應兩個互為伙伴的塊,為1表示其中一塊已經分配出去了,為0表示兩塊都空閑。伙伴中無論是分配還是釋放都只是相對的位圖進行異或操作。分配內存時對位圖的
是為釋放過程服務,釋放過程根據位圖判斷伙伴是否存在,如果對相應位的異或操作得1,則沒有伙伴可以合並,如果異或操作得0,就進行合並,並且繼續按這種方式合並伙伴,直到不能合並為止。
Buddy內存管理的實現:
提到buddy 就會想起linux 下的物理內存的管理 ,這里的memory pool 上實現的 buddy 系統
和linux 上按page 實現的buddy系統有所不同的是,他是按照字節的2的n次方來做block的size
實現的機制中主要的結構如下:
整個buddy 系統的結構:
struct mem_pool_table
{
#define MEM_POOL_TABLE_INIT_COOKIE (0x62756479)
uint32 initialized_cookie; /* Cookie 指示內存已經被初始化后的魔數, 如果已經初始化設置為0x62756479*/
uint8 *mem_pool_ptr;/* 指向內存池的地址*/
uint32 mem_pool_size; /* 整個pool 的size,下面是整個max block size 的大小*/
boolean assert_on_empty; /* 如果該值被設置成TRUE,內存分配請求沒有完成就返回 並輸出出錯信息*/
uint32 mem_remaining; /* 當前內存池中剩余內存字節數*/
uint32 max_free_list_index; /* 最大freelist 的下標,*/
struct mem_free_hdr_type *free_lists[MAX_LEVELS];/* 這個就是伙伴系統的level數組*/
#ifdef FEATURE_MEM_CHECK
uint32 max_block_requested;
uint32 min_free_mem; /* 放mem_remaining */
#endif /* FEATURE_ONCRPC_MEM_CHECK*/
};
這個結構是包含在free node 或alloc node 中的結構:
其中check 和 fill 都被設置為某個pattern
用來檢查該node 的合法性
#define MEM_HDR_CHECK_PATTERN ((uint16)0x3CA4)
#define MEM_HDR_FILL_PATTERN ((uint8)0x5C)
typedef struct tagBuddyMemBlockHeadType
{
mem_pool_type pool; /*回指向內存池*/
uint16 check;
uint8 state; /* bits 0-3 放該node 屬於那1級 bit 7 如果置1,表示已經分配(not free)
uint8 fill;
} BUDDY_MEM_BLOCK_HEAD_TYPE;
這個結構就是包含node 類型結構的 free header 的結構:
typedef struct tagBuddyMemHeadType
{
mem_node_hdr_type hdr;
struct mem_free_hdr_type * pNext; /* next,prev,用於連接free header的雙向 list*/
struct mem_free_hdr_type * pPrev;
} mem_free_hdr_type;
這個結構就是包含node 類型結構的 alloc header 的結構:
已分配的mem 的node 在內存中就是這樣表示的
- typedef struct mem_alloc_hdr_type
- {
- mem_node_hdr_type hdr;
- #ifdef FEATURE_MEM_CHECK_OVERWRITE
- uint32 in_use_size;
- #endif
- } mem_alloc_hdr_type;
其中用in_use_size 來表示如果請求分配的size 所屬的level上實際用了多少
比如申請size=2000bytes, 按size to level 應該是2048,實際in_use_size
為2000,剩下48byte 全部填充為某一數值,然后在以后free 是可以check
是否有overwite 到着48byte 中的數值,一般為了速度,只 檢查8到16byte
另外為什么不把這剩下的48byte 放到freelist 中其他level 中呢,這個可能
因為本來buddy 系統的缺點就是容易產生碎片,這樣的話就更碎了
關於free or alloc node 的示意圖:
假設
最小塊為2^4=16,着是由mem_alloc_hdr_type (12byte)決定的, 實際可分配4byte
如果假定最大max_block_size =1024,
如果pool 有mem_free_hdr_type[0]上掛了兩個1024的block node
上圖是free node, 下圖紫色為alloc node
接下來主要是buddy 系統的操作主要包括pool init , mem alloc ,mem free
pool init :
1. 將實際pool 的大小去掉mem_pool_table 結構大小后的size 放到
mem_pool_size, 並且修改實際mem_pool_ptr指向前進mem_pool_table
結構大小的地址
2. 接下來主要將mem_pool_size 大小的內存,按最大塊掛到free_lists 上
level 為0的list 上,然后小於該level block size 部分,繼續掛大下一
級,循環到全部處理完成 (感覺實際用於pool的size ,應該為減去
mem_pool_table 的大小,然后和最大塊的size 對齊,這樣比較好,
但沒有實際測試過)
mem alloc:
這部分相當簡單,先根據請求mem的size ,實際分配時需要加上mem_alloc_hdr_type
這12byte ,然后根據調整后的size,計算實際應該在那個 level上分配,如果有相應級
很簡單,直接返回,如果沒有,一級一級循環查找,找到后,把省下的部分,在往下一級
一級插入到對應級的freelist 上
mem free:
其中free 的地址,減去12 就可以獲得mem_alloc_hdr_type 結構
然后確定buddy 在該被free block 前,還是后面, 然后合並buddy,
循環尋找上一級的buddy ,有就再合並,只到最大block size 那級
關於這個算法,在<<The Art of Computer Programming>> vol 1,的
動態存儲分配中有描述,對於那些只有OSAL 的小系統,該算法相當有用