我們的物理內存中的某些部分永久的分配給內核,並用來存放內核代碼以及靜態的內核數據結構。其餘的部分我們稱爲動態內存,這不僅是進程所需要的寶貴資源,也是內核本身所需要的寶貴資源。下面通過三部分來描述內核如何給自己分配動態內存,儘可能做到當需要時分配,不需要時釋放。
頁框管理
採用4KB頁框大小作爲標準的內存分配單元。
在以下情況下頁框是不空閒的:包含用戶態進程的數據,某個軟件高速緩存的數據,動態分配的內核數據結構,設備驅動程序緩衝的數據,內核模塊的代碼等等。
頁框的信息保存在一個類型爲page的頁描述符中。所有的page存放在一個mem_map數組中。每個描述符長達爲32字節,所以mem_map所需要的空間略小於整個物理內存大小的1%。
virt_to_page(addr)宏產生線性地址對應的頁描述符地址。就是將虛擬地址轉換爲物理地址。
pfn_to_page(pfn)宏產生和頁框號pfn(page frame number)對應的頁描述符地址。獲取頁框號的物理地址。
頁描述符page中包含有一個_count字段,爲頁的引用計數器。如果爲-1,那麼說明該頁框空閒,可以被分配給任何一個進程或者內核本身;如果大於0或者等於0,則說明頁框被分配給了一個或者多個進程,或者用於存放一些內核數據結構。page_count函數就是返回這個count的值加1,也即是該頁使用者的數目。
flags字段包含多達32個用於描述頁框狀態的標誌。、
非一致性內存訪問(NUMA)
內核2.6支持這種內存訪問模型,這種模型中,給定CPU對不同內存單元的訪問時間可能不一樣。系統的物理內存被劃分爲幾個節點(node).在一個單獨的節點內,任何一個給定CPU訪問頁面所需的時間都是相同的。然而,對不同的CPU,這個時間可能就不同,對於每個CPU而言,內核都試圖把耗時節點的訪問次數減到最小,這就要小心地選擇CPU最常引用的內核數據結構的存放位置。
每個節點中的物理內存又可以分爲幾個管理區。每個節點都有一個pg_data_t的描述符,所有節點的描述符存放在一個單向鏈表中,第一個元素由pgdata_list變量指向。IBM兼容PC使用一致訪問內存(UMA)模型,因此並不真正需要NUMA的支持。但是即使對NUMA的支持沒有被編譯進內核,Linux還是使用一個節點,只是這個節點包含了系統中所有的物理內存。因此pgdata_list指向一個只包含一個節點的鏈表,這個節點也就是節點0的描述符,存放於contig_page_data變量中。這樣做的好處是有助於內存代碼的處理更具可移植性。
內存管理區
由於計算機體系結構有硬件的制約,所以內核必須處理80*86體系結構的兩種硬件約束:
- ISA總線的直接內存存取(DMA)處理器有一個嚴格的限制:只能對主存的前16M尋址。
- 在具有大容量的內存的現代32位計算機中,CPU不能直接訪問所有的物理內存,因爲線性地址空間太小,只有4G。超過4G的部分就不能直接進行尋址了。
爲了應對上述的兩種限制,內核2.6把每個內存節點的物理內存劃分爲3個管理區(zone),在x86UMA體系結構下的管理區爲:
ZONE_DMA包含低於16M的內存頁框
ZONE_KERNEL包含高於16M低於896M的內存頁框
ZONE_HIGHMEN包含從896M開始到高於896M的內存頁框
前兩個包含內存的常規頁框,通過把它們映射到虛擬地址空間中的第4個G,內核就可以直接進行訪問。第三個區包含的內存頁不能由內核直接訪問。
同樣,每個管理區也都有自己的描述符。每個頁描述符都有到內存節點node以及到節點內管理區(即這個頁所在的管理區)的指針。爲了節省空間,這下指針和典型的指針不一樣,而是被編碼成索引放到了flags字段的高位。
page_zone()函數接收一個頁描述符的物理地址作爲參數,讀取頁描述符中flags字段的最高位,然後通過查看zone_table數組來確定相應管理區描述符的地址。在啓動時用所內存節點的所有管理區描述符的地址初始化這個數組。
當內核調用一個內存分配函數的時候,必須指明請求的頁框所在的管理區。爲了在內存分配中請求中指定首選管理區,內核使用zonelist數據結構,也就是管理區描述符指針數組。
保留的頁框池
保留內存的數量(以KB爲單位)存放在min_free_kbytes變量中,初始值在內核初始化時設置,並取決於前兩個區的頁框數量。保留的原因是因爲在原子請求從不被阻塞,如果沒有足夠的空閒頁,那麼就是分配失敗,爲了儘量減少這種情況發生,當內存不足的時候,就會使用保留的頁框池。
zone的描述符中的pages_min字段存儲了管理區內保留的頁框的數據。這個字段和pages_low,pages_high字段一起還在頁框回收算法中起作用。
分區頁框分配器
zoned page frame allocator 的內核子系統,處理對連續頁框組的內存分配請求。
管理區分配器部分接受動態分配和釋放的請求,在請求分配的情況下,搜索一個能滿足請求的一組連續頁框內存的管理區。在每個管理區內,頁框被“夥伴系統”來處理,爲了達到更好的系統性能,一小部分頁框保留在高速緩存中用於快速地滿足對單個頁框的分配請求。
請求和釋放頁框
- alloc_pages(gfp_mask,order)請求2的order次方個連續的頁框
- alloc_page(gfp_mask)
- __get_free_pages(gfp_mask,order)
- get_zeroed_page(gfp_mask)獲取歸零的頁框
__get_dma_pages(gfp_mask,order)獲取適用於DMA的頁框
釋放__free_pages(page,order)從page開始連續的2的order次方的連續頁框不再使用
- free_pages(addr,order)第一個頁框的線性地址
- __free_page(page)對應的頁框號
- free_page(addr)線性地址
高端內存頁框的內核映射
- 永久內核映射
臨時內核映射
夥伴系統算法
內核應該爲分配一組連續的頁框而建立一種健壯、高效的分配策略。頻繁的請求和釋放不同大小的一組連續頁框,必然導致在已分配頁框的塊內分散了許多小塊的空閒頁框。
從本質上來說,避免外碎片的方法有兩種:
利用分頁單元吧一組非連續的空閒頁框映射到連續的線性地址區間。
內核首選第二種方法,由於以下的原因:
夥伴系統算法就是用來解決外碎片問題。
內核試圖把大小爲b的一對空閒夥伴塊合併爲一個大小爲2b的單獨塊,滿足一下條件的兩個塊爲夥伴:
兩個塊具有相同的大小,記作b;
物理地址連續
第一個塊的第一個頁框的物理地址是2*b*2^12的倍數
把所有的空閒頁框分組爲11個塊鏈表,每個塊鏈表分別包含大小爲1,2,4,8,16,32,64,128,256,512,1024個連續的頁框。對1024個頁框的最大請求對應着4M大小的連續內存塊。每個塊的第一個頁框的物理地址是該塊大小的整數倍。
使用的主要數據結構如下:每個管理區都關係到mem_map元素的子集,子集中的第一個元素和元素的個數分別由管理區描述符zone_mem_map和size字段指定。包含有11個元素,元素類型爲free_area的一個數組,每個元素對於一種塊的大小,該數組存放在管理區描述符的free_area字段中。
內存區管理
夥伴系統算法採用頁框作爲基本內存區,適合於大塊內存的請求。對於小內存的請求,則是按照幾何分佈的內存區大小,內存區大小取決於2的次方而不是取決於所存放的數據大小。用一個動態鏈表來記錄每個頁框所包含的空閒內存區。
slab分配器
把內存區看做對象,這些對象由一組數據結構和幾個叫做構造和析構的函數組組成,爲了避免重複初始化對象,slab分配器並不丟棄已分配的對象,而是釋放但把它們保存在內存中,以後又要請求新的對象時,就可以從內存獲取而不是重新初始化。