內存管理(下)

5、物理內存的管理

在內核初始化完成後,內存管理的責任由夥伴系統(高效、高速)承擔。前端

一、夥伴系統的結構

系統內存中的每一個物理內存頁(頁幀),都對應於一個struct page實例。每一個內存域都關聯了一個struct zone的實例,其中保存了用於管理夥伴數據的主要數組。node

1 struct zone {
2 ...
3   struct free_area free_area[MAX_ORDER];    //不一樣長度的空閒區域
4 ...
5 } ;

sruct free_area是一個輔助結構,以下所示。程序員

1 struct free_area {
2     struct list_head free_list[MIGRATE_TYPES];    //用於鏈接空閒頁的鏈表
3     unsigned long nr_free;    //當前內存區中空閒頁塊的數目
4 };

 

階(order)是夥伴系統中一個很是重要的術語。它描述了內存分配的數量單位內存區的管理單位,內存塊的長度是2的order次方。圖1是夥伴系統中相互鏈接的內存區,內存區中第1頁內的鏈表元素,可用於將內存區維持在鏈表中。所以,也沒必要引入新的數據結構來管理物理上連續的頁,不然這些頁不可能在同一內存區中,MAX_ORDER根據硬件不一樣而設置不一樣的值,表示一次分配能夠請求的最大頁數的以2爲底的對數。算法

1 夥伴系統相互鏈接的內存區後端

夥伴沒必要是彼此鏈接的。若是一個內存區在分配其間分解爲兩半,內核會自動將未用的一半加入到對應的鏈表中。若是在將來的某個時刻,因爲內存釋放的緣故,兩個內存區都處於空閒狀態,可經過其地址判斷其是否爲夥伴。數組

基於夥伴系統的內存管理專一於某個結點的某個內存域,例如,DMA或高端內存域。但全部內存域和結點的夥伴系統都經過備用分配列表鏈接起來。如圖2所示。緩存

2 夥伴系統和內存域/結點之間的關係安全

二、避免碎片

Linux系統啓動並長期運行後,物理內存會產生不少碎片。這對用戶空間應用程序沒有問題(其內存經過頁表進行映射,物理內存分佈與應用程序看到的內存無關),但對內核來講,碎片是一個問題(大多數物理內存一致映射到地址空間內核部分)。網絡

1)依據可移動性組織頁數據結構

文件系統的碎片主要經過碎片合併工具解決,不一樣於物理內存,許多物理內存頁不能移動到任意位置,阻礙了該方法的實施。內核處理避免碎片的方法是反碎片(版本2.6.24),試圖從最初開始儘量防止碎片。

內核將已分配頁劃分爲如下3種不一樣類型:

  • 不可移動頁:在內存中有固定位置,不能移動到其餘地方。核心內核分配的大多數內存屬於該類別;
  • 可回收頁:不能直接移動,但能夠刪除,其內容能夠從某些源從新生成。例如,映射自文件的數據屬於該類別,kswapd守護進程會根據可回收頁訪問的頻繁程度,週期性釋放此類內存;
  • 可移動頁:能夠隨意地移動。屬於用戶空間應用程序的頁屬於該類別。它們是經過頁表映射的。若是它們複製到新位置,頁表項能夠相應地更新,應用程序不會注意到任何事。

頁的可移動性,依賴該頁屬於3種類別的哪種。內核使用的反碎片技術,將具備相同可移動性的頁進行分組。根據頁的可移動性,將其分配到不一樣的列表中,防止不可移動的頁位於可移動內存區中間的狀況出現。這樣對於不可移動頁中仍然難以找到較大的連續空閒時間,但對可回收的頁就相對容易了。

內核定義了一些宏來表示遷移類型:

1 #define MIGRATE_UNMOVABLE 0    //類型
2 #define MIGRATE_RECLAIMABLE 1    //類型
3 #define MIGRATE_MOVABLE 2        //類型
4 #define MIGRATE_RESERVE 3    //向具備特定可移動性的列表請求分配內存失敗,從MIGRATE_RESERVE分配內存(緊急分配)
5 #define MIGRATE_ISOLATE 4 //不能從這裏分配,特殊的虛擬區域,用於跨越NUMA結點移動物理內存頁
6 #define MIGRATE_TYPES 5

對夥伴系統的主要數據結構影響是將空閒列表分解爲MIGRATE_TYPE個列表,代碼以下:

1 struct free_area {
2     struct list_head free_list[MIGRATE_TYPES];
3     unsigned long nr_free;    //全部列表上空閒頁的數目
4 };

內核提供了一個備用列表,規定了在指定列表中沒法知足分配請求時,接下來使用的遷移類型的種類。(在內核想要分配不可移動頁時,若是對應鏈表爲空,則後退到可回收頁鏈表,接下來到可移動頁鏈表,最後到緊急分配鏈表。)

頁可移動性分組特性老是編譯到內核中,但只有在系統中有足夠內存能夠分配到多個遷移類型對應的鏈表時,纔會起做用。兩個全局變量pageblock_order和pageblock_nr_pages提供每一個遷移鏈表對應的適當數量的內存。第一個表示內核認爲是「大」的一個分配階,pageblock_nr_pages則表示該分配階對應的頁數。若是體系結構提供了巨型頁機制,則pageblock_order一般定義爲巨型頁對應的分配階(IA-32巨型頁長度是4MB),若是體系結構不支持巨型頁,則將其定義爲第二高的分配階(MAX_ORDER-1)。若是各遷移類型的鏈表中沒有一塊較大的連續內存,那麼頁面遷移不會提供任何好處,所以在可用內存太少時內核會經過設置全局變量page_group_by_mobility爲0關閉該特性(一旦停用了頁面遷移特性,全部頁都是不可移動的)。

在內存子系統初始化期間,memmap_init_zone負責處理內存域的page實例。它將全部的頁最初都標記爲可移動的,此時若是須要分配不可移動的內存,則必須「盜取」(見4分配API)。實際上,啓動期間分配可移動內存區的狀況較少,分配器有很高的概率分配長度最大的內存區,並將其從可移動列表轉換到不可移動列表。因爲分配的內存區長度是最大的,所以不會向可移動內存中引入碎片。這種作法避免了啓動期間內核分配的內存(常常在系統的整個運行時間都不釋放)散佈到物理內存各處,從而使其餘類型的內存分配免受碎片的干擾,這也是頁可移動性分組框架的最重要的目標之一。

2)虛擬可移動內存域

依據可移動性組織頁是防止物理內存碎片的一種可能方法,內核還提供了另外一種阻止該問題的手段:虛擬內存域ZONE_MOVABLE,其特性必須由管理員顯示激活。

基本思想:可用的物理內存劃分爲兩個內存域,一個用於可移動分配,一個用於不可移動分配。

kernelcore參數用來指定用於不可移動分配的內存數量(用於既不能回收也不能遷移的內存數量)。參數movablecore控制用於可移動內存分配的內存數量。若是同時指定兩個參數,內核會按照必定的方法進行計算,取指定值與計算值中較大的一個。

ZONE_MOVABLE並不關聯到任何硬件上有意義的內存範圍,該內存域中的內存取自高端內存域或普通內存域,所以稱虛擬內存域。

從物理內存域提取用於ZONE_MOVABLE的內存數量主要考慮如下兩個因素:

  • 用於不可移動分配的內存會平均地分佈到全部內存結點上;
  • 只使用來自最高內存域的內存(在內存較多的32位系統上,一般是ZONE_HIGHMEM,對於64位系統,使用ZONE_NORMAL或ZONE_DMA32)。

最後是計算結果,用於爲虛擬內存域ZONE_MOVABLE提取內存頁的物理內存域,保存在全局變量movable_zone中;對每一個結點來講,zone_movable_pfn[node_id]表示ZONE_MOVABLE在movable_zone內存域中所取得內存的起始地址。

(虛擬內存域具體的實如今4分配API中)

三、初始化內存域和結點數據結構

在啓動期間,各體系結構相關的代碼須要確立系統中各內存域的頁幀的邊界(max_zone_pfn數組);肯定各結點頁幀的分配狀況(全局變量early_node_map)。

1)管理數據結構的建立

3概述了管理數據結構創建的過程。

3 管理數據結構構建過程示意圖

 

4 free_area_init_nodes的代碼流程圖

free_area_init_nodes代碼流程圖如圖4所示,完成如下工做:

  • 首先分析並改寫特定於體系結構的代碼提供的信息(對照在zone_max_pfn和zone_min_pfn中指定的內存域的邊界,計算各個內存域可以使用的最低和最高的頁幀編號);
  • 根據結點的第一個頁幀start_pfn,對early_node_map中的各項進行排序;
  • 以[low, high]形式描述各個內存域的頁幀區間,存儲在對應的全局變量中;
  • 接下來構建其餘內存域的頁幀區間,方法很直接:第n個內存域的最小頁幀,即前一個(第n-1個)內存域的最大頁幀(當前內存域的最大頁幀由max_zone_pfn給出);
  • 最後遍歷全部活動結點,並分別對各個結點調用free_area_init_node創建數據結構。

2)對各個結點建立數據結構

在內存域邊界已經肯定以後,free_area_init_nodes分別對各個內存域調用free_area_init_node建立數據結構。這涉及到幾個輔助函數(見圖4):

  • calculate_node_totalpages首先累計各個內存域的頁數,計算結點中頁的總數;
  • alloc_node_mem_map負責初始化一個簡單但很是重要的數據結構(struct page);
  • free_area_init_core依次遍歷結點的全部內存域,負責初始化內存域數據結構涉及的繁重工做(內存域的真實長度、系統中的頁數、初始化zone結構中各個表頭、將各個結構成員初始化爲0)。

此時,空閒頁的數目(nr_free)當前仍然規定爲0,這顯然沒有反映真實狀況。直至停用bootmem分配器、普通的夥伴分配器生效,纔會設置正確的數值。

四、分配器API

夥伴系統接口對於NUMA和UMA體系結構沒有差異,可是它只能分配2的整數冪個頁(分配必須指定階),內核中的細粒度分配只能藉助於slab分配器(或者slub、slob分配器)。

  • alloc_pages(mask, order)分配2order頁並返回一個struct page的實例,表示分配的內存塊的起始頁;
  • get_zeroed_page(mask)分配一頁並返回一個page實例,頁對應的內存填充0(全部其餘函數,分配以後頁的內容是未定義的);
  • __get_free_pages(mask, order)和__get_free_page(mask)的工做方式與上述函數相同,但返回分配內存塊的虛擬地址,而不是page實例;
  • get_dma_pages(gfp_mask, order)用來得到適用於DMA的頁。
  • 4個函數用於釋放再也不使用的頁:
  • free_page(struct page *)和free_pages(struct page *, order)用於將一個或2的order次冪的頁返回給內存管理子系統,內存區的起始地址由指向該內存區的第一個page實例的指針表示;
  • __free_page(addr)和__free_pages(addr, order)的語義相似於前兩個函數,但在表示須要釋放的內存區時,使用了虛擬內存地址而不是page實例。

1)分配掩碼

分配器API中的mask參數,稱爲掩碼,它包含了圖5所示的內容。 (GFP表示get free page)

5 GFP掩碼佈局

  • 內存域修飾符(最低4個比特位)用於指定從哪一個內存孕育分配所需的頁;
  • 標誌位在不限制從哪一個物理內存段分配內存的基礎上,改變分配器的行爲(好比查找空閒內存時的積極程度)。(具體含義及用法見源碼及手冊)

 (2)內存分配宏

經過使用標誌、內存域修飾符和各個分配函數,內核提供了一種很是靈活的內存分配體系,全部接口函數均可以追溯到一個基本函數alloc_pages_node,如圖6所示。

 

6 夥伴系統的各分配函數之間關係

  • 分配單頁的函數alloc_page和__get_free_page是藉助於宏定義的,alloc_pages也是一樣;
  • get_zeroed_page的實現是對alloc_pages使用__GFP_ZERO標誌,便可分配填充字節0的頁;
  • __get_free_pages訪問了alloc_pages,而alloc_pages又藉助了alloc_pages_node。

相似地,內存釋放函數也能夠歸約到一個主要的函數__free_pages,如圖7所示(只是調用參數不一樣)。

 

7 夥伴系統各內存釋放函數之間關係

free_pages和__free_pages之間的關係經過函數而不是宏創建,由於首先必須將虛擬地址轉換爲指向struct page的指針。

五、分配頁

內核源代碼將__alloc_pages稱之爲「夥伴系統的心臟」,由於它處理的是實質性的內存分配。

1)選擇頁

內核定義了一些函數使用的標誌,用於控制到達各水印指定的臨界狀態時的行爲。

#define ALLOC_NO_WATERMARKS 0x01 /* 徹底不檢查水印 */
#define ALLOC_WMARK_MIN 0x02 /* 使用pages_min水印 */
#define ALLOC_WMARK_LOW 0x04 /* 使用pages_low水印 */
#define ALLOC_WMARK_HIGH 0x08 /* 使用pages_high水印 */
#define ALLOC_HARDER 0x10 /* 試圖更努力地分配,即放寬限制 */
#define ALLOC_HIGH 0x20 /* 設置了__GFP_HIGH */
#define ALLOC_CPUSET 0x40 /* 檢查內存結點是否對應着指定的CPU集合 */

默認狀況下(即沒有因其餘因素帶來的壓力而須要更多的內存),只有內存域包含頁的數目至少爲zone->pages_high時,才能分配頁。這對應於ALLOC_WMARK_HIGH標誌。若是要使用較低(zone->pages_low)或最低(zone->pages_min)設置,則必須相應地設置ALLOC_WMARK_MIN或ALLOC_WMARK_LOW。ALLOC_HARDER通知夥伴系統在急

需內存時放寬分配規則。在分配高端內存域的內存時,ALLOC_HIGH進一步放寬限制。最後,ALLOC_CPUSET告知內核,內存只能從當前進程容許運行的CPU相關聯的內存結點分配,固然該選項只對NUMA系統有意義。

__alloc_pages是夥伴系統的主函數,函數比較復長,可用內存足夠時必要工做很快完成,可用內存太少或逐漸用完時,函數就會變得比較複雜。

在最簡單的情形中,分配空閒內存區只涉及調用一次get_page_from_freelist,而後返回所需數目的頁(由標號got_pg處的代碼處理)。

其餘狀況中,會進行屢次內存分配嘗試:

  • 第一次內存分配嘗試不會特別積極。若是在某個內存域中沒法找到空閒內存,則意味着內存沒剩下多少了,內核須要增長較多的工做量才能找到更多內存。內核再次遍歷備用列表中的全部內存域,每次都會喚醒負責換出頁的kswapd守護進程(任務見頁面回收和頁同步),此時,空閒內存能夠經過縮減內核緩存和頁面回收得到。
  • 此後,內核開始新的嘗試,在內存域之一查找適當的內存塊。這一次進行的搜索更爲積極,對分配標誌進行了調整,修改成一些在當前特定狀況下更有可能分配成功的標誌。同時,將水印下降到最小值。而後用修改的標誌集,再一次調用get_page_from_freelist,試圖得到所需的頁。
  • 若是再次失敗,若設置了PF_MEMALLOC或進程設置了TIF_MEMDIE標誌,會再次調用get_page_from_freelist試圖得到所需的頁(徹底忽略水印);若沒有設置PF_MEMALLOC,內核仍然還有一些選項能夠嘗試,進入一條低速路徑,分配掩碼中設置__GFP_WAIT標誌,爲使守護進程取得必定的進展,其餘進程可能進入睡眠狀態,而後使用輔助函數try_to_free_pages查找當前不急需的頁,以便換出(若是須要分配多頁,那麼per-CPU緩存中的頁也會被try_to_free_pages拿回到夥伴系統),最後內核再次調用get_page_from_freelist嘗試分配內存。
  • 若是依然申請不到內存(會涉及到一些對VFS層的影響,此處不做介紹),內核只能放棄,並向用戶返回NULL指針,並輸出一條內存請求沒法知足的警告消息。

2)移除選擇的頁

若是內核找到適當的內存域,具備足夠的空閒頁可供分配,那麼還有兩件事情須要完成。首先它必須檢查這些頁是不是連續的;其次,必須按夥伴系統的方式從free_lists移除這些頁,這可能須要分解並重排內存區。

內核將工做委託給輔助函數buffered_rmqueue完成,其代碼流程圖如圖8所示。

8 buffered_rmqueue代碼流程圖

首先,判斷階數,若爲0,則表示只請求一頁。此時,內核試圖藉助於per-CPU緩存加速請求的處理。若是緩存爲空,內核可藉機檢查緩存填充水平。若是per-CPU緩存中沒法找到適當的頁,則向緩存添加一些符合當前要求遷移類型的頁,而後從per-CPU列表移除一頁,接下來進一步處理。

若不是0,則表示請求多頁。內核調用__rmqueue(要求頁連續)會從內存域的夥伴列表中選擇適當的內存塊。若有必要,該函數會自動分解大塊內存,將未用的部分放回列表中。若分配失敗,則會返回NULL指針。全部失敗情形都跳轉到標號failed處理,這能夠確保內核到達當前點以後,page指向一系列有效的頁。在返回指針以前,prep_new_page須要作一些準備工做,以便內核可以處理這些頁(若是所選擇的頁出了問題,則該函數返回正值。在這種狀況下,分配將從頭從新開始)。

六、釋放頁

 __free_pages是一個基礎函數,用於實現內核API中全部涉及內存釋放的函數。其代碼流程圖如圖9所示。

9 __free_pages代碼流程圖

__free_pages首先判斷所需釋放的內存是單頁仍是較大的內存塊?若是釋放單頁,則不還給夥伴系統,而是置於per-CPU緩存中,對極可能出如今CPU高速緩存的頁,則放置到熱頁的列表中。出於該目的,內核提供了free_hot_page輔助函數,該函數只是做一下參數轉換,接下來調用free_hot_cold_page。若是釋放多個頁,那麼__free_pages將工做委託給__free_pages_ok,最後到__free_one_page。與其名稱不一樣,該函數不只處理單頁的釋放,也處理複合頁釋放。

七、內核中不連續頁的分配

物理上連續的映射對內核是最優的,但不可能老是成功使用。對此,內核分配了其虛擬地址空間的一部分,用於創建連續映射。如圖10所示,在IA-32系統中,緊隨直接映射的前892 MiB物理內存,在插入的8 MiB安全隙以後,是一個用於管理不連續內存的區域。這一段具備線性地址空間的全部性質。經過修改負責該區域的內核頁表,能夠將其中的頁映射到物理內存的任何地方。每一個vmalloc分配的子區域都是自包含的,與其餘vmalloc子區域經過一個內存頁分隔。相似於直接映射和vmalloc區域之間的邊界,不一樣vmalloc子區域之間的分隔也是爲防止不正確的內存訪問操做。

10 IA-32系統上內核的虛擬地址空間中的vmalloc區域

1)用vmalloc分配內存

vmalloc是一個接口函數,內核使用它來分配虛擬內存中連續但在物理內存中不必定連續的內存。

void *vmalloc(unsigned long size);

該函數只須要一個參數,用於指定所需內存區的長度(字節)。

內核對模塊的實現中,有不少使用vmalloc的地方,由於函數可能在任什麼時候候加載,若是模塊數比較多,那麼沒法保證有足夠的連續內存可用(尤爲是系統已經運行了比較長時間的狀況下)。由於用於vmalloc的內存頁老是必須映射在內核地址空間中,所以使用ZONE_HIGHMEM內存域的頁要優於其餘內存域。這使得內核能夠節省更寶貴的較低端內存域,而又不會帶來額外的壞處。

vmalloc的代碼流程圖如圖11所示。

11 vmalloc代碼流程圖

vmalloc的實現分爲三個部分,首先,get_vm_area在vmalloc地址空間中找到一個適當的區域。接下來從物理內存分配各個頁,最後將這些頁連續地映射到vmalloc區域中,完成分配虛擬內存的工做。

2)備選映射方法

  • 除了vmalloc以外,還有其餘方法能夠建立虛擬連續映射:
  • vmalloc_32的工做方式與vmalloc相同,但會確保所使用的物理內存老是能夠用普通32位指針尋址;
  • vmap使用一個page數組做爲起點,來建立虛擬連續內存區;
  • 不一樣於上述的全部映射方法,ioremap是一個特定於體系結構上的函數,它將取自物理地址空間、由系統總線用於I/O操做的一個內存塊,映射到內核的地址空間中。

3)釋放內存

有兩個函數用於向內核釋放內存,vfree用於釋放vmalloc和vmalloc_32分配的區域,而vunmap用於釋放由vmap或ioremap建立的映射。兩個函數都會歸結到__vunmap。其代碼流程圖如圖12所示。

12 __vunmap代碼流程圖

  • __vunmap首先在__remove_vm_area(由remove_vm_area在完成鎖定以後調用)中掃描該鏈表,以找到相關項;
  • 而後使用找到的vm_area實例,從頁表刪除再也不須要的項;
  • 若是__vunmap的參數deallocate_pages設置爲1(在vfree中),內核會遍歷指向所涉及的物理內存頁的page實例的指針,而後對每一項調用__free_page,將頁釋放到夥伴系統;
  • 最後釋放用於管理該內存區的內核數據結構。

八、內核映射

 

儘管vmalloc函數族可用於從高端內存域向內核映射頁幀,但這並非這些函數的實際用途。內核提供了其餘函數用於將ZONE_HIGHMEM頁幀顯式映射到內核空間。

1)持久內核映射

若是須要將高端頁幀長期映射(做爲持久映射)到內核地址空間中,必須使用kmap函數。須要映射的頁用指向page的指針指定,做爲該函數的參數。若是沒有啓用高端支持,該函數只須要返回頁的地址;若是啓用了高端支持,則相似於vmalloc,內核首先必須創建高端頁和所映射到的地址之間的關聯,在虛擬地址空間中分配一個區域以映射頁幀,最後,內核必須記錄該虛擬區域的哪些部分在使用中,哪些仍然是空閒的。

內核在IA-32平臺上vmalloc區域以後分配了一個區域,從PKMAP_BASE到FIXADDR_START,該區域用於持久映射,不一樣體系結構使用的方案是相似的。

pkmap_count是一容量爲LAST_PKMAP的整數數組,其中每一個元素都對應於一個持久映射頁。它其實是被映射頁的一個使用計數器,0意味着相關的頁沒有使用,1有特殊語義,n表明內核中有n-1處使用該頁(n≥2)。)

kmap映射的頁,若是再也不須要,必須用kunmap解除映射。

2)臨時內核映射

kmap函數不能用於中斷處理程序,由於它可能進入睡眠狀態(pkmap數組中沒有空閒位置時)。內核提供了kmap_atomic,該函數執行是原子的,比普通的kmap快速,不能用於可能進入睡眠的代碼,對於很快就須要一個臨時頁的簡短代碼是很是理想的。

kmap_atomic的定義在IA-3二、PPC、Sparc32上是特定於體系結構的,但這3種實現只有很是細微的差異,其原型是相同的。

void *kmap_atomic(struct page *page, enum km_type type)       //page是一個指向高端內存頁的管理結構的指針,type定義了所需的映射類型

(內核的固定映射機制,使之能夠在內核地址空間中訪問用於創建原子映射的內存。能夠在FIX_KMAP_BEGIN和FIX_KMAP_END之間創建一個用於映射高端內存頁的區域,該區域位於fixed_addresses數組中,準確的位置須要根據當前活動的CPU和所需映射類型計算。)

在使用kmap_atomic時不會阻塞。若是發生阻塞,那麼另外一個進程可能創建一樣類型的映射,覆蓋現存的項。

kunmap_atomic函數從虛擬內存解除一個現存的原子映射,該函數根據映射類型和虛擬地址,從頁表刪除對應的項。

3)沒有高端內存的計算機上的映射函數

許多體系結構不須要支持高端內存(好比AMD64),爲了避免須要老是區分高端內存和非高端內存體系結構,內核定義了幾個在普通內存實現兼容函數的宏(在支持高端內存的計算機上,若是停用了高端內存,也會使用這些宏)。

 1 #ifdef CONFIG_HIGHMEM
 2 ...
 3 #else
 4 static inline void *kmap(struct page *page)
 5 {
 6     might_sleep();
 7     return page_address(page);
 8 }
 9 #define kunmap(page) do { (void) (page); } while (0)
10 #define kmap_atomic(page, idx) page_address(page)
11 #define kunmap_atomic(addr, idx) do { } while (0)
12 #endif

 

6、slab分配器

相似於C語言中的malloc,slab分配器提供小塊內存,同時它也用做一個緩存,主要針對常常分配並釋放的對象。slab分配器將釋放內存塊保存在一個內部列表中,並不立刻返回給夥伴系統,以便下一次高速的內存分配。這樣內核沒必要使用夥伴系統算法,處理時間會變短,同時該內存塊仍然駐留在CPU告訴緩存的機率較高。

slab分配器有兩大好處:

  • 調用夥伴系統的操做對系統的數據和指令高速緩存有至關的影響。內核越浪費這些資源,這些資源對用戶空間進程就越不可用。更輕量級的slab分配器在可能的狀況下減小了對夥伴系統的調用。
  • 若是數據存儲在夥伴系統直接提供的頁中,那麼其地址老是出如今2的冪次的整數倍附近(許多將頁劃分爲更小塊的其餘分配方法,也有一樣的特徵)。這對CPU高速緩存的利用有負面影響,因爲這種地址分佈,使得某些緩存行過分使用,而其餘的則幾乎爲空。多處理器系統可能會加重這種不利狀況,由於不一樣的內存地址可能在不一樣的總線上傳輸,上述狀況會致使某些總線擁塞,而其餘總線則幾乎沒有使用。經過slab着色(slab coloring),slab分配器可以均勻地分佈對象,以實現均勻的緩存利用。

 一、備選分配器

在大型系統上僅slab的數據結構就須要不少GB內存。對嵌入式系統來講,slab分配器代碼量和複雜性都過高,所以誕生了slob分配器和slub分配器。

slob分配器進行了特別優化,以便減小代碼量。它圍繞一個簡單的內存塊鏈表展開,在分配內存時,使用了一樣簡單的最早適配算法(速度非最高效,不適用大型系統);

slub分配器經過將頁幀打包爲組,並經過struct page中未使用的字段來管理這些組,試圖最小化所需的內存開銷。

全部分配器的前端接口都是相同的。每一個分配器都實現了一組特定的函數,用於內存分配和緩存。

  • kmalloc、__kmalloc和kmalloc_node是通常的(特定於結點)內存分配函數;
  • kmem_cache_alloc、kmem_cache_alloc_node提供(特定於結點)特定類型的內核緩存。

13闡釋了物理頁幀、夥伴系統、通用分配器與通常內核代碼接口關聯。

13 夥伴系統、通用分配器與通常內核代碼接口關聯示意圖

二、內核中的內存管理

內核中通常的內存分配和釋放函數與C標準庫中等價函數的名稱相似,用法也幾乎相同。

  • kmalloc(size, flags)分配長度爲size字節的一個內存區,並返回指向該內存區起始處的一個void指針,若是沒有足夠內存,則結果爲NULL指針;
  • kfree(*ptr)釋放*ptr指向的內存區。

與用戶空間程序設計相比,內核還包括percpu_alloc和percpu_free函數,用於爲各個系統CPU分配和釋放所需內存區。

全部活動緩存的列表保存在/proc/slabinfo中(終端輸入cat /proc/slabinfo便可查看),包含用於標識各個緩存的字符串名稱,緩存中活動對象的數量,緩存中對象的總數(已用和未用),所管理對象的長度(按字節計算),一個slab中對象的數量,每一個slab中頁的數量,活動slab的數量,在內核決定向緩存分配更多內存時,所分配對象的數量。

三、slab分配的原理

slab分配器由一個緊密地交織的數據和內存結構的網絡組。如圖14所示,slab緩存由保存管理性數據的緩存對象和保存被管理對象的各個slab。

14 slab分配器各個部分

每一個緩存只負責一種對象類型,或提供通常性的緩衝區。各個緩存中slab的數目各有不一樣,這與已經使用的頁的數目、對象長度和被管理對象的數目有關。

系統中全部的緩存都保存在一個雙鏈表中。這使得內核有機會依次遍歷全部的緩存。

1)緩存的精細結構

15 slab緩存的精細結構

15描述了緩存各組成部分,除了管理性數據,緩存結構包括兩個特別重要的成員:

  • 指向一個數組的指針,其中保存了各個CPU最後釋放的對象;
  • 每一個內存結點都對應3個表頭,用於組織slab的鏈表。第1個鏈表包含徹底用盡的slab,第2個是部分空閒的slab,第3個是空閒的slab。

緩存結構指向一個數組,其中包含了與系統CPU數目相同的數組項。每一個元素都是一個指針,指向一個進一步的結構稱之爲數組緩存(array cache),其中包含了對應於特定系統CPU的管理數據(就整體來看,不是用於緩存)。管理性數據以後的內存區包含了一個指針數組,各個數組項指向slab中未使用的對象。

爲最好地利用CPU高速緩存,在分配和釋放對象時,採用後進先出原理(LIFO,last in first out)。內核假定剛釋放的對象仍然處於CPU高速緩存中,會盡快再次分配它。僅當per-CPU緩存爲空時,纔會用slab中的空閒對象從新填充它們。這樣,對象分配的體系就造成了一個三級的層次結構(分配成本和操做對CPU高速緩存和TLB的負面影響逐級升高):

  • 仍然處於CPU高速緩存中的per-CPU對象;
  • 現存slab中未使用的對象;
  • 剛使用夥伴系統分配的新slab中未使用的對象。

2)slab精細結構

用於每一個對象的長度進行了舍入,以知足某些對齊方式的要求,對於對齊方案,有兩種:

slab建立時使用標誌SLAB_HWCACHE_ALIGN,slab用戶能夠要求對象按硬件緩存行對齊;

若是不要求按硬件緩存行對齊,那麼內核保證對象按BYTES_PER_WORD對齊,該值是表示void指針所需字節的數目。

32位處理器上,void指針須要4個字節。所以,對有6個字節的對象,則須要8 = 2×4個字節,多餘的字節稱爲填充字節。填充字節能夠加速對slab中對象的訪問,若是使用對齊的地址,那麼在幾乎全部的體系結構上,內存的訪問都會更快。

slab的起始處是管理結構,保存了全部的管理數據(和用於鏈接緩存鏈表的鏈表元素)。其後面是一個數組,每一個(整數)數組項對應於slab中的一個對象(只有在對象沒有分配時,相應的數組項纔有意義)。此時,它指定了下一個空閒對象的索引。因爲最低編號的空閒對象的編號還保存在slab起始處的管理結構中,內核無需使用鏈表或其餘複雜的關聯機制,便可找到當前可用的全部對象。數組的最後一項老是一個結束標記,值爲BUFCTL_END。管理數組與slab對象的關係如圖16所示。

16 slab中空閒對象的管理

管理數據能夠放置在slab自身,也能夠放置到使用kmalloc分配的不一樣內存區中。內核的選擇取決於slab的長度和已用對象的數量。

最後,內核經過對象自身(page結構的一個鏈表元素lru.next和lru.prev)識別slab(以及對象駐留的緩存)。根據對象的物理內存地址,找到相關的頁,從而在全局mem_map數組中找到對應的page實例。

四、實現

 slab系統帶有大量調試選項,代碼中遍及着預處理語句:

  • 危險區(Red Zoning):在每一個對象的開始和結束處增長一個額外的內存區,其中填充已知的字節模式。若是模式被修改,程序員在分析內核內存時注意到,可能某些代碼訪問了不屬於它們的內存區;
  • 對象毒化(Object Poisoning):在創建和釋放slab時,將對象用預約義的模式填充。若是在對象分配時注意到該模式已經改變,此處已經發生了未受權訪問等。

1)數據結構

每一個緩存由kmem_cache結構的一個實例表示。結構內容以下:

 1 struct kmem_cache {
 2 /* 1) per-CPU數據,在每次分配/釋放期間都會訪問 */
 3     struct array_cache *array[NR_CPUS];    //指向數組的指針,每一個數組項都對應於系統中的一個CPU。每一個數組項都包含了另外一個指針,指向下文討論的array_cache結構的實例
 4 /* 2) 可調整的緩存參數。由cache_chain_mutex保護 */
 5     unsigned int batchcount;    //per-CPU列表爲空時,從緩存的slab中獲取對象的數目;還表示在緩存增加時分配的對象數目
 6     unsigned int limit;    //指定了per-CPU列表中保存的對象的最大數目
 7     unsigned int shared;
 8     unsigned int buffer_size;        //指定了緩存中管理的對象的長度
 9     u32 reciprocal_buffer_size;
10 /* 3) 後端每次分配和釋放內存時都會訪問 */
11     unsigned int flags; /* 常數標誌 */
12     unsigned int num; /* 每一個slab中對象的數量 */
13 /* 4) 緩存的增加/縮減 */
14     unsigned int gfporder;    //指定了slab包含的頁數目以2爲底的對數
15 /* 強制的GFP標誌,例如GFP_DMA */
16     gfp_t gfpflags;        
17     size_t colour;     //顏色的最大數目
18     unsigned int colour_off;     //着色偏移 
19     struct kmem_cache *slabp_cache;    //slab頭部的管理數據存儲在slab外部時,指向分配所需內存的通常性緩存; slab頭部在slab上時,爲NULL指針
20     unsigned int slab_size;
21     unsigned int dflags;     // 標誌集合,描述slab的「動態性質」
22     void (*ctor)(struct kmem_cache *, void *);    //指向在對象建立時調用的構造函數
23 /* 5) 緩存建立/刪除 */
24     const char *name;    //是一個字符串,包含該緩存的名稱
25     struct list_head next;    //用於將kmem_cache的全部實例保存在全局鏈表cache_chain上
26 /* 6) 統計量 */
27 ...
28     struct kmem_list3 *nodelists[MAX_NUMNODES];
29 };

2)初始化

爲初始化slab數據結構,內核須要若干小內存塊(最適合由kmalloc分配),可是隻有slab系統啓用以後,才能使用kmalloc,於是內核藉助了一些技巧。

kmem_cache_init函數用於初始化slab分配器。它在內核初始化階段(start_kernel)、夥伴系統啓用以後調用。第一步:kmem_cache_init建立系統中的第一個slab緩存,以便爲kmem_cache的實例提供內存,內核使用的主要是在編譯時建立的靜態數據;第二步:kmem_cache_init接下來初始化通常性的緩存,用做kmalloc內存的來源(針對所需的各個緩存長度,分別調用kmem_cache_create);第三步:在kmem_cache_init的最後一步,把到如今爲止一直使用的數據結構的全部靜態實例化的成員,用kmalloc動態分配的版本替換。

3)建立緩存

建立新的slab緩存必須調用kmem_cache_create,這是一個冗長的過程,其代碼示意圖如圖17所示。

17 kmem_cache_create的代碼流程圖

  • 首先,進行參數檢查,以確保沒有指定無效值,而後才執行第一個重要步驟,計算對齊所需填充字節數;
  • 接着在數據對齊值計算完畢後,分配struct kmem_cache一個實例(一個獨立的slab緩存,名爲cache_cache);
  • 而後肯定是否將slab頭存儲在slab之上,若是對象長度大於頁幀的1/8,則將頭部管理數據存儲在slab以外,不然存儲在slab上,隨後,增長對象的長度size,直至對應到上文計算的對齊值;
  • 至此,對象長度定義完成,如下定義slab長度(選擇適當的頁數做爲slab長度)。
  • 首先,內核經過calculate_slab_order進行迭代,找到理想的slab長度(基於給定對象長度,cache_estimate針對特定的頁數,來計算對象數目、浪費的空間、着色所需的空間);
  • 接着計算顏色(即slab上的浪費空間除以顏色偏移量的商);
  • 而後經過enable_cpucache產生per-CPU緩存;
  • 最後將初始化過的kmem_cache實例添加到全局鏈表,表頭爲cache_chain。

4)分配對象

kmem_cache_alloc用於從特定的緩存獲取對象,它須要用於獲取對象的緩存,以及精確描述分配特徵的標誌變量兩個參數,結果多是指向分配內存區的指針,也可能分配失敗返回NULL。

18 kmem_cache_alloc的代碼流程圖

  • 首先,kmem_cache_alloc基於參數相同的內部函數__cache_alloc,後者能夠直接調用(採用這種結構,目的是儘快合併kmalloc和kmem_cache_alloc的實現)。__cache_allloc只是一個前端函數,只執行了全部必要的鎖定操做。實際工做委託給____cache_alloc進行;
  • 而後選擇被緩存對象,若是在per-CPU緩存中有對象,則從緩存中獲取對象後返回;若是沒有對象在per-CPU緩存中,須要調用cache_alloc_refill從新填充緩存,內核先按必定的順序掃描slab,若是找到空閒對象則返回,若是沒有找到空閒對象,那麼必須使用cache_grow擴大緩存(見下)。

5)緩存的增加

19描述了cache_grow代碼流程圖。

19 cache_grow的代碼流程圖

  • 首先計算顏色和偏移量,若是達到了顏色的最大數目,則內核從新開始從0計數,這自動致使零偏移;
  • 接着使用kmem_getpages輔助函數從夥伴系統逐頁分配所需的內存空間;
  • 而後調用相關的alloc_slabmgmt函數分配所需空間;
  • 接下來,調用slab_map_pages建立slab的各頁與slab或緩存之間的關聯;
  • 隨後cache_init_objs調用各個對象的構造器函數(假若有的話),初始化新slab中的對象;
  • 最後將徹底初始化的slab添加到緩存的slabs_free鏈表中。

6)釋放對象

當一個分配的對象再也不須要時,使用kmem_cache_free將其返回給slab分配器。圖20爲kmem_cache_free代碼流程圖。

20 kmem_cache_free的代碼流程圖

當即調用__cache_free,根據per-CPU緩存的狀態不一樣,執行如下兩種操做:

  • 若是per-CPU緩存中的對象數目低於容許的限制,則在其中存儲一個指向緩存中對象的指針;
  • 不然,必須將一些對象(準確的數目由array_cache->batchcount給出)從緩存移回slab,從編號最低的數組元素開始:緩存的實現依據先進先出原理,這些對象在數組中已經很長時間,所以不太可能仍然駐留在CPU高速緩存中。此後,將slab從新插入到緩存的鏈表中,若是刪除後,slab中全部對象都未使用,則置於slabs_free鏈表,若是同時包含使用和未使用對象,則插入slabs_partial鏈表。

7)銷燬緩存

若是要銷燬只包含未使用對象的一個緩存,則必須調用kmem_cache_destroy函數。該函數主要在刪除模塊時調用,此時須要將分配的內存都釋放。主要步驟以下:

  • 依次掃描slabs_free鏈表上的slab。首先對每一個slab上的每一個對象調用析構器函數,而後將slab的內存空間返回給夥伴系統;
  • 釋放用於per-CPU緩存的內存空間;
  • cache_cache鏈表移除相關數據。

五、通用緩存

若是不涉及對象緩存,而是傳統意義上的分配/釋放內存,則必須調用kmalloc和kfree函數。kmalloc和kfree實現爲slab分配器的前端,其語義儘量地模仿C標準庫malloc和free。

7、處理器高速緩存和TLB控制

內核提供了一些命令直接做用於處理器的高速緩存和TLB,用於維護緩存內容的一致性,確保不出現不正確和過期的緩存項。

不一樣體系結構上,高速緩存和TLB的硬件實現不一樣,所以內核須要創建TLB和高速緩存的視圖,在其中考慮到各類不一樣的硬件實現方法,兼顧各個體系結構的特定性質。

TLB的語義抽象是將虛擬地址轉換爲物理地址的一種機制;

內核將高速緩存視爲經過虛擬地址快速訪問數據的一種機制,該機制無需訪問物理內存。數

據和指令高速緩存並不老是明確區分。

內核中各個特定於CPU的部分都必須提供下列函數(即便只是空操做),以便控制TLB和高速緩存:

  • flush_tlb_all和flush_cache_all刷出整個TLB/高速緩存;
  • flush_tlb_mm和flush_cache_mm刷出全部屬於地址空間mm的TLB/高速緩存項;
  • flush_tlb_range和flush_cache_range刷出地址範圍vma->vm_mm中虛擬地址start和end之間的全部TLB/高速緩存項;
  • flush_tlb_page和flush_cache_page刷出虛擬地址在[page, page + PAGE_SIZE]範圍內全部的TLB/高速緩存項;
  • update_mmu_cache在處理頁失效以後調用。它在處理器的內存管理單元MMU中加入信息,使得虛擬地址address由頁表項pte描述。

內核對數據和指令高速緩存不做區分。若是須要區分,特定於處理器的代碼可根據vm_area_struct->flags的VM_EXEC標誌位是否設置,來肯定高速緩存包含的是指令仍是數據。

flush_cache_...和flush_tlb_...函數常常成對出現。

好比在使用fork複製進程地址空間時,操做的順序是:刷出高速緩存、操做內存、刷出TLB,緣由有兩個:

  • 若是順序反過來,那麼在TLB刷出以後、正確信息提供以前,多處理器系統中的另外一個CPU可能從進程的頁表取得錯誤的信息。
  • 在刷出高速緩存時,某些體系結構須要依賴TLB中的「虛擬->物理」轉換規則。flush_tlb_mm必須在flush_cache_mm以後執行,以確保這點。
相關文章
相關標籤/搜索