1. 程式人生 > >記憶體管理(下)

記憶體管理(下)

五、實體記憶體的管理

在核心初始化完成後,記憶體管理的責任由夥伴系統(高效、高速)承擔。

1、夥伴系統的結構

系統記憶體中的每個實體記憶體頁(頁幀),都對應於一個struct page例項。每個記憶體域都關聯了一個struct zone的例項,其中儲存了用於管理夥伴資料的主要陣列。

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 夥伴系統和記憶體域/結點之間的關係

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中)

3、初始化記憶體域和結點資料結構

在啟動期間,各體系結構相關的程式碼需要確立系統中各記憶體域的頁幀的邊界(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分配器、普通的夥伴分配器生效,才會設定正確的數值。

4、分配器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的指標。

5、分配頁

核心原始碼將__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需要做一些準備工作,以便核心能夠處理這些頁(如果所選擇的頁出了問題,則該函式返回正值。在這種情況下,分配將從頭重新開始)。

6、釋放頁

 __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。與其名稱不同,該函式不僅處理單頁的釋放,也處理複合頁釋放。

7、核心中不連續頁的分配

物理上連續的對映對核心是最優的,但不可能總是成功使用。對此,核心分配了其虛擬地址空間的一部分,用於建立連續對映。如圖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,將頁釋放到夥伴系統;
  • 最後釋放用於管理該記憶體區的核心資料結構。

8、核心對映

 

儘管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-32、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

 

六、slab分配器

類似於C語言中的malloc,slab分配器提供小塊記憶體,同時它也用作一個快取,主要針對經常分配並釋放的物件。slab分配器將釋放記憶體塊儲存在一個內部列表中,並不馬上返回給夥伴系統,以便下一次高速的記憶體分配。這樣核心不必使用夥伴系統演算法,處理時間會變短,同時該記憶體塊仍然駐留在CPU告訴快取的概率較高。

slab分配器有兩大好處:

  • 呼叫夥伴系統的操作對系統的資料和指令快取記憶體有相當的影響。核心越浪費這些資源,這些資源對使用者空間程序就越不可用。更輕量級的slab分配器在可能的情況下減少了對夥伴系統的呼叫。
  • 如果資料儲存在夥伴系統直接提供的頁中,那麼其地址總是出現在2的冪次的整數倍附近(許多將頁劃分為更小塊的其他分配方法,也有同樣的特徵)。這對CPU快取記憶體的利用有負面影響,由於這種地址分佈,使得某些快取行過度使用,而其他的則幾乎為空。多處理器系統可能會加劇這種不利情況,因為不同的記憶體地址可能在不同的總線上傳輸,上述情況會導致某些匯流排擁塞,而其他匯流排則幾乎沒有使用。通過slab著色(slab coloring),slab分配器能夠均勻地分佈物件,以實現均勻的快取利用。

 1、備選分配器

在大型系統上僅slab的資料結構就需要很多GB記憶體。對嵌入式系統來說,slab分配器程式碼量和複雜性都太高,因此誕生了slob分配器和slub分配器。

slob分配器進行了特別優化,以便減少程式碼量。它圍繞一個簡單的記憶體塊連結串列展開,在分配記憶體時,使用了同樣簡單的最先適配演算法(速度非最高效,不適用大型系統);

slub分配器通過將頁幀打包為組,並通過struct page中未使用的欄位來管理這些組,試圖最小化所需的記憶體開銷。

所有分配器的前端介面都是相同的。每個分配器都實現了一組特定的函式,用於記憶體分配和快取。

  • kmalloc、__kmalloc和kmalloc_node是一般的(特定於結點)記憶體分配函式;
  • kmem_cache_alloc、kmem_cache_alloc_node提供(特定於結點)特定型別的核心快取。

13闡釋了物理頁幀、夥伴系統、通用分配器與一般核心程式碼介面關聯。

13 夥伴系統、通用分配器與一般核心程式碼介面關聯示意圖

2、核心中的記憶體管理

核心中一般的記憶體分配和釋放函式與C標準庫中等價函式的名稱類似,用法也幾乎相同。

  • kmalloc(size, flags)分配長度為size位元組的一個記憶體區,並返回指向該記憶體區起始處的一個void指標,如果沒有足夠記憶體,則結果為NULL指標;
  • kfree(*ptr)釋放*ptr指向的記憶體區。

與使用者空間程式設計相比,核心還包括percpu_alloc和percpu_free函式,用於為各個系統CPU分配和釋放所需記憶體區。

所有活動快取的列表儲存在/proc/slabinfo中(終端輸入cat /proc/slabinfo即可檢視),包含用於標識各個快取的字串名稱,快取中活動物件的數量,快取中物件的總數(已用和未用),所管理物件的長度(按位元組計算),一個slab中物件的數量,每個slab中頁的數量,活動slab的數量,在核心決定向快取分配更多記憶體時,所分配物件的數量。

3、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例項。

4、實現

 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連結串列移除相關資料。

5、通用快取

如果不涉及物件快取,而是傳統意義上的分配/釋放記憶體,則必須呼叫kmalloc和kfree函式。kmalloc和kfree實現為slab分配器的前端,其語義儘可能地模仿C標準庫malloc和free。

七、處理器快取記憶體和TLB控制

核心提供了一些命令直接作用於處理器的快取記憶體和TLB,用於維護快取內容的一致性,確保不出現不正確和過時的快取項。

不同體系結構上,快取記憶體和TLB的硬體實現不同,因此核心需要建立TLB和快取記憶體的檢視,在其中考慮到各種不同的硬體實現方法,兼顧各個體系結構的特定性質。

TLB的語義抽象是將虛擬地址轉換為實體地址的一種機制;

核心將快取記憶體視為通過虛擬地址快速訪問資料的一種機制,該機制無需訪問實體記憶體。數

據和指令快取記憶體並不總是明確區分。

核心中各個特定於CPU的部分都必須提供下列函式(即使只是空操作),以便控制TLB和快取記憶體:

  • flush_tlb_all和flush_cache_all刷出整個TLB/快取記憶體;
  • flush_tlb_mm和