Linux内存管理2(基于6.1内核)

Linux内存管理2(基于6.1内核)---内存节点node

一、内存节点概述


在 Linux 内核中,内存节点是 NUMA 支持的一部分。内存节点在内核的内存管理中起到了至关重要的作用,尤其在多处理器系统中,每个节点的内存区域被单独管理,以便优化内存分配和访问。

在 Linux 内核中,内存节点的管理主要依赖于 NUMA(Non-Uniform Memory Access) 架构的支持,内核使用 node 来标识每个内存节点。每个内存节点会有一个唯一的编号,通常从 0 开始递增。

  • NUMA 系统中的节点:每个节点上的内存都可以看作是一个独立的内存池,内存分配、内存访问等操作都考虑到 NUMA 的拓扑结构。
  • 内存节点和 CPU 绑定:在 NUMA 系统中,每个处理器可能会与一个或多个内存节点相关联,内存分配策略也会根据 CPU 核心与内存节点的关系来优化。

二、内存节点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

在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到。

2.3 pg_data_t描述内存节点


表示node的数据结构include/linux/mmzone.h,结构体的内容如下

/*
 * On NUMA machines, each NUMA node would have a pg_data_t to describe
 * it's memory layout. On UMA machines there is a single pglist_data which
 * describes the whole memory.
 *
 * Memory statistics and page replacement data structures are maintained on a
 * per-zone basis.
 */
typedef struct pglist_data {
	/*
	 * node_zones contains just the zones for THIS node. Not all of the
	 * zones may be populated, but it is the full list. It is referenced by
	 * this node's node_zonelists as well as other node's node_zonelists.
	 */
	struct zone node_zones[MAX_NR_ZONES];

	/*
	 * node_zonelists contains references to all zones in all nodes.
	 * Generally the first zones will be references to this node's
	 * node_zones.
	 */
	struct zonelist node_zonelists[MAX_ZONELISTS];

	int nr_zones; /* number of populated zones in this node */
#ifdef CONFIG_FLATMEM	/* means !SPARSEMEM */
	struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
	struct page_ext *node_page_ext;
#endif
#endif
#if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
	/*
	 * Must be held any time you expect node_start_pfn,
	 * node_present_pages, node_spanned_pages or nr_zones to stay constant.
	 * Also synchronizes pgdat->first_deferred_pfn during deferred page
	 * init.
	 *
	 * pgdat_resize_lock() and pgdat_resize_unlock() are provided to
	 * manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG
	 * or CONFIG_DEFERRED_STRUCT_PAGE_INIT.
	 *
	 * Nests above zone->lock and zone->span_seqlock
	 */
	spinlock_t node_size_lock;
#endif
	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;
	wait_queue_head_t kswapd_wait;
	wait_queue_head_t pfmemalloc_wait;

	/* workqueues for throttling reclaim for different reasons. */
	wait_queue_head_t reclaim_wait[NR_VMSCAN_THROTTLE];

	atomic_t nr_writeback_throttled;/* nr of writeback-throttled tasks */
	unsigned long nr_reclaim_start;	/* nr pages written while throttled
					 * when throttling started. */
#ifdef CONFIG_MEMORY_HOTPLUG
	struct mutex kswapd_lock;
#endif
	struct task_struct *kswapd;	/* Protected by kswapd_lock */
	int kswapd_order;
	enum zone_type kswapd_highest_zoneidx;

	int kswapd_failures;		/* Number of 'reclaimed == 0' runs */

#ifdef CONFIG_COMPACTION
	int kcompactd_max_order;
	enum zone_type kcompactd_highest_zoneidx;
	wait_queue_head_t kcompactd_wait;
	struct task_struct *kcompactd;
	bool proactive_compact_trigger;
#endif
	/*
	 * This is a per-node reserve of pages that are not available
	 * to userspace allocations.
	 */
	unsigned long		totalreserve_pages;

#ifdef CONFIG_NUMA
	/*
	 * node reclaim becomes active if more unmapped pages exist.
	 */
	unsigned long		min_unmapped_pages;
	unsigned long		min_slab_pages;
#endif /* CONFIG_NUMA */

	/* Write-intensive fields used by page reclaim */
	CACHELINE_PADDING(_pad1_);

#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
	struct deferred_split deferred_split_queue;
#endif

#ifdef CONFIG_NUMA_BALANCING
	/* start time in ms of current promote rate limit period */
	unsigned int nbp_rl_start;
	/* number of promote candidate pages at start time of current rate limit period */
	unsigned long nbp_rl_nr_cand;
	/* promote threshold in ms */
	unsigned int nbp_threshold;
	/* start time in ms of current promote threshold adjustment period */
	unsigned int nbp_th_start;
	/*
	 * number of promote candidate pages at start time of current promote
	 * threshold adjustment period
	 */
	unsigned long nbp_th_nr_cand;
#endif
	/* Fields commonly accessed by the page reclaim scanner */

	/*
	 * NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.
	 *
	 * Use mem_cgroup_lruvec() to look up lruvecs.
	 */
	struct lruvec		__lruvec;

	unsigned long		flags;

#ifdef CONFIG_LRU_GEN
	/* kswap mm walk data */
	struct lru_gen_mm_walk mm_walk;
	/* lru_gen_folio list */
	struct lru_gen_memcg memcg_lru;
#endif

	CACHELINE_PADDING(_pad2_);

	/* Per-node vmstats */
	struct per_cpu_nodestat __percpu *per_cpu_nodestats;
	atomic_long_t		vm_stat[NR_VM_NODE_STAT_ITEMS];
#ifdef CONFIG_NUMA
	struct memory_tier __rcu *memtier;
#endif
#ifdef CONFIG_MEMORY_FAILURE
	struct memory_failure_stats mf_stats;
#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_mapnode中的第一个page,它可以指向mem_map中的任何一个page,指向page实例数组的指针,用于描述该节点所拥有的的物理内存页,它包含了该页面所有的内存页,被放置在全局mem_map数组中
bdata这个仅用于引导程序boot 的内存分配,内存在启动时,也需要使用内存,在这里内存使用了自举内存分配器,这里bdata是指向内存自举分配器的数据结构的实例
node_start_pfnpfn是page frame number的缩写。这个成员是用于表示node中的开始那个page在物理内存中的位置的。是当前NUMA节点的第一个页帧的编号,系统中所有的页帧是依次进行编号的,这个字段代表的是当前节点的页帧的起始值,对于UMA系统,只有一个节点,所以该值总是0
node_present_pagesnode中的真正可以使用的page数量
node_spanned_pages该节点以页帧为单位的总长度,这个不等于前面的node_present_pages,因为这里面包含空洞内存
node_idnode的NODE ID 当前节点在系统中的编号,从0开始
kswapd_waitnode的等待队列,交换守护列队进程的等待列表
kswapd_max_order需要释放的区域的长度,以页阶为单位
classzone_idx这个字段暂时没弄明白,不过其中的zone_type是对ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGH,ZONE_MOVABLE,__MAX_NR_ZONES的枚举

2.4 结点的内存管理域


 include/linux/mmzone.h

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.5 结点的内存页面


 include/linux/mmzone.h

typedef struct pglist_data
{
...
#ifdef CONFIG_FLATMEM	/* means !SPARSEMEM */
	struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
	struct page_ext *node_page_ext;
#endif
#endif    /*  指向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.6 交换守护进程


 include/linux/mmzone.h

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.1 结点状态标识node_states


内核用enum node_state变量标记了内存结点所有可能的状态信息,include/linux/nodemask.h

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 结点状态设置函数


内核提供了辅助函数来设置或者清楚位域活特定结点的一个比特位:

include/linux/nodemask.h 

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

#if MAX_NUMNODES > 1
    /*   some real function  */
#else
    /*  some NULL function  */
#endif

四、查找内存结点


4.1 linux6.1的实现


for_each_online_pgdat遍历所有的内存结点:

 

内核提供了include/linux/mmzone.h来遍历节点:

/**
 * 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

#define first_online_node	first_node(node_states[N_ONLINE])
#define first_memory_node	first_node(node_states[N_MEMORY])
static inline unsigned 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信息。

extern struct pglist_data *node_data[];

#define NODE_DATA(nid)	(node_data[(nid)])

first_online_pgdat和next_online_pgdat返回结点的pg_data_t

  • 首先通过first_online_node和next_online_node找到节点的编号

  • 然后通过NODE_DATA(node_id)查找到对应编号的结点的pg_data_t信息

mm/mmzone.c 

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);
}

五、总结

1. NUMA 节点的管理

在 Linux 内核中,NUMA 节点通常通过以下几个数据结构和机制来管理:

  • struct node:这个结构体表示一个 NUMA 节点,内核通过它来管理每个节点的资源和状态。
  • sysfs 文件系统:通过 /sys/devices/system/node/ 路径来访问内存节点的信息。
  • numactrl 工具:一个命令行工具,用来显示和设置 NUMA 拓扑结构。

2. 查找内存节点的方法

Linux 6.1 提供了多种查找内存节点的方法,既可以通过命令行工具查看,也可以通过内核代码进行访问。

2.1. 通过 sysfs 查找内存节点

在 Linux 中,sysfs 文件系统提供了访问内存节点的接口。内存节点的信息存储在 /sys/devices/system/node/ 路径下。

可以使用以下命令来列出所有的 NUMA 节点:



可以通过读取每个节点目录下的文件来查看该节点的详细信息,例如查看每个节点的内存大小:
cat /sys/devices/system/node/node0/meminfo

 这个文件包含了关于节点的内存使用情况,包括总内存、可用内存、已用内存等信息。

2.2. 通过 lscpu 命令查看 NUMA 节点

lscpu 命令是一个显示 CPU 架构信息的工具,它也会显示系统中的 NUMA 节点信息。运行以下命令:

输出中会包含一个 NUMA node(s) 字段,显示系统中有多少个 NUMA 节点。

 

2.3. 通过 /proc 文件系统查找内存节点

Linux 还通过 /proc 文件系统暴露了内存和 CPU 信息,虽然它主要用于提供运行时信息,但也可以间接帮助了解 NUMA 节点。

此命令会显示当前系统内存的详细信息,但不会直接显示 NUMA 节点的详细信息。为了获得 NUMA 节点的细节,依赖于 /sys 文件系统是更为直接的方式。

2.4. 通过内核代码查找 NUMA 节点

在 Linux 内核代码中,可以通过访问内核中的相关数据结构来查找 NUMA 节点。例如,内核中的 node_to_cpu() 函数用于获取 NUMA 节点与 CPU 核心的映射。

#include <linux/topology.h>

unsigned int node_to_cpu(unsigned int node);

3. 内存节点的数据结构

在内核中,NUMA 节点的管理是通过一些关键数据结构来实现的。以下是一些主要的结构体:

3.1. struct node

每个 NUMA 节点由 struct node 来表示,该结构体包含了与节点相关的内存管理信息、CPU 关联信息等。

struct node {
    unsigned long node_id;
    struct pglist_data *node_data;
    struct cpumask node_cpumask;
    // 其他内存和CPU相关的数据
};

3.2. pglist_data

pglist_data 是与每个 NUMA 节点相关联的内存数据结构。它包含了该节点上所有物理内存的管理信息。

struct pglist_data {
    unsigned long node_start_pfn;
    unsigned long node_spanned_pages;
    struct free_area free_area[MAX_ORDER];
    // 其他与内存分配相关的数据
};

3.3. numactrlnuma_node

Linux 中的 numactrl 工具通过与 numa_node 进行交互来查询和管理 NUMA 节点。每个节点都有一个唯一的标识符 node_id,用于区分不同的内存节点。

4. 内核中查找 NUMA 节点的实现

内核通过访问 NUMA 数据结构中的节点信息来查找 NUMA 节点。例如,node_online() 函数用于检查一个 NUMA 节点是否处于活动状态:

int node_online(int node)
{
    return node_state(node, N_NORMAL_MEMORY);
}

node_state() 函数检查特定节点的状态,以确定该节点是否在线(即是否具有可用内存)。

5. 内存节点的拓扑结构

在 NUMA 系统中,每个节点可以包含多个 CPU 核心,每个 CPU 核心可以访问本地或远程的内存。操作系统会通过 NUMA 拓扑结构来优化内存访问,确保每个进程尽可能访问本地节点的内存,从而减少远程内存访问的开销。

内核会根据这些拓扑结构分配内存给进程,并通过 NUMA 策略确保进程尽量在本地节点执行,以提高性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值