Linux的夥伴演算法把所有的空閑頁面分為10個塊組,每組中塊的大小是2的冪次方個頁面,例如,第0組中塊的大小都為20 (1個頁面),第1組中塊的大小為都為21(2個頁面),第9組中塊的大小都為29(512個頁面)。也就是說,每一組中塊的大小是相同的,且這同樣大小的塊形成一個鏈表。
夥伴演算法把滿足以上條件的兩個塊合并為一個塊,該演算法是迭代演算法,如果合并後的塊還可以跟相鄰的塊進行合并,那麼該演算法就繼續合并。
我們通過一個簡單的例子來說明該演算法的工作原理。
假設要求分配的塊其大小為128個頁面(由多個頁面組成的塊我們就叫做頁面塊)。該演算法先在塊大小為128個頁面的鏈表中尋找,看是否有這樣一個空閑塊。如果有,就直接分配;如果沒有,該演算法會尋找下一個更大的塊,具體地說,就是在塊大小為256個頁面的鏈表中尋找一個空閑塊。如果存在這樣的空閑塊,核心就把這256個頁面分為兩等份,一份分配出去,另一份插入到塊大小為128個頁面的鏈表中。如果在塊大小為256個頁面的鏈表中也沒有找到空閑頁塊,就繼續找更大的塊,即512個頁面的塊。如果存在這樣的塊,核心就從512個頁面的塊中分出128個頁面滿足請求,然後從384個頁面中取出256個頁面插入到塊大小為256個頁面的鏈表中。然後把剩餘的128個頁面插入到塊大小為128個頁面的鏈表中。如果512個頁面的鏈表中還沒有空閑塊,該演算法就放棄分配,並發出出錯訊號。
以上過程的逆過程就是塊的釋放過程,這也是該演算法名字的來由。滿足以下條件的兩個塊稱為夥伴:
兩個塊的大小相同;
兩個塊的物理地址連續。
夥伴演算法把滿足以上條件的兩個塊合并為一個塊,該演算法是迭代演算法,如果合并後的塊還可以跟相鄰的塊進行合并,那麼該演算法就繼續合并
linux通過夥伴演算法管理和分配頁,但由於硬體的原因,記憶體中的不同地區會有不同的特性。主要有以下兩個問題: 一些硬體只能用某些特定的記憶體位址來執行DMA; 一些體繫結構中有一些記憶體不能永久映射到核心空間上。因此某些記憶體必須從特定地區上分配,不能由單一的夥伴系統管理。為了區分這些記憶體地區,Linux使用了3個Zone,每個Zone由一個自己的夥伴系統來管理,如下: Zone_DMA包含可以用來執行DMA操作的記憶體; Zone_NORMAL包含可以正常映射到虛擬位址的記憶體地區; Zone_HIGHMEM包含不能永久映射到核心地址空間的記憶體地區。 這些地區的劃分和具體的體繫結構有關,例如,某些體繫結構中Zone_NORMAL覆蓋了所有的記憶體地區,而另外兩個區均為空白。在X86系統中,Zone_DMA為0~16MB的記憶體範圍,Zone_HIGHMEM包含了所有高於896MB的實體記憶體,Zone_NORMAL覆蓋地區部分。這些地區的劃分被分別管理,而在分配時,不同的任務會從不同的地區分配,例如,DMA任務只能從Zone_DMA中分配記憶體,而普通的記憶體請求則會依次嘗試從Zone_NORMAL、Zone_HIGHMEM和Zone_DMA中分配。在分配器看來,這3個區只是3個不同的記憶體池對象,用相同的方法即可進行處理。 獲得記憶體頁面 系統中對於物理頁有大量的需求,當程式映像載入到記憶體中時,作業系統需要分配頁;當程式結束執行並卸載時,作業系統需要釋放這些頁。另外,為了存放相關的資料結構(如頁表自身),也需要物理頁。這種用於分配和回收也的機制和資料結構對於維護虛擬記憶體子系統的效率是非常重要的。 系統中的所有物理頁都使用page資料結構來描述。每一個屋裡也對應一個page變數。一個zone的所有page變數集合形成數組,由zone的zone_mem_map成員指標指向數組的起始地址,頁數組的初始化在系統啟動時完成。 頁分配器的演算法是在夥伴系統之上的,夥伴系統將記憶體地區組織為以頁為單位的塊,n稱為該塊的“層級”,具有相同層級的塊用鏈表接在一起。每次分配必須指定一個層級,並以該層級塊大小為單位。在分配時,依次從層級n到最大層級開始搜尋,直到找到一個非空的塊為止。如果這個非空塊層級不是n,則將其拆成兩份,一份放到其對應的層級的空閑塊中,另一份如果還不是層級n就繼續拆分,直到最後返回到那個層級為n的塊。在回收時,首先計算處被回收的塊的夥伴,然後查看你這個夥伴是否在層級為n的空閑鏈中。如果找到了,則將這個塊與這個夥伴合并(夥伴從空閑鏈刪除,並整“當前塊”的位置即可),然後n=n+1,繼續這個合并過程。當夥伴不在該空閑鏈中時,合并過程結束。 slab分配器 Linux核心中有許多記憶體動態分配的需求,而其中的對象大小也參差不齊,Linux核心提供了slab層,扮演了通用資料結構緩衝層的角色。slab層根據對象的類型來分組不同的Cache,每個Cache存放不同類型的對象,例如,一個Cache被用來儲存task_struct,而另一個存放Inode等,這些Cache包含幾個slab,而slab由一個活多個物理上連續的page組成。對於一般的資料結構,每個slab只有一個頁即可。 每個slab都包含一些對象成員,即被管理的資料結構。系統指派至時就從slab中取得。首先從這個Cache中部分滿的slab中分配,如果沒有這樣的slab,便從空的slab中分配,如果也沒有這樣的slab,便從空的slab中分配,如果也沒有,就建立一個新的slab來分配即可。 因為每個slab是包含同一種對象的Cache塊,它對對象的分配和釋放會變得更為容易和快速。另外由於每個對象在釋放時幾乎處於分配好並且初始化好的狀態,還可以節省不少初始化的時間。例如分配inode變數,首先需要一塊malloc(sizeof(inode))大小的記憶體,然後初始化Inode中的資料成員,在使用完畢後用free釋放分配的記憶體在free之後記憶體中的內容和剛剛初始化時差不多,例如Inode的引用計數Count一定是降為零。 Kernel有許多資料結構都是還原為初始化時的狀態後才會free掉,例如一個Mutexlock初始化時和釋放時都處於Unlock狀態。因此只要在Cache初始化時蠻久將對象置於合法狀態,以後每次指派至的這些field一定是確定的,從而不必重複初始化,可以節省不少開銷。Linux的kmem_cache_create中有一個參數ctor初始化函數,可以被用作這一目的,但Linux似乎並沒有使用slab的這一特性(因為它有調用ctor函數)。 kmalloc 核心的kmalloc記憶體配置函數和應用程式層malloc函數很相似,只是這個函數運行得很快-除非它被阻塞。kmalloc函數不清零它獲得的記憶體空間分配給它的地區仍存放著原有的資料。 系統實體記憶體的管理是由核心負責的,實體記憶體只能按頁大小進行分配。這就需要一個面向頁的分配技術以取得電腦記憶體管理上最大的靈活性。類似malloc函數的簡單的線性分配技術不再有效了。在像Linux核心這樣的面向頁的系統中,如果記憶體使用量線性分配的策略就很難維護。空洞的處理很快就會成為一個問題,會導致記憶體浪費,降低系統的效能。 Linux是通過維護頁面池來處理kmalloc的分配要求的,這樣頁面就可以很容易地放進或取出頁面池。為了能夠滿足超過PAGE_SIZE位元組數大小的記憶體配置請求,mm/slab.c檔案維護頁面簇的列表。每個頁面簇都存放著連續若干頁,可用於DMA分配。Linux所使用的分配策略的最終方案是,核心只能分配一些預定義的固定大小的位元組數組。如果申請任一大小的記憶體空間,那麼很可能系統會多分配一點。 這些預定義的記憶體大小一般“稍小於2的某次方”(而在更新的實現中系統管理的記憶體大小恰好為2的某次方)。如果能記住這一點,就可以更有效地使用記憶體。例如需要一個2000B左右的緩衝區,最好還是申請2000B,而不要申請2048B。申請恰好是2的冪次的記憶體空間是最糟糕的情況了——核心會分配兩倍於申請空間大小的記憶體。 高端記憶體 一般情況下,Linux在初始化時總是儘可能地將所有的實體記憶體映射到核心地址空間中去。如果核心地址空間起始於0xC0000000,為vmalloc保留的虛擬位址空間是128MB,那麼最多隻能有(1GB-128MB)的實體記憶體直接映射到核心空間中,核心可以直接存取。如果還有更多的實體記憶體,就稱為高端記憶體,核心不能直接存取,只能通過修改頁表映射後才能進行訪問。 記憶體分區可以使核心頁分配更加合理。當系統實體記憶體大於1GB時,核心不能將所有的實體記憶體都預先映射到核心空間中,這樣就產生了高端記憶體,高端記憶體最適合於映射到使用者進程空間中。預映射的部分可直接用於核心緩衝區,其中有一小塊可用與DMA操作的記憶體,留給DMA操作分配用,一般不會輕易分配。記憶體分區還可以適應不連續的實體記憶體分布,是非一致性記憶體存取體系(NUMA)的基礎。
硬體已經趨向使用多條系統匯流排,每條系統匯流排為一小組處理器提供服務。每組處理器都有自己的記憶體,並可能有自己的 I/O 通道。但是,每個 CPU 都可以通過一致的方式訪問與其他組關聯的記憶體。每個組稱為一個“NUMA 節點”。NUMA 節點中的 CPU 數量取決於硬體供應商。訪問本地記憶體比訪問與其他 NUMA 節點關聯的記憶體快。這就是“非統一記憶體存取體繫結構”名稱的由來。
在32位機器上,頁表通常只可以儲存在低端記憶體中。低端記憶體只限於實體記憶體的前896MB,同時還要滿足核心其餘的大部分要求。在應用程式使用了大量進程並映射了大量記憶體的情況下,低端記憶體可能很快就不夠用了。在2.6核心中有一個配置選項稱為Highmem PTE,讓頁表條目可以存放在高端記憶體中,釋放出更多的低端記憶體地區給那些必須放在這裡的其他核心資料結構,使用這些頁表條目的進程會稍微慢一些。不過,對於那些有大量進程在啟動並執行系統來說,將頁表格儲存體到高端記憶體中可以再低端記憶體地區擠出更多的記憶體。 虛擬記憶體的申請和釋放 申請和釋放較小且連續的記憶體空間時,使用kmalloc和kfree函數在實體記憶體中進行分配,使用較大的記憶體空間時們可以使用vmalloc函數。由vmalloc函數申請的記憶體空間在虛擬記憶體中是連續的,它們映射到實體記憶體時,可以使用不連續的物理頁面,而且僅把當前訪問的部分放在物理頁面中。 vmalloc 儘管這段地區在物理上可能是不連續的(要訪問其中的每個頁面都必須獨立地調用函數__get_free_page),核心卻認為它們在地址上是連續的。分配的記憶體空間被映射到核心資料區段中,使用者空間是不可見的,這一點與其他分配技術不同。vmalloc發生錯誤時返回0(NULL地址),成功時返回一個指向一個大小為size的線性地址空間的指標。vmalloc函數在核心中所分配的記憶體由vm_struct結構的鏈表所支援。 與其他記憶體配置函數不同的是,vmalloc返回很“高”的地址值——這些地址要高於實體記憶體的頂部。由於vmalloc對頁表調整後允許用連續的“高”地址訪問分配得到的頁面,因此處理器是可以訪問返回得到的記憶體地區的。核心能和其他地址一樣使用vmalloc返回的地址,但程式中用到的這個地址與地址匯流排上的地址並不相同。用vmalloc分配得到的地址是不能在微處理器之外使用的,因為他們只有在處理器的分頁單元上才有意義。當驅動程式需要真正的物理地址時(像外設用於驅動系統匯流排的DMA地址),這樣的地址是不能通過vmalloc函數分配的。正確使用vmalloc函數的場合是為軟體分配一大塊連續的用於緩衝的記憶體地區。注意,vmalloc的開銷要比__get_free_page大,因為它處理擷取記憶體還要建立頁表。因此,不值得用vmalloc函數只分配一頁的記憶體空間。 vmalloc分配的核心虛擬記憶體與kmalloc/__get_free_page分配的核心邏輯記憶體位於不同的區間,不會重疊。因為核心空間被分區管理,各司其職。使用者空間被分配在0~3GB之間,3GB之後緊跟著實體記憶體映射區間,然後才是vmalloc_start開始的用於vmalloc分配記憶體的地址空間。 使用vmalloc函數的一個例子函數式create_module系統調用,它利用vmalloc函數來擷取被建立模組需要的記憶體空間。而在insmod調用重定位模組代碼後,將會調用memcpy_fromfs函數把模組本身複製進分配而得的空間內。 用vmalloc分配得到的記憶體空間用vfree函數來釋放,這就像是要用kfree函數來釋放kmalloc函數分配得到的記憶體空間。 和vmalloc一樣,ioremap也建立新的頁表,但和vmalloc不同的是,ioremap實際上並不分配記憶體,ioremap的傳回值是一個虛擬位址,可以用來訪問指定的實體記憶體地區,得到的這個虛擬位址最後要調用vfree來釋放掉。 ioremap用於將高記憶體空間的PCI緩衝區映射到使用者空間。例如如果VGA裝置的框架緩衝區被映射到地址0xf0000000(典型的一個值)後,ioremap就可以建立正確的頁表,從而讓讓讓處理器可以訪問。而系統初始化時建立的頁表只是用於訪問低於物理地址空間的記憶體地區。系統的初始化過程並不檢測PCI緩衝區,而是由各個驅動程式自己負責管理自己緩衝區。 如果希望驅動程式能在不同的平台間移植,那麼使用ioremap時就要小心。在一些平台上是不能直接將PCI記憶體區域對應到處理器的地址空間的,如在Alpha上就不行。此時就不能像普通記憶體地區那樣對重新對應地區進行訪問,而應該用reeadb函數或其他一些I/O函數。這些函數可以再不同平台間移植。 對vmalloc和ioremap函數可分配的記憶體空間大小並沒有什麼限制,但為了能檢測到程式員犯的一些錯誤,vmalloc不允許分配超過實體記憶體大小的記憶體空間。但是vmalloc函數請求過多的記憶體空間會產生一些和調用kmalloc函數時相同的問題。 ioremap和vmalloc函數都是面向頁的(它們都會修改頁表),因此分配或釋放的記憶體空間實際上都會上調為最近的一個頁邊界。而且,ioremap函數並不考慮如何重新對應不是頁邊界的物理地址。