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 描述, 而 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 Windows */
- 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 http://marc.info/?l=lhms-devel&m=111595348412761 和 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 内存管理(二)
来源: http://www.bubuko.com/infodetail-2854912.html