日期 | 內核版本 | 架構 | 作者 | GitHub | CSDN |
---|---|---|---|---|---|
2016-06-14 | Linux-4.7 | X86 & arm | gatieme | LinuxDeviceDrivers | Linux內存管理 |
#1 前景回顧
前面我們講到服務器體系(SMP, NUMA, MPP)與共享存儲器架構(UMA和NUMA)
#1.1 UMA和NUMA兩種模型
共享存儲型多處理機有兩種模型
-
均勻存儲器存取(Uniform-Memory-Access,簡稱UMA)模型
-
非均勻存儲器存取(Nonuniform-Memory-Access,簡稱NUMA)模型
UMA模型
物理存儲器被所有處理機均勻共享。所有處理機對所有存儲字具有相同的存取時間,這就是為什么稱它為均勻存儲器存取的原因。每台處理機可以有私用高速緩存,外圍設備也以一定形式共享。
NUMA模型
NUMA模式下,處理器被划分成多個"節點"(node), 每個節點被分配有的本地存儲器空間。 所有節點中的處理器都可以訪問全部的系統物理存儲器,但是訪問本節點內的存儲器所需要的時間,比訪問某些遠程節點內的存儲器所花的時間要少得多。
#1.2 (N)UMA模型中linux內存的機構
非一致存儲器訪問(NUMA)模式下
-
處理器被划分成多個"節點"(node), 每個節點被分配有的本地存儲器空間. 所有節點中的處理器都可以訪問全部的系統物理存儲器,但是訪問本節點內的存儲器所需要的時間,比訪問某些遠程節點內的存儲器所花的時間要少得多
-
內存被分割成多個區域(BANK,也叫"簇"),依據簇與處理器的"距離"不同, 訪問不同簇的代碼也會不同. 比如,可能把內存的一個簇指派給每個處理器,或則某個簇和設備卡很近,很適合DMA,那么就指派給該設備。因此當前的多數系統會把內存系統分割成2塊區域,一塊是專門給CPU去訪問,一塊是給外圍設備板卡的DMA去訪問
在UMA系統中, 內存就相當於一個只使用一個NUMA節點來管理整個系統的內存. 而內存管理的其他地方則認為他們就是在處理一個(偽)NUMA系統.
Linux把物理內存划分為三個層次來管理
層次 | 描述 |
---|---|
存儲節點(Node) | CPU被划分為多個節點(node), 內存則被分簇, 每個CPU對應一個本地物理內存, 即一個CPU-node對應一個內存簇bank,即每個內存簇被認為是一個節點 |
管理區(Zone) | 每個物理內存節點node被划分為多個內存管理區域, 用於表示不同范圍的內存, 內核可以使用不同的映射方式映射物理內存 |
頁面(Page) | 內存被細分為多個頁面幀, 頁面是最基本的頁面分配的單位 | |
#2 內存節點node
##2.1 為什么要用node來描述內存
這點前面是說的很明白了, NUMA結構下, 每個處理器CPU與一個本地內存直接相連, 而不同處理器之前則通過總線進行進一步的連接, 因此相對於任何一個CPU訪問本地內存的速度比訪問遠程內存的速度要快
Linux適用於各種不同的體系結構, 而不同體系結構在內存管理方面的差別很大. 因此linux內核需要用一種體系結構無關的方式來表示內存.
因此linux內核把物理內存按照CPU節點划分為不同的node, 每個node作為某個cpu結點的本地內存, 而作為其他CPU節點的遠程內存, 而UMA結構下, 則任務系統中只存在一個內存node, 這樣對於UMA結構來說, 內核把內存當成只有一個內存node節點的偽NUMA
##2.2 內存結點的概念
CPU被划分為多個節點(node), 內存則被分簇, 每個CPU對應一個本地物理內存, 即一個CPU-node對應一個內存簇bank,即每個內存簇被認為是一個節點
系統的物理內存被划分為幾個節點(node), 一個node對應一個內存簇bank,即每個內存簇被認為是一個節點
內存被划分為結點. 每個節點關聯到系統中的一個處理器, 內核中表示為pg_data_t
的實例. 系統中每個節點被鏈接到一個以NULL結尾的pgdat_list
鏈表中<而其中的每個節點利用pg_data_tnode_next
字段鏈接到下一節.而對於PC這種UMA結構的機器來說, 只使用了一個成為contig_page_data的靜態pg_data_t結構.
內存中的每個節點都是由pg_data_t描述,而pg_data_t由struct pglist_data定義而來, 該數據結構定義在include/linux/mmzone.h, line 615
在分配一個頁面時, Linux采用節點局部分配的策略, 從最靠近運行中的CPU的節點分配內存, 由於進程往往是在同一個CPU上運行, 因此從當前節點得到的內存很可能被用到
##2.3 pg_data_t描述內存節點
表示node的數據結構為typedef struct pglist_data pg_data_t
, 這個結構定義在include/linux/mmzone.h, line 615中,結構體的內容如下
/* * The pg_data_t structure is used in machines with CONFIG_DISCONTIGMEM * (mostly NUMA machines?) to denote a higher-level memory zone than the * zone denotes. * * On NUMA machines, each NUMA node would have a pg_data_t to describe * it's memory layout. * * Memory statistics and page replacement data structures are maintained on a * per-zone basis. */ struct bootmem_data; typedef struct pglist_data { /* 包含了結點中各內存域的數據結構 , 可能的區域類型用zone_type表示*/ struct zone node_zones[MAX_NR_ZONES]; /* 指點了備用結點及其內存域的列表,以便在當前結點沒有可用空間時,在備用結點分配內存 */ struct zonelist node_zonelists[MAX_ZONELISTS]; int nr_zones; /* 保存結點中不同內存域的數目 */ #ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */ struct page *node_mem_map; /* 指向page實例數組的指針,用於描述結點的所有物理內存頁,它包含了結點中所有內存域的頁。 */ #ifdef CONFIG_PAGE_EXTENSION struct page_ext *node_page_ext; #endif #endif #ifndef CONFIG_NO_BOOTMEM /* 在系統啟動boot期間,內存管理子系統初始化之前, 內核頁需要使用內存(另外,還需要保留部分內存用於初始化內存管理子系統) 為解決這個問題,內核使用了自舉內存分配器 此結構用於這個階段的內存管理 */ struct bootmem_data *bdata; #endif #ifdef CONFIG_MEMORY_HOTPLUG /* * Must be held any time you expect node_start_pfn, node_present_pages * or node_spanned_pages stay constant. Holding this will also * guarantee that any pfn_valid() stays that way. * * pgdat_resize_lock() and pgdat_resize_unlock() are provided to * manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG. * * Nests above zone->lock and zone->span_seqlock * 當系統支持內存熱插撥時,用於保護本結構中的與節點大小相關的字段。 * 哪調用node_start_pfn,node_present_pages,node_spanned_pages相關的代碼時,需要使用該鎖。 */ spinlock_t node_size_lock; #endif /* /*起始頁面幀號,指出該節點在全局mem_map中的偏移 系統中所有的頁幀是依次編號的,每個頁幀的號碼都是全局唯一的(不只是結點內唯一) */ unsigned long node_start_pfn; unsigned long node_present_pages; /* total number of physical pages 結點中頁幀的數目 */ unsigned long node_spanned_pages; /* total size of physical page range, including holes 該結點以頁幀為單位計算的長度,包含內存空洞 */ int node_id; /* 全局結點ID,系統中的NUMA結點都從0開始編號 */ wait_queue_head_t kswapd_wait; /* 交換守護進程的等待隊列, 在將頁幀換出結點時會用到。后面的文章會詳細討論。 */ wait_queue_head_t pfmemalloc_wait; struct task_struct *kswapd; /* Protected by mem_hotplug_begin/end() 指向負責該結點的交換守護進程的task_struct。 */ int kswapd_max_order; /* 定義需要釋放的區域的長度 */ enum zone_type classzone_idx; #ifdef CONFIG_COMPACTION int kcompactd_max_order; enum zone_type kcompactd_classzone_idx; wait_queue_head_t kcompactd_wait; struct task_struct *kcompactd; #endif #ifdef CONFIG_NUMA_BALANCING /* Lock serializing the migrate rate limiting window */ spinlock_t numabalancing_migrate_lock; /* Rate limiting time interval */ unsigned long numabalancing_migrate_next_window; /* Number of pages migrated during the rate limiting time interval */ unsigned long numabalancing_migrate_nr_pages; #endif #ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT /* * If memory initialisation on large machines is deferred then this * is the first PFN that needs to be initialised. */ unsigned long first_deferred_pfn; #endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */ #ifdef CONFIG_TRANSPARENT_HUGEPAGE spinlock_t split_queue_lock; struct list_head split_queue; unsigned long split_queue_len; #endif } pg_data_t;
字段 | 描述 |
---|---|
node_zones | 每個Node划分為不同的zone,分別為ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM |
node_zonelists | 這個是備用節點及其內存域的列表,當當前節點的內存不夠分配時,會選取訪問代價最低的內存進行分配。分配內存操作時的區域順序,當調用free_area_init_core()時,由mm/page_alloc.c文件中的build_zonelists()函數設置 |
nr_zones | 當前節點中不同內存域zone的數量,1到3個之間。並不是所有的node都有3個zone的,比如一個CPU簇就可能沒有ZONE_DMA區域 |
node_mem_map | node中的第一個page,它可以指向mem_map中的任何一個page,指向page實例數組的指針,用於描述該節點所擁有的的物理內存頁,它包含了該頁面所有的內存頁,被放置在全局mem_map數組中 |
bdata | 這個僅用於引導程序boot 的內存分配,內存在啟動時,也需要使用內存,在這里內存使用了自舉內存分配器,這里bdata是指向內存自舉分配器的數據結構的實例 |
node_start_pfn | pfn是page frame number的縮寫。這個成員是用於表示node中的開始那個page在物理內存中的位置的。是當前NUMA節點的第一個頁幀的編號,系統中所有的頁幀是依次進行編號的,這個字段代表的是當前節點的頁幀的起始值,對於UMA系統,只有一個節點,所以該值總是0 |
node_present_pages | node中的真正可以使用的page數量 |
node_spanned_pages | 該節點以頁幀為單位的總長度,這個不等於前面的node_present_pages,因為這里面包含空洞內存 |
node_id | node的NODE ID 當前節點在系統中的編號,從0開始 |
kswapd_wait | node的等待隊列,交換守護列隊進程的等待列表 |
kswapd_max_order | 需要釋放的區域的長度,以頁階為單位 |
classzone_idx | 這個字段暫時沒弄明白,不過其中的zone_type是對ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGH,ZONE_MOVABLE,__MAX_NR_ZONES的枚舉 |
##2.5 結點的內存管理域
typedef struct pglist_data { /* 包含了結點中各內存域的數據結構 , 可能的區域類型用zone_type表示*/ struct zone node_zones[MAX_NR_ZONES]; /* 指點了備用結點及其內存域的列表,以便在當前結點沒有可用空間時,在備用結點分配內存 */ struct zonelist node_zonelists[MAX_ZONELISTS]; int nr_zones; /* 保存結點中不同內存域的數目 */ } pg_data_t;
node_zones[MAX_NR_ZONES]數組保存了節點中各個內存域的數據結構,
而node_zonelist則指定了備用節點以及其內存域的列表, 以便在當前結點沒有可用空間時, 在備用節點分配內存.
nr_zones存儲了結點中不同內存域的數目
##2.6 結點的內存頁面
typedef struct pglist_data { struct page *node_mem_map; /* 指向page實例數組的指針,用於描述結點的所有物理內存頁,它包含了結點中所有內存域的頁。 */ /* /*起始頁面幀號,指出該節點在全局mem_map中的偏移 系統中所有的頁幀是依次編號的,每個頁幀的號碼都是全局唯一的(不只是結點內唯一) */ unsigned long node_start_pfn; unsigned long node_present_pages; /* total number of physical pages 結點中頁幀的數目 */ unsigned long node_spanned_pages; /* total size of physical page range, including holes 該結點以頁幀為單位計算的長度,包含內存空洞 */ int node_id; /* 全局結點ID,系統中的NUMA結點都從0開始編號 */ } pg_data_t;
其中node_mem_map是指向頁面page實例數組的指針, 用於描述結點的所有物理內存頁. 它包含了結點中所有內存域的頁.
node_start_pfn是該NUMA結點的第一個頁幀的邏輯編號. 系統中所有的節點的頁幀是一次編號的, 每個頁幀的編號是全局唯一的. node_start_pfn在UMA系統中總是0, 因為系統中只有一個內存結點, 因此其第一個頁幀編號總是0.
node_present_pages指定了結點中頁幀的數目, 而node_spanned_pages則給出了該結點以頁幀為單位計算的長度. 二者的值不一定相同, 因為結點中可能有一些空洞, 並不對應真正的頁幀.
##2.7 交換守護進程
typedef struct pglist_data { wait_queue_head_t kswapd_wait; /* 交換守護進程的等待隊列, 在將頁幀換出結點時會用到。后面的文章會詳細討論。 */ wait_queue_head_t pfmemalloc_wait; struct task_struct *kswapd; /* Protected by mem_hotplug_begin/end() 指向負責該結點的交換守護進程的task_struct。 */ };
kswapd指向了負責將該結點的交換守護進程的task_struct. 在將頁幀換出結點時會喚醒該進程.
kswap_wait是交換守護進程(swap daemon)的等待隊列
而kswapd_max_order用於頁交換子系統的實現, 用來定義需要釋放的區域的長度.
#3 結點狀態
##3.1 結點狀態標識node_states
內核用enum node_state變量標記了內存結點所有可能的狀態信息, 其定義在include/linux/nodemask.h?v=4.7, line 381
enum node_states { N_POSSIBLE, /* The node could become online at some point 結點在某個時候可能變成聯機*/ N_ONLINE, /* The node is online 節點是聯機的*/ N_NORMAL_MEMORY, /* The node has regular memory 結點是普通內存域 */ #ifdef CONFIG_HIGHMEM N_HIGH_MEMORY, /* The node has regular or high memory 結點是普通或者高端內存域*/ #else N_HIGH_MEMORY = N_NORMAL_MEMORY, #endif #ifdef CONFIG_MOVABLE_NODE N_MEMORY, /* The node has memory(regular, high, movable) */ #else N_MEMORY = N_HIGH_MEMORY, #endif N_CPU, /* The node has one or more cpus */ NR_NODE_STATES };
狀態 | 描述 |
---|---|
N_POSSIBLE | 結點在某個時候可能變成聯機 |
N_ONLINE | 節點是聯機的 |
N_NORMAL_MEMORY | 結點是普通內存域 |
N_HIGH_MEMORY | 結點是普通或者高端內存域 |
N_MEMORY | 結點是普通,高端內存或者MOVEABLE域 |
N_CPU | 結點有一個或多個CPU |
其中N_POSSIBLE, N_ONLINE和N_CPU用於CPU和內存的熱插拔.
對內存管理有必要的標志是N_HIGH_MEMORY和N_NORMAL_MEMORY, 如果結點有普通或高端內存則使用N_HIGH_MEMORY, 僅當結點沒有高端內存時才設置N_NORMAL_MEMORY
N_NORMAL_MEMORY, /* The node has regular memory 結點是普通內存域 */ #ifdef CONFIG_HIGHMEM N_HIGH_MEMORY, /* The node has regular or high memory 結點是高端內存域*/ #else /* 沒有高端內存域, 仍設置N_NORMAL_MEMORY */ N_HIGH_MEMORY = N_NORMAL_MEMORY, #endif
同樣ZONE_MOVABLE內存域同樣用類似的方法設置, 僅當系統中存在ZONE_MOVABLE內存域內存域(配置了CONFIG_MOVABLE_NODE參數)時, N_MEMORY才被設定, 否則則被設定成N_HIGH_MEMORY, 而N_HIGH_MEMORY設定與否同樣依賴於參數CONFIG_HIGHMEM的設定
#ifdef CONFIG_MOVABLE_NODE N_MEMORY, /* The node has memory(regular, high, movable) */ #else N_MEMORY = N_HIGH_MEMORY, #endif
##3.2 結點狀態設置函數
內核提供了輔助函數來設置或者清楚位域活特定結點的一個比特位
static inline int node_state(int node, enum node_states state) static inline void node_set_state(int node, enum node_states state) static inline void node_clear_state(int node, enum node_states state) static inline int num_node_state(enum node_states state)
此外宏for_each_node_state(__node, __state)用來迭代處於特定狀態的所有結點,
#define for_each_node_state(__node, __state) \ for_each_node_mask((__node), node_states[__state])
而for_each_online_node(node)則負責迭代所有的活動結點.
如果內核編譯只支持當個結點(即使用平坦內存模型), 則沒有結點位圖, 上述操作該位圖的函數則變成空操作, 其定義形式如下, 參見include/linux/nodemask.h?v=4.7, line 406
參見內核
#if MAX_NUMNODES > 1 /* some real function */ #else /* some NULL function */ #endif
#4 查找內存結點
node_id作為全局節點id。 系統中的NUMA結點都是從0開始編號的
##4.1 linux-2.4中的實現
pgdat_next指針域和pgdat_list內存結點鏈表
而對於NUMA結構的系統中, 在linux-2.4.x之前的內核中所有的節點,內存結點pg_data_t都有一個next指針域pgdat_next指向下一個內存結點. 這樣一來系統中所有結點都通過單鏈表pgdat_list鏈接起來, 其末尾是一個NULL指針標記.
這些節點都放在該鏈表中,均由函數init_bootmem_core()初始化結點
for_each_pgdat(pgdat)來遍歷node節點
那么內核提供了宏函數for_each_pgdat(pgdat)來遍歷node節點, 其只需要沿着node_next以此便立即可, 參照include/linux/mmzone.h?v=2.4.37, line 187
/** * for_each_pgdat - helper macro to iterate over nodes * @pgdat - pg_data_t * variable * Meant to help with common loops of the form * pgdat = pgdat_list; * while(pgdat) { * ... * pgdat = pgdat->node_next; * } */ #define for_each_pgdat(pgdat) \ for (pgdat = pgdat_list; pgdat; pgdat = pgdat->node_next)
##4.2 linux-3.x~4.x的實現
node_data內存節點數組
在新的linux3.x~linux4.x的內核中,內核移除了pg_data_t的pgdat_next之指針域, 同時也刪除了pgdat_list鏈表, 參見Remove pgdat list和Remove pgdat list ver.2
但是定義了一個大小為MAX_NUMNODES類型為pg_data_t
數組node_data,數組的大小根據CONFIG_NODES_SHIFT的配置決定. 對於UMA來說,NODES_SHIFT為0,所以MAX_NUMNODES的值為1.
for_each_online_pgdat遍歷所有的內存結點
內核提供了for_each_online_pgdatfor_each_online_pgdat(pgdat)來遍歷節點
/** * for_each_online_pgdat - helper macro to iterate over all online nodes * @pgdat - pointer to a pg_data_t variable */ #define for_each_online_pgdat(pgdat) \ for (pgdat = first_online_pgdat(); \ pgdat; \ pgdat = next_online_pgdat(pgdat))
其中first_online_pgdat可以查找到系統中第一個內存節點的pg_data_t信息, next_online_pgdat則查找下一個內存節點.
下面我們來看看first_online_pgdat和next_online_pgdat是怎么實現的.
first_online_node和next_online_node返回結點編號
由於沒了next指針域pgdat_next和全局node鏈表pgdat_list, 因而內核提供了first_online_node指向第一個內存結點, 而通過next_online_node來查找其下一個結點, 他們是通過狀態node_states的位圖來查找結點信息的, 定義在include/linux/nodemask.h?v4.7, line 432
// http://lxr.free-electrons.com/source/include/linux/nodemask.h?v4.7#L432 #define first_online_node first_node(node_states[N_ONLINE]) #define first_memory_node first_node(node_states[N_MEMORY]) static inline int next_online_node(int nid) { return next_node(nid, node_states[N_ONLINE]); }
first_online_node和next_online_node返回所查找的node結點的編號, 而有了編號, 我們直接去node_data數組中按照編號進行索引即可去除對應的pg_data_t的信息.內核提供了NODE_DATA(node_id)宏函數來按照編號來查找對應的結點, 它的工作其實其實就是從node_data數組中進行索引
NODE_DATA(node_id)查找編號node_id的結點pg_data_t信息
移除了pg_data_t->pgdat_next指針域. 但是所有的node都存儲在node_data數組中, 內核提供了函數NODE_DATA直接通過node編號索引節點pg_data_t信息, 參見NODE_DATA的定義
extern struct pglist_data *node_data[]; #define NODE_DATA(nid) (node_data[(nid)])
在UMA結構的機器中, 只有一個node結點即contig_page_data, 此時NODE_DATA直接指向了全局的contig_page_data, 而與node的編號nid無關, 參照include/linux/mmzone.h?v=4.7, line 858, 其中全局唯一的內存node結點contig_page_data定義在mm/nobootmem.c?v=4.7, line 27, linux-2.4.37
#ifndef CONFIG_NEED_MULTIPLE_NODES extern struct pglist_data contig_page_data; #define NODE_DATA(nid) (&contig_page_data) #define NODE_MEM_MAP(nid) mem_map else /* ...... */ #endif
first_online_pgdat和next_online_pgdat返回結點的pg_data_t
-
首先通過first_online_node和next_online_node找到節點的編號
-
然后通過NODE_DATA(node_id)查找到對應編號的結點的pg_data_t信息
struct pglist_data *first_online_pgdat(void) { return NODE_DATA(first_online_node); } struct pglist_data *next_online_pgdat(struct pglist_data *pgdat) { int nid = next_online_node(pgdat->node_id); if (nid == MAX_NUMNODES) return NULL; return NODE_DATA(nid); }