1、概述
2. 从 CPU 角度看物理内存模型
内核是以页为基本单位对物理内存进行管理的,每页大小为 4K,而为了快速索引到具体的物理内存页,内核为每个物理页 struct page 结构体定义了一个索引编号:PFN(Page Frame Number)。PFN 与 struct page 是一一对应的关系。
内核提供了两个宏来完成 PFN 与 物理页结构体 struct page 之间的相互转换。它们分别是 page_to_pfn 与 pfn_to_page。
内核中如何组织管理这些物理内存页 struct page 的方式我们称之为做物理内存模型,不同的物理内存模型,应对的场景以及 page_to_pfn 与 pfn_to_page 的计算逻辑都是不一样的。
2.1 FLATMEM 平坦内存模型
我们先把物理内存想象成一片地址连续的存储空间,在这一大片地址连续的内存空间中,内核将这块内存空间分为一页一页的内存块 struct page 。
由于这块物理内存是连续的,物理地址也是连续的,划分出来的这一页一页的物理页必然也是连续的,并且每页的大小都是固定的,所以我们很容易想到用一个数组来组织这些连续的物理内存页 struct page 结构,其在数组中对应的下标即为 PFN 。这种内存模型就叫做平坦内存模型 FLATMEM 。
内核中使用了一个 mem_map 的全局数组用来组织所有划分出来的物理内存页。mem_map 全局数组的下标就是相应物理页对应的 PFN 。
在平坦内存模型下 ,page_to_pfn 与 pfn_to_page 的计算逻辑就非常简单,本质就是基于 mem_map 数组进行偏移操作。
#if defined(CONFIG_FLATMEM)
#define __pfn_to_page(pfn) (mem_map + ((pfn)-ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page)-mem_map) + ARCH_PFN_OFFSET)
#endif
ARCH_PFN_OFFSET 是 PFN 的起始偏移量。
Linux 早期使用的就是这种内存模型,因为在 Linux 发展的早期所需要管理的物理内存通常不大(比如几十 MB),那时的 Linux 使用平坦内存模型 FLATMEM 来管理物理内存就足够高效了。
内核中的默认配置是使用 FLATMEM 平坦内存模型。
2.2 DISCONTIGMEM 非连续内存模型
FLATMEM 平坦内存模型只适合管理一整块连续的物理内存,而对于多块非连续的物理内存来说使用 FLATMEM 平坦内存模型进行管理则会造成很大的内存空间浪费。
因为 FLATMEM 平坦内存模型是利用 mem_map 这样一个全局数组来组织这些被划分出来的物理页 page 的,而对于物理内存存在大量不连续的内存地址区间这种情况时,这些不连续的内存地址区间就形成了内存空洞。
由于用于组织物理页的底层数据结构是 mem_map 数组,数组的特性又要求这些物理页是连续的,所以只能为这些内存地址空洞也分配 struct page 结构用来填充数组使其连续。
而每个 struct page 结构大部分情况下需要占用 40 字节(struct page 结构在不同场景下内存占用会有所不同,这一点我们后面再说),如果物理内存中存在的大块的地址空洞,那么为这些空洞而分配的 struct page 将会占用大量的内存空间,导致巨大的浪费。
为了组织和管理这些不连续的物理内存,内核于是引入了 DISCONTIGMEM 非连续内存模型,用来消除这些不连续的内存地址空洞对 mem_map 的空间浪费。
在 DISCONTIGMEM 非连续内存模型中,内核将物理内存从宏观上划分成了一个一个的节点 node (微观上还是一页一页的物理页),每个 node 节点管理一块连续的物理内存。这样一来这些连续的物理内存页均被划归到了对应的 node 节点中管理,就避免了内存空洞造成的空间浪费。
内核中使用 struct pglist_data 表示用于管理连续物理内存的 node 节点(内核假设 node 中的物理内存是连续的),既然每个 node 节点中的物理内存是连续的,于是在每个 node 节点中还是采用 FLATMEM 平坦内存模型的方式来组织管理物理内存页。每个 node 节点中包含一个 struct page *node_mem_map
数组,用来组织管理 node 中的连续物理内存页。
typedef struct pglist_data {
#ifdef CONFIG_FLATMEM
struct page *node_mem_map;
#endif
}
我们可以看出 DISCONTIGMEM 非连续内存模型其实就是 FLATMEM 平坦内存模型的一种扩展,在面对大块不连续的物理内存管理时,通过将每段连续的物理内存区间划归到 node 节点中进行管理,避免了为内存地址空洞分配 struct page 结构,从而节省了内存资源的开销。
由于引入了 node 节点这个概念,所以在 DISCONTIGMEM 非连续内存模型下 page_to_pfn 与 pfn_to_page 的计算逻辑就比 FLATMEM 内存模型下的计算逻辑多了一步定位 page 所在 node 的操作。
-
通过 arch_pfn_to_nid 可以根据物理页的 PFN 定位到物理页所在 node。
-
通过 page_to_nid 可以根据物理页结构 struct page 定义到 page 所在 node。
当定位到物理页 struct page 所在 node 之后,剩下的逻辑就和 FLATMEM 内存模型一模一样了。
#if defined(CONFIG_DISCONTIGMEM)
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
unsigned long __nid = arch_pfn_to_nid(__pfn); \
NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg)); \
(unsigned long)(__pg - __pgdat->node_mem_map) + \
__pgdat->node_start_pfn; \
})
2.3 SPARSEMEM 稀疏内存模型
随着内存技术的发展,内核可以支持物理内存的热插拔了,这样一来物理内存的不连续就变为常态了,在上小节介绍的 DISCONTIGMEM 内存模型中,其实每个 node 中的物理内存也不一定都是连续的。
而且每个 node 中都有一套完整的内存管理系统,如果 node 数目多的话,那这个开销就大了,于是就有了对连续物理内存更细粒度的管理需求,为了能够更灵活地管理粒度更小的连续物理内存,SPARSEMEM 稀疏内存模型就此登场了。
SPARSEMEM 稀疏内存模型的核心思想就是对粒度更小的连续内存块进行精细的管理,用于管理连续内存块的单元被称作 section 。物理页大小为 4k 的情况下, section 的大小为 128M ,物理页大小为 16k 的情况下, section 的大小为 512M。
在内核中用 struct mem_section 结构体表示 SPARSEMEM 模型中的 section。
struct mem_section {
/*
* This is, logically, a pointer to an array of struct
* pages. However, it is stored with some other magic.
* (see sparse.c::sparse_init_one_section())
*
* Additionally during early boot we encode node id of
* the location of the section here to guide allocation.
* (see sparse.c::memory_present())
*
* Making it a UL at least makes someone do a cast
* before using it wrong.
*/
unsigned long section_mem_map;
/* See declaration of similar field in struct zone */
unsigned long *pageblock_flags;
#ifdef CONFIG_PAGE_EXTENSION
/*
* If SPARSEMEM, pgdat doesn't have page_ext pointer. We use
* section. (see page_ext.h about this.)
*/
struct page_ext *page_ext;
unsigned long pad;
#endif
/*
* WARNING: mem_section must be a power-of-2 in size for the
* calculation and use of SECTION_ROOT_MASK to make sense.
*/
};
由于 section 被用作管理小粒度的连续内存块,这些小的连续物理内存在 section 中也是通过数组的方式被组织管理,每个 struct mem_section 结构体中有一个 section_mem_map 指针用于指向 section 中管理连续内存的 page 数组。
SPARSEMEM 内存模型中的这些所有的 mem_section 会被存放在一个全局的数组中,并且每个 mem_section 都可以在系统运行时改变 offline / online (下线 / 上线)状态,以便支持内存的热插拔(hotplug)功能。
#ifdef CONFIG_SPARSEMEM_EXTREME
extern struct mem_section *mem_section[NR_SECTION_ROOTS];
在 SPARSEMEM 稀疏内存模型下 page_to_pfn 与 pfn_to_page 的计算逻辑又发生了变化。
-
在 page_to_pfn 的转换中,首先需要通过 page_to_section 根据 struct page 结构定位到 mem_section 数组中具体的 section 结构。然后在通过 section_mem_map 定位到具体的 PFN。
在 struct page 结构中有一个
unsigned long flags
属性,在 flag 的高位 bit 中存储着 page 所在 mem_section 数组中的索引,从而可以定位到所属 section。
-
在 pfn_to_page 的转换中,首先需要通过 __pfn_to_section 根据 PFN 定位到 mem_section 数组中具体的 section 结构。然后在通过 PFN 在 section_mem_map 数组中定位到具体的物理页 Page 。
PFN 的高位 bit 存储的是全局数组 mem_section 中的 section 索引,PFN 的低位 bit 存储的是 section_mem_map 数组中具体物理页 page 的索引。
#if defined(CONFIG_SPARSEMEM)
/*
* Note: section's mem_map is encoded to reflect its start_pfn.
* section[i].section_mem_map == mem_map's address - start_pfn;
*/
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
int __sec = page_to_section(__pg); \
(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \
})
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
__section_mem_map_addr(__sec) + __pfn; \
})
#endif
从以上的内容介绍中,我们可以看出 SPARSEMEM 稀疏内存模型已经完全覆盖了前两个内存模型的所有功能,因此稀疏内存模型可被用于所有内存布局的情况。
2.3.1 物理内存热插拔
前面提到随着内存技术的发展,物理内存的热插拔 hotplug 在内核中得到了支持,由于物理内存可以动态的从主板中插入以及拔出,所以导致了物理内存的不连续已经成为常态,因此内核引入了 SPARSEMEM 稀疏内存模型以便应对这种情况,提供对更小粒度的连续物理内存的灵活管理能力。
本小节笔者就为大家介绍一下物理内存热插拔 hotplug 功能在内核中的实现原理,作为 SPARSEMEM 稀疏内存模型的扩展内容补充。
在大规模的集群中,尤其是现在我们处于云原生的时代,为了实现集群资源的动态均衡,可以通过物理内存热插拔的功能实现集群机器物理内存容量的动态增减。
集群的规模一大,那么物理内存出故障的几率也会大大增加,物理内存的热插拔对提供集群高可用性也是至关重要的。
从总体上来讲,内存的热插拔分为两个阶段:
-
物理热插拔阶段:这个阶段主要是从物理上将内存硬件插入(hot-add),拔出(hot-remove)主板的过程,其中涉及到硬件和内核的支持。
-
逻辑热插拔阶段:这一阶段主要是由内核中的内存管理子系统来负责,涉及到的主要工作为:如何动态的上线启用(online)刚刚 hot-add 的内存,如何动态下线(offline)刚刚 hot-remove 的内存。
物理内存拔出的过程需要关注的事情比插入的过程要多的多,困难的是物理内存的动态拔出,因为此时即将要被拔出的物理内存中可能已经为进程分配了物理页,如何妥善安置这些已经被分配的物理页是一个棘手的问题。
前边我们介绍 SPARSEMEM 内存模型的时候提到,每个 mem_section 都可以在系统运行时改变 offline ,online 状态,以便支持内存的热插拔(hotplug)功能。 当 mem_section offline 时, 内核会把这部分内存隔离开, 使得该部分内存不可再被使用, 然后再把 mem_section 中已经分配的内存页迁移到其他 mem_section 的内存上. 。
但是这里会有一个问题,就是并非所有的物理页都可以迁移,因为迁移意味着物理内存地址的变化,而内存的热插拔应该对进程来说是透明的,所以这些迁移后的物理页映射的虚拟内存地址是不能变化的。
这一点在进程的用户空间是没有问题的,因为进程在用户空间访问内存都是根据虚拟内存地址通过页表找到对应的物理内存地址,这些迁移之后的物理页,虽然物理内存地址发生变化,但是内核通过修改相应页表中虚拟内存地址与物理内存地址之间的映射关系,可以保证虚拟内存地址不会改变。
但是在内核态的虚拟地址空间中,有一段直接映射区,在这段虚拟内存区域中虚拟地址与物理地址是直接映射的关系,虚拟内存地址直接减去一个固定的偏移量(0xC000 0000 ) 就得到了物理内存地址。
直接映射区中的物理页的虚拟地址会随着物理内存地址变动而变动, 因此这部分物理页是无法轻易迁移的,然而不可迁移的页会导致内存无法被拔除,因为无法妥善安置被拔出内存中已经为进程分配的物理页。那么内核是如何解决这个头疼的问题呢?
既然是这些不可迁移的物理页导致内存无法拔出,那么我们可以把内存分一下类,将内存按照物理页是否可迁移,划分为不可迁移页,可回收页,可迁移页。
大家这里需要记住一点,内核会将物理内存按照页面是否可迁移的特性进行分类,笔者后面在介绍内核如何避免内存碎片的时候还会在提到
然后在这些可能会被拔出的内存中只分配那些可迁移的内存页,这些信息会在内存初始化的时候被设置,这样一来那些不可迁移的页就不会包含在可能会拔出的内存中,当我们需要将这块内存热拔出时, 因为里边的内存页全部是可迁移的, 从而使内存可以被拔除。
3. 从 CPU 角度看物理内存架构
在上小节中笔者为大家介绍了三种物理内存模型,这三种物理内存模型是从 CPU 的视角来看待物理内存内部是如何布局,组织以及管理的,主角是物理内存。
在本小节中笔者为大家提供一个新的视角,这一次我们把物理内存看成一个整体,从 CPU 访问物理内存的角度来看一下物理内存的架构,并从 CPU 与物理内存的相对位置变化来看一下不同物理内存架构下对性能的影响。
3.1 一致性内存访问 UMA 架构
我们在上篇文章 《深入理解 Linux 虚拟内存管理》的 “ 8.2 CPU 如何读写主存” 小节中提到 CPU 与内存之间的交互是通过总线完成的。
-
首先 CPU 将物理内存地址作为地址信号放到系统总线上传输。随后 IO bridge 将系统总线上的地址信号转换为存储总线上的电子信号。
-
主存感受到存储总线上的地址信号并通过存储控制器将存储总线上的物理内存地址 A 读取出来。
-
存储控制器通过物理内存地址定位到具体的存储器模块,从 DRAM 芯片中取出物理内存地址对应的数据。
-
存储控制器将读取到的数据放到存储总线上,随后 IO bridge 将存储总线上的数据信号转换为系统总线上的数据信号,然后继续沿着系统总线传递。
-
CPU 芯片感受到系统总线上的数据信号,将数据从系统总线上读取出来并拷贝到寄存器中。
上图展示的是单核 CPU 访问内存的架构图,那么在多核服务器中多个 CPU 与内存之间的架构关系又是什么样子的呢?
在 UMA 架构下,多核服务器中的多个 CPU 位于总线的一侧,所有的内存条组成一大片内存位于总线的另一侧,所有的 CPU 访问内存都要过总线,而且距离都是一样的,由于所有 CPU 对内存的访问距离都是一样的,所以在 UMA 架构下所有 CPU 访问内存的速度都是一样的。这种访问模式称为 SMP(Symmetric multiprocessing),即对称多处理器。
这里的一致性是指同一个 CPU 对所有内存的访问的速度是一样的。即一致性内存访问 UMA(Uniform Memory Access)。
但是随着多核技术的发展,服务器上的 CPU 个数会越来越多,而 UMA 架构下所有 CPU 都是需要通过总线来访问内存的,这样总线很快就会成为性能瓶颈,主要体现在以下两个方面:
-
总线的带宽压力会越来越大,随着 CPU 个数的增多导致每个 CPU 可用带宽会减少
-
总线的长度也会因此而增加,进而增加访问延迟
UMA 架构的优点很明显就是结构简单,所有的 CPU 访问内存速度都是一致的,都必须经过总线。然而它的缺点笔者刚刚也提到了,就是随着处理器核数的增多,总线的带宽压力会越来越大。解决办法就只能扩宽总线,然而成本十分高昂,未来可能仍然面临带宽压力。
为了解决以上问题,提高 CPU 访问内存的性能和扩展性,于是引入了一种新的架构:非一致性内存访问 NUMA(Non-uniform memory access)。
3.2 非一致性内存访问 NUMA 架构
在 NUMA 架构下,内存就不是一整片的了,而是被划分成了一个一个的内存节点 (NUMA 节点),每个 CPU 都有属于自己的本地内存节点,CPU 访问自己的本地内存不需要经过总线,因此访问速度是最快的。当 CPU 自己的本地内存不足时,CPU 就需要跨节点去访问其他内存节点,这种情况下 CPU 访问内存就会慢很多。
在 NUMA 架构下,任意一个 CPU 都可以访问全部的内存节点,访问自己的本地内存节点是最快的,但访问其他内存节点就会慢很多,这就导致了 CPU 访问内存的速度不一致,所以叫做非一致性内存访问架构。
如上图所示,CPU 和它的本地内存组成了 NUMA 节点,CPU 与 CPU 之间通过 QPI(Intel QuickPath Interconnect)点对点完成互联,在 CPU 的本地内存不足的情况下,CPU 需要通过 QPI 访问远程 NUMA 节点上的内存控制器从而在远程内存节点上分配内存,这就导致了远程访问比本地访问多了额外的延迟开销(需要通过 QPI 遍历远程 NUMA 节点)。
在 NUMA 架构下,只有 DISCONTIGMEM 非连续内存模型和 SPARSEMEM 稀疏内存模型是可用的。而 UMA 架构下,前面介绍的三种内存模型都可以配置使用。
3.2.1 NUMA 的内存分配策略
NUMA 的内存分配策略是指在 NUMA 架构下 CPU 如何请求内存分配的相关策略,比如:是优先请求本地内存节点分配内存呢 ?还是优先请求指定的 NUMA 节点分配内存 ?是只能在本地内存节点分配呢 ?还是允许当本地内存不足的情况下可以请求远程 NUMA 节点分配内存 ?
内存分配策略 | 策略描述 |
---|---|
MPOL_BIND | 必须在绑定的节点进行内存分配,如果内存不足,则进行 swap |
MPOL_INTERLEAVE | 本地节点和远程节点均可允许分配内存 |
MPOL_PREFERRED | 优先在指定节点分配内存,当指定节点内存不足时,选择离指定节点最近的节点分配内存 |
MPOL_LOCAL (默认) | 优先在本地节点分配,当本地节点内存不足时,可以在远程节点分配内存 |
我们可以在应用程序中通过 libnuma 共享库中的 API 调用 set_mempolicy 接口设置进程的内存分配策略。
#include <numaif.h>
long set_mempolicy(int mode, const unsigned long *nodemask,
unsigned long maxnode);
-
mode : 指定 NUMA 内存分配策略。
-
nodemask:指定 NUMA 节点 Id。
-
maxnode:指定最大 NUMA 节点 Id,用于遍历远程节点,实现跨 NUMA 节点分配内存。
libnuma 共享库 API 文档:https://man7.org/linux/man-pages/man3/numa.3.html#top_of_page
set_mempolicy 接口文档:https://man7.org/linux/man-pages/man2/set_mempolicy.2.html
3.2.2 NUMA 的使用简介
在我们理解了物理内存的 NUMA 架构,以及在 NUMA 架构下的内存分配策略之后,本小节笔者来为大家介绍下如何正确的利用 NUMA 提升我们应用程序的性能。
前边我们介绍了这么多的理论知识,但是理论的东西总是很虚,正所谓眼见为实,大家一定想亲眼看一下 NUMA 架构在计算机中的具体表现形式,比如:在支持 NUMA 架构的机器上到底有多少个 NUMA 节点?每个 NUMA 节点包含哪些 CPU 核,具体是怎样的一个分布情况?
前面也提到 CPU 在访问本地 NUMA 节点中的内存时,速度是最快的。但是当访问远程 NUMA 节点,速度就会相对很慢,那么到底有多慢?本地节点与远程节点之间的访问速度差异具体是多少 ?
3.2.2.1 查看 NUMA 相关信息
numactl 文档:https://man7.org/linux/man-pages/man8/numactl.8.html
针对以上具体问题,numactl -H
命令可以给出我们想要的答案:
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 64794 MB
node 0 free: 55404 MB
node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 1 size: 65404 MB
node 1 free: 58642 MB
node 2 cpus: 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
node 2 size: 65404 MB
node 2 free: 61181 MB
node 3 cpus: 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
node 3 size: 65402 MB
node 3 free: 55592 MB
node distances:
node 0 1 2 3
0: 10 16 32 33
1: 16 10 25 32
2: 32 25 10 16
3: 33 32 16 10
numactl -H
命令可以查看服务器的 NUMA 配置,上图中的服务器配置共包含 4 个 NUMA 节点(0 - 3),每个 NUMA 节点中包含 16个 CPU 核心,本地内存大小约为 64G。
大家可以关注下最后 node distances:
这一栏,node distances 给出了不同 NUMA 节点之间的访问距离,对角线上的值均为本地节点的访问距离 10 。比如 [0,0] 表示 NUMA 节点 0 的本地内存访问距离。
我们可以很明显的看到当出现跨 NUMA 节点访问的时候,访问距离就会明显增加,比如节点 0 访问节点 1 的距离 [0,1] 是16,节点 0 访问节点 3 的距离 [0,3] 是 33。距离越远,跨 NUMA 节点内存访问的延时越大。应用程序运行时应减少跨 NUMA 节点访问内存。
此外我们还可以通过 numactl -s
来查看 NUMA 的内存分配策略设置:
policy: default
preferred node: current
通过 numastat
还可以查看各个 NUMA 节点的内存访问命中率:
node0 node1 node2 node3
numa_hit 1296554257 918018444 1296574252 828018454
numa_miss 8541758 40297198 7544751 41267108
numa_foreign 40288595 8550361 41488585 8450375
interleave_hit 45651 45918 46654 49718
local_node 1231897031 835344122 1141898045 915354158
other_node 64657226 82674322 594657725 82675425
-
numa_hit :内存分配在该节点中成功的次数。
-
numa_miss : 内存分配在该节点中失败的次数。
-
numa_foreign:表示其他 NUMA 节点本地内存分配失败,跨节点(numa_miss)来到本节点分配内存的次数。
-
interleave_hit : 在 MPOL_INTERLEAVE 策略下,在本地节点分配内存的次数。
-
local_node:进程在本地节点分配内存成功的次数。
-
other_node:运行在本节点的进程跨节点在其他节点上分配内存的次数。
numastat 文档:https://man7.org/linux/man-pages/man8/numastat.8.html
3.2.2.2 绑定 NUMA 节点
numactl 工具可以让我们应用程序指定运行在哪些 CPU 核心上,同时也可以指定我们的应用程序可以在哪些 NUMA 节点上分配内存。通过将应用程序与具体的 CPU 核心和 NUMA 节点绑定,从而可以提升程序的性能。
numactl --membind=nodes --cpunodebind=nodes command
-
通过
--membind
可以指定我们的应用程序只能在哪些具体的 NUMA 节点上分配内存,如果这些节点内存不足,则分配失败。 -
通过
--cpunodebind
可以指定我们的应用程序只能运行在哪些 NUMA 节点上。
numactl --physcpubind=cpus command
另外我们还可以通过 --physcpubind
将我们的应用程序绑定到具体的物理 CPU 上。这个选项后边指定的参数我们可以通过 cat /proc/cpuinfo
输出信息中的 processor 这一栏查看。例如:通过 numactl --physcpubind= 0-15 ./numatest.out
命令将进程 numatest 绑定到 0~15 CPU 上执行。
我们可以通过 numactl 命令将 numatest 进程分别绑定在相同的 NUMA 节点上和不同的 NUMA 节点上,运行观察。
numactl --membind=0 --cpunodebind=0 ./numatest.out
numactl --membind=0 --cpunodebind=1 ./numatest.out
大家肯定一眼就能看出绑定在相同 NUMA 节点的进程运行会更快,因为通过前边对 NUMA 架构的介绍,我们知道 CPU 访问本地 NUMA 节点的内存是最快的。
除了 numactl 这个工具外,我们还可以通过共享库 libnuma 在程序中进行 NUMA 相关的操作。这里笔者就不演示了,感兴趣可以查看下 libnuma 的 API 文档:https://man7.org/linux/man-pages/man3/numa.3.html#top_of_page
ref:
服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA) - Smah - 博客园