內存管理(上)

1、概述

內存管理涵蓋領域:node

  • 內存中的物理內存頁管理;
  • 分配大塊內存的夥伴系統;
  • 分配較小塊內存的slab、slub和slob分配器;
  • 分配連續內存塊的vmalloc機制;
  • 進程的地址空間。

Linux內核通常將處理器的虛擬地址分爲兩個部分,以IA-32爲例,地址空間在用戶進程和內核之間的劃分比例爲3:1。4GB的虛擬地址空間,3GB用於用戶空間,1GB用於內核。linux

IA-32系統中,假設物理內存4GB,則全部物理內存沒法直接映射到內核態(內核的地址空間小於1GB),對於用戶空間的內存,沒法進行直接映射。爲了便於管理,定義內核空間896MB內存用於DMA和直接映射到物理內存,剩餘的部分被稱爲高端內存,內核藉助內核空間剩餘的128MB空間實現對全部物理內存(896MB-4GB)的管理。數組

若是採用PAE(page address extension)技術,在IA-32中能夠管理64GB內存,但每次只能尋址一個4GB的內存段。(目前內存超過4GB的IA-32系統不多,主要使AMD64體系結構替代,目前64位無需高端內存模式)緩存

有兩種類型計算機,對應着兩種方法管理內存:安全

  • UMA(uniform memory access,一致內存訪問)計算機將可用內存以連續方式組織起來(可能有小的缺口)。SMP系統中的每一個處理器訪問各個內存區都是一樣快。
  • NUMA(non-uniform memory access,非一致內存訪問)計算機老是多處理器計算機。系統的各個CPU都有本地內存,可支持特別快速的訪問。各個處理器之間經過總線鏈接起來,以支持對其餘CPU的本地內存的訪問,速度比訪問本地內存慢。

1 UMA和NUMA系統性能優化

1描述了UMA核NUMA計算機的區別。兩種類型計算機的混合也是存在的,好比在UMA系統中,內存不是連續的,有比較大的洞,這裏應用NUMA體系結構的原理一般有所幫助,可使內核的內存訪問更簡單。網絡

內核會區分3 種配置選項: FLATMEM(平坦內存模型)、DISCONTIGMEM(不連續內存模型)和SPARSEMEM(稀疏內存模型)。SPARSEMEM和DISCONTIGMEM實際上做用相同。通常認爲SPARSEMEM更可能是試驗性的,不那麼穩定,但有一些性能優化。DISCONTIGMEM相關代碼更穩定一些,但不具有內存熱插拔之類的新特性。多數配置中都使用該內存組織類型爲FLATMEM(內核的默認值)。真正的NUMA須要設置CONFIG_NUMA,對於UMA系統,則不用不考慮(不意味着NUMA相關的數據結構能夠徹底忽略。因爲UMA系統能夠在地址空間包含比較大的洞時選擇配置選項CONFIG_DISCONTIGMEM,這種狀況下在不採用NUMA技術的系統上也會有多個內存結點)。圖2綜述了內存佈局有關的各類可能配置選項。數據結構

2 UMA和NUMA計算機上可能的內存配置(平攤、稀疏和不連續模型)app

*對於分配階(alloction order),表示內存區中頁的數目取以2爲底的對數。異步

2、(N)UMA模型中的內存組織

 內核對一致和非一致內存訪問系統使用相同的數據結構,在UMA系統上,只使用一個NUMA結點來管理整個系統內存,內存管理的其餘部分認爲是在處理一個僞NUMA系統。

一、概述

3是對於NUMA系統內存劃分的示例。

3 NUMA系統中的內存劃分

將內存劃分爲結點。每一個結點關聯到系統中的一個處理器,在內核中表示爲pg_data_t的實例。各個節點又劃分爲內存域(從低到高爲DMA內存域,普通內存域和超出內核段物理內存的高端內存域)。

內核使用常量枚舉系統中的全部內存域:

 1 enum zone_type {
 2 #ifdef CONFIG_ZONE_DMA
 3     ZONE_DMA,        //標記適合DMA的內存域,長度依賴於處理器類型
 4 #endif
 5 #ifdef CONFIG_ZONE_DMA32
 6     ZONE_DMA32,        //標記了使用32位地址字可尋址、適合DMA的內存域。
 7 #endif
 8     ZONE_NORMAL,        //標記了可直接映射到內核段的普通內存域
 9 #ifdef CONFIG_HIGHMEM
10     ZONE_HIGHMEM,
11 #endif
12     ZONE_MOVABLE,
13     MAX_NR_ZONES
14 };

二、數據結構 

pg_data_t是用於表示結點的基本元素,定義以下:

 1 typedef struct pglist_data {
 2     struct zone node_zones[MAX_NR_ZONES];     //包含告終點中各內存域的數據結構
 3     struct zonelist node_zonelists[MAX_ZONELISTS];    //指定了備用結點及其內存域的列表
 4     int nr_zones;    //結點中不一樣內存域的數目
 5     struct page *node_mem_map;    //指向page實例數組的指針,用於描述結點的全部物理內存頁
 6     struct bootmem_data *bdata;     //指向自舉內存分配器數據結構的實例
 7     unsigned long node_start_pfn;    //該NUMA結點第一個頁幀的邏輯編號
 8     unsigned long node_present_pages; /* 物理內存頁的總數 */
 9     unsigned long node_spanned_pages; /* 物理內存頁的總長度,包含洞在內 */
10     int node_id;    //全局結點ID,系統中的NUMA結點都從0開始編號
11     struct pglist_data *pgdat_next;    //鏈接到下一個內存結點
12     wait_queue_head_t kswapd_wait;    //交換守護進程(swap daemon)的等待隊列
13     struct task_struct *kswapd;    //指向負責該結點的交換守護進程的task_struct
14     int kswapd_max_order;        //用來定義須要釋放的區域的長度
15 } pg_data_t;
struct pglist_data

 

若是系統中結點多於一個,內核會維護一個位圖,用以提供各個結點的狀態信息。狀態是用位掩碼指定的,可以使用下列值:

 1 enum node_states {
 2     N_POSSIBLE, /* 結點在某個時候可能變爲聯機,用於內存的熱插拔 */
 3     N_ONLINE, /* 結點是聯機的 ,用於內存的熱插拔*/
 4     N_NORMAL_MEMORY, /* 結點有普通內存域 */
 5 #ifdef CONFIG_HIGHMEM
 6     N_HIGH_MEMORY, /* 結點有普通或高端內存域 */
 7 #else
 8     N_HIGH_MEMORY = N_NORMAL_MEMORY,
 9 #endif
10     N_CPU, /* 結點有一個或多個CPU ,用於CPU的熱插拔*/
11     NR_NODE_STATES
12 };
enum node_states

若是內核編譯爲只支持單個結點(即便用平坦內存模型),則沒有結點位圖。

內核使用zone結構來描述內存域。

 1 struct zone {
 2 /*該結構是由ZONE_PADDING分隔爲幾個部分,內核使用ZONE_PADDING宏生成「填充」字段添加到結構中,以確保每一個自旋鎖都處於自身的緩存行中。還使用了編譯器關鍵字__cacheline_maxaligned_in_smp,用以實現最優的高速緩存對齊方式。該結構的最後兩個部分也經過填充字段彼此分隔開來。二者都不包含鎖,主要目的是將數據保持在一個緩存行中,便於快速訪問,從而無需從內存加載數據*/
 3 
 4 /*一般由頁分配器訪問的字段 */
 5     unsigned long pages_min, pages_low, pages_high;     //頁換出時使用的「水印」(內存不足內核能夠將頁寫入硬盤)
 6     unsigned long lowmem_reserve[MAX_NR_ZONES];    //分別爲各類內存域指定了若干頁,用於一些不管如何都不能失敗的關鍵性內存分配
 7     struct per_cpu_pageset pageset[NR_CPUS];    //用於實現每一個CPU的熱/冷頁幀列表
 8 /*
 9 * 不一樣長度的空閒區域
10 */
11     spinlock_t lock;
12     struct free_area free_area[MAX_ORDER];    //用於實現夥伴系統
13 ZONE_PADDING(_pad1_)
14 /*第二部分涉及的結構成員,用來根據活動狀況對內存域中使用的頁進行編目。*/
15 /* 一般由頁面收回掃描程序訪問的字段 */
16     spinlock_t lru_lock;
17     struct list_head active_list;    //活動頁的集合
18     struct list_head inactive_list;    //不活動頁的集合
19     unsigned long nr_scan_active;    //在回收內存時須要掃描的活動頁的數目
20     unsigned long nr_scan_inactive;    //在回收內存時須要掃描的不活動頁的數目
21     unsigned long pages_scanned; /* 上一次回收以來掃描過的頁 */
22 unsigned long flags; /* 內存域標誌*/
23 /* 內存域統計量 */
24     atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];    //維護了大量有關該內存域的統計信息
25     int prev_priority;    //存儲了上一次掃描操做掃描該內存域的優先級
26 ZONE_PADDING(_pad2_)
27 /* 不多使用或大多數狀況下只讀的字段 */
28     wait_queue_head_t * wait_table;
29     unsigned long wait_table_hash_nr_entries;
30     unsigned long wait_table_bits;    // 以上三個變量實現了一個等待隊列,可供等待某一頁變爲可用的進程使用
31 /* 支持不連續內存模型的字段。 */
32     struct pglist_data *zone_pgdat;    //創建內存域和父結點之間的關聯
33     unsigned long zone_start_pfn;    //內存域第一個頁幀的索引
34     unsigned long spanned_pages; /* 總長度,包含空洞 */
35     unsigned long present_pages; /* 內存數量(除去空洞) */
36 /*
37 * 不多使用的字段:
38 */
39     char *name;
40 } ____cacheline_maxaligned_in_smp;
struct zone

內存域水印值:須要爲關鍵性分配保留的內存空間的最小值;該值該值隨可用內存的大小而非線性增加,保存在全局變量min_free_kbytes中。

數據結構中水印值的填充由init_per_zone_pages_min處理,該函數由內核在啓動期間調用,無需顯式調用。圖4爲主內存大小與可用於關鍵性分配的內存空間最小值之間的關係示意圖。

4 主內存大小與可用於關鍵性分配的內存空間最小值之間的關係

冷熱頁:熱頁表示頁已加載到CPU告訴緩存,冷頁則沒有。struct zone的pageset成員用於實現冷熱分配器(hot-n-cold allocator)。在多處理器系統上每一個CPU都有一個或多個高速緩存,各個CPU的管理必須是獨立的。

儘管內存域可能屬於一個特定的NUMA結點,於是關聯到某個特定的CPU,但其餘CPU的高速緩存仍然能夠包含該內存域中的頁。因此,每一個處理器均可以訪問系統中全部的頁,儘管速度不一樣(特定於內存域的數據結構不只要考慮到所屬NUMA結點相關的CPU,還必須照顧到系統中其餘的CPU)。

相關數據結構及定義以下:

1 struct zone {
2 ...
3     struct per_cpu_pageset pageset[NR_CPUS];    // NR_CPUS是是內核支持的CPU的最大數目
4 ...
5 };
struct zone

 

1 struct per_cpu_pages {
2     int count; /* 列表中頁數 */
3     int high; /* 頁數上限水印,在須要的狀況下清空列表 */
4     int batch; /* 添加/刪除多頁塊的時候,塊的大小 */
5     struct list_head list; /* 頁的鏈表 */
6 };
struct per_cpu_pages

頁幀:頁幀表明系統內存的最小單位,對內存中的每一個頁都會建立struct page的一個實例(內核盡力保持這個結構儘量小)。

頁的普遍使用,增長了保持結構長度的難度:內存管理的許多部分都使用頁,用於各類不一樣的用途。對此,內核的一個部分可能徹底依賴於struct page提供的特定信息,而該信息對內核的另外一部分可能徹底無用,該部分依賴於struct page提供的其餘信息,爲了使struct page結構儘量小,C語言的聯合(union)對某些字段進行了雙重解釋。

page的定義以下:

 1 struct page {
 2     unsigned long flags; /* 原子標誌,有些狀況下會異步更新 */
 3     atomic_t _count; /* 使用計數,見下文。 */
 4     union {
 5         atomic_t _mapcount; /* 內存管理子系統中映射的頁表項計數,
 6 * 用於表示頁是否已經映射,還用於限制逆向映射搜索。
 7 */
 8         unsigned int inuse; /* 用於SLUB分配器:對象的數目 */
 9 };
10     union {
11         struct {
12             unsigned long private; /* 由映射私有,不透明數據:
13 *若是設置了PagePrivate,一般用於buffer_heads;
14 *若是設置了PageSwapCache,則用於swp_entry_t;
15 * 若是設置了PG_buddy,則用於表示夥伴系統中的階。
16 */
17             struct address_space *mapping; /* 若是最低位爲0,則指向inode
18 * address_space,或爲NULL。
19 * 若是頁映射爲匿名內存,最低位置位,
20 * 並且該指針指向anon_vma對象:
21 * 參見下文的PAGE_MAPPING_ANON。
22 */
23         };
24 ...
25         struct kmem_cache *slab; /* 用於SLUB分配器:指向slab的指針 */
26         struct page *first_page; /* 用於複合頁的尾頁,指向首頁 */
27 };
28 union {
29     pgoff_t index; /* 在映射內的偏移量 */
30     void *freelist; /* SLUB: freelist req. slab lock */
31 };
32     struct list_head lru; /* 換出頁列表,例如由zone->lru_lock保護的active_list!
33 */
34 #if defined(WANT_PAGE_VIRTUAL)
35     void *virtual; /* 內核虛擬地址(若是沒有映射則爲NULL,即高端內存) */
36 #endif /* WANT_PAGE_VIRTUAL */
37 };
38                                                                                         
struct page

體系結構無關的頁標誌頁的不一樣屬性經過一系列頁標誌描述,存儲爲struct page的flags成員中的各個比特位。這些標誌獨立於使用的體系結構,於是沒法提供特定於CPU或計算機的信息(該信息保存在頁表中)。各個標誌是由page-flags.h中的宏定義的,此外還生成了一些宏,用於標誌的設置、刪除、查詢。這樣作時,內核遵照了一種通用的命名方案(宏的實現是原子操做)。

3、頁表

層次化的頁表用於支持對大地址空間的快速、高效的管理。

頁表用於創建用戶進程的虛擬地址空間和系統物理內存(內存、頁幀)之間的關聯。頁表用於向每一個進程提供一致的虛擬地址空間。應用程序看到的地址空間是一個連續的內存區。該表也將虛擬內存頁映射到物理內存,於是支持共享內存的實現(幾個進程同時共享的內存),還能夠在不額外增長物理內存的狀況下,將頁換出到塊設備來增長有效的可用內存空間。

頁表管理分爲兩個部分,第一部分依賴於體系結構(不一樣的CPU的實現有一些較大差異),第二部分是體系結構無關的。

一、數據結構

C語言中,一般用void *定義可能指向內存中任何字節位置的指針。在Linux支持的全部體系結構中,sizeof(void *) == sizeof(unsigned long),因此它們之間進行強制轉換不會損失信息。內存管理多使用unsigned long類型變量,它更易於處理和操做(技術上二者都有效)。

(1)內存地址的分解

根據四級頁表結構的須要,虛擬內存地址分爲5部分(4個表項用於選擇頁,1個索引表示頁內位置)。各個體系結構不只地址字長度不一樣,並且地址字拆分的方式也不一樣。內核定義了宏將地址分解爲各個份量。

5 分解虛擬內存地址

5爲用比特位移定義地址字各份量位置的方法。每一個指針末端的幾個比特位,用於指定所選頁幀內部的位置。比特位的具體數目由PAGE_SHIFT指定。PMD_SHIFT指定了頁內偏移量和最後一級頁表項所需比特位的總數。PUD_SHIFT 由PMD_SHIFT加上中間層頁表索引所需的比特位長度組成。PGDIR_SHIFT由PUD_SHIFT加上上層頁表索引所需的比特位長度組成。

在各級頁目錄/頁表中所能存儲的指針數目經過宏定義肯定。PTRS_PER_PGD指定了全局頁目錄中項的數目,PTRS_PER_PMD對應於中間頁目錄,PTRS_PER_PUD對應於上層頁目錄中項的數目,PTRS_PER_PTE則是頁表中項的數目。兩級頁表的體系結構會將PTRS_PER_PMD和PTRS_PER_PUD定義爲1。

(2)頁表的格式

內核提供了4個數據結構來標識

  • pgd_t用於全局頁目錄項。
  • pud_t用於上層頁目錄項。
  • pmd_t用於中間頁目錄項。
  • pte_t用於直接頁表項。

此外,根據不一樣的體系結構,內核經過宏定義或內聯函數定義了一些用於分析頁表項的標準函數。

儘管使用了C結構來表示頁表項,但大多數頁表項都只有一個成員,一般是unsigned long類型,AMD64體系結構的頁表項以下:

1 typedef struct { unsigned long pte; } pte_t;
2 typedef struct { unsigned long pmd; } pmd_t;
3 typedef struct { unsigned long pud; } pud_t;
4 typedef struct { unsigned long pgd; } pgd_t;

使用struct而不是基本類型,以確保頁表項的內容只能由相關的輔助函數處理,而決不能直接訪問。

虛擬地址分爲幾個部分,用做各個頁表的索引。根據使用的體系結構字長不一樣,各個單獨的部分長度小於32或64個比特位。內核(以及處理器)使用32或64位類型來表示頁表項(無論頁表的級數)。這意味着並不是表項的全部比特位都存儲了有用的數據,即下一級表的基地址。多餘的比特位用於保存額外的信息。

(3)特定於PTE的信息

最後一級頁表中的項不只包含了指向頁的內存位置的指針,還在上述的多餘比特位包含了與頁有關的附加信息(有關頁訪問控制的一些信息)。

_PAGE_PRESENT指定了虛擬內存頁是否存在於內存中;CPU每次訪問頁時,會自動設置_PAGE_ACCESSED(活躍程度);_PAGE_DIRTY表示該頁內容是否已修改;_PAGE_FILE的數值與_PAGE_DIRTY相同,用於頁不在內存中的時候;_PAGE_USER指定是否容許用戶空間代碼訪問該頁;_PAGE_READ、_PAGE_WRITE和_PAGE_EXECUTE指定了普通的用戶進程是否容許讀取、寫入、執行該頁中的機器代碼;IA-32和AMD64提供了_PAGE_BIT_NX,做爲保護位用於將頁標記爲不可執行的功能。

內核還定義了各類函數,用於查詢和設置內存頁與體系結構相關的狀態。(不詳述)

二、頁表項的建立和操做

 

6 用於建立新頁表項的函數

全部體系結構都實現了圖6中的函數,以便於內存管理代碼建立和銷燬頁表。

4、初始化內存管理

許多CPU須要顯式設置適用於Linux內核的內存模型(IA-32須要切換到保護模式),內核在內存管理徹底初始化以前就須要使用內存,在系統啓動過程期間,使用一個額外的簡化形式的內存管理模塊,而後丟棄,確認系統中內存的總數量,及其在各個結點和內存域之間的分配狀況。

一、創建數據結構

對相關數據結構的初始化是從全局啓動start_kernel開始的,因爲內存管理在內核中很是重要,特定於體系結構的設置步驟中檢測內存並肯定系統中內存的分佈狀況後,會當即執行內存的初始化。此時,已經對各類系統內存模式生成了一個pgdata_t實例,用於保存諸如結點中內存數量以及內存在各個內存域之間分配狀況的信息。

(1)先決條件

內核在mm/page_alloc.c中定義了一個pg_data_t實例(稱做contig_page_data)管理全部的系統內存,以保證內存管理代碼的可移植性。

體系結構相關的初始化代碼將numnodes變量設置爲系統中結點的數目。在UMA系統上由於只有一個(形式上的)結點,所以該數量是1。

(2)系統啓動

7 從內存管理看內核初始化

7是start_kernel代碼流程圖,首先是設置函數setup_arch(其中會初始化自舉分配器);而後setup_per_cpu_areas定義靜態per_cpu變量(非SMP系統上爲空操做);接着build_all_zonelists創建結點和內存域的數據結構;以後mem_init停用bootmem分配器並遷移到實際的內存管理函數;最後setup_per_cpu_pageset爲pageset數組的第一個數組元素分配內存。

(3)結點和內存域初始化

內核定義了內存的一個層次結構,首先試圖分配「廉價的」內存。若是失敗,則根據訪問速度和容量,逐漸嘗試分配「更昂貴的」內存。高端內存是最廉價的,普通內存域的狀況其次,DMA內存域最昂貴。內核還針對當前內存結點的備選結點,定義了一個等級次序(build_zonelists做用)。用於當前結點全部內存域的內存都用盡時,肯定備選結點。

build_all_zonelists將全部工做都委託給__build_all_zonelists,後者對系統中的各個NUMA結點分別調用build_zonelists,對全部內存建立內存域列表。UMA系統中,build_zonelists在當前處理的結點和系統中其餘結點的內存域之間創建一種等級次序(這種次序在指望的結點內存域中沒有空閒內存時很重要)。

8以某個系統的結點2爲例,描述了一個備用列表在屢次循環中不斷填充的過程。(numnodes=4)

 

8 連續填充備用列表

第一步以後,列表中的分配目標是高端內存,接下來是第二個結點的普通和DMA內存域。第二步檢查大於當前結點編號的一個結點,最後檢查編號小於當前結點的結點生成備用列表項。備用列表中項的數目通常沒法準確知道,由於系統中不一樣結點的內存域配置可能並不相同。所以列表的最後一項賦值爲空指針,顯式標記列表結束。

對總數N個結點中的結點m來講,內核生成備用列表時,選擇備用結點的順序老是:m、m+一、m+二、…、N一、0、一、…、m1。

9爲4個結點系統中爲第三個結點創建的備用列表。

 

9 完成的備用列表

二、特定於體系結構的設置

(1)內核在內存中的佈局

IA-32體系結構中,對於其物理內存,前4KB(第一個頁幀)留給BIOS用,接下來640K空白,該區域後用於映射各類ROM(一般是系統BIOS和顯卡ROM)。IA-32內核使用0x100000做爲起始地址,對應於內存中第二兆字節的開始處。

內核佔據的內存分爲幾個段,其邊界保存在變量中:

  • _text和_etext是代碼段的起始和結束地址,包含了編譯後的內核代碼;
  • 數據段位於_etext和_edata之間,保存了大部份內核變量;
  • 初始化數據在內核啓動過程結束後再也不須要(例如,包含初始化爲0的全部靜態全局變量的BSS段)保存在最後一段,從_edata到_end。

編譯器在編譯時,只有在目標文件連接完成後,才能知道內核大小確切的數值,接下來則打包爲二進制文件。該操做是由arch/arch/vmlinux.ld.S控制的(對IA-32來講,該文件是arch/x86/vmlinux_32.ld.S),其中也劃定了內核的內存佈局。

每次編譯內核時,都生成一個文件System.map並保存在源代碼目錄下(cat System.map命令查看)。除了全部其餘(全局)變量、內核定義的函數和例程的地址,該文件還包括_text,_etext,_edata,_end等常數的值。

用戶和內核地址空間之間採用標準的3 : 1劃分,內核段的起始地址0xC0000000,該地址是虛擬地址,由於物理內存映射到虛擬地址空間的時候,採用了從該地址開始的線性映射方式。減去0xC0000000,則可得到對應的物理地址。

(2)初始化步驟

內核載入內存、初始化的彙編部分執行完畢後,內核須要執行的內存管理相關操做流程如圖10所示。

10 IA-32系統上內存初始化的代碼流程圖

  • 首先調用machine_specific_memory_setup,建立一個列表,包括系統佔據的內存區和空閒內存區。
  • 內核接下來用parse_cmdline_early分析命令行,主要關注相似mem=XXX[KkmM]、highmem=XXX[kKmM]或memmap=XXX[KkmM]" "@XXX[KkmM]之類的參數。
  • 下一個主要步驟在setup_memory中執行,該函數有兩個版本。一個用於連續內存系統,另外一個用於不連續內存系統,兩者的效果相同:肯定(每一個結點)可用的物理內存頁的數目;初始化bootmem分配器;接下來分配各類內存區。
  • paging_init初始化內核頁表並啓用內存分頁。
  • 最後調用zone_sizes_init會初始化系統中全部結點的pgdata_t實例。
  • AMD64計算機上內存有關的初始化次序很是相似。

 (3)分頁機制初始化

 IA-32系統上,內核一般將總4GB可用虛擬地址空間按3:1的劃分,劃分緣由有兩個:

  • 在用戶應用程序的執行切換到核心態時,內核必須裝載在一個可靠的環境中。所以有必要將地址空間的一部分分配給內核專用。
  • 物理內存頁則映射到內核地址空間的起始處,以便內核直接訪問,而無需複雜的頁表操做(安全問題:不能讓全部物理內存頁都映射到用戶空間進程的地址空間中)。

雖然用於用戶層進程的虛擬地址部分隨進程切換而改變,可是內核部分老是相同的。

如圖11所示,內核地址空間自身又分爲各個段,標明瞭虛擬地址空間的各個區域的用途(與物理內存的分配無關)。

11 IA-32系統上內核地址空間劃分

地址的第一段與系統的全部物理內存頁映射到內核虛擬地址空間中,是一個簡單的線性平移。直接映射區域從0xC0000000到high_memory(最高896MB)地址,若是物理內存超過896MB,則內核沒法直接映射所有物理內存。剩餘的128MB(超過896MB到1GB的部分)用做如下三個用途:

  • 虛擬內存中連續、但物理內存中不連續的內存區,能夠在vmalloc區域分配。該機制一般用於用戶過程,內核自身會試圖盡力避免非連續的物理地址。內核一般會成功,由於大部分大的內存塊都在啓動時分配給內核,那時內存的碎片尚不嚴重。若是在已經運行了很長時間的系統上,在內核須要物理內存時,就可能出現可用空間不連續的狀況。此類狀況,主要出如今動態加載模塊時;
  • 持久映射用於將高端內存域中的非持久頁映射到內核中;
  • 固定映射是與物理地址空間中的固定頁關聯的虛擬地址空間項,但具體關聯的頁幀能夠自由選擇。它與經過固定公式與物理內存關聯的直接映射頁相反,虛擬固定映射地址與物理內存位置之間的關聯能夠自行定義,定義後不能改變。優勢在於,在編譯時對此類地址的處理相似於常數,內核一啓動即爲其分配了物理地址(地址解引用比普通指針快)。

__VMALLOC_RESERVE設置了vmalloc區域的長度, MAXMEM表示內核能夠直接尋址的物理內存的最大可能數量,內核中,將內存劃分爲各個區域是經過圖11所示的各個常數控制的(常數值可能不一樣),直接映射的邊界由high_memory指定)

vmalloc區域的起始地址,取決於在直接映射物理內存時,使用了多少虛擬地址空間內存(high_memory)。

vmalloc區域在何處結束取決因而否啓用了高端內存支持。若是沒有啓用,那麼就不須要持久映射區域,由於整個物理內存均可以直接映射。

IA-32系統中,將虛擬地址空間按3:1比例劃分不是惟一選項,經過修改__PAGE_OFFSET實現。按3∶1以外的比例劃分地址空間,在特定的應用場景下多是有意義的。好比對主要在內核中運行代碼的計算機,例如網絡路由器。

 

paging_init負責創建只能用於內核的頁表,用戶空間沒法訪問。代碼流程圖如圖12所示。

12 paging_init代碼流程圖

  • 首先劃分虛擬地址空間;
  • 而後由pagetable_init以swapper_pg_dir(全局頁目錄指針)爲基礎初始化系統的頁表。接下來啓用在全部現代IA-32系統上可用的兩個擴展(只有一些很是古老的Pentium實現不支持這些)。
  • 對超大內存頁的支持。這些特別標記的頁,其長度爲4 MiB,而不是普通的4 KiB。該選項用於不會換出的內核頁。增長頁大小,意味着須要的頁表項變少,這對地址轉換後備緩衝器(TLB)的影響是正面的,能夠減小其中來自內核的緩存項。
  • 若有可能,內核頁會設置另外一個屬性(__PAGE_GLOBAL),這也是__PAGE_KERNEL和__PAGE_KERNEL_EXEC變量中__PAGE_GLOBAL比特位已經置位的緣由。這些變量指定內核自身分配頁幀時的標誌集,所以這些設置會自動地應用到內核頁。
  • 接着藉助於kernel_physical_mapping_init,將物理內存頁(或前896 MiB)映射到虛擬地址空間中從PAGE_OFFSET開始的位置。內核接下來掃描各個頁目錄的全部相關項,將指針設置爲正確的值。
  • 隨後創建固定映射項和持久內核映射對應的內存區。一樣是用適當的值填充頁表。
  • 最後將cr3寄存器設置爲指向全局頁目錄(swapper_pg_dir)的指針,激活新的頁表,使用__flush_all_tlb刷出於TLB緩存項仍然包含了啓動時分配的一些內存地址數據,將kmap_init初始化全局變量kmap_pte(用於從高端內存域將頁映射到內核地址空間)。

 

對於冷熱(per-CPU)緩存,zone_pcp_init負責初始化該緩存。

 1 static __devinit void zone_pcp_init(struct zone *zone)
 2 {
 3     int cpu;
 4     unsigned long batch = zone_batchsize(zone);
 5     for (cpu = 0; cpu < NR_CPUS; cpu++) {
 6         setup_pageset(zone_pcp(zone,cpu), batch);
 7 }
 8     if (zone->present_pages)
 9         printk(KERN_DEBUG " %s zone: %lu pages, LIFO batch:%lu\n",
10         zone->name, zone->present_pages, batch);
11 }
__devinit void zone_pcp_init

在用zone_batchsize算出批量大小(用於計算最小和最大填充水平的基礎)後,代碼將遍歷系統中的全部CPU,同時調用setup_pageset填充每一個per_cpu_pageset實例的常量。在調用該函數時,使用了zone_pcp宏來選擇與當前CPU相關的內存域的pageset實例。

 對於冷熱頁的水印計算,內核首先會計算出batch,batch = zone->present_pages / 1024,大約至關於內存域中的頁數的0.25‰。對熱頁來講,下限爲0,上限爲6*batch,緩存中頁的平均數量大約是4*batch,由於內核不會讓緩存水平降到過低。batch*4至關於內存域中頁數的千分之一。冷頁列表的水印稍低一些,由於冷頁並不放置到緩存中,只用於一些不太關注性能的操做,其上限是batch值的兩倍。

(4)註冊活動內存區

活動內存區就是不包含空洞的內存區。必須使用add_active_range在全局變量early_node_map中註冊內存區。當前註冊的內存區數目記載在nr_nodemap_entries中。不一樣內存區的最大數目由MAX_ACTIVE_REGIONS給出。該值能夠由特定於體系結構的代碼使用CONFIG_MAX_ACTIVE_REGIONS設置。若是不設置,在默認狀況下內核容許每一個內存結點註冊256個活動內存區(若是在超過32個結點的系統上,容許每一個NUMA結點註冊50個內存區)。

  • IA-32上:除了調用add_active_range以外,zone_sizes_init函數以頁幀爲單位,存儲了不一樣內存區的邊界。物理內存頁映射到從PAGE_OFFSET開始的虛擬地址空間,而物理內存的前16 MiB適合於DMA操做,十六進制表示就是前0x1000000字節。用virt_to_phys轉換,能夠得到物理內存地址,而右移PAGE_SHIFT位則至關於除以頁大小,計算最後獲得適用於DMA的頁數。
  • AMD64上:根據BIOS提供的信息遍歷全部的內存區,並針對每一個內存區找到活動內存區,所以與IA-32對比,add_active_range可能會調用屢次。

(5)AMD64地址空間設置

64位地址空間沒有高端內存域,當前使用的地址字寬度多爲48位,能夠尋址256TB地址空間。虛擬地址使用的是64位指針,虛擬地址空間的某些部分沒法尋址,對此,採用了符號擴展(sign extension)的方式做爲解決方案。

13 AMD64計算機上虛擬地址空間到物理地址空間可能的映射方式

虛擬地址的低47位,即[0, 46],能夠任意設置。而比特位[47, 63]的值老是相同的:或者全0,或者全1。此類地址稱之爲規範的。所以整個地址空間劃分爲3部分:下半部、上半部、兩者之間的禁用區。

14 AMD64系統上虛擬地址空間組織

14位Linux內核在AMD64計算機上對虛擬地址空間佈局示意圖。

可訪問的地址空間的整個下半部用做用戶空間,而整個上半部專用於內核。因爲兩個空間都極大,無須調整劃分比例之類的參數。

內核地址空間起始於一個起防禦做用的空洞,以防止偶然訪問地址空間的非規範部分,若是發生這種狀況,處理器會引起一個通常性保護異常(general protection exception);另外一個防禦性空洞位於一致映射內存區和vmalloc內存區之間,後者的範圍從VMALLOC_START到VMALLOC_END。

虛擬內存映射(virtual memory map,VMM)內存區緊接着vmalloc內存區以後,長爲1 TiB。只有內核使用了稀疏內存模型,該內存區纔是有用的。

內核代碼段映射到從__START_KERNEL_MAP開始的內存區,還有一個編譯時可配置的偏移量CONFIG_PHYSICAL_START。

最後,還必須提供一些空間用於映射模塊,該內存區從MODULES_VADDR到MODULES_END。

三、啓動過程期間的內存管理

在啓動過程期間,儘管內存管理還沒有初始化,但內核仍然須要分配內存以建立各類數據結構。bootmem分配器用於在啓動階段早期分配內存。對該分配器的需求集中於簡單性方面,而非通用性。所以內核開發者決定實現一個最早適配(first-fit)分配器用於在啓動階段管理內存。該分配器使用一個位圖來管理頁,位圖比特位的數目與系統中物理內存頁的數目相同。比特位爲1,表示已用頁;比特位爲0,表示空閒頁。在須要分配內存時,分配器逐位掃描位圖,直至找到一個能提供足夠連續頁的位置。該過程不是很高效,由於每次分配都必須從頭掃描比特鏈。所以在內核徹底初始化以後,不能將該分配器用於內存管理。夥伴系統(連同slab、slub或slob分配器)是一個好得多的備選方案。

(1)數據結構

最早適配分配器必須管理一些數據。內核(爲系統中的每一個結點都)提供了一個bootmem_data結構的實例,用於該用途。該結構所需的內存沒法動態分配,必須在編譯時分配給內核。

UMA系統上該分配的實現與CPU無關(NUMA系統採用了特定於體系結構的解決方案)。bootmem_data結構定義以下:

1 typedef struct bootmem_data {
2     unsigned long node_boot_start;    //保存了系統中第一個頁的編號,大多數體系結構下都是零
3     unsigned long node_low_pfn;        //能夠直接管理的物理地址空間中最後一頁的編號
4     void *node_bootmem_map;    //指向存儲分配位圖的內存區的指針
5     unsigned long last_offset;    //若沒有請求分配整個頁,則last_offset用做該頁內部的偏移量
6     unsigned long last_pos;    //上一次分配的頁的編號
7     unsigned long last_success;    //定位圖中上一次成功分配內存的位置
8     struct list_head list;    //全部註冊的分配器的鏈表的表頭
9 } bootmem_data_t;
struct bootmem_data

(2)初始化

bootmem分配器的初始化是一個特定於體系結構的過程,還取決於所述計算機的內存佈局。

15爲IA-32系統上初始化bootmem分配器涉及的各個步驟。圖16爲AMD64系統上初始化的步驟。

 

15 IA-32計算機上初始化bootmem分配器

IA-32系統中,首先由setup_memory分析檢測到的內存區,以找到低端內存區中最大的頁幀號,基於該信息,setup_bootmem_allocator負責發起全部必要的步驟,以初始化bootmem分配器。它首先調用通用函數init_bootmem將全部頁標記爲已用,而後由register_bootmem_low_pages經過將位圖中對應的比特位清零,釋放全部潛在可用的內存頁, bootmem分配器須要一些內存頁管理分配位圖,須要首先調用reserve_bootmem分配這些內存頁。

有一些其餘的內存區已經在使用中,必須相應地標記出來。所以,還須要用reserve_bootmem註冊相應的頁。須要註冊的內存區的確切數目,高度依賴於內核配置。其餘的reserve_bootmem調用則分配與內核配置相關的內存區,例如,用於ACPI數據或SMP啓動時的配置。

 

16 AMD64計算機上初始化bootmem分配器

AMD64中,由contig_initmem負責分配任務。首先,bootmem_bootmap_bitmap計算bootmem位圖所需頁的數目,而後,使用init_bootmem將該信息填充到體系結構無關的bootmem數據結構中,最後,調用一次reserve_bootmem註冊bootmem分配位圖所需的空間。與IA-32相反,AMD64不須要爲遺留信息在內存中分配空間。

 (3)對內核接口

內核提供了各類函數,用於在初始化期間分配內存:

  • alloc_bootmem(size)和alloc_bootmem_pages(size)按指定大小在ZONE_NORMAL內存域分配內存。
  • alloc_bootmem_low和alloc_bootmem_low_pages的工做方式相似於上述函數,只是從ZONE_DMA內存域分配內存。
  • 內核提供了free_bootmem函數來釋放內存。它須要兩個參數:須要釋放的內存區的起始地址和長度。

NUMA系統上等價函數的名稱爲free_bootmem_node,它須要一個額外的參數來指定結點。

(4)釋放初始化數據

許多內核代碼塊和數據表只在系統初始化階段須要,此後這些數據即可丟棄,free_initmem負責釋放用於初始化的內存區,並將相關的頁返回給夥伴系統。內核提供了兩個屬性(__init和__initcall)用於標記初始化函數和數據,這些必須置於函數或數據的聲明以前。

1 #define __init __attribute__ ((__section__ (".init.text"))) __cold
2 #define __initdata __attribute__ ((__section__ (".init.data")))

__attribute__是一個特殊的GNU C關鍵字,屬性就經過該關鍵字使用。__section__屬性用於通知編譯器將隨後的數據或函數分別寫入二進制文件的.init.data和.init.text段(ELF文件結構)。前綴__cold還通知編譯器,通向該函數的代碼路徑可能性較低,即該函數不會常常調用,對初始化函數一般是這樣。

相關文章
相關標籤/搜索