这点前面是说的很明白了, NUMA结构下, 每一个处理器CPU与一个本地内存直接相连, 而不一样处理器以前则经过总线进行进一步的链接, 所以相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快html
Linux适用于各类不一样的体系结构, 而不一样体系结构在内存管理方面的差异很大. 所以linux内核须要用一种体系结构无关的方式来表示内存.node
所以linux内核把物理内存按照CPU节点划分为不一样的node, 每一个node做为某个cpu结点的本地内存, 而做为其余CPU节点的远程内存, 而UMA结构下, 则任务系统中只存在一个内存node, 这样对于UMA结构来讲, 内核把内存当成只有一个内存node节点的伪NUMAlinux
CPU被划分为多个节点(node), 内存则被分簇, 每一个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每一个内存簇被认为是一个节点
系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每一个内存簇被认为是一个节点c#
内存被划分为结点. 每一个节点关联到系统中的一个处理器, 内核中表示为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上运行, 所以从当前节点获得的内存极可能被用到electron
表示node的数据结构为typedef struct pglist_data pg_data_t
, 这个结构定义在include/linux/mmzone.h, line 615中,结构体的内容以下:ide
/* * 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的枚举 |
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则指定了备用节点以及其内存域的列表, 以便在当前结点没有可用空间时, 在备用节点分配内存.oop
nr_zones存储告终点中不一样内存域的数目
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则给出了该结点以页帧为单位计算的长度. 两者的值不必定相同, 由于结点中可能有一些空洞, 并不对应真正的页帧.
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用于页交换子系统的实现, 用来定义须要释放的区域的长度.
内核用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
内核提供了辅助函数来设置或者清楚位域活特定结点的一个比特位
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
node_id做为全局节点id。 系统中的NUMA结点都是从0开始编号的
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)
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
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); }