Linux Kernel:NUMA 节点探测

547 阅读1小时+
fm_numa_detect.png

本文采用 Linux 内核 v3.10 版本 x86_64 架构

一、概述

在以前的文章中,我们介绍了物理内存布局探测CPU 拓扑探测,本篇我们来介绍 NUMA 节点探测。在一个 NUMA 节点中,通常包括 CPU 和内存资源。所以 NUMA 节点探测的一个主要任务就要确认节点的内存和 CPU;另一个任务就是确定不同节点之间的距离

NUMA 总体架构图示意如下:

NUMA_arch.png

二、基本原理

我们在 CPU 拓扑探测 一文中介绍过,系统会为每一个逻辑处理器分配一个唯一的 APIC ID,该 ID 可用来标识处理器。类似的,系统使用邻近域proximity domain)来标识 NUMA 节点。本文我们会用到 ACPI 中的 SRAT(System Resource Affinity Table)和 SLIT(System Locality Information Table)表。在这两张表中,都使用了邻近域,其中 SRAT 表描述了处理器、内存与邻近域(节点)的映射关系;SLIT 表描述了各邻近域(节点)间的距离(内存延迟)。

下面我们来看下这两种表的格式。

2.1 SRAT 格式

SRAT 表描述了处理器、内存与邻近域(proximity domain)的映射关系,该表由表头和不同类型的表项组成。

2.1.1 ACPI 通用表头

SRAT 表以 ACPI 通用表头开始,通用表头占用 36 字节,其格式如下:

FieldByte LengthByte OffsetDescription
Signature40表的签名,用来区分不同的表。SRAT 表的签名为 “SRAT”。
Length44表的完整大小(包括表头)
Revision18表的修订版
Checksum19Entire table must sum to zero.
OEMID610OEM ID.
OEM Table ID816OEM Table ID
OEM Revision424OEM Revision
OEM Revision428OEM Revision
Creator Revision432Creator Revision

在通用表头中,我们只关注签名(Signature)和 长度(Length) 字段。签名字段用于区分不同类型的表,对于 SRAT 表来说,该字段的值是 “SRAT”。长度字段指示该表的总大小(包括表头)。

2.1.2 SRAT 表头专用字段

在通用表头之后,是 SRAT 表头的专用字段:

FieldByte LengthByte OffsetDescription
Reserved436Reserved to be 1 for backward compatibility
Reserved840Reserved

这 2 个字段均为保留字段。为了保证向后兼容性,第一个保留字段的值必须为 1。第二个保留字段未使用。

2.1.3 表项

在表头之后,是一系列表项结构。表项有多种类型,我们只关注与处理器和内存相关的表项:

  • Processor Local APIC/SAPIC Affinity Structure
  • Memory Affinity Structure
  • Processor Local x2APIC Affinity Structure
2.1.3.1 SRAT 子表头

所有的 SRAT 表项,都包含一个通用子表头。 子表头有 2 个字段,每个字段占用一个字节。第 1 个字段指示表项类型,第 2 个字段指示表项大小

FieldByte LengthByte Offset
Type10
Length11
2.1.3.2 Type 0:Processor Local APIC/SAPIC Affinity Structure

该表项描述了处理器的 APIC ID (x86 架构)或者 SAPIC ID (IA-64 架构)所关联的邻近域(proximity domain),其格式如下:

FieldByte LengthByte OffsetDescription
Type100
Length1116
Proximity Domain [7:0]12邻近域的 [7:0] 位
APIC ID13Local APIC ID.
Flags44标志位
Local SAPIC EID18Local SAPIC EID
Proximity Domain [31:8]39邻近域的 [31:8] 位
Clock Domain412时钟域

该结构中,我们主要关注 3 个字段:

  • APIC ID:8 位的 APIC ID,用来标识处理器
  • Proximity Domain:邻近域,用来标识 NUMA 节点。可以看到,该字段被分割成 2 段。低版本的 SRAT 表,只会使用最低的 8 位;较新版本会使用完整的 32 位。
  • Flags:标志位,指示表项是否可用。只有 Enabled 位(位 0 )起作用,其它位保留未使用。Flags 字段的详细说明见下表。
FieldBit LengthBit OffsetDescription
Enabled10如果该位被清除,说明表项不可用,应该被忽略。
Reserved311保留位,必须为 0。
2.1.3.3 Type 1:Memory Affinity Structure

该表项提供了以下信息:

  • 内存区域所关联的邻近域;
  • 内存区域是否能够热插拔。

该表项的详细格式如下:

FieldByte LengthByte OffsetDescription
Type101
Length1140
Proximity Domain42一个整数,指示内存区域所关联的临近域,用来标识 NUMA 节点
Reserved26保留未使用
Base Address Low48内存区域基地址的低 32 位
Base Address High412内存区域基地址的高 32 位
Length Low416内存区域大小的低 32 位
Length High420内存区域大小的高 32 位
Reserved424保留
Flags428标志位。指示内存区域是否可用以及是否支持热插拔。
Reserved832保留

Flags 字段各比特位说明见下表:

FieldBit LengthBit OffsetDescription
Enabled10Enabled 位,指示表项是否可用。如果该位置位,说明表项可用;否则,说明不可用。
Hot Pluggable11指示内存区域是否支持热插拔。该位依赖 Enabled 位的状态。如果 Enabled 位置位,该位也置位,指示该内存区域可用且是可插拔的;如果 Enabled 位置位,该位被清除,说明内存区域可用但不支持热插拔;如果 Enabled 位被清除,忽略该标志位。
NonVolatile12如果该位被置位,指示内存区域是非易失性内存。
Reserved293保留位,必须为 0。
2.1.3.4 Type 2:Processor Local x2APIC Affinity Structure

该表项描述了处理器的 Local x2APIC ID 所关联的邻近域,其格式如下:

FieldByte LengthByte OffsetDescription
Type102
Length1124
Reserved22保留,必须为 0
Proximity Domain44处理器所属的 32 位邻近域
X2APIC ID48Local x2APIC ID.
Flags412同 Processor Local APIC/SAPIC Affinity Structure 表项中的 Flags 字段。
Clock Domain416时钟域
Reserved420保留

我们主要关注以下几个字段:

  • Proximity Domain:邻近域,用来标识 NUMA 节点;
  • X2APIC ID:32 位的 x2APIC ID;
  • Flags:标志位,指示该处理器是否可用。同 Processor Local APIC/SAPIC Affinity Structure 中的 Flags 字段。

2.2 SLIT 格式

2.2.1 SLIT 简介

SLIT 表提供了一个矩阵,用于描述所有系统位置(也称为邻近域)之间的相对距离(内存延迟)。 为了与 SRAT 表中的描述统一,下文将系统位置称为邻近域。矩阵元素 Entry[i,j] 的值(其中 i 代表矩阵的行,j 代表矩阵的列)表示从邻近域 i 到邻近域 j (包括自身)的相对距离,该值是一个单字节的无符号整数

如果系统中邻近域的数量为 N,那么矩阵元素的数量为 N2N^{2} ,Entry[i,j] 表示矩阵的第 i*N + j 个元素。除了邻近域到自身的相对距离(对角元素)之外,每个相对距离在矩阵中存储两次。

矩阵的对角元素,即邻近域到自身的相对距离,被标准化为 10,非对角元素的相对距离被缩放为相对于 10 的值。例如,如果从邻近域 i 到邻近域 j 的相对距离为 2.4,那么矩阵元素 i*N + jj*N + i 的值为 24 ,其中 N 是邻近域的数量。

如果从一个地点无法到达另一地点,则对应矩阵元素的值为 255 (0xFF)。

注:相对距离中 0-9 是保留值,没有任何意义。

2.2.2 SLIT 表格式

与 SRAT 表类似,该表也是以一个 ACPI 通用表头开始。在表头之后,“Number of System Localities” 字段指示系统中邻近域的数量。接下来是矩阵元素,从 Entry[0][0] 到 Entry [Number of System Localities-1] [Number of System Localities-1]。

FieldByte LengthByte OffsetDescription
Signature40“SLIT”。SLIT 表签名。
Length44SLIT 表的总大小,包括表头
Revision181
Checksum19Entire table must sum to zero.
OEMID610OEM ID.
OEM Table ID816OEM Table ID
OEM Revision424OEM Revision
Creator ID428Creator ID
Creator Revision432Creator Revision
Number of System Localities836系统中邻近域的数量
Entry[0][0]144矩阵元素 entry (0,0),值为 10
Entry[0][Number of System Localities-1]1矩阵元素 entry (0, Number of System Localities-1)
Entry[1][0]1矩阵元素 entry (1,0)
Entry [Number of System Localities-1] [Number of System Localities-1]1矩阵元素 entry (Number of System Localities-1, Number of System Localities-1),值为 10。

三、数据结构

3.1 SRAT 相关

3.1.1 ACPI 通用表头 -- acpi_table_header

内核使用 acpi_table_header 结构描述 ACPI 通用表头,每个字段的说明请参考 “2.1.1 ACPI 通用表头” 小节。

struct acpi_table_header {
     char signature[ACPI_NAME_SIZE]; /* ASCII table signature */
     u32 length;     /* Length of table in bytes, including this header */
     u8 revision;        /* ACPI Specification minor version number */
     u8 checksum;        /* To make sum of entire table == 0 */
     char oem_id[ACPI_OEM_ID_SIZE];  /* ASCII OEM identification */
     char oem_table_id[ACPI_OEM_TABLE_ID_SIZE];  /* ASCII OEM table identification */
     u32 oem_revision;   /* OEM revision number */
     char asl_compiler_id[ACPI_NAME_SIZE];   /* ASCII ASL compiler vendor ID */
     u32 asl_compiler_revision;  /* ASL compiler version */
 };

我们主要关注 2 个字段:

  • signature:表的签名,用来区分不同的表;
  • length:表的总大小(包括表头);

3.1.2 SRAT 表头 -- acpi_table_srat

acpi_table_srat 结构体用于描述 SRAT 表头。SRAT 表头中,除了包含 ACPI 通用表头外,还有 2 个专用字段。专用字段的描述详见 “2.1.2 SRAT 表头专用字段” 小节。

// file: include/acpi/actbl1.h
struct acpi_table_srat {
	struct acpi_table_header header;	/* Common ACPI table header */
	u32 table_revision;	/* Must be value '1' */
	u64 reserved;		/* Reserved, must be zero */
};

3.1.3 SRAT 表项类型

内核定义了枚举类型 acpi_srat_type 来表示 SRAT 中不同的表项类型。

// file: include/acpi/actbl1.h
/* Values for subtable type in struct acpi_subtable_header */
enum acpi_srat_type {
	ACPI_SRAT_TYPE_CPU_AFFINITY = 0,
	ACPI_SRAT_TYPE_MEMORY_AFFINITY = 1,
	ACPI_SRAT_TYPE_X2APIC_CPU_AFFINITY = 2,
	ACPI_SRAT_TYPE_RESERVED = 3	/* 3 and greater are reserved */
};

其中,ACPI_SRAT_TYPE_RESERVED 仅用来指示表项类型的数量,不代表具体类型。

3.1.4 子表头 -- acpi_subtable_header

acpi_subtable_header 描述 SRAT 表项的子表头。

我们在 “2.1.3.1 SRAT 子表头” 小节中介绍过,对于 SRAT 表的每一个表项,都包含一个通用子表头。子表头包含 2 个字段,每个字段的大小为 1 个字节。

 // file: include/acpi/actbl1.h
 /* Generic subtable header (used in MADT, SRAT, etc.) */
 struct acpi_subtable_header {
     u8 type;
     u8 length;
 };

其中,type 字段表示表项类型;length 字段表示表项大小。

3.1.5 APIC 亲和性 -- acpi_srat_cpu_affinity

结构体 acpi_srat_cpu_affinity 描述了拥有 Local APIC ID 的处理器与邻近域的关联关系。结构详情请参考 “2.1.3.2 Type 0:Processor Local APIC/SAPIC Affinity Structure” 节。

// file: include/acpi/actbl1.h
/* 0: Processor Local APIC/SAPIC Affinity */
struct acpi_srat_cpu_affinity {
	struct acpi_subtable_header header;
	u8 proximity_domain_lo;
	u8 apic_id;
	u32 flags;
	u8 local_sapic_eid;
	u8 proximity_domain_hi[3];
	u32 reserved;		/* Reserved, must be zero */
};

其中,proximity_domain_lo 表示邻近域的低 8 位,proximity_domain_hi 表示临近域的高 24 位;apic_id 字段表示 8 位的 Local APIC ID。

另外,flags 字段的 enabled 位(位 0 ),指示该表项是否可用;其它位保留未使用。内核定义了宏 ACPI_SRAT_CPU_USE_AFFINITY 来表示 enabled 位的掩码。

#define ACPI_SRAT_CPU_USE_AFFINITY  (1)	/* 00: Use affinity structure */

3.1.6 内存亲和性 -- acpi_srat_mem_affinity

结构体 acpi_srat_mem_affinity 描述了内存区域与邻近域的关联关系。结构详情请参考 “2.1.3.3 Type 1:Memory Affinity Structure” 小节。

// file: include/acpi/actbl1.h
/* 1: Memory Affinity */
struct acpi_srat_mem_affinity {
	struct acpi_subtable_header header;
	u32 proximity_domain;
	u16 reserved;		/* Reserved, must be zero */
	u64 base_address;
	u64 length;
       u32 reserved1;
	u32 flags;
       u64 reserved2;	       /* Reserved, must be zero */
};

其中,proximity_domain 字段表示邻近域;base_address 字段表示内存区域的基地址;length 字段表示内存区域的大小;flags 字段指示表项的标志。

flags 字段中,位 0 是 enabled 位,位 1 是 hot-pluggable 位,位 2 是 non-volatile 位,其它位保留未使用。

内核定义了 3 个宏,表示 flags 字段中不同标志位的掩码:

#define ACPI_SRAT_MEM_ENABLED       (1)	/* 00: Use affinity structure */
#define ACPI_SRAT_MEM_HOT_PLUGGABLE (1<<1)	/* 01: Memory region is hot pluggable */
#define ACPI_SRAT_MEM_NON_VOLATILE  (1<<2)	/* 02: Memory region is non-volatile */

3.1.7 x2APIC 亲和性 -- acpi_srat_x2apic_cpu_affinity

结构体 acpi_srat_x2apic_cpu_affinity 描述了拥有 x2APIC ID 的处理器与邻近域的关联关系。

// file: include/acpi/actbl1.h
/* 2: Processor Local X2_APIC Affinity (ACPI 4.0) */
struct acpi_srat_x2apic_cpu_affinity {
	struct acpi_subtable_header header;
	u16 reserved;		/* Reserved, must be zero */
	u32 proximity_domain;
	u32 apic_id;
	u32 flags;
	u32 clock_domain;
	u32 reserved2;
};

其中,proximity_domain 字段表示邻近域;apic_id 字段表示 32 位的 x2APIC ID;flags 字段表示表项标志。flags 字段的 enabled 位(位 0 ),指示该表项是否可用,其它位保留未使用。

3.2 SLIT 相关

3.2.1 acpi_table_slit

内核使用 acpi_table_slit 结构体描述 SLIT 表。其中 header 字段表示 ACPI 通用表头,locality_count 字段表示系统中邻近域的数量,entry 字段表示距离矩阵的首个元素,实际的矩阵元素数量为 locality_count 的平方。

// file: include/acpi/actbl1.h
struct acpi_table_slit {
	struct acpi_table_header header;	/* Common ACPI table header */
	u64 locality_count;
	u8 entry[1];		/* Real size = localities^2 */
};

3.3 nodemask 相关

内核使用位图来管理节点状态,每个节点对应着位图中的一个比特位。由于节点状态很多,所以内核使用了多个位图来表示不同状态的节点。这些管理节点状态的位图统称为节点掩码 nodemask。

3.3.1 宏 DECLARE_BITMAP

DECLARE_BITMAP 用来声明一个位图,其定义如下:

// file: include/linux/types.h
#define DECLARE_BITMAP(name,bits) \
	unsigned long name[BITS_TO_LONGS(bits)]

该宏接收 2 个参数:

  • name -- 位图名称
  • bits -- 位图的容量

由于我们使用 unsigned long 数组来表示位图,所以需要将比特位数量转换成数组的成员数量。宏 BITS_TO_LONGS 用来实现对应的转换,其定义如下:

  // file: include/linux/bitops.h
  #define BITS_TO_LONGS(nr)   DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long))

BITS_TO_LONGS 中又引用了宏 DIV_ROUND_UPBITS_PER_BYTE

BITS_PER_BYTE 扩展为 8,表示每个字节包含 8 个比特位:

  // file: include/linux/bitops.h
  #define BITS_PER_BYTE       8

BITS_PER_BYTE * sizeof(long) 计算 long 类型数据占用的比特位数量。

DIV_ROUND_UP 用来把整数 n 向上圆整到 d 的倍数,其实现方式就是把 n 加上 d-1 后再除以 d,该宏定义如下:

  // file: include/linux/kernel.h
  #define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))

在我们的案例中,就是将比特位数量向上圆整到unsigned long类型的整数倍。

由于执行了向上圆整操作,实际的位图中有些比特位是无效的,如下图所示:

DECLARE_BITMAP.png

3.3.2 nodemask_t 结构体

内核使用结构体 nodemask_t 来指示 NUMA 各节点的状态。

nodemask_t 是对位图的包装,其内部通过宏 DECLARE_BITMAP 定义了一个位图。

// file: include/linux/nodemask.h
typedef struct { DECLARE_BITMAP(bits, MAX_NUMNODES); } nodemask_t;

将宏 DECLARE_BITMAP 展开,nodemask_t 实际定义如下:

typedef struct { unsigned long bits[BITS_TO_LONGS(MAX_NUMNODES)]; } nodemask_t;

可以看到,结构体 nodemask_t 包含一个名为 bits 的成员,该成员是拥有 MAX_NUMNODES 个有效比特位的位图。

MAX_NUMNODES 表示系统支持的最大节点数量,其定义如下:

// file: include/linux/numa.h
#define MAX_NUMNODES    (1 << NODES_SHIFT)

NODES_SHIFT 扩展为 CONFIG_NODES_SHIFT

#define NODES_SHIFT     CONFIG_NODES_SHIFT

CONFIG_NODES_SHIFT 是内核配置选项,默认值为 10:

// file: include/generated/autoconf.h
#define CONFIG_NODES_SHIFT 10

最终,宏 MAX_NUMNODES扩展为 1 << 10,即 1024;所以 nodemask_t 本质上是一个容量为 1024 的位图。

3.3.3 NODE_MASK_ALL

该宏创建了一个有效比特位全部为 1 的节点掩码,其定义如下:

#define NODE_MASK_ALL							\
((nodemask_t) { {							\
	[0 ... BITS_TO_LONGS(MAX_NUMNODES)-2] = ~0UL,			\
	[BITS_TO_LONGS(MAX_NUMNODES)-1] = NODE_MASK_LAST_WORD		\
} })

由于节点掩码中将位图组织成 unsigned long 类型的数组,而节点数量是不确定的。当节点数量不是 unsigned long 容量的整数倍时,最后一个数组成员中有些比特位是无效的。所以在对节点掩码进行初始化时,最后一个数组成员需要特殊处理。在 NODE_MASK_ALL 的实现中,最后一个数组成员被初始化为宏 CPU_MASK_LAST_WORD ,其它成员的比特位全部初始化为 1。

CPU_MASK_LAST_WORD 定义如下,该宏内部又引用了 BITMAP_LAST_WORD_MASK

// file: include/linux/nodemask.h
#define NODE_MASK_LAST_WORD BITMAP_LAST_WORD_MASK(MAX_NUMNODES)

BITMAP_LAST_WORD_MASK 会根据比特位数量 MAX_NUMNODES ,计算最后一个数组成员中的有效比特位,并将这些位设置为 1,其它位为 0,该宏定义如下:

 // file: include/linux/bitmap.h
 #define BITMAP_LAST_WORD_MASK(nbits)                    \
 (                                   \
     ((nbits) % BITS_PER_LONG) ?                 \
         (1UL<<((nbits) % BITS_PER_LONG))-1 : ~0UL       \
 )

当比特位数量不是 long 类型容量的整数倍时,宏 BITMAP_LAST_WORD_MASK 效果如下:

BITMAP_LAST_WORD_MASK.png

当比特位数量是 long 类型容量的整数倍时,全部比特位都为 1。

3.3.4 NODE_MASK_NONE

NODE_MASK_NONE 定义了一个比特位全部为 0 的节点掩码。

// file: include/linux/nodemask.h
#define NODE_MASK_NONE							\
((nodemask_t) { {							\
	[0 ... BITS_TO_LONGS(MAX_NUMNODES)-1] =  0UL			\
} })

3.3.5 nodes_found_map

节点掩码 nodes_found_map 用来指示已经找到的节点。该掩码被初始化为 NODE_MASK_NONE,即位图的每个比特位都初始化为 0。

// file: drivers/acpi/numa.c
static nodemask_t nodes_found_map = NODE_MASK_NONE;

另外,该掩码也用来生成节点 ID。由于我们从 SRAT 中能获取到的只有邻近域,每找到一个邻近域,就要生成对应的节点 ID。生成的方法就是在 nodes_found_map 中找到第一个为 0 的比特位的索引,该索引就作为节点 ID。当然,生成节点 ID 后,还需要将该位图中对应的比特位置位。

在解析 SRAT 的三种表项时,会在 setup_node 函数中通过该位图为邻近域生成对应的节点 ID,并建立邻近域和节点的双向映射关系。

3.3.6 numa_nodes_parsed

节点掩码 numa_nodes_parsed 用来指示已经成功解析的节点。

// file: arch/x86/mm/numa.c
nodemask_t numa_nodes_parsed __initdata;

该掩码也是在解析 SRAT 的三种表项时被置位。此时已经通过 nodes_found_map 位图生成了节点 ID,但还要进行必要的有效性验证,验证通过后,才会将 numa_nodes_parsed 中对应的比特位置位。也就是说,numa_nodes_parsed 中的置位比特位可能会比 nodes_found_map中的少。从名字上也能看出来,一个是已发现的,一个是成功解析的。

3.3.7 node_to_cpumask_map

node_to_cpumask_map 是一个包含 MAX_NUMNODES 个成员的数组,每个成员都是一个 CPU 掩码。

// file: arch/x86/mm/numa.c
cpumask_var_t node_to_cpumask_map[MAX_NUMNODES];

其中,MAX_NUMNODES 表示最大节点数量,扩展为 1024。

由于每个节点可能包含多个处理器,对于节点来说,既要知道处理器的数量,还要知道处理器编号,所以每个节点都使用了一个 cpumask_var_t 类型的位图来表示节点包含的处理器。

3.3.8 node_states

node_states 是一个拥有 NR_NODE_STATES 个成员的数组,数组的每一个成员都是 nodemask_t 类型的位图。换句话说,node_states 为节点的每一种状态都定义了一个位图。

// file: mm/page_alloc.c
/*
 * Array of node states.
 */
nodemask_t node_states[NR_NODE_STATES] __read_mostly = {
	[N_POSSIBLE] = NODE_MASK_ALL,
	[N_ONLINE] = { { [0] = 1UL } },
#ifndef CONFIG_NUMA
	[N_NORMAL_MEMORY] = { { [0] = 1UL } },
#ifdef CONFIG_HIGHMEM
	[N_HIGH_MEMORY] = { { [0] = 1UL } },
#endif
#ifdef CONFIG_MOVABLE_NODE
	[N_MEMORY] = { { [0] = 1UL } },
#endif
	[N_CPU] = { { [0] = 1UL } },
#endif	/* NUMA */
};

其中,宏 NR_NODE_STATES 和其它以 N_ 开头的宏,都是枚举类型 node_states 的成员。其中宏 NR_NODE_STATES 仅用来表示节点状态数量,不表示具体状态;其它宏分别表示不同的节点状态。

node_states 的成员中,索引为 N_POSSIBLE 的成员(possible 状态的位图)被初始化为 NODE_MASK_ALL;其它成员只有第一个比特位被置位。

从定义中能够看到,如果配置了内核选项 CONFIG_NUMA,只会初始化 possible 和 online 两种状态位图。

3.3.8.1 节点状态

内核定了枚举类型 node_states 来表示节点状态。其中成员 NR_NODE_STATES 仅用来表示成员数量,不表示具体状态;其它成员表示不同的节点状态。

// file: include/linux/nodemask.h
/*
 * Bitmasks that are kept for all the nodes.
 */
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
};

node_states 中定义的节点状态说明如下:

  • N_POSSIBLE:系统可能使用的节点,也许目前不在线,但以后可能会上线;
  • N_ONLINE:处于上线状态;
  • N_NORMAL_MEMORY:节点拥有常规内存;
  • N_HIGH_MEMORY:节点拥有高端内存。如果系统没有配置高端内存,那么该值等于 N_NORMAL_MEMORY
  • N_MEMORY:节点拥有内存,不管是任何类型的(常规、高端、可移动)。如果系统没有设置 CONFIG_MOVABLE_NODE,那么该值等于 N_HIGH_MEMORY
  • N_CPU:节点拥有一个或多个 CPU;
  • NR_NODE_STATES:仅用来表示节点状态数量,不表示具体状态。

当内核配置选项 CONFIG_MOVABLE_NODE 为 1 时,允许节点只包含可移动的内存;默认值为 0。

// file: mm/Kconfig
config MOVABLE_NODE
	boolean "Enable to assign a node which has only movable memory"
	depends on HAVE_MEMBLOCK
	depends on NO_BOOTMEM
	depends on X86_64
	depends on NUMA
	default n
	help
	  Allow a node to have only movable memory.  Pages used by the kernel,
	  such as direct mapping pages cannot be migrated.  So the corresponding
	  memory device cannot be hotplugged.  This option allows users to
	  online all the memory of a node as movable memory so that the whole
	  node can be hotplugged.  Users who don't use the memory hotplug
	  feature are fine with this option on since they don't online memory
	  as movable.

	  Say Y here if you want to hotplug a whole node.
	  Say N here if you want kernel to use memory on all nodes evenly.

如果内核支持热插拔,那么在将某个内存设备移除时,需要将该设备的数据迁移到其它设备上,此时数据的物理地址将发生变化。对于用户空间使用的内存,可以在数据迁移后重建页表项来完成虚拟地址到新物理地址的映射,所以这是没有问题的,而且这一切会用户进程来说都是透明的。但是对于内核使用的页来说,是不能迁移的。比如,内核空间的直接映射区,该区间的物理地址加上一个常量 PAGE_OFFSET 后就得到虚拟地址,内核会通过 __pa__va 宏对直接映射区的物理和虚拟地址进行转换:

// file: arch/x86/include/asm/page.h
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

如果该区间的数据发生了迁移,那么映射关系就会错乱,所以这部分内存就是不可迁移的。可迁移的内存,其对应的设备就是可插拔的;否则,就是不可插拔的。

如果该配置选项为真,那么有的节点内存就是完全可移动,该节点就能够进行热插拔,内核就不能在该节点上为自己分配内存;否则,内核可以使用所有节点的内存。

3.3.8.2 node_online_map、node_possible_map

node_states 数组的不同状态位图中,内核为 onlinepossible 状态位图定义了专门的宏:

// file: include/linux/nodemask.h
#define node_online_map 	node_states[N_ONLINE]
#define node_possible_map 	node_states[N_POSSIBLE]

node_online_map 表示 online 状态的节点位图;node_possible_map 表示 possible 状态的节点位图。

3.4 cpu、apicid、node、pxm 之间的映射

3.4.1 x86_cpu_to_apicid

per-cpu 变量x86_cpu_to_apicid 中保存着 CPU 对应的 APIC ID ,初始化为 BAD_APICID

// file: arch/x86/kernel/apic/apic.c
DEFINE_EARLY_PER_CPU_READ_MOSTLY(u16, x86_cpu_to_apicid, BAD_APICID);

我们在 Linux Kernel:CPU 拓扑结构探测 一文中 5.2.1.8 generic_processor_info 小节介绍过该变量。在 generic_processor_info 函数中,会生成处理器信息并统计处理器数量。如果处理器可用,会将处理器对应的 APIC ID 保存到 per-cpu 变量 x86_cpu_to_apicid 中。

 ......
 #if defined(CONFIG_SMP) || defined(CONFIG_X86_64)
     early_per_cpu(x86_cpu_to_apicid, cpu) = apicid;
     early_per_cpu(x86_bios_cpu_apicid, cpu) = apicid;
 #endif
 ......

BAD_APICID 扩展为 0xFFFF:

// file: arch/x86/include/asm/apicdef.h
#define BAD_APICID 0xFFFFu

3.4.2 x86_cpu_to_node_map

per-cpu 变量 x86_cpu_to_node_map 中保存着 CPU 对应的节点 ID,初始化为 NUMA_NO_NODE

// file: arch/x86/mm/numa.c
DEFINE_EARLY_PER_CPU(int, x86_cpu_to_node_map, NUMA_NO_NODE);

NUMA_NO_NODE 扩展为 -1:

// file: include/linux/numa.h
#define	NUMA_NO_NODE	(-1)

3.4.3 numa_node

per-cpu 变量 numa_node 中保存着 CPU 对应的节点 ID。

// file: mm/page_alloc.c
DEFINE_PER_CPU(int, numa_node);

3.4.4 pxm_to_node_map、node_to_pxm_map

pxm_to_node_mapnode_to_pxm_map 都是拥有 MAX_PXM_DOMAINS 个成员的 int 数组。其中,pxm_to_node_map 用来保存邻近域对应的节点,数组的索引是邻近域,值为节点 ID;node_to_pxm_map 用来保存节点对应的邻近域,数组的索引是节点 ID,值为邻近域。我们从 SRAT 表里能够直接得到的只有邻近域(proximity domain)的值,节点 ID 是内核生成的,所以要保存两者的映射关系。

// file: drivers/acpi/numa.c
/* maps to convert between proximity domain and logical node ID */
static int pxm_to_node_map[MAX_PXM_DOMAINS]
			= { [0 ... MAX_PXM_DOMAINS - 1] = NUMA_NO_NODE };
static int node_to_pxm_map[MAX_NUMNODES]
			= { [0 ... MAX_NUMNODES - 1] = PXM_INVAL };

pxm_to_node_map 的每个成员被初始化为 NUMA_NO_NODE(扩展为 -1),表示无效的节点。

node_to_pxm_map 的每个成员被初始化为 PXM_INVAL(扩展为 -1),表示无效的临近域。

// file: include/linux/acpi.h
#define PXM_INVAL	(-1)

MAX_PXM_DOMAINS 扩展为 MAX_NUMNODES ,即最大节点数量。

// file: include/acpi/acpi_numa.h
#define MAX_PXM_DOMAINS MAX_NUMNODES

3.4.5 __apicid_to_node

// file: arch/x86/mm/numa.c
/*
 * apicid, cpu, node mappings
 */
s16 __apicid_to_node[MAX_LOCAL_APIC] = {
	[0 ... MAX_LOCAL_APIC-1] = NUMA_NO_NODE
};

__apicid_to_node 是一个包含 MAX_LOCAL_APIC 个成员数组,用来保存 APIC ID 到节点 ID 的映射关系。数组的所有成员被初始化为 NUMA_NO_NODE(扩展为 -1)。

MAX_LOCAL_APIC 表示 Local APIC 的最大数量,扩展为 32768:

# define MAX_LOCAL_APIC 32768

虽然在 x2APIC 架构下,x2APIC ID 是 32 位的,但是内核支持的最大数量为 215=327682^{15} = 32768 个,并使用 0xFFFF 来标识无效的 APIC ID。

3.4.6 DEFINE_EARLY_PER_CPU 与 DEFINE_PER_CPU 的关系

我们在上文看到,x86_cpu_to_apicidx86_cpu_to_node_map 分别是通过宏 DEFINE_EARLY_PER_CPU_READ_MOSTLYDEFINE_EARLY_PER_CPU 定义的,而numa_node 是通过宏 DEFINE_PER_CPU 定义的。我们以宏 DEFINE_EARLY_PER_CPU 为例,来说明下 early_per_cpu 变量和 per_cpu 变量的区别。

// file: arch/x86/include/asm/percpu.h
/*
 * Define the "EARLY_PER_CPU" macros.  These are used for some per_cpu
 * variables that are initialized and accessed before there are per_cpu
 * areas allocated.
 */
#define	DEFINE_EARLY_PER_CPU(_type, _name, _initvalue)			\
	DEFINE_PER_CPU(_type, _name) = _initvalue;			\
	__typeof__(_type) _name##_early_map[NR_CPUS] __initdata =	\
				{ [0 ... NR_CPUS-1] = _initvalue };	\
	__typeof__(_type) *_name##_early_ptr __refdata = _name##_early_map

可以看到,DEFINE_EARLY_PER_CPU 宏除了使用 DEFINE_PER_CPU 在 per-cpu 区域创建了变量外,还在非 per-cpu 区域创建了一个包含 NR_CPUS 个成员的数组。 宏NR_CPUS 是通过内核配置选项设置的最大 CPU 数量。注意,这个数组是由 __initdata 修饰的,也就是说该数组占用的内存在内核初始化完成之后会被释放。

那么,为甚么既要创建 per-cpu 变量,又要创建数组呢?因为通过 DEFINE_EARLY_PER_CPU 宏创建的 per-cpu 变量,在系统初始化早期就要使用,此时 per-cpu 内存区域还未分配,同时又要为每个 CPU 保存数据,所以只能创建数组临时保存。等内核初始化完成之后,这些数据就会被保存到通过 DEFINE_PER_CPU 定义的 per-cpu 变量中,而数组中的数据就不再需要,可以释放了。

per-cpu 区域的构建是在函数 setup_per_cpu_areas 中完成的,解析 SRAT 和 SLIT 表都在此之前完成的,所以我们会看到函数中使用了 DEFINE_EARLY_PER_CPU 宏创建的变量。

而在获取 early_per_cpu 类型的变量时,会优先从数组中查找,如果数组不存在,才会查找真正的 per-cpu 变量。

// file: arch/x86/include/asm/percpu.h
#define	early_per_cpu(_name, _cpu) 				\
	*(early_per_cpu_ptr(_name) ?				\
		&early_per_cpu_ptr(_name)[_cpu] :		\
		&per_cpu(_name, _cpu))

#define	early_per_cpu_ptr(_name) (_name##_early_ptr)

不同函数执行的先后顺序:

early_per_cpu.png

3.5 节点距离相关

内核定义了两个静态变量 numa_distancenuma_distance_cnt ,分别表示节点距离矩阵以及矩阵中的节点数量。

// file: arch/x86/mm/numa.c
static int numa_distance_cnt;
static u8 *numa_distance;

在解析 SLIT 表时,会为这两个变量赋值。

3.6 NUMA 内存相关

3.6.1 struct numa_meminfo

内核使用 numa_meminfo 结构体来描述 NUMA 的内存信息,该结构包含 2 个字段:

  • nr_blks:NUMA 中内存块的数量,也是 blk 数组的成员数量。
  • blk:内存块数组,数组的每个成员描述了一段内存信息。
// file: arch/x86/mm/numa.c
struct numa_meminfo {
	int			nr_blks;
	struct numa_memblk	blk[NR_NODE_MEMBLKS];
};

blk 的成员数量为 NR_NODE_MEMBLKS,扩展为 MAX_NUMNODES*2,即最大节点数量的 2 倍。在解析 SRAT 表时,由于节点对应的内存块可能不止一个,为了保证容量充足,所以此处数组的容量为节点的 2 倍。

// file: arch/x86/include/asm/numa.h
#define NR_NODE_MEMBLKS		(MAX_NUMNODES*2)

MAX_NUMNODES 表示系统支持的最大节点数量,扩展为 1024。

使用numa_memblk 结构体来表示 NUMA 内存块,其定义如下:

// file: arch/x86/mm/numa.c
struct numa_memblk {
	u64			start;
	u64			end;
	int			nid;
};

该结构体中包含内存块的 3 个重要属性:起始地址、结束地址、节点 ID。

3.6.2 静态变量 numa_meminfo

numa_meminfostruct numa_meminfo 结构体的同名静态变量,用来保存 NUMA 的内存信息。

// file: arch/x86/mm/numa.c
static struct numa_meminfo numa_meminfo

3.6.3 NUMA 内存与 memblock 的关系

在 NUMA 节点探测之前,内核已经通过 e820 接口获取到系统内存,并使用 memblock 分配器管理 e820 内存。在为 memblock 分配器填充内存时,由于尚未探测内存节点,所以将每个内存块的节点初始化为 MAX_NUMNODES

memblock_x86_fill 函数会将 e820 接口探测到的内存填充到 memblock 管理的内存块中。

 // file: arch/x86/kernel/e820.c
oid __init memblock_x86_fill(void)
{
......

	for (i = 0; i < e820.nr_map; i++) {
		struct e820entry *ei = &e820.map[i];

		end = ei->addr + ei->size;
......

		memblock_add(ei->addr, ei->size);
	}
......
}

memblock_add 函数会将内存块的节点设置为 MAX_NUMNODES

// file: mm/memblock.c
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{
	return memblock_add_region(&memblock.memory, base, size, MAX_NUMNODES);
}

在探测到节点内存后,会为 memblock 管理的内存块重新设置节点。

3.7 节点描述符和节点数组

3.7.1 pglist_data

内核使用结构体 pglist_data 来描述节点信息。在进行节点内存解析时,会填充该结构的以下 3 个字段:

// file: include/linux/mmzone.h
typedef struct pglist_data {
......
    unsigned long node_start_pfn;
......
	unsigned long node_spanned_pages; /* total size of physical page
					     range, including holes */
	int node_id;
......
}

3.7.2 node_data

所有的节点信息被放置到数组 node_data 中。

struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;

MAX_NUMNODES 指示最大节点数量,扩展为 1024。

四、相关接口

4.1 nodemask_t 位图操作

nodemask_t 操作,底层使用了位图相关接口。位图相关内容,请参考:Linux Kernel:内核数据结构之位图(Bitmap)

4.1.1 node_set

node_set 将位图 @dst@node 位置的比特位置位。

该宏接收 2 个参数:

  • @node:节点 ID
  • @dst:目标位图

其内部调用位图接口 set_bit 完成设置功能。

#define node_set(node, dst) __node_set((node), &(dst))
static inline void __node_set(int node, volatile nodemask_t *dstp)
{
	set_bit(node, dstp->bits);
}

4.1.2 node_clear

node_clear 将位图 @dst@node 位置的比特位清除。

该宏接收 2 个参数:

  • @node:节点 ID
  • @dst:目标位图

其内部调用位图接口 clear_bit 完成位清除功能。

#define node_clear(node, dst) __node_clear((node), &(dst))
static inline void __node_clear(int node, volatile nodemask_t *dstp)
{
	clear_bit(node, dstp->bits);
}

4.1.3 nodes_weight

nodes_weight 统计指定节点位图中为 1 的比特位数量,该宏扩展为 __nodes_weight 函数。

__nodes_weight 函数接收 2 个参数:

  • @srcp:节点掩码/位图
  • @nbits:位图容量

__nodes_weight 函数统计位图 @srcp 中前 @nbits 个比特位中为 1 的数量,其内部调用位图接口 bitmap_weight 完成统计功能。

#define nodes_weight(nodemask) __nodes_weight(&(nodemask), MAX_NUMNODES)
static inline int __nodes_weight(const nodemask_t *srcp, int nbits)
{
	return bitmap_weight(srcp->bits, nbits);
}

4.1.4 first_unset_node

first_unset_node 会查找并返回指定位图中第一个为 0 的比特位的位置,如果未找到的话,会返回最大节点数量 MAX_NUMNODES

该宏扩展为 __first_unset_node 函数,该函数内部会调用 min_t 函数获取 MAX_NUMNODESfind_first_zero_bit 函数的较小值。

位图接口 find_first_zero_bit 用于获取指定位图中第一个为 0 的比特位位置,其第一个参数是要搜索的位图,第二个参数是位图容量。如果找到,会返回对应比特位的位置;否则,返回位图容量。

#define first_unset_node(mask) __first_unset_node(&(mask))
static inline int __first_unset_node(const nodemask_t *maskp)
{
	return min_t(int,MAX_NUMNODES,
			find_first_zero_bit(maskp->bits, MAX_NUMNODES));
}

4.1.5 first_node

first_node 用于获取指定节点位图中的第一个为 1 的比特位位置,如果未找到,会返回最大节点数量 MAX_NUMNODES

其实现过程与 first_unset_node 类似,只不过位图接口 find_first_bit 获取到的是指定位图中第一个为 1 的比特位位置。

// file: include/linux/nodemask.h
#define first_node(src) __first_node(&(src))
static inline int __first_node(const nodemask_t *srcp)
{
	return min_t(int, MAX_NUMNODES, find_first_bit(srcp->bits, MAX_NUMNODES));
}

4.1.6 next_node

next_node 用于获取位图中指定比特位(不包括)之后的第一个为 1 的比特位索引,如果未找到,返回最大节点数量 MAX_NUMNODES

该宏接收 2 个参数:

  • @n:搜索起始位置(不包括)
  • @src:节点位图

其实现过程与 first_node 类似,只不过 find_next_bit 会从指定位置开始搜索第一个为 1 的比特位,而不是从位置 0 开始搜索。

// file: include/linux/nodemask.h
#define next_node(n, src) __next_node((n), &(src))
static inline int __next_node(int n, const nodemask_t *srcp)
{
	return min_t(int,MAX_NUMNODES,find_next_bit(srcp->bits, MAX_NUMNODES, n+1));
}

4.1.7 for_each_node_mask

for_each_node_mask 用于遍历节点位图中为 1 的比特位。该宏内部引用了接口 first_nodenext_node

#define for_each_node_mask(node, mask)			\
	for ((node) = first_node(mask);			\
		(node) < MAX_NUMNODES;			\
		(node) = next_node((node), (mask)))

4.1.8 nodes_empty

nodes_empty 宏检测指定的节点位图是否所有比特位全为 0。如果全都为 0,返回 1;否则,返回 0。

该宏内部使用位图接口 bitmap_empty 完成检测功能。

// file: include/linux/nodemask.h
#define nodes_empty(src) __nodes_empty(&(src), MAX_NUMNODES)
static inline int __nodes_empty(const nodemask_t *srcp, int nbits)
{
	return bitmap_empty(srcp->bits, nbits);
}

4.2 节点状态接口

4.2.1 node_state

node_state 函数用于确定节点是否处于指定状态。

该函数接收 2 个参数:

  • @node:节点 ID
  • @state:状态值

函数内部,通过 node_isset 宏检测状态位图中节点对应的比特位是否被置位。

// file: include/linux/nodemask.h
static inline int node_state(int node, enum node_states state)
{
	return node_isset(node, node_states[state]);
}

node_states 是一个位图数组,数组的每个成员都是一个位图,代表着不同的节点状态。所以,通过查看状态位图中该节点对应的比特位是否置位,就能确定节点是否处于指定状态。

node_states 详见 “3.3.8 node states” 小节。

4.2.1.1 node_isset

node_isset 内部,通过位图接口 test_bit 完成比特位测试功能。

/* No static inline type checking - see Subtlety (1) above. */
#define node_isset(node, nodemask) test_bit((node), (nodemask).bits)

4.2.2 node_set_state

node_set_state 函数将指定状态位图中对应节点的比特位置位。

该函数接收 2 个参数:

  • @node:节点 ID
  • @state:节点状态
// file: include/linux/nodemask.h
static inline void node_set_state(int node, enum node_states state)
{
	__node_set(node, &node_states[state]);
}

其内部使用了 __node_set 接口完成设置置位功能。 __node_set 接口请参考 ”4.1.1 node_set“ 小节。

4.2.3 num_node_state

num_node_state 函数计算指定状态的节点数量。

// file: include/linux/nodemask.h
static inline int num_node_state(enum node_states state)
{
	return nodes_weight(node_states[state]);
}

函数内部,通过 nodes_weight 接口计算出节点状态位图中为 1 的比特位数量。

4.2.4 node_online

node_online 检测节点是否处于 online 状态。其内部通过 node_state 函数实现查看功能。

// file: include/linux/nodemask.h
#define node_online(node)	node_state((node), N_ONLINE)

4.2.5 node_set_online

node_set_online 函数将 online 状态位图中对应节点的比特位置位。

// file: include/linux/nodemask.h
static inline void node_set_online(int nid)
{
	node_set_state(nid, N_ONLINE);
	nr_online_nodes = num_node_state(N_ONLINE);
}

除了通过 node_set_state 函数完成置位功能外,还通过 num_node_state 函数计算出在线节点的数量并保存到 nr_online_nodes 变量中。

nr_online_nodes 是一个全局变量,其初始值为 1。

// file: mm/page_alloc.c
int nr_online_nodes __read_mostly = 1;

4.2.6 for_each_node_state

for_each_node_state 会遍历指定状态的节点位图中为 1 的比特位,其内部引用了宏 for_each_node_mask 完成遍历功能。

#define for_each_node_state(__node, __state) \
	for_each_node_mask((__node), node_states[__state])

4.2.7 for_each_online_node

for_each_online_node 宏会遍历节点在线位图,查找所有在线节点。

// file: include/linux/nodemask.h
#define for_each_online_node(node) for_each_node_state(node, N_ONLINE)

4.3 cpu、apicid、node、pxm 映射接口

4.3.1 early_cpu_to_node

该函数用于在内核初始化早期,获取 CPU 对应的节点。此时,CPU 和节点的映射关系保存在 early-per-cpu 变量 x86_cpu_to_node_map 中,直接读取即可。

// file: arch/x86/include/asm/topology.h
/* Same function but used if called before per_cpu areas are setup */
static inline int early_cpu_to_node(int cpu)
{
	return early_per_cpu(x86_cpu_to_node_map, cpu);
}

4.3.2 pxm_to_node

pxm_to_node 函数获取邻近域 pxm 对应的节点 ID。

// file: drivers/acpi/numa.c
int pxm_to_node(int pxm)
{
	if (pxm < 0)
		return NUMA_NO_NODE;
	return pxm_to_node_map[pxm];
}

如果 pxm 小于 0,说明是无效值,返回 NUMA_NO_NODE

否则,直接从 pxm_to_node_map 数组中获取并返回。

4.3.3 acpi_map_pxm_to_node

acpi_map_pxm_to_node 函数为邻近域设置节点。

// file: drivers/acpi/numa.c
int acpi_map_pxm_to_node(int pxm)
{
	int node = pxm_to_node_map[pxm];

	if (node < 0) {
		if (nodes_weight(nodes_found_map) >= MAX_NUMNODES)
			return NUMA_NO_NODE;
		node = first_unset_node(nodes_found_map);
		__acpi_map_pxm_to_node(pxm, node);
		node_set(node, nodes_found_map);
	}

	return node;
}

函数内部,首先从数组 pxm_to_node_map 中查找 pxm 对应的节点 ID。

如果节点 ID 小于 0,说明该邻近域对应的节点 ID 还处于初始状态(初始值为 -1),需要设置;否则,直接返回节点 ID。

为邻近域设置节点 ID 的流程如下:

  • 调用 nodes_weight 函数从位图 nodes_found_map 中计算出已发现的节点数量,如果数量已经达到或超过上限值 MAX_NUMNODES,直接返回 无效节点值NUMA_NO_NODE(扩展为 -1);否则,执行下一步;
  • 通过 first_unset_node 宏从 nodes_found_map 中获取第一个为 0 的比特位索引,该值就是邻近域对应的节点 ID
  • 调用 _acpi_map_pxm_to_node 函数,在 pxm_to_node_mapnode_to_pxm_map 中对邻近域和节点进行双向映射。
  • 调用 node_set 宏将 nodes_found_map 中对应的比特位置位。
4.3.3.1 __acpi_map_pxm_to_node

__acpi_map_pxm_to_node 函数用于建立邻近域和节点的双向映射关系。

// file: drivers/acpi/numa.c
void __acpi_map_pxm_to_node(int pxm, int node)
{
	if (pxm_to_node_map[pxm] == NUMA_NO_NODE || node < pxm_to_node_map[pxm])
		pxm_to_node_map[pxm] = node;
	if (node_to_pxm_map[node] == PXM_INVAL || pxm < node_to_pxm_map[node])
		node_to_pxm_map[node] = pxm;
}

其内部实现,就是在 pxm_to_node_mapnode_to_pxm_map 数组中保存对应的映射关系。

4.3.4 set_apicid_to_node

set_apicid_to_node 函数用于建立 APIC ID 到节点的映射。

该函数接收 2 个参数:

  • @apicid:APIC ID
  • @node:NUMA 节点 ID

函数定义如下:

static inline void set_apicid_to_node(int apicid, s16 node)
{
	__apicid_to_node[apicid] = node;
}

其内部实现,是将节点 ID 保存到下标为 apicid 的 __apicid_to_node 数组成员中。

4.3.5 numa_cpu_node

numa_cpu_node 函数获取 CPU 对应的 NUMA 节点。

该函数先从 early-per-cpu 变量 x86_cpu_to_apicid 中获取 CPU 对应的 APIC ID,然后从 __apicid_to_node 中获取 APIC ID 对应的节点 ID。如果获取到的 APIC ID 无效,则返回无效的节点值 NUMA_NO_NODE

int __cpuinit numa_cpu_node(int cpu)
{
	int apicid = early_per_cpu(x86_cpu_to_apicid, cpu);

	if (apicid != BAD_APICID)
		return __apicid_to_node[apicid];
	return NUMA_NO_NODE;
}

其中,宏 BAD_APICID 扩展为 0xFFFF, NUMA_NO_NODE 扩展为 -1。

4.3.6 set_cpu_numa_node

set_cpu_numa_node 函数设置 CPU 对应的节点。

函数内部,直接将映射关系保存在 per-cpu 变量 numa_node 中。

static inline void set_cpu_numa_node(int cpu, int node)
{
	per_cpu(numa_node, cpu) = node;
}

4.3.7 numa_set_node

该函数设置 CPU 对应的节点,其内部调用了 “4.3.5 set_cpu_numa_node” 函数。

// file: arch/x86/mm/numa.c
void numa_set_node(int cpu, int node)
{
	int *cpu_to_node_map = early_per_cpu_ptr(x86_cpu_to_node_map);

	/* early setting, no percpu area yet */
	if (cpu_to_node_map) {
		cpu_to_node_map[cpu] = node;
		return;
	}

#ifdef CONFIG_DEBUG_PER_CPU_MAPS
	......
#endif
	per_cpu(x86_cpu_to_node_map, cpu) = node;

	set_cpu_numa_node(cpu, node);
}

首先,通过 early_per_cpu_ptr 函数获取到 early-per-cpu 变量 x86_cpu_to_node_map 的指针。

如果指针为真,说明 per-cpu 区域还未构建,需要将数据保存在数组中,此时 x86_cpu_to_node_map 是数组地址;否则,将节点保存在真正的 per-cpu 变量 x86_cpu_to_node_map 中,然后还要调用 set_cpu_numa_node 函数将节点保存到 per-cpu 变量 numa_node 中。

4.3.8 numa_clear_node

函数内部直接调用 “4.3.6 numa_set_node” 函数将 CPU 对应的节点设置为无效节点 NUMA_NO_NODE

// file: arch/x86/mm/numa.c
void numa_clear_node(int cpu)
{
	numa_set_node(cpu, NUMA_NO_NODE);
}

4.4 NUMA 内存相关接口

4.4.1 numa_nodemask_from_meminfo

从名字也能看出,该函数从 NUMA 内存信息 numa_meminfo 中获取到所有的节点 ID,然后用这些节点 ID 填充指定的节点掩码。

该函数接收 2 个参数:

  • @nodemask:节点位图
  • @mi:NUMA 内存信息变量 numa_meminfo 的指针
// file: arch/x86/mm/numa.c
/*
 * Set nodes, which have memory in @mi, in *@nodemask.
 */
static void __init numa_nodemask_from_meminfo(nodemask_t *nodemask,
					      const struct numa_meminfo *mi)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(mi->blk); i++)
		if (mi->blk[i].start != mi->blk[i].end &&
		    mi->blk[i].nid != NUMA_NO_NODE)
			node_set(mi->blk[i].nid, *nodemask);
}

该函数会遍历 numa_meminfo 中的所有内存块,如果该内存块有效(内存块空间不为 0 且所属节点有效),则将节点位图 nodemask 中内存块所属节点对应的比特位置位。

ARRAY_SIZE 用于计算数组的成员数量,另外还会对入参类型进行验证。如果入参不是数组类型,就会产生编译时错误。ARRAY_SIZE 的实现细节可参考:Linux Kernel:内核数据结构之基数树(Radix Tree) 中 “2.3.2.1 宏 ARRAY_SIZE()” 小节。

4.4.2 numa_add_memblk_to

该函数用于向 NUMA 内存信息中添加内存块,其接收 4 个参数:

  • @nid:内存块所属的节点
  • @start:内存块的起始地址
  • @end:内存块的结束地址
  • @mi:NUMA 内存信息的指针

函数定义如下:

// file: arch/x86/mm/numa.c
static int __init numa_add_memblk_to(int nid, u64 start, u64 end,
				     struct numa_meminfo *mi)
{
	/* ignore zero length blks */
	if (start == end)
		return 0;

	/* whine about and ignore invalid blks */
	if (start > end || nid < 0 || nid >= MAX_NUMNODES) {
		pr_warning("NUMA: Warning: invalid memblk node %d [mem %#010Lx-%#010Lx]\n",
			   nid, start, end - 1);
		return 0;
	}

	if (mi->nr_blks >= NR_NODE_MEMBLKS) {
		pr_err("NUMA: too many memblk ranges\n");
		return -EINVAL;
	}

	mi->blk[mi->nr_blks].start = start;
	mi->blk[mi->nr_blks].end = end;
	mi->blk[mi->nr_blks].nid = nid;
	mi->nr_blks++;
	return 0;
}

如果内存块的起始地址等于结束地址,说明这个内存块的空间为 0,忽略。

如果起始地址大于结束地址,或者节点 ID 小于 0,或者节点 ID 超出最大值 MAX_NUMNODES-1,这些都属于无效数据,打印警告信息并返回 0。

如果内存块的数量已经达到最大值 NR_NODE_MEMBLKS(扩展为 MAX_NUMNODES*2),说明内存块数量超限了,返回错误码 -EINVAL

排除所有异常情况后,将内存块信息(起始地址、结束地址、节点 ID)写入内存块数组中,并将内存块数量加 1。

添加成功后,返回成功码 0。

4.4.3 numa_add_memblk

该函数用于向 NUMA 内存信息中添加内存块,其内部调用了 “4.4.2 numa_add_memblk_to” 函数。

该函数接收 3 个参数:

  • @nid:内存块所属的节点
  • @start:内存块的起始地址
  • @end:内存块的结束地址

该函数定义如下:

// file: arch/x86/mm/numa.c
/**
 * numa_add_memblk - Add one numa_memblk to numa_meminfo
 * @nid: NUMA node ID of the new memblk
 * @start: Start address of the new memblk
 * @end: End address of the new memblk
 *
 * Add a new memblk to the default numa_meminfo.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
int __init numa_add_memblk(int nid, u64 start, u64 end)
{
	return numa_add_memblk_to(nid, start, end, &numa_meminfo);
}

该函数是对 numa_add_memblk_to 函数封装,简化了使用方法。

4.4.4 numa_meminfo_cover_memory

我们在以前的文章介绍过,通过 BIOS 的 e820 接口,可以探测物理内存的大小及布局。在本文中,我们从 SRAT 表中也可以获取到内存区域以及关联的节点。虽然获取途径不同,但内存总量应该是大致相等的。如果相差太多,说明接口数据有问题,无法使用。

该函数检查通过 SRAT 接口探测到的内存总量是否与 e820 接口探测到的基本一致。如果相差不大(小于 1MB),返回 true;否则,返回 false。

// file: arch/x86/mm/numa.c
/*
 * Sanity check to catch more bad NUMA configurations (they are amazingly
 * common).  Make sure the nodes cover all memory.
 */
static bool __init numa_meminfo_cover_memory(const struct numa_meminfo *mi)
{
	u64 numaram, e820ram;
	int i;

	numaram = 0;
	for (i = 0; i < mi->nr_blks; i++) {
		u64 s = mi->blk[i].start >> PAGE_SHIFT;
		u64 e = mi->blk[i].end >> PAGE_SHIFT;
		numaram += e - s;
		numaram -= __absent_pages_in_range(mi->blk[i].nid, s, e);
		if ((s64)numaram < 0)
			numaram = 0;
	}

	e820ram = max_pfn - absent_pages_in_range(0, max_pfn);

	/* We seem to lose 3 pages somewhere. Allow 1M of slack. */
	if ((s64)(e820ram - numaram) >= (1 << (20 - PAGE_SHIFT))) {
		printk(KERN_ERR "NUMA: nodes only cover %LuMB of your %LuMB e820 RAM. Not used.\n",
		       (numaram << PAGE_SHIFT) >> 20,
		       (e820ram << PAGE_SHIFT) >> 20);
		return false;
	}
	return true;
}

首先,声明了变量 numarame820ram,并将 numaram 初始化为 0。numaram表示通过 SRAT 表获取到的物理页数量;e820ram表示通过 e820 接口获取到的物理页数量。

接下来,遍历 NUMA 中的内存块,计算该内存块包含的实际物理页数量。可以看到,对每个内存块执行以下几步:

  • 将内存块的起始地址右移 PAGE_SHIFT(扩展为 12) 位后得到起始地址的页帧号
  • 将内存块的结束地址右移 PAGE_SHIFT(扩展为 12) 位后得到结束地址的页帧号
  • 两者相减,得到该内存块跨越的页数。然后让 numaram 增加对应的页数。
  • 由于内存块中可能有空洞,调用 __absent_pages_in_range 函数计算该内存块中的空洞包含的页数,并从 numaram 中减去该值。
  • 如果计算后 numaram 小于 0,将 numaram 设置为 0。

遍历完成后,numaram 中保存的就是通过 SRAT 获取到的实际可用的物理页数量。

接下来,计算 e820 接口获取到的可用内存页数。

先是调用 absent_pages_in_range 函数,计算从页帧范围 0 到 max_pfn 之间的空洞所包括的页数。其中,max_pfn 是通过 e820 接口获取到的最大页帧号。然后,用 max_pfn 减去空洞所包含的页数,就得到 e820 接口探测到的实际可用的页数,并保存到 e820ram 中 。

如果两者相差超过 1<< 8 个页,即 1MB 内存空间,说明相差太大,打印错误信息并返回 false;否则,返回 true。

4.4.4.1 __absent_pages_in_range

该函数计算指定节点中一定内存区域内,内存空洞所占用的页数。如果节点 ID 为 MAX_NUMNODES,则会计算所有节点中的空洞所占用的页数。

该函数接收 3 个参数:

  • @nid:要查找的节点
  • @range_start_pfn:待查找内存区域的起始页帧
  • @range_end_pfn:待查找内存区域的结束页帧
// file: mm/page_alloc.c
/*
 * Return the number of holes in a range on a node. If nid is MAX_NUMNODES,
 * then all holes in the requested range will be accounted for.
 */
unsigned long __meminit __absent_pages_in_range(int nid,
				unsigned long range_start_pfn,
				unsigned long range_end_pfn)
{
	unsigned long nr_absent = range_end_pfn - range_start_pfn;
	unsigned long start_pfn, end_pfn;
	int i;

	for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, NULL) {
		start_pfn = clamp(start_pfn, range_start_pfn, range_end_pfn);
		end_pfn = clamp(end_pfn, range_start_pfn, range_end_pfn);
		nr_absent -= end_pfn - start_pfn;
	}
	return nr_absent;
}

首先,将空洞页数设置为总页数,即结束页帧与起始页帧之差。

接下来,遍历 memblock 中 memory 类型的内存块。在每次循环中,获取到内存块的起始页帧和结束页帧。两者相减,就得到有效的页帧数量。然后,从总页数中减去有效页数,就得到空洞页数。

其中,clamp 宏用于将内存块的起始或结束页帧限制在要查找的范围内,防止超出限制。

4.4.4.2 for_each_mem_pfn_range

该宏接收 5 个参数:

  • @i:循环变量
  • @nid:节点 ID,在该节点中查找。如果为 MAX_NUMNODES,表示可以在任何节点中查找。
  • @p_start:输出参数,查找到的内存块的起始页帧号
  • @p_end:输出参数,查找到的内存块的结束页帧号
  • @p_nid:输出参数,查找到的内存块的节点 ID
// file: include/linux/memblock.h
/**
 * for_each_mem_pfn_range - early memory pfn range iterator
 * @i: an integer used as loop variable
 * @nid: node selector, %MAX_NUMNODES for all nodes
 * @p_start: ptr to ulong for start pfn of the range, can be %NULL
 * @p_end: ptr to ulong for end pfn of the range, can be %NULL
 * @p_nid: ptr to int for nid of the range, can be %NULL
 *
 * Walks over configured memory ranges.
 */
#define for_each_mem_pfn_range(i, nid, p_start, p_end, p_nid)		\
	for (i = -1, __next_mem_pfn_range(&i, nid, p_start, p_end, p_nid); \
	     i >= 0; __next_mem_pfn_range(&i, nid, p_start, p_end, p_nid))

该宏会遍历 memblock 中 memory 类型的有效内存块,内存块信息会保存到 p_startp_endp_nid 所指向的变量中。如果 i 的值为 -1,说明未找到,循环结束。

4.4.4.3 __next_mem_pfn_range

该函数接收 5 个参数:

  • @idx:起始索引指针,从该索引处开始查找
  • @nid:节点 ID,在该节点中查找
  • @out_start_pfn:输出参数,查找到的内存块的起始页帧号
  • @out_end_pfn:输出参数,查找到的内存块的结束页帧号
  • @out_nid:输出参数,查到到的内存块的节点 ID

该函数会在 memblock 的 memory 类型内存中,从索引 @idx 处开始查找属于节点 @nid 的第一个有效内存块。当 @nidMAX_NUMNODES 时,允许在任何节点中查找。如果找到,3 个以 out_ 开头的输出参数会保存该内存块的信息;否则,@idx 会输出 -1。该函数用于循环中,当 @idx 输出 -1 时,循环停止。

// file: include/linux/memblock.h
/*
 * Common iterator interface used to define for_each_mem_range().
 */
void __init_memblock __next_mem_pfn_range(int *idx, int nid,
				unsigned long *out_start_pfn,
				unsigned long *out_end_pfn, int *out_nid)
{
	struct memblock_type *type = &memblock.memory;
	struct memblock_region *r;

	while (++*idx < type->cnt) {
		r = &type->regions[*idx];

		if (PFN_UP(r->base) >= PFN_DOWN(r->base + r->size))
			continue;
		if (nid == MAX_NUMNODES || nid == r->nid)
			break;
	}
	if (*idx >= type->cnt) {
		*idx = -1;
		return;
	}

	if (out_start_pfn)
		*out_start_pfn = PFN_UP(r->base);
	if (out_end_pfn)
		*out_end_pfn = PFN_DOWN(r->base + r->size);
	if (out_nid)
		*out_nid = r->nid;
}

函数开始,获取到 memory 类型内存块的地址。

接下来,在 while 循环里,从索引 idx 处开始查找有效内存块。

查找过程中,如果内存块的空间小于一页,直接忽略。其中宏 PFN_UP 先将地址向上圆整对齐到页大小,然后获取到圆整后的页帧号;宏 PFN_DOWN 获取向下对齐到页大小后的页帧号。

// file: include/linux/pfn.h
#define PFN_UP(x)	(((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x)	((x) >> PAGE_SHIFT)

如果要查找的节点 ID 为 MAX_NUMNODES,或者内存块的节点 ID 与指定的节点一致,说明找到了,则跳出循环。

如果循环结束也没找到,则在 idx 中输出 -1,指示查找失败,然后直接返回。

最后,在 3 个输出参数中分别存入圆整后的内存块起始地址、结束地址以及节点 ID。

4.4.4.4 clamp

clamp 宏用于将输入值限定在指定范围内,并进行严格的类型检查。该函数接收 3 个参数:

  • @val:输入值
  • @min:限定的最小值
  • @max:限定的最大值
// file: include/linux/kernel.h
/**
 * clamp - return a value clamped to a given range with strict typechecking
 * @val: current value
 * @min: minimum allowable value
 * @max: maximum allowable value
 *
 * This macro does strict typechecking of min/max to make sure they are of the
 * same type as val.  See the unnecessary pointer comparisons.
 */
#define clamp(val, min, max) ({			\
	typeof(val) __val = (val);		\
	typeof(min) __min = (min);		\
	typeof(max) __max = (max);		\
	(void) (&__val == &__min);		\
	(void) (&__val == &__max);		\
	__val = __val < __min ? __min: __val;	\
	__val > __max ? __max: __val; })

函数内部,会比较三个值的类型。如果类型不一致,则会导致编译时错误。接下来,将输入值限定在最大和最小值之间。

4.4.4.5 absent_pages_in_range

该函数会查找所有节点中在位于内存区域中的空洞所占用的页数。其内部会将查找工作委托给 “4.4.4.1 __absent_pages_in_range” 函数。

/**
 * absent_pages_in_range - Return number of page frames in holes within a range
 * @start_pfn: The start PFN to start searching for holes
 * @end_pfn: The end PFN to stop searching for holes
 *
 * It returns the number of pages frames in memory holes within a range.
 */
unsigned long __init absent_pages_in_range(unsigned long start_pfn,
							unsigned long end_pfn)
{
	return __absent_pages_in_range(MAX_NUMNODES, start_pfn, end_pfn);
}

4.4.5. numa_remove_memblk_from

该函数会删除 NUMA 内存块数组中指定索引的成员。

// file: arch/x86/mm/numa.c
/**
 * numa_remove_memblk_from - Remove one numa_memblk from a numa_meminfo
 * @idx: Index of memblk to remove
 * @mi: numa_meminfo to remove memblk from
 *
 * Remove @idx'th numa_memblk from @mi by shifting @mi->blk[] and
 * decrementing @mi->nr_blks.
 */
void __init numa_remove_memblk_from(int idx, struct numa_meminfo *mi)
{
	mi->nr_blks--;
	memmove(&mi->blk[idx], &mi->blk[idx + 1],
		(mi->nr_blks - idx) * sizeof(mi->blk[0]));
}

函数的实现非常简单,就是调用 memmove 函数,将索引之后的数组成员前移一个单位,然后把内存块总数减一。

4.5 节点距离接口

4.5.1 node_distance

node_distance 返回两个节点间的相对距离,其内部扩展为 __node_distance 函数。

// file: arch/x86/include/asm/topology.h
#define node_distance(a, b) __node_distance(a, b)
4.5.1.1 __node_distance

__node_distance 函数获取两个节点间的相对距离,该函数接受 2 个参数:

  • @from:起始节点
  • @to:目的节点
// file: arch/x86/mm/numa.c
int __node_distance(int from, int to)
{
	if (from >= numa_distance_cnt || to >= numa_distance_cnt)
		return from == to ? LOCAL_DISTANCE : REMOTE_DISTANCE;
	return numa_distance[from * numa_distance_cnt + to];
}

其中,numa_distance_cnt 表示矩阵中节点的数量;numa_distance 表示距离矩阵(数组)。

如果两个节点中任意一个超出了节点的最大值 numa_distance_cnt -1,则它们的相对距离无法计算,只能返回默认值。对于节点到自身的距离,其默认值为 LOCAL_DISTANCE;否则,返回 REMOTE_DISTANCE

// file: include/linux/topology.h
/* Conform to ACPI 2.0 SLIT distance definitions */
#define LOCAL_DISTANCE		10
#define REMOTE_DISTANCE		20

如果节点有效,则直接从距离矩阵 numa_distance 中获取相对距离。 numa_distance 的值会在解析 SLIT 表时进行填充。

我们在 “2.2 SLIT 格式” 小节中介绍过,对于任意两个节点 ij,它们的相对距离保存在矩阵中第 i*N + j 个成员中,其中 N 为节点数量。在本例中,就是 numa_distance 数组的第 from * numa_distance_cnt + to 个成员。

4.5.2 numa_reset_distance

numa_reset_distance 函数重置 NUMA 节点距离矩阵及矩阵节点数量。

// file: arch/x86/mm/numa.c
/**
 * numa_reset_distance - Reset NUMA distance table
 *
 * The current table is freed.  The next numa_set_distance() call will
 * create a new one.
 */
void __init numa_reset_distance(void)
{
	size_t size = numa_distance_cnt * numa_distance_cnt * sizeof(numa_distance[0]);

	/* numa_distance could be 1LU marking allocation failure, test cnt */
	if (numa_distance_cnt)
		memblock_free(__pa(numa_distance), size);
	numa_distance_cnt = 0;
	numa_distance = NULL;	/* enable table creation */
}

其中,numa_distance_cnt 表示矩阵中节点的数量;numa_distance 表示距离矩阵。

矩阵是以数组形式存储的,所以先要计算出数组大小。size 就是计算后的大小(以字节为单位)。

如果节点数量大于 0,调用 memblock_free 函数,将数组占用的空间释放。

memblock_free 函数接受 2个参数:待释放内存的起始物理地址和大小。该函数的实现请参考:Linux Kernel:启动时内存管理(MemBlock 分配器) 中 ”5.5 释放内存“ 小节。

然后将 numa_distance_cnt 设置为 0,将 numa_distance 设置为 NULL。

4.5.3 numa_alloc_distance

numa_alloc_distance 函数为节点的距离矩阵分配内存空间,并对其进行初始化。

// file: arch/x86/mm/numa.c
static int __init numa_alloc_distance(void)
{
	nodemask_t nodes_parsed;
	size_t size;
	int i, j, cnt = 0;
	u64 phys;

	/* size the new table and allocate it */
	nodes_parsed = numa_nodes_parsed;
	numa_nodemask_from_meminfo(&nodes_parsed, &numa_meminfo);

	for_each_node_mask(i, nodes_parsed)
		cnt = i;
	cnt++;
	size = cnt * cnt * sizeof(numa_distance[0]);

	phys = memblock_find_in_range(0, PFN_PHYS(max_pfn_mapped),
				      size, PAGE_SIZE);
	if (!phys) {
		pr_warning("NUMA: Warning: can't allocate distance table!\n");
		/* don't retry until explicitly reset */
		numa_distance = (void *)1LU;
		return -ENOMEM;
	}
	memblock_reserve(phys, size);

	numa_distance = __va(phys);
	numa_distance_cnt = cnt;

	/* fill with the default distances */
	for (i = 0; i < cnt; i++)
		for (j = 0; j < cnt; j++)
			numa_distance[i * cnt + j] = i == j ?
				LOCAL_DISTANCE : REMOTE_DISTANCE;
	printk(KERN_DEBUG "NUMA: Initialized distance table, cnt=%d\n", cnt);

	return 0;
}

函数内部,将已解析节点的位图 numa_nodes_parsed 复制一份保存到 nodes_parsed 中。接着,调用 “4.4.1 numa_nodemask_from_meminfo” 函数使用 NUMA 内存块的节点填充 nodes_parsed 中的比特位。

然后遍历节点位图 nodes_parsed,获取权重最高的为 1 的比特位索引,将索引值加一后,就得到节点的数量。

然后,根据节点的数量,计算出矩阵所占用的内存空间。其中,cnt * cnt 计算出矩阵的成员数量,sizeof(numa_distance[0]) 计算出单个成员的字节大小,两者相乘就得到矩阵占用的总空间。

再接着,调用 memblock_find_in_range 函数从可用内存中分配指定大小的空间,并返回内存的物理地址。memblock_find_in_range 函数接收 4 个参数:候选区域的起始地址、候选区域的结束地址、要分配的内存大小、对齐字节。该函数实现可参考:Linux Kernel:启动时内存管理(MemBlock 分配器) 中 ”5.3.1 memblock_find_in_range()“ 小节。

如果分配失败,说明没有内存可用,打印警告信息并返回错误码 -ENOMEM

接下来,调用 memblock_reserve 函数将已分配内存加入到保留内存区间中。

再接着,将已分配内存的物理地址转换成虚拟地址,并赋值给 numa_distance,作为矩阵的起始地址;将节点数量保存到变量 numa_distance_cnt 中。

然后,对距离矩阵进行初始化。可以将矩阵看做一个二维数组,如果行等于列,说明是相对于自身的距离,则初始化为 LOCAL_DISTANCE(扩展为 10),否则,初始化为 REMOTE_DISTANCE(扩展为 20)。

最后,返回成功码 0。

五、NUMA 节点探测代码实现

NUMA 节点探测的主要实现函数为 x86_numa_init,其执行路径为:

x86_numa_init.png

5.1 x86_numa_init

x86_numa_init 函数会执行 NUMA 初始化,其主要工作就是解析 SRAT 表以及 SLIT 表,获取 NUMA 相关信息。

// file: arch/x86/mm/numa.c
/**
 * x86_numa_init - Initialize NUMA
 *
 * Try each configured NUMA initialization method until one succeeds.  The
 * last fallback is dummy single node config encomapssing whole memory and
 * never fails.
 */
void __init x86_numa_init(void)
{
	if (!numa_off) {
#ifdef CONFIG_X86_NUMAQ
		if (!numa_init(numaq_numa_init))
			return;
#endif
#ifdef CONFIG_ACPI_NUMA
		if (!numa_init(x86_acpi_numa_init))
			return;
#endif
#ifdef CONFIG_AMD_NUMA
		if (!numa_init(amd_numa_init))
			return;
#endif
	}

	numa_init(dummy_numa_init);
}

numa_off 是一个全局变量(默认值为 0),指示是否关闭 NUMA 支持。如果为 1,关闭 NUMA;为 0,开启 NUMA。

// file: arch/x86/mm/numa.c
int __initdata numa_off;

numa_off 的值依赖于启动参数 numa,当 numa=off 时,只会建立一个 NUMA 节点。

// file: Documentation/x86/x86_64/boot-options.txt
NUMA

  numa=off	Only set up a single NUMA node spanning all memory.

  numa=noacpi   Don't parse the SRAT table for NUMA setup

  ......

numa 参数在 numa_setup 函数中被解析,如果参数为 “off”,numa_off 被设置为 1。

static __init int numa_setup(char *opt)
{
	if (!opt)
		return -EINVAL;
	if (!strncmp(opt, "off", 3))
		numa_off = 1;
......
	return 0;
}
early_param("numa", numa_setup);

如果关闭了对 NUMA 的支持,则会使用 dummy_numa_init 函数对 NUMA 进行初始化。该函数会建立唯一的节点,并将所有内存添加到该节点中。

// file: arch/x86/mm/numa.c
/**
 * dummy_numa_init - Fallback dummy NUMA init
 *
 * Used if there's no underlying NUMA architecture, NUMA initialization
 * fails, or NUMA is disabled on the command line.
 *
 * Must online at least one node and add memory blocks that cover all
 * allowed memory.  This function must not fail.
 */
static int __init dummy_numa_init(void)
{
	printk(KERN_INFO "%s\n",
	       numa_off ? "NUMA turned off" : "No NUMA configuration found");
	printk(KERN_INFO "Faking a node at [mem %#018Lx-%#018Lx]\n",
	       0LLU, PFN_PHYS(max_pfn) - 1);

	node_set(0, numa_nodes_parsed);
	numa_add_memblk(0, 0, PFN_PHYS(max_pfn));

	return 0;
}

否则,会根据不同的内核配置选项,执行不同的初始化函数。对于本例来说,我们配置了 CONFIG_ACPI_NUMACONFIG_AMD_NUMA 选项,但是会优先执行 numa_init(x86_acpi_numa_init) 函数。

5.2 numa_init

numa_init 函数接收一个参数,即实际的初始化函数。在执行实际初始化工作前后,会进行前置和后置处理。

// file: arch/x86/mm/numa.c
static int __init numa_init(int (*init_func)(void))
{
	int i;
	int ret;

	for (i = 0; i < MAX_LOCAL_APIC; i++)
		set_apicid_to_node(i, NUMA_NO_NODE);

	nodes_clear(numa_nodes_parsed);
	nodes_clear(node_possible_map);
	nodes_clear(node_online_map);
	memset(&numa_meminfo, 0, sizeof(numa_meminfo));
	WARN_ON(memblock_set_node(0, ULLONG_MAX, MAX_NUMNODES));
	numa_reset_distance();

	ret = init_func();
	if (ret < 0)
		return ret;
	ret = numa_cleanup_meminfo(&numa_meminfo);
	if (ret < 0)
		return ret;

	numa_emulation(&numa_meminfo, numa_distance_cnt);

	ret = numa_register_memblks(&numa_meminfo);
	if (ret < 0)
		return ret;

	for (i = 0; i < nr_cpu_ids; i++) {
		int nid = early_cpu_to_node(i);

		if (nid == NUMA_NO_NODE)
			continue;
		if (!node_online(nid))
			numa_clear_node(i);
	}
	numa_init_array();
	return 0;
}

函数执行流程如下。

	int i;
	int ret;

	for (i = 0; i < MAX_LOCAL_APIC; i++)
		set_apicid_to_node(i, NUMA_NO_NODE);
......

首先,调用 “4.3.3 set_apicid_to_node” 函数将每个 APIC ID 对应的节点初始化为 NUMA_NO_NODE(扩展为 -1)。该函数会将 apicid 和节点的对应关系保存到 __apicid_to_node 数组。

......
	nodes_clear(numa_nodes_parsed);
	nodes_clear(node_possible_map);
	nodes_clear(node_online_map);
......

接下来,调用 “4.1.2 nodes_clear” 宏,将三个位图中的比特位全部设置为 0。

numa_nodes_parsed 表示已经解析的节点的位图;node_online_mapnode_possible_map 分别表示 possible 和 online 状态的节点位图。

......
	memset(&numa_meminfo, 0, sizeof(numa_meminfo));
......

再接着,调用 memset 函数,使用 0 来填充变量 numa_meminfonuma_meminfo 是同名结构体的静态变量,用来保存 NUMA 内存信息,详见 “3.6 NUMA 内存相关”。

......
	WARN_ON(memblock_set_node(0, ULLONG_MAX, MAX_NUMNODES));
......

再接着,调用 memblock_set_node 函数,把从物理地址 0 到 ULLONG_MAX 的所有内存块的节点 ID 全部初始化为 MAX_NUMNODES

memblock_set_node 函数的实现,请参考:Linux Kernel:启动时内存管理(MemBlock 分配器) 中 ”5.1.10 memblock_set_node()“ 小节。

......
    numa_reset_distance();
......

然后,调用 numa_reset_distance 函数重置 NUMA 节点距离矩阵。 该函数将距离矩阵的指针设置为 NULL,并将矩阵中的节点数重置为 0。具体实现请参考 ”4.5.2 numa_reset_distance“ 小节。

.....
	ret = init_func();
	if (ret < 0)
		return ret;
......

前置工作完成后,调用实际初始化函数。在本例中,就是 “5.2.2 x86_acpi_numa_init” 函数。如果 x86_acpi_numa_init 函数返回值小于 0,说明执行过程中出现错误,直接返回。

......
	ret = numa_cleanup_meminfo(&numa_meminfo);
	if (ret < 0)
		return ret;
......

接下来,执行 numa_cleanup_meminfo 函数对 numa 内存进行”净化“,包括删除无效内存块,合并重叠内存块、邻居内存块等。如果该函数返回负的错误码,说明执行失败,直接返回。

......
	numa_emulation(&numa_meminfo, numa_distance_cnt);
......

接下来执行 numa_emulation 函数,该函数的实现依赖于内核配置选项 CONFIG_NUMA_EMU

// file: arch/x86/mm/numa_internal.h
#ifdef CONFIG_NUMA_EMU
void __init numa_emulation(struct numa_meminfo *numa_meminfo,
			   int numa_dist_cnt);
#else
static inline void numa_emulation(struct numa_meminfo *numa_meminfo,
				  int numa_dist_cnt)
{ }

CONFIG_NUMA_EMU 选项用来进行 NUMA 模拟,主要用于调试意图。当设置了该选项后,平坦内存会被分割成几个虚拟节点。

// file: arch/x86/Kconfig
config NUMA_EMU
	bool "NUMA emulation"
	depends on NUMA
	---help---
	  Enable NUMA emulation. A flat machine will be split
	  into virtual nodes when booted with "numa=fake=N", where N is the
	  number of nodes. This is only useful for debugging.

由于我们并未配置该选项,所以该函数是一个空函数。

......
	ret = numa_register_memblks(&numa_meminfo);
	if (ret < 0)
		return ret;
......

接下来,调用 numa_register_memblks 函数,将 NUMA 内存信息写入节点描述符 struct pglist_data,并将节点保存到节点数组 node_data 中。

......
	for (i = 0; i < nr_cpu_ids; i++) {
		int nid = early_cpu_to_node(i);

		if (nid == NUMA_NO_NODE)
			continue;
		if (!node_online(nid))
			numa_clear_node(i);
	}
......

然后,遍历每个 CPU,获取 CPU 所属的节点。如果该节点存在但未上线,则调用 numa_clear_node 函数将该 CPU 对应的节点设置为无效节点 NUMA_NO_NODE

......
	numa_init_array();
	return 0;
}

最后,调用 numa_init_array 函数,为那些属于无效节点的 CPU 分配节点。分配时采用 “round robin” 策略,从 node_online_map 位图中依次选择节点。

5.2.1 numa_reset_distance

该函数重置 NUMA 节点距离矩阵及矩阵节点数量。其中,距离矩阵 numa_distance 被设置为 NULL, 矩阵中的节点数 numa_distance_cnt 被设置为 0。

该函数的详细实现请参考 ”4.5.2 numa_reset_distance“ 小节。

5.2.2 x86_acpi_numa_init

x86_acpi_numa_init 函数是 NUMA 节点探测的实际执行函数。

int __init x86_acpi_numa_init(void)
{
	int ret;

	ret = acpi_numa_init();
	if (ret < 0)
		return ret;
	return srat_disabled() ? -EINVAL : 0;
}

函数内部,直接调用了 acpi_numa_init 函数。

5.2.2.1 acpi_numa_init

acpi_numa_init 函数中,我们完成了 NUMA 节点探测的所有工作,包括:节点处理器探测、节点内存探测以及节点距离探测。

// file: drivers/acpi/numa.c
int __init acpi_numa_init(void)
{
	int cnt = 0;

	/*
	 * Should not limit number with cpu num that is from NR_CPUS or nr_cpus=
	 * SRAT cpu entries could have different order with that in MADT.
	 * So go over all cpu entries in SRAT to get apicid to node mapping.
	 */

	/* SRAT: Static Resource Affinity Table */
	if (!acpi_table_parse(ACPI_SIG_SRAT, acpi_parse_srat)) {
		acpi_table_parse_srat(ACPI_SRAT_TYPE_X2APIC_CPU_AFFINITY,
				     acpi_parse_x2apic_affinity, 0);
		acpi_table_parse_srat(ACPI_SRAT_TYPE_CPU_AFFINITY,
				     acpi_parse_processor_affinity, 0);
		cnt = acpi_table_parse_srat(ACPI_SRAT_TYPE_MEMORY_AFFINITY,
					    acpi_parse_memory_affinity,
					    NR_NODE_MEMBLKS);
	}

	/* SLIT: System Locality Information Table */
	acpi_table_parse(ACPI_SIG_SLIT, acpi_parse_slit);

	acpi_numa_arch_fixup();

	if (cnt < 0)
		return cnt;
	else if (!parsed_numa_memblks)
		return -ENOENT;
	return 0;
}

函数开始,调用 acpi_table_parse 函数检查 SRAT 表是否存在。如果存在,则调用 acpi_parse_srat 函数进行解析,并返回 0;否则,返回 1。其中,宏 ACPI_SIG_SRAT 扩展为 ”SRAT“,即 SRAT 表的签名。

 // file: include/acpi/actbl1.h
 #define ACPI_SIG_SRAT           "SRAT"	/* System Resource Affinity Table */

当确认 SRAT 表存在时,调用 acpi_table_parse_srat 函数分别对 ”Processor Local x2APIC Affinity Structure“、”Processor Local APIC/SAPIC Affinity Structure“ 、”Memory Affinity Structure“ 三种结构进行解析。acpi_table_parse_srat 函数作为解析 SRAT 表项的统一入口,不同表项的实际处理器函数分别为 acpi_parse_x2apic_affinityacpi_parse_processor_affinityacpi_parse_memory_affinity

解析完 SRAT 表后,调用 acpi_table_parse 函数对 SLIT 表进行解析,实际解析工作委托给 acpi_parse_slit 函数。

再接着,调用 acpi_numa_arch_fixup 函数执行修复工作。该函数是架构相关的,在 x86 架构下,这是一个空函数。

// file: arch/x86/mm/srat.c
void __init acpi_numa_arch_fixup(void) {}

如果 cnt 小于 0,说明解析 SLIT 表时出错,此时 cnt 指示错误码,所以直接返回 cnt

如果 parsed_numa_memblks 为 0,说明未解析到任何节点内存,返回错误码 -ENOENT

5.2.2.2 acpi_table_parse

acpi_table_parse 函数接收 2 个参数:

  • @id:要搜索的表的 id,也就是表的签名;
  • @handler:处理函数。

acpi_table_parse 函数会扫描所有 ACPI 系统描述表(System Description Table,STD),查找与 @id 匹配的表。如果找到,就调用 handler 函数对其进行处理,并返回 0;否则,返回 1。

// file: drivers/acpi/tables.c
/**
 * acpi_table_parse - find table with @id, run @handler on it
 *
 * @id: table id to find
 * @handler: handler to run
 *
 * Scan the ACPI System Descriptor Table (STD) for a table matching @id,
 * run @handler on it.  Return 0 if table found, return on if not.
 */
int __init acpi_table_parse(char *id, acpi_tbl_table_handler handler)
{
	struct acpi_table_header *table = NULL;
	acpi_size tbl_size;

	if (acpi_disabled)
		return -ENODEV;

	if (!handler)
		return -EINVAL;

	if (strncmp(id, ACPI_SIG_MADT, 4) == 0)
		acpi_get_table_with_size(id, acpi_apic_instance, &table, &tbl_size);
	else
		acpi_get_table_with_size(id, 0, &table, &tbl_size);

	if (table) {
		handler(table);
		early_acpi_os_unmap_memory(table, tbl_size);
		return 0;
	} else
		return 1;
}

在函数内部,首先进行条件检测。如果系统不支持 ACPI 或 ACPI 被禁止,返回错误码 -ENODEV。如果处理函数 handler 不存在,返回错误码 -EINVAL

// file: include/uapi/asm-generic/errno-base.h
#define	ENODEV		19	/* No such device */
#define	EINVAL		22	/* Invalid argument */

接下来,调用 acpi_get_table_with_size 函数在 ACPI 系统描述表中查找与 id 相匹配的表。由于 MADT 表可能有 2 个,其它表只有一个,所以当要查找的是 MADT 表时,需要在 acpi_get_table_with_size 函数中通过参数 acpi_apic_instance 指定要用的是第几个 MADT 表。

如果找到匹配的表,acpi_get_table_with_size 函数会将表的地址及大小分别保存到入参 tablesize 中,供后续使用。

如果 table 为真,说明表存在,调用 handler 函数进行处理,并返回 0;否则,返回 1。

5.2.2.3 acpi_parse_srat
// file: drivers/acpi/numa.c
static int __init acpi_parse_srat(struct acpi_table_header *table)
{
	struct acpi_table_srat *srat;
	if (!table)
		return -EINVAL;

	srat = (struct acpi_table_srat *)table;
	acpi_srat_revision = srat->header.revision;

	/* Real work done in acpi_table_parse_srat below. */

	return 0;
}

acpi_parse_srat 只是简单记录了 SRAT 的版本,然后返回 0。实际的解析工作是在 acpi_table_parse_srat 函数中完成的。

5.2.2.4 acpi_table_parse_srat

该函数将解析工作全部委托给 acpi_table_parse_entries 函数。

// file: drivers/acpi/numa.c
static int __init
acpi_table_parse_srat(enum acpi_srat_type id,
		      acpi_tbl_entry_handler handler, unsigned int max_entries)
{
	return acpi_table_parse_entries(ACPI_SIG_SRAT,
					    sizeof(struct acpi_table_srat), id,
					    handler, max_entries);
}
5.2.2.5 acpi_table_parse_entries

acpi_table_parse_entries 函数用来解析 ACPI 的某类表项,该函数接收 5 个参数:

  • @id:表的类型,对应于表的签名
  • @table_size:表头的大小
  • @entry_id:表项类型;
  • @handler:处理函数,不同表项有不同的处理函数
  • @max_entries:表项的最大数量,为 0 时表示只处理一次

该函数定义如下:

// file: drivers/acpi/tables.c
int __init
acpi_table_parse_entries(char *id,
			     unsigned long table_size,
			     int entry_id,
			     acpi_tbl_entry_handler handler,
			     unsigned int max_entries)
{
	struct acpi_table_header *table_header = NULL;
	struct acpi_subtable_header *entry;
	unsigned int count = 0;
	unsigned long table_end;
	acpi_size tbl_size;

	if (acpi_disabled)
		return -ENODEV;

	if (!handler)
		return -EINVAL;

	if (strncmp(id, ACPI_SIG_MADT, 4) == 0)
		acpi_get_table_with_size(id, acpi_apic_instance, &table_header, &tbl_size);
	else
		acpi_get_table_with_size(id, 0, &table_header, &tbl_size);

	if (!table_header) {
		printk(KERN_WARNING PREFIX "%4.4s not present\n", id);
		return -ENODEV;
	}

	table_end = (unsigned long)table_header + table_header->length;

	/* Parse all entries looking for a match. */

	entry = (struct acpi_subtable_header *)
	    ((unsigned long)table_header + table_size);

	while (((unsigned long)entry) + sizeof(struct acpi_subtable_header) <
	       table_end) {
		if (entry->type == entry_id
		    && (!max_entries || count++ < max_entries))
			if (handler(entry, table_end))
				goto err;

		/*
		 * If entry->length is 0, break from this loop to avoid
		 * infinite loop.
		 */
		if (entry->length == 0) {
			pr_err(PREFIX "[%4.4s:0x%02x] Invalid zero length\n", id, entry_id);
			goto err;
		}

		entry = (struct acpi_subtable_header *)
		    ((unsigned long)entry + entry->length);
	}
	if (max_entries && count > max_entries) {
		printk(KERN_WARNING PREFIX "[%4.4s:0x%02x] ignored %i entries of "
		       "%i found\n", id, entry_id, count - max_entries, count);
	}

	early_acpi_os_unmap_memory((char *)table_header, tbl_size);
	return count;
err:
	early_acpi_os_unmap_memory((char *)table_header, tbl_size);
	return -EINVAL;
}

在函数内部,首先进行条件检测。如果系统不支持 ACPI 或 ACPI 被禁止,返回错误码 -ENODEV。如果处理函数 handler 不存在,返回错误码 -EINVAL

接下来,调用 acpi_get_table_with_size 函数在 ACPI 系统描述表中查找与 id 相匹配的表。由于 MADT 表可能有 2 个,其它表只有一个,所以当要查找的是 MADT 表时,需要在 acpi_get_table_with_size 函数中通过参数 acpi_apic_instance 指定要用的是第几个 MADT 表。

如果没找到匹配的表,打印错误信息并返回错误码 -ENODEV

上面这些步骤我们在 acpi_table_parse 函数中已经遇到过了。

......
	table_end = (unsigned long)table_header + table_header->length;

	/* Parse all entries looking for a match. */

	entry = (struct acpi_subtable_header *)
	    ((unsigned long)table_header + table_size);
......

table_header 表示表头的地址,table_header 中的 length 字段,指示该表的总大小。所以,table_end 指示该表的结束地址。

table_size 指示表头的大小,所以 entry 表示表头的结束地址,也是第一个表项的起始地址。

......
	while (((unsigned long)entry) + sizeof(struct acpi_subtable_header) <
	       table_end) {
		if (entry->type == entry_id
		    && (!max_entries || count++ < max_entries))
			if (handler(entry, table_end))
				goto err;

		/*
		 * If entry->length is 0, break from this loop to avoid
		 * infinite loop.
		 */
		if (entry->length == 0) {
			pr_err(PREFIX "[%4.4s:0x%02x] Invalid zero length\n", id, entry_id);
			goto err;
		}

		entry = (struct acpi_subtable_header *)
		    ((unsigned long)entry + entry->length);
	}
......

接下来是一个 while 循环。循环条件是表项空间不能越界,即必须小于 table_end

循环内部,检查表项类型以及最大表项数量。如果表项类型相符并且最大表项数为 0 或者已处理的表项数小于最大数量时,则调用 handler 函数处理表项。如果 handler 函数处理失败,跳转到标签 err 处执行。

如果解析后,表项的大小为 0,说明出现错误,打印错误信息后,跳转到标签 err 处执行。

再接着,计算下一个表项的地址,进行下一轮循环。

......
	if (max_entries && count > max_entries) {
		printk(KERN_WARNING PREFIX "[%4.4s:0x%02x] ignored %i entries of "
		       "%i found\n", id, entry_id, count - max_entries, count);
	}
......

循环结束后,如果发现已处理的表项超出了指定的最大数量,则打印出警告信息。

......
	early_acpi_os_unmap_memory((char *)table_header, tbl_size);
	return count;
......

early_acpi_os_unmap_memory 函数会调用早期的内存映射接口 early_iounmap,将以 table_header 为起始虚拟地址,大小为 tbl_size 的空间取消映射。

// file: drivers/acpi/osl.c
void __init early_acpi_os_unmap_memory(void __iomem *virt, acpi_size size)
{
	if (!acpi_gbl_permanent_mmap)
		__acpi_unmap_table(virt, size);
}
// file: arch/x86/kernel/acpi/boot.c
void __init __acpi_unmap_table(char *map, unsigned long size)
{
	if (!map || !size)
		return;

	early_iounmap(map, size);
}

关于早期 I/O 内存映射的相关内容,请参考:Linux Kernel:内存管理之早期 I/O 内存映射(early ioremap)

取消映射后,返回查找到的表项数量 count

......
err:
	early_acpi_os_unmap_memory((char *)table_header, tbl_size);
	return -EINVAL;
}

最后,err 标签处也是调用 early_acpi_os_unmap_memory 函数取消映射,只不过返回的是错误码 -EINVAL

5.2.2.6 acpi_parse_x2apic_affinity

该函数用于解析 SRAT 表中的 “Processor Local x2APIC Affinity” 表项。

// file: drivers/acpi/numa.c
static int __init
acpi_parse_x2apic_affinity(struct acpi_subtable_header *header,
			   const unsigned long end)
{
	struct acpi_srat_x2apic_cpu_affinity *processor_affinity;

	processor_affinity = (struct acpi_srat_x2apic_cpu_affinity *)header;
	if (!processor_affinity)
		return -EINVAL;

	acpi_table_print_srat_entry(header);

	/* let architecture-dependent part to do it */
	acpi_numa_x2apic_affinity_init(processor_affinity);

	return 0;
}

函数内部,首先对表项指针进行校验。如果指针为空,说明是无效表项,返回错误码 -EINVAL

接下来,调用 acpi_table_print_srat_entry 函数打印表项的相关信息,该函数与本文关系不大,略过。

然后,执行 acpi_numa_x2apic_affinity_init 函数对表项进行解析。

最后,返回成功码 0。

5.2.2.7 acpi_numa_x2apic_affinity_init

该函数是解析 ”Processor Local x2APIC Affinity“ 结构的实际函数,主要用于建立 APIC ID 和节点的映射关系,其定义如下:

// file: arch/x86/mm/srat.c
/* Callback for Proximity Domain -> x2APIC mapping */
void __init
acpi_numa_x2apic_affinity_init(struct acpi_srat_x2apic_cpu_affinity *pa)
{
	int pxm, node;
	int apic_id;

	if (srat_disabled())
		return;
	if (pa->header.length < sizeof(struct acpi_srat_x2apic_cpu_affinity)) {
		bad_srat();
		return;
	}
	if ((pa->flags & ACPI_SRAT_CPU_ENABLED) == 0)
		return;
	pxm = pa->proximity_domain;
	apic_id = pa->apic_id;
	if (!apic->apic_id_valid(apic_id)) {
		printk(KERN_INFO "SRAT: PXM %u -> X2APIC 0x%04x ignored\n",
			 pxm, apic_id);
		return;
	}
	node = setup_node(pxm);
	if (node < 0) {
		printk(KERN_ERR "SRAT: Too many proximity domains %x\n", pxm);
		bad_srat();
		return;
	}

	if (apic_id >= MAX_LOCAL_APIC) {
		printk(KERN_INFO "SRAT: PXM %u -> APIC 0x%04x -> Node %u skipped apicid that is too big\n", pxm, apic_id, node);
		return;
	}
	set_apicid_to_node(apic_id, node);
	node_set(node, numa_nodes_parsed);
	acpi_numa = 1;
	printk(KERN_INFO "SRAT: PXM %u -> APIC 0x%04x -> Node %u\n",
	       pxm, apic_id, node);
}

首先,进行一些必要的条件检查。

如果 SRAT 被禁用,则直接返回。

// file: arch/x86/mm/srat.c
static __init inline int srat_disabled(void)
{
	return acpi_numa < 0;
}

如果表头里指示的大小和表的实际大小不符,说明 SRAT 表无效,则调用 bad_srat 函数,将 acpi_numa 设置为 -1,指示 SRAT 表不可用,然后直接返回。

static __init void bad_srat(void)
{
	printk(KERN_ERR "SRAT: SRAT not used.\n");
	acpi_numa = -1;
}

接下来,根据表项里的 flags 字段,检测当前表项是否可用。

flags 字段 enabled 位(位 0),指示表项是否可用。如果该位为 1 ,说明表项可用;否则,说明表项不可用。宏 ACPI_SRAT_CPU_ENABLED 扩展是 enabled 位的掩码,扩展为 1。如果表项不可用,直接返回。

再接着,从表项里获取到邻近域以及 APIC ID 的值,并保存到变量 pxm 以及 apic_id 中。

然后,通过 APIC 驱动程序中的 apic_id_valid 函数,检测 APIC ID 是否有效。如果无效,打印错误信息并返回。

接下来,通过 setup_node 函数,建立邻近域和节点的映射关系,并返回映射的节点。该函数会在数组 pxm_to_node_mapnode_to_pxm_map 中分别设置邻近域到节点以及节点到邻近域的映射关系。

如果返回的节点 ID 小于 0,说明邻近域超出数量限制,无法完成映射,则打印错误信息,并调用 bad_srat 函数禁用 SRAT 后返回。

接下来,如果 APIC ID 超出最大值 MAX_LOCAL_APIC,该 APIC ID 会被忽略。

然后,调用set_apicid_to_node 函数,将 APIC ID 到节点的映射关系保存到 __apicid_to_node 数组中。

接下来,调用 node_set 函数,将位图 numa_nodes_parsed 中节点对应的比特位置位。

最后,将 acpi_numa 设置为 1,并打印邻近域、APIC ID 以及节点的相关信息。

5.2.2.7.1 setup_node

setup_node 函数用来为邻近域设置节点,其内部将实现完全委托给 acpi_map_pxm_to_node 函数。

// file: arch/x86/mm/srat.c
static __init int setup_node(int pxm)
{
	return acpi_map_pxm_to_node(pxm);
}

acpi_map_pxm_to_node 函数的实现请参考 ”4.3.3 acpi_map_pxm_to_node“ 小节。

5.2.2.8 acpi_parse_processor_affinity

acpi_parse_processor_affinity 函数用于解析 SRAT 表中的 “Processor Local APIC/SAPIC Affinity” 表项。

// file: drivers/acpi/numa.c
static int __init
acpi_parse_processor_affinity(struct acpi_subtable_header *header,
			      const unsigned long end)
{
	struct acpi_srat_cpu_affinity *processor_affinity;

	processor_affinity = (struct acpi_srat_cpu_affinity *)header;
	if (!processor_affinity)
		return -EINVAL;

	acpi_table_print_srat_entry(header);

	/* let architecture-dependent part to do it */
	acpi_numa_processor_affinity_init(processor_affinity);

	return 0;
}

函数内部,首先校验表项指针。如果指针为空,说明是无效表项,返回错误码 -EINVAL

接下来,调用 acpi_table_print_srat_entry 函数打印表项的相关信息,该函数与本文关系不大,略过。

然后,执行 acpi_numa_processor_affinity_init 函数对表项进行解析。

最后,返回成功码 0。

5.2.2.8.1 acpi_numa_processor_affinity_init

该函数是 SRAT 表项 “Processor Local APIC/SAPIC Affinity” 的实际解析函数,主要用于获取 APIC 和节点的映射关系。

// file: arch/x86/mm/srat.c
/* Callback for Proximity Domain -> LAPIC mapping */
void __init
acpi_numa_processor_affinity_init(struct acpi_srat_cpu_affinity *pa)
{
	int pxm, node;
	int apic_id;

	if (srat_disabled())
		return;
	if (pa->header.length != sizeof(struct acpi_srat_cpu_affinity)) {
		bad_srat();
		return;
	}
	if ((pa->flags & ACPI_SRAT_CPU_ENABLED) == 0)
		return;
	pxm = pa->proximity_domain_lo;
	if (acpi_srat_revision >= 2)
		pxm |= *((unsigned int*)pa->proximity_domain_hi) << 8;
	node = setup_node(pxm);
	if (node < 0) {
		printk(KERN_ERR "SRAT: Too many proximity domains %x\n", pxm);
		bad_srat();
		return;
	}

	if (get_uv_system_type() >= UV_X2APIC)
		apic_id = (pa->apic_id << 8) | pa->local_sapic_eid;
	else
		apic_id = pa->apic_id;

	if (apic_id >= MAX_LOCAL_APIC) {
		printk(KERN_INFO "SRAT: PXM %u -> APIC 0x%02x -> Node %u skipped apicid that is too big\n", pxm, apic_id, node);
		return;
	}

	set_apicid_to_node(apic_id, node);
	node_set(node, numa_nodes_parsed);
	acpi_numa = 1;
	printk(KERN_INFO "SRAT: PXM %u -> APIC 0x%02x -> Node %u\n",
	       pxm, apic_id, node);
}

该函数的执行流程与 acpi_numa_x2apic_affinity_init 函数基本一致,不再赘述。

5.2.2.9 acpi_parse_memory_affinity

acpi_parse_memory_affinity 函数用于解析 SRAT 表中的 “Memory Affinity” 表项。

static int __initdata parsed_numa_memblks;

static int __init
acpi_parse_memory_affinity(struct acpi_subtable_header * header,
			   const unsigned long end)
{
	struct acpi_srat_mem_affinity *memory_affinity;

	memory_affinity = (struct acpi_srat_mem_affinity *)header;
	if (!memory_affinity)
		return -EINVAL;

	acpi_table_print_srat_entry(header);

	/* let architecture-dependent part to do it */
	if (!acpi_numa_memory_affinity_init(memory_affinity))
		parsed_numa_memblks++;
	return 0;
}

函数内部,首先对表项指针进行校验。如果指针为空,说明是无效表项,返回错误码 -EINVAL

接下来,调用 acpi_table_print_srat_entry 函数打印表项的相关信息,该函数与本文关系不大,略过。

然后,执行 acpi_numa_memory_affinity_init 函数对表项进行解析。如果解析成功,则 parsed_numa_memblks 自增一。

最后,返回成功码 0。

5.2.2.9.1 acpi_numa_memory_affinity_init

该函数是解析 ”Memory Affinity“ 表项的事件执行函数。执行成功返回 0,否则返回负的错误码。

// file: arch/x86/mm/srat.c
/* Callback for parsing of the Proximity Domain <-> Memory Area mappings */
int __init
acpi_numa_memory_affinity_init(struct acpi_srat_mem_affinity *ma)
{
	u64 start, end;
	int node, pxm;

	if (srat_disabled())
		goto out_err;
	if (ma->header.length != sizeof(struct acpi_srat_mem_affinity))
		goto out_err_bad_srat;
	if ((ma->flags & ACPI_SRAT_MEM_ENABLED) == 0)
		goto out_err;
	if ((ma->flags & ACPI_SRAT_MEM_HOT_PLUGGABLE) && !save_add_info())
		goto out_err;

	start = ma->base_address;
	end = start + ma->length;
	pxm = ma->proximity_domain;
	if (acpi_srat_revision <= 1)
		pxm &= 0xff;

	node = setup_node(pxm);
	if (node < 0) {
		printk(KERN_ERR "SRAT: Too many proximity domains.\n");
		goto out_err_bad_srat;
	}

	if (numa_add_memblk(node, start, end) < 0)
		goto out_err_bad_srat;

	node_set(node, numa_nodes_parsed);

	printk(KERN_INFO "SRAT: Node %u PXM %u [mem %#010Lx-%#010Lx]\n",
	       node, pxm,
	       (unsigned long long) start, (unsigned long long) end - 1);

	return 0;
out_err_bad_srat:
	bad_srat();
out_err:
	return -1;
}

函数内部,首先进行有效性检查。如果 SRAT 被禁用,跳转到标签 out_err 处执行,在标签 out_err 处,直接返回 -1。

如果子表头中 length 字段的值与表项的实际大小不符,说明表项有误,跳转到 out_err_bad_srat 标签处执行。在 out_err_bad_srat 标签处,调用 bad_srat 函数禁用 SRAT,然后返回错误码 -1。

接下来,对表项的标志位进行检查。根据 “Memory Affinity” 表项的格式,其 Flags 字段的位 0 指示该内存块是否可用,位 1 指示内存块是否可热插拔。如果内存块不可用,则跳转到 out_err 标签处执行;如果内存块是可热插拔的但 save_add_info 函数返回 0,则跳转到标签 out_err 处执行。

// file: arch/x86/mm/srat.c
#ifdef CONFIG_MEMORY_HOTPLUG
static inline int save_add_info(void) {return 1;}
#else
static inline int save_add_info(void) {return 0;}
#endif

save_add_info 函数的返回值依赖于内核配置选项 CONFIG_MEMORY_HOTPLUG,如果设置了该选项,则返回 1;否则,返回 0。

接下来,获取表项的起始和结束地址以及邻近域的值。如果 SRAT 表的版本不大于 1,那么邻近域中只有低 8 位是有效的,通过掩码 0xff 获取到实际的邻近域的值。

再接着,通过 setup_node 函数,建立近似域和节点的双向映射关系,并返回映射的节点。

如果返回的节点小于 0,说明邻近域超出数量限制,无法完成映射,则打印错误信息,并调用 bad_srat 函数禁用 SRAT 后返回。

接下来,调用 “4.4.3 numa_add_memblk” 函数将探测到的内存块信息添加到 numa_meminfo 中。如果 numa_add_memblk 函数返回负值,说明添加失败,跳转到 out_err_bad_srat 标签处执行。

然后,调用 node_set 函数,将位图 numa_nodes_parsed 中的节点对应的比特位置位。

最后,打印节点、近似域、内存块的相关信息,并返回成功码 0。

5.2.2.10 acpi_parse_slit

acpi_parse_slit 函数用于解析 SLIT 表。

// file: drivers/acpi/numa.c
static int __init acpi_parse_slit(struct acpi_table_header *table)
{
	struct acpi_table_slit *slit;

	if (!table)
		return -EINVAL;

	slit = (struct acpi_table_slit *)table;

	if (!slit_valid(slit)) {
		printk(KERN_INFO "ACPI: SLIT table looks invalid. Not used.\n");
		return -EINVAL;
	}
	acpi_numa_slit_init(slit);

	return 0;
}

其内部先是进行有效性验证。如果 SLIT 表为空,或者 SLIT 表无效,直接返回错误码 -EINVALslit_valid 函数会检查距离矩阵的每一个元素,如果对角元素的值不等于 LOCAL_DISTANCE,或者任何非对角元素的值不大于 LOCAL_DISTANCE,说明 SLIT 表无效,返回 0;否则,返回 1。

验证通过后,会调用 acpi_numa_slit_init 函数执行实际的解析工作。

5.2.2.10.1 slit_valid

slit_valid 函数会验证 SLIT 表的有效性。

如果对角元素的值不等于 LOCAL_DISTANCE,或者任何非对角元素的值不大于 LOCAL_DISTANCE,说明 SLIT 表无效,返回 0;否则,返回 1。

// file: drivers/acpi/numa.c
/*
 * A lot of BIOS fill in 10 (= no distance) everywhere. This messes
 * up the NUMA heuristics which wants the local node to have a smaller
 * distance than the others.
 * Do some quick checks here and only use the SLIT if it passes.
 */
static __init int slit_valid(struct acpi_table_slit *slit)
{
	int i, j;
	int d = slit->locality_count;
	for (i = 0; i < d; i++) {
		for (j = 0; j < d; j++)  {
			u8 val = slit->entry[d*i + j];
			if (i == j) {
				if (val != LOCAL_DISTANCE)
					return 0;
			} else if (val <= LOCAL_DISTANCE)
				return 0;
		}
	}
	return 1;
}
5.2.2.10.2 acpi_numa_slit_init
// file: arch/x86/mm/srat.c
/* Callback for SLIT parsing */
void __init acpi_numa_slit_init(struct acpi_table_slit *slit)
{
	int i, j;

	for (i = 0; i < slit->locality_count; i++)
		for (j = 0; j < slit->locality_count; j++)
			numa_set_distance(pxm_to_node(i), pxm_to_node(j),
				slit->entry[slit->locality_count * i + j]);
}

SLIT 表中的 locality_count 字段,表示邻近域的数量。节点距离矩阵的成员数量为 locality_count×locality_countlocality\_count \times locality\_count ,矩阵成员从 entry[0,0]entry[locality_count-1,locality_count-1]

我们在 “2.2.1 SLIT 简介” 中介绍过,邻近域 i 和邻近域 j 之间的距离保存在矩阵的第 i*N + j 个元素中。此处 N 的值为 locality_count

该函数遍历矩阵元素,调用 numa_set_distance 函数将节点距离保存到数组 numa_distance 中。其中,pxm_to_node 函数用于获取邻近域对应的节点。

5.2.2.10.3 numa_set_distance

该函数用于设置两个节点间的相对距离,其接收 3 个参数:

  • @from:起始节点
  • @to:目的节点
  • @distance:节点距离
// file: arch/x86/mm/numa.c
/**
 * numa_set_distance - Set NUMA distance from one NUMA to another
 * @from: the 'from' node to set distance
 * @to: the 'to'  node to set distance
 * @distance: NUMA distance
 *
 * Set the distance from node @from to @to to @distance.  If distance table
 * doesn't exist, one which is large enough to accommodate all the currently
 * known nodes will be created.
 *
 * If such table cannot be allocated, a warning is printed and further
 * calls are ignored until the distance table is reset with
 * numa_reset_distance().
 *
 * If @from or @to is higher than the highest known node or lower than zero
 * at the time of table creation or @distance doesn't make sense, the call
 * is ignored.
 * This is to allow simplification of specific NUMA config implementations.
 */
void __init numa_set_distance(int from, int to, int distance)
{
	if (!numa_distance && numa_alloc_distance() < 0)
		return;

	if (from >= numa_distance_cnt || to >= numa_distance_cnt ||
			from < 0 || to < 0) {
		pr_warn_once("NUMA: Warning: node ids are out of bound, from=%d to=%d distance=%d\n",
			    from, to, distance);
		return;
	}

	if ((u8)distance != distance ||
	    (from == to && distance != LOCAL_DISTANCE)) {
		pr_warn_once("NUMA: Warning: invalid distance parameter, from=%d to=%d distance=%d\n",
			     from, to, distance);
		return;
	}

	numa_distance[from * numa_distance_cnt + to] = distance;
}

函数内部,首先判断节点距离表是否存在。我们在 numa_reset_distance 函数中重置过节点距离表,重置后 numa_distance 的值为 NULL。此处,如果 numa_distance 不为 NULL,说明距离表已经存在,可以直接使用;否则,需要调用 numa_alloc_distance 函数为距离表分配内存并对其进行初始化。初始化之后,距离表的所有对角元素的值为 LOCAL_DISTANCE (扩展为 10),非对角元素的值为 REMOTE_DISTANCE (扩展为 20)。如果内存分配失败,直接返回。

接下来进行参数校验:

  • 如果任何一个节点的 ID 大于最大值 numa_distance_cnt -1或者为负值,都属于无效节点,打印警告信息并返回。

  • 在 SRAT 表中,节点的距离是 8 位的整数。如果将 distance 转换成 u8 类型后与原值不一致,说明距离值超出了正常范围,该距离值无效。

  • 如果对角元素的距离不等于 LOCAL_DISTANCE,该距离也无效。

如果参数校验失败,打印警告信息后直接返回。

参数校验通过后,直接在数组 numa_distance 中保存节点间的距离,下标为 from * numa_distance_cnt + to

5.2.3 numa_register_memblks

该函数会将探测到的节点内存写入节点描述符 pglist_data,主要写入三个字段: node_start_pfn(节点内存起始页帧号)、node_spanned_pages(节点内存的物理页数量,包含空洞占用的页数)、node_id(节点 ID)。

// file: arch/x86/mm/numa.c
static int __init numa_register_memblks(struct numa_meminfo *mi)
{
	unsigned long uninitialized_var(pfn_align);
	int i, nid;

	/* Account for nodes with cpus and no memory */
	node_possible_map = numa_nodes_parsed;
	numa_nodemask_from_meminfo(&node_possible_map, mi);
	if (WARN_ON(nodes_empty(node_possible_map)))
		return -EINVAL;

......

函数内部,将已解析节点的位图 numa_nodes_parsed 复制一份保存到 node_possible_map 中。接着,调用 numa_nodemask_from_meminfo 函数使用探测到的内存块节点填充位图 node_possible_map 中的比特位。如果 node_possible_map 为空,说明未解析到有效节点,这种情况下系统根本无法运行,所以通过 WARN_ON 宏打印警告信息,并返回 -EINVAL 错误码。

......
	for (i = 0; i < mi->nr_blks; i++) {
		struct numa_memblk *mb = &mi->blk[i];
		memblock_set_node(mb->start, mb->end - mb->start, mb->nid);
	}
......

在未探测节点内存之前,内核将 memblock 管理的所有内存块的节点都设置成了 MAX_NUMNODES。在探测出内存对应的节点后,就需要将节点信息同步到 memblock 管理的内存块中。此处把探测到的 NUMA 内存的信息,通过 memblock_set_node 函数同步到 memblock 管理的内存中。

......
	/*
	 * If sections array is gonna be used for pfn -> nid mapping, check
	 * whether its granularity is fine enough.
	 */
#ifdef NODE_NOT_IN_PAGE_FLAGS
	pfn_align = node_map_pfn_alignment();
	if (pfn_align && pfn_align < PAGES_PER_SECTION) {
		printk(KERN_WARNING "Node alignment %LuMB < min %LuMB, rejecting NUMA config\n",
		       PFN_PHYS(pfn_align) >> 20,
		       PFN_PHYS(PAGES_PER_SECTION) >> 20);
		return -EINVAL;
	}
#endif
......

接下来,如果定义了宏 NODE_NOT_IN_PAGE_FLAGS,会进行一些检查操作。该宏指示 page->flags 字段中是否包含节点 ID。在 Linux 内核中,使用 struct page 结构体来描述物理页,使用 page->flags 来指示页面标志。

// file: include/linux/mm_types.h
struct page {
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */
......
}

而页面标志有 5 种可能的布局:

// file: include/linux/page-flags-layout.h
/*
 * page->flags layout:
 *
 * There are five possibilities for how page->flags get laid out.  The first
 * pair is for the normal case without sparsemem. The second pair is for
 * sparsemem when there is plenty of space for node and section information.
 * The last is when there is insufficient space in page->flags and a separate
 * lookup is necessary.
 *
 * No sparsemem or sparsemem vmemmap: |       NODE     | ZONE |          ... | FLAGS |
 *         " plus space for last_nid: |       NODE     | ZONE | LAST_NID ... | FLAGS |
 * classic sparse with space for node:| SECTION | NODE | ZONE |          ... | FLAGS |
 *         " plus space for last_nid: | SECTION | NODE | ZONE | LAST_NID ... | FLAGS |
 * classic sparse no space for node:  | SECTION |     ZONE    | ... | FLAGS |
 */

其中,前两种用于非稀疏 或 sparse vmemmap 类型内存;后三种用于经典稀疏(classic sparce) 类型内存。最后一种用于 page->flags 空间不足时的布局。可以看到,flags 字段最多由 5 部分组成,其中 NODE 部分表示物理页所属的节点 ID。但是,由于 page->flags 空间限制,该部分是否存在需要经过一系列计算。如果page->flags的位数不足以存储这部分信息,那么此时就会定义宏 NODE_NOT_IN_PAGE_FLAGS,表示page->flags 中不包含节点信息。具体计算过程在文件 include/linux/page-flags-layout.h 中。

在我们的案例中,并没有定义 NODE_NOT_IN_PAGE_FLAGS ,所以相关内容我们就不深入查看了。

......
	if (!numa_meminfo_cover_memory(mi))
		return -EINVAL;
......

接下来,调用 “4.4.4 numa_meminfo_cover_memory” 检查 NUMA 内存是否覆盖了 e820 内存。如果返回 false,说明两者相差较大(超过 1MB),返回错误码 -EINVAL

......
	/* Finally register nodes. */
	for_each_node_mask(nid, node_possible_map) {
		u64 start = PFN_PHYS(max_pfn);
		u64 end = 0;

		for (i = 0; i < mi->nr_blks; i++) {
			if (nid != mi->blk[i].nid)
				continue;
			start = min(mi->blk[i].start, start);
			end = max(mi->blk[i].end, end);
		}

		if (start < end)
			setup_node_data(nid, start, end);
	}
......

再接着,要根据探测到的节点内存设置节点描述符,这是在一个双重循环中完成的。

在外层循环会遍历 node_possible_map 位图中的节点,并将内存块起始地址设置为最大页帧号对应物理地址,将内存块的结束地址设置为 0。

在内层循环会遍历 NUMA 内存块,找到属于该节点的内存块,并更新内存块的起始地址和结束地址。每次更新,都会导致起始地址更小而结束地址变大。换句话说,循环后获取到所有属于该节点的内存块的最小的起始地址和最大的结束地址,得到最大的节点内存范围。这个内存范围是可能存在空洞的,如下图所示:

numa_register_memblks.png

内存循环完成后,如果该节点的内存区域的起始地址小于结束地址,说明这个节点内存有效,调用 “5.2.3.2 setup_node_data” 函数设置节点。

......
	/* Dump memblock with node info and return. */
	memblock_dump_all();
	return 0; 
}

最后,调用 memblock_dump_all 函数打印出 memblock 信息,并返回成功码 0。

5.2.3.1 PFN_PHYS

PFN_PHYS 的将物理地址转换成页帧号。

#define PFN_PHYS(x)	((phys_addr_t)(x) << PAGE_SHIFT)
5.2.3.2 setup_node_data

该函数为节点描述符 pg_data_t 分配内存,并对节点描述符进行初步初始化,主要是填充 3 个字段: node_idnode_start_pfnnode_spanned_pages。此外,还要将节点设置为在线状态。

该函数接收 3 个参数:

  • @nid:节点 ID
  • @start:内存块起始地址
  • @end:内存块结束地址
// file: arch/x86/mm/numa.c
/* Initialize NODE_DATA for a node on the local memory */
static void __init setup_node_data(int nid, u64 start, u64 end)
{
	const size_t nd_size = roundup(sizeof(pg_data_t), PAGE_SIZE);
	u64 nd_pa;
	void *nd;
	int tnid;

	/*
	 * Don't confuse VM with a node that doesn't have the
	 * minimum amount of memory:
	 */
	if (end && (end - start) < NODE_MIN_SIZE)
		return;

	start = roundup(start, ZONE_ALIGN);

	printk(KERN_INFO "Initmem setup node %d [mem %#010Lx-%#010Lx]\n",
	       nid, start, end - 1);

	/*
	 * Allocate node data.  Try node-local memory and then any node.
	 * Never allocate in DMA zone.
	 */
	nd_pa = memblock_alloc_nid(nd_size, SMP_CACHE_BYTES, nid);
	if (!nd_pa) {
		pr_err("Cannot find %zu bytes in node %d\n",
		       nd_size, nid);
		return;
	}
	nd = __va(nd_pa);

	/* report and initialize */
	printk(KERN_INFO "  NODE_DATA [mem %#010Lx-%#010Lx]\n",
	       nd_pa, nd_pa + nd_size - 1);
	tnid = early_pfn_to_nid(nd_pa >> PAGE_SHIFT);
	if (tnid != nid)
		printk(KERN_INFO "    NODE_DATA(%d) on node %d\n", nid, tnid);

	node_data[nid] = nd;
	memset(NODE_DATA(nid), 0, sizeof(pg_data_t));
	NODE_DATA(nid)->node_id = nid;
	NODE_DATA(nid)->node_start_pfn = start >> PAGE_SHIFT;
	NODE_DATA(nid)->node_spanned_pages = (end - start) >> PAGE_SHIFT;

	node_set_online(nid);
}

函数内部,首先声明了一些变量。其中,nd_size 是将节点描述符 pg_data_t 向上圆整到对齐 PAGE_SIZE 后的大小。

如果内存块空间小于系统要求的最小值,那么直接返回。

NODE_MIN_SIZE (扩展为 4MB)表示节点内存的最小值:

// file: arch/x86/include/asm/numa.h
/*
 * Too small node sizes may confuse the VM badly. Usually they
 * result from BIOS bugs. So dont recognize nodes as standalone
 * NUMA entities that have less than this amount of RAM listed:
 */
#define NODE_MIN_SIZE (4*1024*1024)

如果一个节点的内存小于 4MB ,这通常是由 BIOS 的 bug 引起的,所以内核直接忽略这类节点。

接下来,将内存的起始地址向上圆整对齐到 ZONE_ALIGN

// file: arch/x86/include/asm/numa.h
#define ZONE_ALIGN (1UL << (MAX_ORDER+PAGE_SHIFT))

其中,宏 MAX_ORDER 扩展为 11,PAGE_SHIFT 扩展为 12,所以 ZONE_ALIGN 扩展为 1<< 23,即 8MB。

再接着,调用 memblock_alloc_nid 函数为节点描述符分配内存,并将内存的起始地址保存到变量 nd_pa 中。如果分配失败,打印错误信息并返回。

memblock_alloc_nid 函数在指定节点分配连续内存,该函数接收 3 个参数:待分配的内存大小,对齐字节以及指定节点。该函数的实现请参考:Linux Kernel:启动时内存管理(MemBlock 分配器) 中 ”5.4.5 memblock_alloc_nid()“ 小节。

然后,通过 __va 宏,将内存的物理地址转换成虚拟地址,并打印节点内存信息。

再接着,通过 early_pfn_to_nid 函数,获取页帧号对应的节点 ID。如果已分配的内存的节点和指定的节点不一致,则打印提示信息。

然后,将节点描述符保存到节点数组 node_data 中。

接下来,先是通过 memset 函数将节点描述符初始化为 0,然后使用内存信息填充该描述符的 node_idnode_start_pfnnode_spanned_pages 字段。

最后,调用 node_set_online 将节点设置为在线状态。

5.2.4 numa_init_array

根据函数注释,numa_init_array 函数实际是一种降级策略。由于有的主板只能把内存分配给一个 CPU,导致 CPU 和节点不能正常映射。为了避免这种情况,需要对 CPU 映射的节点进行修正。

// file: arch/x86/mm/numa.c
/*
 * There are unfortunately some poorly designed mainboards around that
 * only connect memory to a single CPU. This breaks the 1:1 cpu->node
 * mapping. To avoid this fill in the mapping for all possible CPUs,
 * as the number of CPUs is not known yet. We round robin the existing
 * nodes.
 */
static void __init numa_init_array(void)
{
	int rr, i;

	rr = first_node(node_online_map);
	for (i = 0; i < nr_cpu_ids; i++) {
		if (early_cpu_to_node(i) != NUMA_NO_NODE)
			continue;
		numa_set_node(i, rr);
		rr = next_node(rr, node_online_map);
		if (rr == MAX_NUMNODES)
			rr = first_node(node_online_map);
	}
}

numa_init_array 函数会找出所有对应着无效节点(NUMA_NO_NODE)的 CPU,为它们分配节点。 分配时采用 “round robin” 策略,从 node_online_map 位图中依次选择节点。

六、映射 CPU 与 NUMA 节点

我们在解析 SRAT 表时,既建立了 APIC ID 和节点的映射关系,也建立了邻近域和节点的映射关系,唯独没有建立 CPU 到节点的映射关系。此处,我们需要为 CPU 和节点建立映射关系。

6.1 init_cpu_to_node

init_cpu_to_node 函数为每个可能(上线)的 CPU 设置节点。

// file: arch/x86/mm/numa.c
/*
 * Setup early cpu_to_node.
 *
 * Populate cpu_to_node[] only if x86_cpu_to_apicid[],
 * and apicid_to_node[] tables have valid entries for a CPU.
 * This means we skip cpu_to_node[] initialisation for NUMA
 * emulation and faking node case (when running a kernel compiled
 * for NUMA on a non NUMA box), which is OK as cpu_to_node[]
 * is already initialized in a round robin manner at numa_init_array,
 * prior to this call, and this initialization is good enough
 * for the fake NUMA cases.
 *
 * Called before the per_cpu areas are setup.
 */
void __init init_cpu_to_node(void)
{
	int cpu;
	u16 *cpu_to_apicid = early_per_cpu_ptr(x86_cpu_to_apicid);

	BUG_ON(cpu_to_apicid == NULL);

	for_each_possible_cpu(cpu) {
		int node = numa_cpu_node(cpu);

		if (node == NUMA_NO_NODE)
			continue;
		if (!node_online(node))
			node = find_near_online_node(node);
		numa_set_node(cpu, node);
	}
}

函数内部,首先通过 early_per_cpu_ptr 函数,获取到 early-per-cpu 变量 x86_cpu_to_apicid 的地址。这是一个数组,保存着各 CPU 和 APIC ID 的映射关系。内核会在解析 MADT 表时,在 generic_processor_info 函数中将每个 CPU 对应的 APIC ID 保存在该数组中。

如果该地址为 NULL,说明这是一个内核 bug,BUG_ON 宏会打印错误信息并将系统挂起。

接下来,遍历每一个 possible 状态的 CPU,通过 “4.3.5 numa_cpu_node” 函数获取到该 CPU 对应的节点。如果节点为默认值 NUMA_NO_NODE,说明该 CPU 当前并不存在,可能在未来的某个时刻插入,略过。如果节点存在但并未上线,则调用 find_near_online_node 函数找到离当前节点距离最近的上线节点,并将该节点设置为 CPU 的节点。

6.1.1 find_near_online_node

该函数查找与指定节点相对距离最近的上线节点。

// file: arch/x86/mm/numa.c
static __init int find_near_online_node(int node)
{
	int n, val;
	int min_val = INT_MAX;
	int best_node = -1;

	for_each_online_node(n) {
		val = node_distance(node, n);

		if (val < min_val) {
			min_val = val;
			best_node = n;
		}
	}

	return best_node;
}

七、 完整流程图

cpu_numa_detect.png

八、在 Linux 中查看节点信息

在 Linux 中,可以通过 numactl 命令查看节点信息,示例如下:

numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 12 13 14 15 16 17
node 0 size: 32143 MB
node 0 free: 188 MB
node 1 cpus: 6 7 8 9 10 11 18 19 20 21 22 23
node 1 size: 32254 MB
node 1 free: 69 MB
node distances:
node   0   1 
  0:  10  21 
  1:  21  10

该服务器有 2 个节点,每个节点有 12 个逻辑处理器和 32GB 内存。node distances 指示节点间的距离,对角线元素全部为 0,表示节点到自身的距离,其它元素的值都比 10 要大。

九、参考资料

1、NUMA

2、5.2.16. System Resource Affinity Table (SRAT)

3、5.2.17. System Locality Information Table (SLIT)

4、numactl