Linux內存描述之內存節點node--Linux內存管理(二)
1 內存節點node
1.1 為什麽要用node來描述內存
這點前面是說的很明白了, NUMA結構下, 每個處理器CPU與一個本地內存直接相連, 而不同處理器之前則通過總線進行進一步的連接, 因此相對於任何一個CPU訪問本地內存的速度比訪問遠程內存的速度要快
Linux適用於各種不同的體系結構, 而不同體系結構在內存管理方面的差別很大. 因此linux內核需要用一種體系結構無關的方式來表示內存.
因此linux內核把物理內存按照CPU節點劃分為不同的node, 每個node作為某個cpu結點的本地內存, 而作為其他CPU節點的遠程內存, 而UMA結構下, 則任務系統中只存在一個內存node, 這樣對於UMA結構來說, 內核把內存當成只有一個內存node節點的偽NUMA
1.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由struct pglist_data定義而來, 該數據結構定義在include/linux/mmzone.h, line 615
在分配一個頁面時, Linux采用節點局部分配的策略, 從最靠近運行中的CPU的節點分配內存, 由於進程往往是在同一個CPU上運行, 因此從當前節點得到的內存很可能被用到
1.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_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的枚舉 |
1.4 結點的內存管理域
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存儲了結點中不同內存域的數目
1.5 結點的內存頁面
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則給出了該結點以頁幀為單位計算的長度. 二者的值不一定相同, 因為結點中可能有一些空洞, 並不對應真正的頁幀.
1.6 交換守護進程
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用於頁交換子系統的實現, 用來定義需要釋放的區域的長度.
2 結點狀態
2.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
2.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
3 查找內存結點
node_id作為全局節點id。 系統中的NUMA結點都是從0開始編號的
3.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節點, 其只需要沿著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)
3.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);
}
Linux內存描述之內存節點node--Linux內存管理(二)