卡卷网
当前位置:卡卷网 / 每日看点 / 正文

Linux系统启动之后,物理内存的布局是怎么样的?

作者:卡卷网发布时间:2024-12-04 16:07浏览数量:209次评论数量:0次

本文基于 Linux 6.0,x86/64 系统,编程语言 C/C++。

Linux系统启动之后,物理内存的布局是怎么样的?  第1张

面试时我总会问候选人一个问题,一个 32 位操作系统能支持的最大物理内存是多少?

还是有些同学答不上来,可能现在的同学,在一开始使用电脑时就是 64 位系统,不像我们那时候是 32 位系统的,最多插两根 2GB 内存条,系统只能识别到 4GB 内存,原因是内存中每个字节都有一个唯一的物理地址(Physical Address,PA)对应,那么 32 位的地址最多能支持 2^32 个地址,也就是最多能支持 4GB 大小的内存。

虽然 32 位系统最多支持 4GB 内存,但是在你打开 Window 系统任务管理器时会发现显示的内存小于 4GB,这是因为还有其他硬件和系统组件占用了一部分内存,这些硬件和系统组件包括:

  • BIOS 或 UEFI:这是系统的基本输入输出系统或统一固件接口,它需要一定的内存来运行;
  • 图形显卡:显卡通常需要一部分系统内存作为视频缓冲区,用于显示图形界面;
  • 系统硬件:包括主板、芯片组、网络适配器等,这些硬件通常会占用一部分内存作为缓冲区或驱动程序的工作区;
  • 内核空间:在 Windows 等操作系统中,有一部分内存被保留给操作系统的内核使用。

一、内存模型

通常情况下,内核以页为单位管理物理内存,每页大小为 4K,并且以页大小对齐,使用 struct page 结构体来描述一页物理内存,struct page 中保存物理页的各种信息,后面会详细介绍 struct page,为了方便描述后面统称 struct page物理页描述符,简称页描述符页框。内核为每个页框制定了一个全局唯一的索引编号PFN(Page Frame Number),其实就是物理地址左移 12 位,因为每页大小 4KB(2^12),提供 page_to_pfnpfn_to_page 两个方法,互相快速定位对方。

内核管理物理页的方式称为物理内存模型,主要由下面二种模型。

1.1 平坦内存模型 FLATMEM

FLATMEM(flat memory model)就是把物理内存划分连续的物理页,然后使用一个大的页框数组与之一一映射,数组下标就是 PFN。这里的页框数组就是 mem_map,定义如下:

struct page *mem_map;

提供 page_to_pfnpfn_to_page 两个方法,定义如下:

#if defined(CONFIG_FLATMEM) #define ARCH_PFN_OFFSET (0UL) #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

Linux系统启动之后,物理内存的布局是怎么样的?  第2张

图1

Linux 早期版本就使用这种内存模型,因为那时物理内存比较小(几十MB),使用平坦内存模型比较简单。内核默认使用平坦内存模型。

但是对于物理内存不连续,存在内存空洞的情况,空洞的产生原因有很多种,例如不同类型的内存芯片(如DDR3、DDR4等)可能具有不同的规格和排列方式导致内存空洞,那么平坦内存模型就不适合了。因为mem_map数组中为所有的物理内存都创建一个页框,不管是正常内存,还是大块内存空洞,会导致mem_map数组中大量的页框指向了无意义的物理内存。页框本身是有大小的(40多字节),也是要占用物理内存的,所以空洞就导致了大量浪费物理内存空间。

后来 Linux 引入了非连续内存模型 DISCONTIGMEM (discontiguous memory model),不过非连续内存模型是个稍纵即逝的内存模型,后来被稀疏内存模型完全替代。

1.2 稀疏内存模型 SPARSEMEM

SPARSEMEM (sparse memory model) 很好的支持了内存空洞以及后面提到的热插拔,细分两个小内存模型。

1、Node(粗粒度)

内核将物理内存划分几个 Node 节点,Node 节点对应的结构体是 struct pglist_data,每个节点包含一个页框数组,管理「尽量」连续的物理内存,尽量避免为内存空洞分配页框而浪费内存,说白了每个 Node 节点中还是采用平坦内存模型来管理物理页。

//部分代码 typedef struct pglist_data { struct page *node_mem_map; } pg_data_t;

Linux系统启动之后,物理内存的布局是怎么样的?  第3张

图2

从上图中可以看到,虽然物理内存不是连续的,存在空洞,但是PFN是连续的,是一种虚拟连续,内核提供page_to_pfnpfn_to_page两个方法,定义如下:

#if defined(CONFIG_SPARSEMEM_VMEMMAP) /* memmap is virtually contiguous. */ #define __pfn_to_page(pfn) (vmemmap + (pfn)) #define __page_to_pfn(page) (unsigned long)((page) - vmemmap) #endif

2、Section(细粒度)

如果想更小粒度管理连续的物理内存,那就需要划分更多的 Node 节点,因为 Node 节点的 struct pglist_data 结构体很大,划分越多额外占用的内存就越多,也就是浪费的内存越多,所以另一种稀疏内存模型诞生了。

另一种稀疏内存模型是对更小粒度的连续物理内存块进行管理,每一段物理内存块使用 section 来管理,所用的 struct mem_section 结构体体积比较小,仅有 16 或 32 字节(本小节只讨论 64 位系统),避免浪费空间的同时进一步减少为内存空洞分配页框。struct mem_section 定义如下:

struct mem_section { unsigned long section_mem_map; struct mem_section_usage *usage; #ifdef CONFIG_PAGE_EXTENSION /* * 如果使用了 SPARSEMEM,pgdat 将没有 page_ext 指针。 * 我们会使用 section。 */ struct page_ext *page_ext; unsigned long pad; #endif // 警告:mem_section 的大小必须是2的幂,以便计算和使用 SECTION_ROOT_MASK。 };

这个内存模型的组织管理物理内存的方式如下图:

Linux系统启动之后,物理内存的布局是怎么样的?  第4张

图3

上图中根节点是一个 struct mem_section 二维数组,定义如下:

#ifdef CONFIG_SPARSEMEM_EXTREME extern struct mem_section **mem_section; #else extern struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]; #endif

下面代码列出了很多跟 Section 相关的宏定义,其中SECTION_SIZE_BITS(27)决定每个 struct mem_section 对应一个连续的 128MB(2^27)物理内存块,通常情况下每个物理页大小为 4KB,那么每个 struct mem_section 对应 32K 个物理页。

#define SECTION_SIZE_BITS 27 // 128MB #define PFN_SECTION_SHIFT (SECTION_SIZE_BITS - PAGE_SHIFT) // 15 #define PAGES_PER_SECTION (1UL << PFN_SECTION_SHIFT) #define PAGE_SECTION_MASK (~(PAGES_PER_SECTION-1)) #define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof (struct mem_section)) // 4096 / 16 = 256 # define MAX_PHYSMEM_BITS (pgtable_l5_enabled() ? 52 : 46) #define SECTIONS_SHIFT (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS) // 46 - 27 = 19 #define NR_MEM_SECTIONS (1UL << SECTIONS_SHIFT)//1 << 19 #define __KERNEL_DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) #define NR_SECTION_ROOTS DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT) // 2048 #if defined(CONFIG_SPARSEMEM) && !defined(CONFIG_SPARSEMEM_VMEMMAP) #define SECTIONS_WIDTH SECTIONS_SHIFT // 19 #else #define SECTIONS_WIDTH 0 #endif /* Page flags: | [SECTION] | [NODE] | ZONE | [LAST_CPUPID] | ... | FLAGS | */ #define SECTIONS_PGOFF ((sizeof(unsigned long)*8) - SECTIONS_WIDTH) // 64 - 19 = 45 #define SECTIONS_PGSHIFT (SECTIONS_PGOFF * (SECTIONS_WIDTH != 0))

Section 内存模型也提供了两个宏定义 __page_to_pfn(pg) 和 __pfn_to_page(pfn),目的是在页和页号之间建立映射关系,以便在需要的情况下可以高效地进行转换。

#if defined(CONFIG_SPARSEMEM) && !defined(CONFIG_SPARSEMEM_VMEMMAP) #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

下面是对这两个函数的解释说明:

  1. __page_to_pfn(pg): 这个宏将一个页(page)转换为对应的页号(PFN)。
  2. 它首先将传递的页指针 pg 赋值给 __pg,然后通过调用 page_to_section(__pg) 来获取该页所在的内存的段号,其存储在 struct page->flags的高位中;
  3. 接着,通过计算页与内存段起始地址的偏移量,得到页号。
  4. __pfn_to_page(pfn): 这个宏将一个页帧号(PFN)转换为对应的页(page)指针。
  5. 它首先将传递的页帧号 pfn 赋值给 __pfn。然后,通过调用 __pfn_to_section(__pfn) 来获取该页号所在的内存的段号,其存储在 PFN 的高位中。
  6. 接着,通过计算页在内存段中的偏移量,得到页的指针。

表达式 __section_mem_map_addr(__sec)的值是struct mem_section->section_mem_map清零低位标志位后的值。下面是 __section_mem_map_addr 函数代码:

static inline struct page *__section_mem_map_addr(struct mem_section *section) { unsigned long map = section->section_mem_map; map &= SECTION_MAP_MASK; return (struct page *)map; }

然后,表达式__section_mem_map_addr(__sec) + __pfn为啥要 + __pfn 呢,研究了很久,后来通过查资料才明白,这里面暗藏玄机,struct mem_section->section_mem_map通常低 3 位存储了标志位和高 61 位存储了本段(section) 指向的 struct page 数组的第一个 struct page 的起始虚拟地址(mem_map)减掉该 struct page 的 PFN的差值,这样将差值代入 __page_to_pfn(pg)__pfn_to_page(pfn) 函数就能想明白了,目的是减少一次减法操作,初始化 section 时就把这个减法做了,提升性能。

static void __meminit sparse_init_one_section(struct mem_section *ms, unsigned long pnum, struct page *mem_map, struct mem_section_usage *usage, unsigned long flags) { ms->section_mem_map &= ~SECTION_MAP_MASK; ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum) | SECTION_HAS_MEM_MAP | flags; ms->usage = usage; } static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum) { unsigned long coded_mem_map = (unsigned long)(mem_map - (section_nr_to_pfn(pnum))); BUILD_BUG_ON(SECTION_MAP_LAST_BIT > PFN_SECTION_SHIFT); BUG_ON(coded_mem_map & ~SECTION_MAP_MASK); return coded_mem_map; }

上面提到 struct mem_section->section_mem_map 低 3 位是标志位,用来表示本 Section 的状态,其中SECTION_IS_ONLINE_BIT表示是否在线,用来支持热插拔

enum { SECTION_MARKED_PRESENT_BIT, SECTION_HAS_MEM_MAP_BIT, SECTION_IS_ONLINE_BIT, //热插拔 SECTION_IS_EARLY_BIT, #ifdef CONFIG_ZONE_DEVICE SECTION_TAINT_ZONE_DEVICE_BIT, #endif SECTION_MAP_LAST_BIT, };

二、内存架构

下面介绍两种内存架构,均匀内存访问架构(UMA)和非均匀内存访问架构(NUMA)。它们都是 SMP(对称多处理器,Symmetric multiprocessing)架构的具体实现。

2.1 UMA

UMA(Uniform Memory Access)均匀内存访问架构。所有 CPU 都是经过总线到内存控制器再到物理内存,访问相同的物理内存,并且访问距离和时间也相同。

下图是一个典型的 x86 UMA 内存架构,四路 CPU 通过前端系统总线(FSB, Front Side Bus)和主板上北桥(North Bridge)芯片中内存控制器 (MCH, Memory Controller Hub) 相连,再与物理内存相连。

Linux系统启动之后,物理内存的布局是怎么样的?  第5张

图4

UMA 架构简单,但是随着多核技术发展,CPU 数量不断增加,前端系统总线(FSB)成为了系统性能的瓶颈,主要表现有下面两个方面:

  • 随着 CPU 数量增多,每个 CPU 分到的总线的带宽月来越少;
  • 总线要与所有 CPU 相连,那么CPU越多,总线就越长,时延自然增加。

为了解决上面的瓶颈,AMD 和 Intel 都在 x86_64 中实现了一种新架构 NUMA。

2.2 NUMA

2.2.1 节点结构

NUMA(Non-Uniform Memory Access)非均匀内存访问架构。内存划分为多个块,每个 CPU 到不同内存块距离有远近之分,距离一个 CPU 近的内存块称为该 CPU 的本地内存;距离相对远的内存块称为该 CPU 的非本地内存(也叫远端内存)。

如下图所示,NUMA 内存架构把 CPU 和本地内存封装在一个 Node 节点里,并且将内存控制器芯片被集成到 CPU 内部,CPU 间通过 QPI(QuickPath Interconnect)链路相连。每个 CPU 访问本地内存非常快,没有了总线,相当于直接访问。但是有时例如本地内存空间不足等情况,一个 CPU 可以通过 QPI 访问另一个 CPU 所在 Node 节点内的本地内存,也就是一个 CPU 可以访问非本地内存。有的架构将PCI-E总线资源(IOH)也集成到了 CPU 内部。

Linux系统启动之后,物理内存的布局是怎么样的?  第1张

图5

一个 Node 节点由一个物理 CPU、本地内存和本地 IO 资源组成。一个物理 CPU 由多个 CPU Core(核心)和一个 UnCore部分组成。

  • 每个 CPU Core 一般有 2 个 CPU Thread,也称逻辑 CPU Core(核心),top 命令里可见,每个逻辑核心独立运行,共享 Core 内部的逻辑运算单元(ALU)、浮点运算单元(FPU)、L1 和 L2 缓存;
  • Uncore 集成了内存控制器 iMC(Integrated Memory Controller)、PCIe Root Complex、QPI 控制器、L3 缓存和 CBox(负责缓存一致性),及其它外设控制器。

图 5 中,CPU 通过 Uncore 里的 iMC 直接访问本地内存,所以速度很快。但是访问非本地内存,则需要通过 QPI 链路到目标 Node 节点再通过 iMC 间接访问目标节点的本地内存,所以速度较慢。由此,应用和系统应尽量访问当前 CPU 的本地内存,来降低延时和提升性能。

对于 PCI-E 等外设,通过 DMA 访问的内存,最好和外设在一个节点内,这样访问速度最快,要不然也和 CPU 一样,需要经过 QPI 链路访问远端内存。外设访问完 DMA 内存后,触发的硬中断和下半部的软中断会在同一个 CPU 上,那么这个 DMA 内存最好是该 CPU 的本地内存,这样速度才能快,也就是说要把同一个节点内外设的硬中断绑定到该节点内的 CPU 上。

下图是 2 核心 4 线程的示例:

Linux系统启动之后,物理内存的布局是怎么样的?  第7张

图6

目前 x86 NUMA 具体实现是 ccNUMA(Cache Coherent NUMA),在 NUMA 架构之上引入了缓存一致性协议,以确保不同节点之间的数据一致性,降低内核编写难度。

通过下面命令查看 NUMA 节点的CPU和内存的距离信息

$ numactl -H available: 2 nodes (0-1) node 0 cpus: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 node 0 size: 65441 MB node 0 free: 311 MB node 1 cpus: 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 node 1 size: 65536 MB node 1 free: 3704 MB node distances: node 0 1 0: 10 21 1: 21 10

上面输出结果中包含 2 个 NUMA 节点(0 - 1),每个 NUMA 节点中包含 16 个 CPU 核心,内存大小约为 64GB。

`node distances`下面有个矩阵,表示不同 NUMA 节点之间的访问距离,例如,[0,0] 表示 NUMA 节点 0 的访问本地内存访问距离为 10,[1,0] 表示 NUMA 节点 1 访问 NUMA 节点 0 的距离为 21,比访问本地节点距离多出一倍。

通过下面命令还可以查看各个 NUMA 节点的内存访问命中率:

$ numastat node0 node1 numa_hit 85567733 180252357 numa_miss 974366 107879428 numa_foreign 107879428 974366 interleave_hit 3475 6018 local_node 85554510 180592099 other_node 987585 107539786

  • numa_hit:在本节点内分配内存成功次数。
  • numa_miss:在本节点内分配内存失败次数。
  • numa_foreign:在本节点内分配内存失败,然后到其他节点内分配内存次数。
  • interleave_hit:在 MPOL_INTERLEAVE 策略下,在本节点内分配内存的次数。
  • local_node:运行在本节点 CPU 上的进程在本节点内分配内存成功次数。
  • other_node:运行在本节点 CPU 上的进程在其他节点上分配内存次数。

libnuma动态库封装了 NUMA 相关操作,下面代码判断本系统是否支持 NUMA。

#include<stdio.h> #include<numa.h> int main() { if (numa_available() < 0) printf("本系统不支持 NUMA\n"); else printf("最大 NUMA 节点 id: %d\n", numa_max_node()); return 0; }

编译命令如下:

gcc -o test test.c -lnuma

2.2.2 分配策略

NUMA 的内存分配策略决定内存分配时的行为,制定了例如在本地分配还是去其他节点分配内存等规则,下面列出了几种分配策略:

内存分配策策略描述
MPOL_DEFAULT先从本节点分配内存,如果失败去系统认为比较近的其他节点分配内存。
MPOL_BIND必须在指定的一个或多个节点分配内存,如果分配失败,即使其他节点有内存也会进行 Swap 或 OOM。
MPOL_INTERLEAVE从指定的一个或多个节点内交错分配内存。
MPOL_PREFERRED优先在指定一个或多个节点内分配内存,当分配失败时去其他节点分配内存。
MPOL_LOCAL(默认)与 MPOL_DEFAULT 相似,也是优先在本地节点分配,当分配失败时去其他节点分配内存。

应用程可以调用 libnuma 库中的 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 的内存分配策略:

$ numactl -s policy: default //默认策略 preferred node: current physcpubind: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 cpubind: 0 1 nodebind: 0 1 membind: 0 1

2.2.3 绑定节点

numactl工具可以使应用程序(a.out)指定运行在哪些 CPU 核心上和指定在哪个节点上分配内存,从而提升程序的性能。

$ numactl --membind=0 --cpunodebind=0 ./a.out $ numactl --membind=1 --cpunodebind=0 ./a.out

  • 参数membind指定程序只能在哪些 NUMA 节点上分配内存,如果这些节点内存不足,则分配失败。
  • 参数cpunodebind指定程序只能运行在哪些 NUMA 节点内的 CPU(s) 上。

通过下面命令将应用程序(a.out)绑定到具体的物理 CPU 上:

$ numactl --physcpubind=0 ./a.out #绑定到 0 号 CPU $ numactl --physcpubind=0-5 ./a.out #绑定到 0~5 号 CPU

CPU id 可以通过下面命令获得:

[root@929533e6b321 test]# cat /proc/cpuinfo | grep processor processor : 0 processor : 1 processor : 2 processor : 3 processor : 4 processor : 5

2.2.4 节点状态

NUMA 节点状态存储在 nodemask_t 数组 node_states 中。

typedef struct { DECLARE_BITMAP(bits, MAX_NUMNODES); } nodemask_t; /* 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 [N_MEMORY] = { { [0] = 1UL } }, [N_CPU] = { { [0] = 1UL } }, #endif /* NUMA */ }; /* 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 N_MEMORY, /* The node has memory(regular, high, movable) */ N_CPU, /* The node has one or more cpus */ N_GENERIC_INITIATOR,/* The node has one or more Generic Initiators */ NR_NODE_STATES };

  • N_POSSIBLE:表示节点在某个时刻可变为 online 状态。
  • N_ONLINE:表示节点当前的状态为 online 状态。
  • N_NORMAL_MEMORY:表示节点没有高端内存,只有 ZONE_NORMAL 内存区域。
  • N_HIGH_MEMORY:表示节点有 ZONE_NORMAL 或 ZONE_HIGHMEM 内存区域。
  • N_MEMORY:表示节点有 ZONE_NORMAL、ZONE_HIGHMEM 和 ZONE_MOVABLE 内存区域。
  • N_CPU:表示节点有一个或多个 CPU。
  • N_GENERIC_INITIATOR:标识节点有一个或多个通用启动器。
  • NR_NODE_STATES:表示节点状态的数量。

三、物理内存节点

3.1 节点结构

Linux 中,在 NUMA(非一致性存储访问)架构的机器上,每个 NUMA 节点都有一个 pglist_data 结构来描述其内存结构。在 UMA(统一内存访问)架构的机器上,只有一个 pglist_data 结构来描述整个内存结构。

Linux系统启动之后,物理内存的布局是怎么样的?  第8张

图7

内核中定义了一个全局的 Node 节点数组来存储这些节点,定义如下:

#define MAX_NUMNODES (1 << NODES_SHIFT) struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;

当内存架构是 UMA 情况下 NODES_SHIFT 是 0,数组内只有一个成员; 当是 NUMA 情况下,NODES_SHIFT数组长度在配置文件Kconfig中定义,可以修改。

config NODES_SHIFT int "Maximum NUMA Nodes (as a power of 2)" if !MAXSMP range 1 10 default "10" if MAXSMP default "6" if X86_64 default "3" depends on NUMA help Specify the maximum number of NUMA Nodes available on the target system. Increases memory reserved to accommodate various tables.

Node 节点描述符 pglist_data 的定义如下:

typedef struct pglist_data { /* * node_zones 只包含此节点的内存区域(zone)。并不是所有区域都会被填充,但它是完整的区域列表。 * 它被本节点的 node_zonelists 引用,以及其他节点的 node_zonelists 引用。 */ struct zone node_zones[MAX_NR_ZONES]; /* * node_zonelists 包含对所有节点中所有区域的引用。 * 通常,前面的区域引用是指向此节点的 node_zones。 */ struct zonelist node_zonelists[MAX_ZONELISTS]; int nr_zones; /* 当前节点中已填充的内存区域数量 */ #ifdef CONFIG_FLATMEM /* means !SPARSEMEM */ struct page *node_mem_map; #ifdef CONFIG_PAGE_EXTENSION struct page_ext *node_page_ext; #endif #endif #if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT) /* * 在你期望 node_start_pfn、node_present_pages、node_spanned_pages 或 nr_zones 保持不变时,必须持有此锁。 * 同时还在延迟的页面初始化期间同步 pgdat->first_deferred_pfn。 * 通过 pgdat_resize_lock() 和 pgdat_resize_unlock() 函数来操作 node_size_lock,而不需要检查 CONFIG_MEMORY_HOTPLUG 或 CONFIG_DEFERRED_STRUCT_PAGE_INIT。 * 嵌套在 zone->lock 和 zone->span_seqlock 之上。 */ spinlock_t node_size_lock; #endif unsigned long node_start_pfn; unsigned long node_present_pages; /* 物理页的总数 */ unsigned long node_spanned_pages; /* 包含空洞的物理页总数 */ int node_id; /* 节点 id */ wait_queue_head_t kswapd_wait; wait_queue_head_t pfmemalloc_wait; /* 用于不同原因的回收限制的工作队列。 */ wait_queue_head_t reclaim_wait[NR_VMSCAN_THROTTLE]; atomic_t nr_writeback_throttled; /* 受限制写回的任务数 */ unsigned long nr_reclaim_start; /* 在限制开始时写入的页面数 */ struct task_struct *kswapd; /* 在 mem_hotplug_begin/done() 期间受保护 */ int kswapd_order; enum zone_type kswapd_highest_zoneidx; int kswapd_failures; /* 'reclaimed == 0' 的次数 */ #ifdef CONFIG_COMPACTION int kcompactd_max_order; enum zone_type kcompactd_highest_zoneidx; wait_queue_head_t kcompactd_wait; struct task_struct *kcompactd; bool proactive_compact_trigger; #endif /* 这是每个节点的保留页数量,不可用于用户空间分配。*/ unsigned long totalreserve_pages; #ifdef CONFIG_NUMA /* 如果存在更多的未映射页面,则节点回收变得活跃。*/ unsigned long min_unmapped_pages; unsigned long min_slab_pages; #endif /* CONFIG_NUMA */ /* 用于页面回收的写密集型字段(Write-intensive fields)*/ ZONE_PADDING(_pad1_) #ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT /* 如果在大型机器上的内存初始化被延迟执行,则这是需要初始化的第一个 PFN。*/ unsigned long first_deferred_pfn; #endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */ #ifdef CONFIG_TRANSPARENT_HUGEPAGE struct deferred_split deferred_split_queue; #endif /* 页面回收扫描器常用的字段 */ /* * 注意:如果启用了 MEMCG,则此字段未使用。 * 使用 mem_cgroup_lruvec() 来查找 lruvecs。 */ struct lruvec __lruvec; unsigned long flags; ZONE_PADDING(_pad2_) /* Per-node vmstats,每个节点的虚拟内存统计 */ struct per_cpu_nodestat __percpu *per_cpu_nodestats; atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS]; } pg_data_t;

介绍其中几个重要的字段:

  • node_id:NUMA 节点 id。
  • node_mem_map:节点的struct page数组,包含所有的物理页。
  • node_start_pfn:节点的起始页号(PFN)。
  • node_spanned_pages:节点中所有物理页的总数,包含空洞所在的物理页。
  • node_present_pages:节点中真正可以用的物理页数,不包含空洞所在的物理页。
  • node_zones:只包含此节点的内存区域(zone),并不是所有区域都会被填充,但它是完整的区域列表。它是一个区域数组,大小为 MAX_NR_ZONES,数组索引就是区域的类型,不是每个节点包含所有类型的区域,所以说数组中存在没有被填充的元素,下文会介绍区域类型定含义。
  • nr_zones:就是 node_zones 数组被填充元素的数目。
  • node_zonelists:引用本节点及其他节点的 node_zones。目的是当本地节点内存不足时,需要分配其他节点的本地内存。虽然速度慢,但是比没有强。
  • kswapd:指向内核为本节点分配的 kswapd 进程,用于回收不经常使用的页。
  • kswapd_wait:用于 kswapd 进程周期性回收页面时使用到的等待队列。
  • kcompactd:指向内核为本节点分配的 kcompactd 进程,用于规整避免内存碎片。
  • kcompactd_wait:用于 kcompactd 进程周期性规整内存时使用到的等待队列。

3.2 区域类型

区域类型 zone_type 枚举值定义如下:

enum zone_type { #ifdef CONFIG_ZONE_DMA ZONE_DMA, #endif #ifdef CONFIG_ZONE_DMA32 ZONE_DMA32, #endif ZONE_NORMAL, #ifdef CONFIG_HIGHMEM ZONE_HIGHMEM, #endif ZONE_MOVABLE, #ifdef CONFIG_ZONE_DEVICE ZONE_DEVICE, #endif __MAX_NR_ZONES // DEFINE(MAX_NR_ZONES, __MAX_NR_ZONES); };

下面介绍不同区域类型的含义:

  • ZONE_DMA:在外设不能 DMA 到所有可寻址物理内存空间(ZONE_NORMAL)时使用。ZONE_DMA 留给具有较小 DMA 寻址范围的外设。64 位和 32 位系统都有此区域;
  • ZONE_DMA32:同 ZONE_DMA 也是在外设不能 DMA 到所有可寻址物理内存空间(ZONE_NORMAL)时使用。不同之处是 ZONE_DMA32 供 32 位外设使用,寻址范围比使用 ZONE_DMA 的外设更大。并且 ZONE_DMA32 只有在 64 位系统中生效,32 位系统没有这个区域。64 位系统为了兼容 32 位外设才有了这个区域;
  • ZONE_NORMAL:直接映射区,物理内存地址加上一个常量就可以得到虚拟内存地址的内核区域,内核可以直接访问;
  • ZONE_HIGHMEM:高端内存区,内核不可以直接访问,需要通过页表动态映射,将虚拟地址转换成物理地址再进行访问。因为 32 位系统寻找空间才有 4GB,所以该区域在 32 位系统中超过 896MB 的虚拟内存空间中;64 位系统不需要该区域,因为 64 位寻找空间非常大(128TB),完全可以放在 ZONE_NORMAL 区域里直接映射;
  • ZONE_MOVABLE:可移动区域,是内核虚拟出来的一个逻辑内存区域,该区域中的物理页均来自其他真实的物理区域,该区域中的物理页都是可以迁移的,目的是防止内存碎片和支持内存热插拔;
  • ZONE_DEVICE:通常与设备相关的内存缓冲区有关,这些缓冲区用于设备之间的数据传输。例如,网络适配器、图形卡、存储控制器等设备可能需要使用ZONE_DEVICE 内存来进行数据传输,而无需将数据映射到通常的系统内存区域。

实际上只有第一个 NUMA 节点可以包含所有区域类型,其它节点只能包含部分区域类型,因为 ZONE_DMA 和 ZONE_DMA32 必须安排在物理内存的低地址,所以只能放在第一个节点。下面是一个示例及对应的图解:

$ cat /proc/zoneinfo | grep Node Node 0, zone DMA Node 0, zone DMA32 Node 0, zone Normal Node 1, zone Normal Node 2, zone Normal Node 2, zone Movable Node 3, zone Normal Node 3, zone Device

Linux系统启动之后,物理内存的布局是怎么样的?  第9张

图8

3.3 节点信息

下面是在我的容器内输出的 NUMA 节点信息

[root@929533e6b321 test]# cat /proc/zoneinfo Node 0, zone DMA per-node stats nr_inactive_anon 111525 nr_active_anon 17352 nr_inactive_file 211346 nr_active_file 124539 nr_unevictable 0 nr_slab_reclaimable 13793 nr_slab_unreclaimable 7843 nr_isolated_anon 0 nr_isolated_file 0 nr_anon_pages 42969 nr_mapped 56654 nr_file_pages 420009 nr_dirty 1 nr_writeback 0 nr_writeback_temp 0 nr_shmem 84124 nr_shmem_hugepages 0 nr_shmem_pmdmapped 0 nr_file_hugepages 0 nr_file_pmdmapped 0 nr_anon_transparent_hugepages 4 nr_vmscan_write 0 nr_vmscan_immediate_reclaim 0 nr_dirtied 245843 nr_written 245771 nr_kernel_misc_reclaimable 0 nr_kernel_stack 8864 pages free 3977 min 44 low 55 high 66 spanned 4095 present 3998 managed 3977 protection: (0, 2992, 5928, 5928, 5928) nr_free_pages 3977 nr_zone_inactive_anon 0 nr_zone_active_anon 0 nr_zone_inactive_file 0 nr_zone_active_file 0 nr_zone_unevictable 0 nr_zone_write_pending 0 nr_mlock 0 nr_page_table_pages 0 nr_bounce 0 nr_free_cma 0 pagesets cpu: 0 count: 0 high: 0 batch: 1 vm stats threshold: 6 cpu: 1 count: 0 high: 0 batch: 1 ··· ··· vm stats threshold: 6 cpu: 5 count: 0 high: 0 batch: 1 vm stats threshold: 6 node_unreclaimable: 0 start_pfn: 1 Node 0, zone DMA32 pages free 765587 min 8505 low 10631 high 12757 spanned 1044480 present 782336 managed 765952 protection: (0, 0, 2936, 2936, 2936) nr_free_pages 765587 nr_zone_inactive_anon 0 nr_zone_active_anon 0 nr_zone_inactive_file 0 nr_zone_active_file 0 nr_zone_unevictable 0 nr_zone_write_pending 0 nr_mlock 0 nr_page_table_pages 0 nr_bounce 0 nr_free_cma 0 pagesets cpu: 0 count: 365 high: 378 batch: 63 vm stats threshold: 36 cpu: 1 count: 0 high: 378 batch: 63 ··· ··· vm stats threshold: 36 cpu: 5 count: 0 high: 378 batch: 63 vm stats threshold: 36 node_unreclaimable: 0 start_pfn: 4096 Node 0, zone Normal pages free 250616 min 8346 low 10432 high 12518 spanned 786432 present 786432 managed 751697 protection: (0, 0, 0, 0, 0) nr_free_pages 250616 nr_zone_inactive_anon 111525 nr_zone_active_anon 17352 nr_zone_inactive_file 211346 nr_zone_active_file 124539 nr_zone_unevictable 0 nr_zone_write_pending 1 nr_mlock 0 nr_page_table_pages 1166 nr_bounce 0 nr_free_cma 0 pagesets cpu: 0 count: 325 high: 378 batch: 63 vm stats threshold: 36 cpu: 1 count: 259 high: 378 batch: 63 ··· ··· vm stats threshold: 36 cpu: 5 count: 131 high: 378 batch: 63 vm stats threshold: 36 node_unreclaimable: 0 start_pfn: 1048576 Node 0, zone Movable pages free 0 min 0 low 0 high 0 spanned 0 present 0 managed 0 protection: (0, 0, 0, 0, 0) Node 0, zone Device pages free 0 min 0 low 0 high 0 spanned 0 present 0 managed 0 protection: (0, 0, 0, 0, 0)

上面提到了每个 Node 节点内包含了多个区域,并且页介绍了区域类型,下面就详细介绍下内存区域(zone)。

四、物理内存区域

4.1 区域结构

内存区域描述符 struct zone 定义如下:

struct zone { /* 几乎都是读操作的字段 */ /* 内存区域水位标记,通过 *_wmark_pages(zone) 宏访问 */ unsigned long _watermark[NR_WMARK]; unsigned long watermark_boost; unsigned long nr_reserved_highatomic; /* * 我们不知道要分配的内存是否可释放,或者最终是否会被释放,所以为了避免浪费几个GB的大量内存,我们必须保留一些较低内存区域的内存。 *(否则我们就会冒着在较低区域运行OOM的风险,尽管较高区域有大量可用的内存) * 此数组在运行时根据 sysctl_lowmem_reserve_ratio 系统控制参数的变化进行重新计算。 */ long lowmem_reserve[MAX_NR_ZONES]; #ifdef CONFIG_NUMA int node; #endif // 指向所在的 NUMA 节点 pglist_data struct pglist_data *zone_pgdat; struct per_cpu_pages __percpu *per_cpu_pageset; struct per_cpu_zonestat __percpu *per_cpu_zonestats; /* * the high and batch values are copied to individual pagesets for * faster access */ int pageset_high; int pageset_batch; #ifndef CONFIG_SPARSEMEM /* * 用于 pageblock_nr_pages 块的标志位。参见 pageblock-flags.h。 * 在 SPARSEMEM 中,此映射存储在 struct mem_section 中。 */ unsigned long *pageblock_flags; #endif /* CONFIG_SPARSEMEM */ /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */ unsigned long zone_start_pfn; atomic_long_t managed_pages; unsigned long spanned_pages; unsigned long present_pages; #if defined(CONFIG_MEMORY_HOTPLUG) unsigned long present_early_pages; #endif #ifdef CONFIG_CMA unsigned long cma_pages; #endif const char *name; #ifdef CONFIG_MEMORY_ISOLATION unsigned long nr_isolate_pageblock; #endif #ifdef CONFIG_MEMORY_HOTPLUG /* see spanned/present_pages for more description */ seqlock_t span_seqlock; #endif int initialized; /* 用于页面分配器的写密集型字段 */ ZONE_PADDING(_pad1_) /* 不同大小的空闲区域 */ struct free_area free_area[MAX_ORDER]; /* 区域标志,见下面的定义 */ unsigned long flags; /* 主要用于保护 free_area */ spinlock_t lock; /* 用于 compaction(压缩)和 vm 统计的写密集型字段 */ ZONE_PADDING(_pad2_) /* * 当空闲页低于此点时,在读取空闲页数时会采取额外步骤, * 以避免 per-cpu 计数器漂移,从而导致水位标记被突破 */ unsigned long percpu_drift_mark; #if defined CONFIG_COMPACTION || defined CONFIG_CMA /* compaction(压缩)空闲扫描器应该从哪个 pfn 开始 */ unsigned long compact_cached_free_pfn; /* compaction(压缩)迁移扫描器应该从哪个 pfn 开始 */ unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC]; unsigned long compact_init_migrate_pfn; unsigned long compact_init_free_pfn; #endif #ifdef CONFIG_COMPACTION /* * 在压缩失败时,会跳过 1<<compact_defer_shift 次压缩, * 然后再次尝试。自上次失败以来尝试的次数由 compact_considered 跟踪。 * compact_order_failed 是最小的压缩失败阶数。 */ unsigned int compact_considered; unsigned int compact_defer_shift; int compact_order_failed; #endif #if defined CONFIG_COMPACTION || defined CONFIG_CMA /* 当 PG_migrate_skip 位应该被清除时设置为 true */ bool compact_blockskip_flush; #endif bool contiguous; ZONE_PADDING(_pad3_) /* 区域统计数据 */ atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; atomic_long_t vm_numa_event[NR_VM_NUMA_EVENT_ITEMS]; } ____cacheline_internodealigned_in_smp;

介绍其中几个重要的字段:

  • zone_start_pfn:本区域(zone)的第一个物理页的 PFN,与节点struct pglist_data中的node_start_pfn相似。
  • spanned_pages:等于最后的页号减去起始的页号得到包含空洞页在内的所有页数。
  • present_pages:等于 spanned_pages 减去 absent_pages(pages in holes)得到不含空洞页的页数。
  • managed_pages:等于 present_pages 减去 reserved_pages 得到 本区域(zone)被伙伴系统管理的所有物理页的数量。
  • per_cpu_pageset:用于区分冷热页。被加载到 CPU 高速缓存里的物理页叫热页(Hot Page),没有加载的物理页叫冷页(Cold Page)。CPU 访问热页的速度非常快。每个 CPU 都有自己的高速缓存,所以每个 CPU 都有一份本区域(zone)的 per_cpu_pageset。
  • vm_stat:保存了该区域(zone)物理内存的使用统计信息,前边介绍的cat /proc/zoneinfo命令的输出数据就来源于这个 vm_stat。

由于内核中 struct zone 数量比较少,多个 CPU 同时读写器中的字段就会比较频繁,就会带来缓存失效,然后去内存读写数据,造成延时增加,也称伪共享。为了降低缓存失效的概率,使用 3 个ZONE_PADDINGstruct zone 的数据成员分割成 4 个部分,ZONE_PADDING作用就是占满一个缓存行(cache line),使其前后的数据放在不同的缓存行里面,避免造成缓存失效。《十年码农内存:缓存篇》有详细讲解,忘记的同学可以复习下。

/* * Add a wild amount of padding here to ensure data fall into separate * cachelines. There are very few zone structures in the machine, so space * consumption is not a concern here. */ #if defined(CONFIG_SMP) struct zone_padding { char x[0]; } ____cacheline_internodealigned_in_smp; #define ZONE_PADDING(name) struct zone_padding name; #else #define ZONE_PADDING(name) #endif

struct zonestruct zone_padding 结构体都使用了____cacheline_internodealigned_in_smp编译器关键字修饰,告知编译器这些结构体需要按照缓存行(cache line)对齐。

伪共享的问题解决了,但是多个CPU可能同时访问这一个 struct zonestruct zone中有一把 spinlock_t lock自旋锁来保证数据的同步,这把锁住要用于保护free_area[MAX_ORDER],这个数组是伙伴系统的核心数据结构,内核为每一个区域(zone)分配一个伙伴系统,用于管理该区域下物理内存的分配和释放。其中managed_pages是该区域被伙伴系统管理的物理页数。

4.2 预留内存

进程申请内存时,如果内存充裕,则立刻获得内存;如果内存紧张时,有以下两种情况:

  • 进程允许阻塞:内核将一部分不经常使用的物理内存回收,回收过程是需要时间的,期间进程被阻塞,回收完成后分配内存给进程;
  • 进程不能阻塞:因为内核核心操作申请内存是不允许失败的,所以每个区域(zone)都预留了一小部分内存供其申请。例如,中断处理函数无法被重新调度或者持有自旋锁时申请内存,进程就不允许睡眠。

nr_reserved_highatomic 是本区域的预留内存大小(128KB~65536KB)。

lowmem_reserve 数组是用于规定本区域为防止数组索引值对应类型的区域对本区域的侵占挤压,必须为本区域保留的物理页数量。

内存区域由低到高顺序为:ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_MOVABLE 和 ZONE_DEVICE,其实与 zone_type 枚举值定义的顺序是一致的。本文只讨论 64 位系统,所以没有 ZONE_HIGHMEM 区域。

因为一些特定的操作,例如 DMA 等,必须在 ZONE_DMA 或 ZONE_DMA32 区域等低位区域分配内存,但是通常可以在高位区域分配内存,那么也可以在低位区域分配,没有限制,如果高位区域内存不足时可以向低位区域寻找空闲内存,进而侵占挤压低位区域。

但是低位区域内存本来就小,并且内核更希望通常的内存在高位区域分配,特定的内存在低位区域分配,所以每个区域都预留一小部分内存,防止被高位区域侵占挤压。这个区域大小就保存在了 lowmem_reserve 数组中。

lowmem_reserve 数组中的值是根据每个区域大小和 lowmem_reserve_ratio 预留比例计算而来,可以通过下面两种命令查看每个区域预留比例:

[root@929533e6b321 /]# cat /proc/sys/vm/lowmem_reserve_ratio 256 256 32 0 0 [root@929533e6b321 /]# sysctl -a|grep lowmem_reserve_ratio vm.lowmem_reserve_ratio = 256 256 32 0 0

输出的结果是在我的 docker 容器中,有 5 个比例,分别对应下面输出的 5 个区域:

[root@929533e6b321 /]# cat /proc/zoneinfo | grep Node Node 0, zone DMA Node 0, zone DMA32 Node 0, zone Normal Node 0, zone Movable Node 0, zone Device

参与计算的是每个区域的managed_pages页数,就是被伙伴系统管理的物理页数,下面的命令输出了每个区域的managed_pages页数:

[root@929533e6b321 /]# cat /proc/zoneinfo|grep managed managed 3977 //DMA managed 765952 //DMA32 managed 751697 //Normal managed 0 //Movable managed 0 //Device

下面的命令可以查看每个区域保留物理内存页数,输出的protection就是保存在lowmem_reserve 数组的值:

[root@929533e6b321 /]# cat /proc/zoneinfo|grep protection protection: (0, 2992, 5928, 5928, 5928) //DMA protection: (0, 0, 2936, 2936, 2936) //DMA32 protection: (0, 0, 0, 0, 0) //Normal protection: (0, 0, 0, 0, 0) //Movable protection: (0, 0, 0, 0, 0) //Device

方便理解做了一个表格,以及后面提供了计算方法。

区域类型lowmem_reserve_ratiomanaged防ZONE_DMA防ZONE_DMA32防Normal防Movable防Device
ZONE_DMA256397702992592859285928
ZONE_DMA3225676595200293629362936
Normal3275169700000
Movable0000000
Device0000000
  • ZONE_DMA 为防止被 ZONE_DMA32 挤压侵占,而为自己预留的物理内存页为:433094 / 256 = 2992。
  • ZONE_DMA 为防止被 Normal 挤压侵占而为自己预留的物理内存页为:(765952 + 751697) / 256 = 5928。
  • ZONE_DMA 为防止被 Movable 挤压侵占而为自己预留的物理内存页为:(765952 + 751697) / 256 = 5928。
  • ZONE_DMA 为防止被 Device 挤压侵占而为自己预留的物理内存页为:(765952 + 751697) / 256 = 5928。
  • ZONE_DMA32 为防止被 Normal 挤压侵占而为自己预留的物理内存页为:(751697 + 0 + 0) / 256 = 2936。
  • ZONE_DMA32 为防止被 Movable 挤压侵占而为自己预留的物理内存页为:(751697 + 0 + 0) / 256 = 2936。
  • ZONE_DMA32 为防止被 Device 挤压侵占而为自己预留的物理内存页为:(751697 + 0 + 0) / 256 = 2936。

使用下面命令修改内核参数lowmem_reserve_ratio,内核根据lowmem_reserve_ratio动态重新计算预留区域大小:

$ sysctl -w vm.lowmem_reserve_ratio=256 256 32 0 0

上面是只有一个 Node 节点的情况,下面是有 Node 0 和 Node 1 两个节点的真实物理机的情况。

$ cat /proc/sys/vm/lowmem_reserve_ratio 256 256 32 $ sysctl -a|grep lowmem_reserve_ratio vm.lowmem_reserve_ratio = 256 256 32 $ cat /proc/zoneinfo | grep Node Node 0, zone DMA Node 0, zone DMA32 Node 0, zone Normal Node 1, zone Normal $ cat /proc/zoneinfo|grep managed managed 3957 //DMA managed 433090 //DMA32 managed 15987300 //Normal 0 managed 16511520 //Normal 1 $ cat /proc/zoneinfo|grep pro protection: (0, 1691, 64142, 64142) //DMA protection: (0, 0, 62450, 62450) //DMA32 protection: (0, 0, 0, 0) //Normal 0 protection: (0, 0, 0, 0) //Normal 1

区域类型lowmem_reserve_ratiomanaged防ZONE_DMA防ZONE_DMA32防Normal(0)防Normal(1)
ZONE_DMA2563957016916414264142
ZONE_DMA32256433090006245062450
Normal(0)32159873000000
Normal(1)32165115200000
  • ZONE_DMA 为防止被 ZONE_DMA32 挤压侵占,而为自己预留的物理内存页为:433094 / 256 = 1691。
  • ZONE_DMA 为防止被 Normal(0) 挤压侵占而为自己预留的物理内存页为:(433090 + 15987300) / 256 = 64142。
  • ZONE_DMA 为防止被 Normal(1) 挤压侵占而为自己预留的物理内存页为:(433090 + 15987300) / 256 = 64142。没错就是这样。
  • ZONE_DMA32 为防止被 Normal(0) 挤压侵占而为自己预留的物理内存页为:15987300 / 256 = 62450。
  • ZONE_DMA32 为防止被 Normal(1) 挤压侵占而为自己预留的物理内存页为:15987300 / 256 = 62450。没错就是这样。

4.3 水位线

内核为每个物理内存区域(zone)画了三条水位线:WMARK_MIN(页最小阈值), WMARK_LOW(页低阈值)和 WMARK_HIGH(页高阈值)。定义在 zone_watermarks 枚举中。

enum zone_watermarks { WMARK_MIN, WMARK_LOW, WMARK_HIGH, WMARK_PROMO, NR_WMARK };

struct zone 结构体中 _watermark[NR_WMARK] 存储了水位线的值,下标就是 zone_watermarks 枚举值,即水位线类型。struct zonewatermark_boost 字段表示基准水位线,通过动态改变该值来减少内存碎片对内存分配的影响。下面代码是获取水位线类型对应的水位线值的方法:

#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost) #define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost) #define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost) #define wmark_pages(z, i) (z->_watermark[i] + z->watermark_boost)

当前水位 = 空闲内存(free) - 预留内存(lowmem_reserve),水位处在不同的水位线时处理逻辑如下:

Linux系统启动之后,物理内存的布局是怎么样的?  第10张

图9

  • 当前水位处在WMARK_HIGH之上时,表示该内存区域的内存非常充足,分配内存毫无压力;
  • 当前水位处在WMARK_HIGHWMARK_LOW之间时,表示内存内存正常,可以满足内存分配;
  • 当前水位处在WMARK_LOWWMARK_MIN之间时,表示内存开始有点紧张了,没那么够用了,但是还可以进行内存分配,当分配完后,唤醒kswapd进程异步回收内存,直到内存回到正常水位之上,期间申请内存的进程不会被阻塞。
  • 当前水位处在WMARK_MIN之下时,表示内存已经紧缺了,不能再分配了,申请内存的进程被阻塞,直到内核直接回收内存完成后并为其分配完内存。

通过下命令来查看不同 NUMA 节点中不同内存区域中的水位线:

$ cat /proc/zoneinfo ··· Node 0, zone Normal pages free 329518 //空闲内存页数 min 8346 //_watermark[WMARK_MIN] low 10432 //_watermark[WMARK_LOW] high 12518 //_watermark[WMARK_HIGH] spanned 786432 present 786432 managed 751697 protection: (0, 0, 0, 0, 0) ···

WMARK_MINWMARK_LOWWMARK_HIGH 水位线都是通过内核参min_free_kbytes分别计算得到,使用sysctl可以动态设置这个参数,达到动态控制水位线的目的。

$ cat /proc/sys/vm/min_free_kbytes 67584

WMARK_MIN 计算规则比较复杂,它是通过 min_free_kbytes 计算而来,下面一步一步来拆解计算过程:

Linux系统启动之后,物理内存的布局是怎么样的?  第11张

其中 managed(DMA) + managed(DMA32) + managed(NORMAL) 表示低位内存区域的managed之和,也就该节点被伙伴系统管理的内存页总数,暂且称之为 nr_free_buffer_pages,所谓低位区域就是NORMAL及之下的区域。

重新计算新的 new_min_free_kbytes 如下:

Linux系统启动之后,物理内存的布局是怎么样的?  第11张

如果new_min_free_kbytes大于user_min_free_kbytes,那么更新min_free_kbytesnew_min_free_kbytes,并且调整其值处在 [123,262144] 区间,即小于 128 则等于 128,大于 262144 则等于 262144,不大不小则不变。这里的user_min_free_kbytes就是用户通过sysctl设置的内存参数/proc/sys/vm/min_free_kbytes的值。

到这里我们已经计算出 min_free_kbytes 的大小。根据每个区域容量大小比例,从min_free_kbytes划分每个区域的 WMARK_MIN 水位线,例如计算 ZONE_NORMAL 区域的 WMARK_MIN如下:

Linux系统启动之后,物理内存的布局是怎么样的?  第11张

有一个内核参数 watermark_scale_factor 用来调节水位线间的距离,避免 WMARK_MINWMARK_LOW 之间的距离过小,导致极端情况(例如短时间大量网络数据到来)直接同时打穿这两条水位线,给进程带来性能抖动。因为水位低于 WMARK_LOW 启用 kswapd 进程异步回收内存,不阻塞申请进程,低于 WMARK_MIN 内核直接回收(direct reclaim)内存,阻塞申请进程。

所以要尽量扩大 WMARK_MINWMARK_LOW 之间的距离,当极端情况发生时有一个缓冲的余地。可以通过 sysctl 来动态调整 watermark_scale_factor内核参数,重新计算水位线之间的间距。间距计算公式如下:

Linux系统启动之后,物理内存的布局是怎么样的?  第11张

通常 WMARK_MIN / 4 是比较大的那个,所以一般情况下 WMARK_HIGHWMARK_LOW 分别是 WMARK_MIN 的 1.5 倍和 1.25 倍:

Linux系统启动之后,物理内存的布局是怎么样的?  第15张

到此WMARK_MINWMARK_LOWWMARK_HIGH都计算出来了。

4.4 冷热页

加载到 CPU 缓存里的物理页叫热页(Hot Page),没有加载的物理页叫冷页(Cold Page)。因为每个 CPU 都有自己的缓存,所以内核为每个 CPU 分配一个本区域(zone)的struct per_cpu_pages结构体链表,热页放在列表的头部,冷页放在列表的尾部。注意,这里的 CPU 指的是所有 CPU,不仅仅是本 NUMA 节点内的 CPU,因为本区域的内存也可以被其他 NUMA 节点内的 CPU 访问。

struct zone { ··· struct per_cpu_pages __percpu *per_cpu_pageset; ··· } ____cacheline_internodealigned_in_smp;

__percpu:这是一个特殊的关键字,通常在 Linux 内核中使用,表示该数据结构是"per-CPU"(每个CPU)的,意味着每个 CPU 都有其自己的独立实例。这可以提高多 CPU 系统的性能,因为不同 CPU 上的数据不会互相干扰,减少了竞态条件和锁的使用。

struct per_cpu_pages是用于管理热页或冷页集合的结构体,定义如下:

/* Fields and list protected by pagesets local_lock in page_alloc.c */ struct per_cpu_pages { spinlock_t lock; /* Protects lists field */ int count; /* number of pages in the list */ int high; /* high watermark, emptying needed */ int batch; /* chunk size for buddy add/remove */ short free_factor; /* batch scaling factor during free */ #ifdef CONFIG_NUMA short expire; /* When 0, remote pagesets are drained */ #endif /* Lists of pages, one per migrate type stored on the pcp-lists */ struct list_head lists[NR_PCP_LISTS]; } ____cacheline_aligned_in_smp;

  • count:标识本集合包含的冷页或热页的数量;
  • high:表示内存页列表的高水位标记。当页数量超过高水位时,需要进行内存页的释放,从缓存中批量释放batch个页归还给内存区域的伙伴系统;
  • batch:用于伙伴系统(buddy system)添加或移除页面的批处理数量;
  • free_factor:表示在释放页面时的批处理规模缩放因子;
  • expire:当启用 NUMA 时,用于控制远程页集合的排空。当expire字段的值为 0 时,表示需要排空远程页集合;
  • list:这是一个数组,存储了不同类型的内存页面的列表。NR_PCP_LISTS是一个宏,表示不同迁移类型的数量。这些列表用于管理不同类型的内存页面,例如可回收页、高优先级页等。

五、物理内存页

5.1 页面结构

物理页是组成物理内存的基本单位,描述物理页的数据结构是struct page,大小在 40 字节左右。这个结构体里面有很多联合体(union),目的是使用更小的结构体大小来应对各种不同的使用场景,使struct page体积维持在一个较小的水平,因为这个结构体被很多地方使用,每增加一个字段可能会影响其他模块。例如,16GB 物理内存需要 160MB 存储对应的struct page,每增加一个字段,不考虑对齐的情况下就需要额外增加 4MB 的开销。struct page 定义如下(不同版本变动比较大):

struct page { /* 原子标志,一些可能会异步更新 */ unsigned long flags; /* * 这个联合中有五个字(20/40 字节)可用。 * 警告:第一个字的第 0 位用于 PageTail()。这意味着此联合的其他用户 * 必须避免使用该位,以避免冲突和错误的 PageTail()。 */ union { struct {/* 页面缓存(Page cache)和匿名页面(anonymous pages) */ /** * @lru: 页面退出列表,例如由 lruvec->lru_lock 保护的 active_list。 * 有时被页面所有者用作通用列表。 */ union { struct list_head lru; struct { /* 或者,对于不可驱逐的“LRU 列表”槽位 */ /* 始终为偶数,以取消 PageTail */ void *__filler; /* 计算 page/folio 的 mlock 数 */ unsigned int mlock_count; }; /* 或者,空闲页面 */ struct list_head buddy_list; struct list_head pcp_list; }; /* 有关 PAGE_MAPPING_FLAGS,请参见 page-flags.h */ struct address_space *mapping; pgoff_t index; /* 在映射中的偏移量。*/ /** * @private: 映射私有的不透明数据。 * 如果 PagePrivate,则通常用于 buffer_heads。 * 如果 PageSwapCache,则用于 swp_entry_t。 * 如果 PageBuddy,则表示在伙伴系统中的阶数。 */ unsigned long private; }; struct { /* 由 netstack 使用的 page_pool */ /*@pp_magic: 用于避免回收非 page_pool 分配的页面。*/ unsigned long pp_magic; struct page_pool *pp; unsigned long _pp_mapping_pad; unsigned long dma_addr; union { /* dma_addr_upper:在 32 位体系结构上可能需要 64 位值。*/ unsigned long dma_addr_upper; /* 用于 frag 页面支持,在带有 64 位 DMA 的 32 位体系结构上不支持。*/ atomic_long_t pp_frag_count; }; }; struct { /* 复合页面(compound page)的首页面 */ unsigned long compound_head; /* 第 0 位被置位 */ /* 仅对第一个尾部页面 */ unsigned char compound_dtor; unsigned char compound_order; atomic_t compound_mapcount; atomic_t compound_pincount; #ifdef CONFIG_64BIT unsigned int compound_nr; /* 1 << compound_order */ #endif }; struct { /* 复合页面(compound page)的尾页面 */ unsigned long _compound_pad_1; /* compound_head */ unsigned long _compound_pad_2; /* 用于 global 和 memcg */ struct list_head deferred_list; }; struct { /* 页表页 */ unsigned long _pt_pad_1; /* compound_head */ pgtable_t pmd_huge_pte; /* protected by page->ptl */ unsigned long _pt_pad_2; /* mapping */ union { struct mm_struct *pt_mm; /* x86 pgds only */ atomic_t pt_frag_refcount; /* powerpc */ }; #if ALLOC_SPLIT_PTLOCKS spinlock_t *ptl; #else spinlock_t ptl; #endif }; struct { /* ZONE_DEVICE pages */ /* pgmap: 指向托管设备页面映射。 */ struct dev_pagemap *pgmap; void *zone_device_data; /* * ZONE_DEVICE 私有页面被视为已映射的页面,因此下面的 3 个字 * 保存源匿名或页面缓存页面的映射、索引和私有字段, * 在页面迁移到设备私有内存时使用。 * ZONE_DEVICE MEMORY_DEVICE_FS_DAX 页面在映射 pmem 后备的 DAX 文件时也使用映射、索引和私有字段。 */ }; /* rcu_head: 可以使用此项通过 RCU 来释放页面。 */ struct rcu_head rcu_head; }; union { /* 此联合的大小为 4 字节。*/ /* 如果页面可以映射到用户空间,则编码了此页被页表引用的次数。*/ atomic_t _mapcount; /* * 如果页既不是 PageSlab 也不能映射到用户空间, * 则存储在此处的值可能有助于确定此页面的用途。 * 有关当前存储在此处的页面类型列表,请参见 page-flags.h。 */ unsigned int page_type; }; /* 使用计数。*请勿直接使用*。请参见 page_ref.h */ atomic_t _refcount; #ifdef CONFIG_MEMCG unsigned long memcg_data; #endif /* * 在将所有 RAM 映射到内核地址空间的机器上, * 我们可以简单地计算虚拟地址。在具有高内存的机器上, * 一些内存被动态地映射到内核虚拟内存中, * 因此我们需要一个地方来存储该地址。 * 请注意,这个字段在 x86 上可能是 16 位 ... ;) * * 在 asm/page.h 中定义了 WANT_PAGE_VIRTUAL * 的体系结构具有较慢的乘法。 */ #if defined(WANT_PAGE_VIRTUAL) void *virtual; /* 内核虚拟地址(如果未 kmapped,则为 NULL,即 highmem) */ #endif /* WANT_PAGE_VIRTUAL */ #ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS int _last_cpupid; #endif } _struct_page_alignment;

这个结构体在不同场景下使用不同的字段,字段的不同组合可以表示页缓存(Page cache)、匿名页(anonymous pages)、复合页(compound page)、页表页ZONE_DEVICE 页等。

5.2 页类型

5.2.1 匿名页

匿名页(Anonymous Page)用于存储进程运行过程中产生的临时数据,直接和进程虚拟地址空间建立映射存储在页表内,没有背靠一个硬盘文件作为数据来源。

struct pagemapping字段最低位为 1 表示匿名页。mapping指向该匿名页在进程虚拟内存空间中唯一对应的匿名映射区 struct anon_vma 结构体,用于物理内存到虚拟内存的反向映射。

虚拟内存到物理内存的映射称为正向映射,页表中的映射关系就是正向映射。那么反过来,物理内存到虚拟内存的映射就是反向映射,一个物理页可能映射到多个进程的虚拟地址空间中,是一对多的关系。有了上面的反向映射关系,当物理页回收或迁移时,内核可以直接找到该物理页映射到的所有进程的虚拟地址空间 VMA(struct vm_area_struct),然后从 VMA 使用的页表中取消映射关系。

当进程通过mallocnew等函数申请内存时,其实内核根本没有为其分配物理内存,而是为进程申请的这块内存创建初始化一段虚拟内存区域struct vm_area_struct结构体。当后面进程真正使用这块内存时会产生缺页中断,缺页中断函数才会分配真正的物理内存,并完成正向和反向映射,正向映射存在页表里,反向映射存在struct pagemapping中。struct page_mapcount字段表示该物理页映射到了多少个进程的虚拟内存空间中。

正向和反向映射过程在后面页中断章节中会详细介绍。

5.2.2 文件页

文件页(Page Cache)中的数据均来自硬盘文件,目的是降低读写硬盘的延时,文件页需要先关联一个硬盘文件,然后再和进程虚拟地址空间建立映射存储在页表内,进程通过操作虚拟内存实现对文件的操作,也称为内存映射文件(Memory-mapped File)。

struct pagemapping字段最低位为 0 表示文件页。mapping指向该文件页关联文件的struct address_space(被文件的 inode 所持有),pgoff_t index字段表示该文件页在struct address_space中的索引。内核会通过 index 字段从 struct address_space 中查找该文件页。

涉及文件系统,这里就不过多介绍了。

5.2.3 复合页

通常一个页大小为 4KB,但是有些特殊情况想要把多个连续的物理页组合成一个大页,也称为复合页(Compound Pages)。

下面是复合页和普通页的优势对比:

复合页的优势

  1. 减少内存碎片: 复合页将多个小页(通常是普通物理页)组合成一个大页,减少了内存碎片的发生。这可以提高内存的空间利用率,减少操作系统内存管理的复杂性。
  2. 减少页表开销: 使用复合页可以减少页表的大小和管理开销。因为一个复合页只需要一个页表项,而多个小页需要更多的页表项,这可以减少内存访问时的额外开销。例如父进程通过 fork 函数创建子进程是拷贝页表的开销小。
  3. 加速内存访问: 复合页可以提高内存访问速度,因为一个较大的页可以容纳更多的数据,减少了内存访问的次数。因为复合页占用的页表项较少,所以节约了 TLB 的空间,并且提升了 TLB 缓存命中率,从而加快访问速度。
  4. 更好的内存局部性: 复合页有助于提高内存局部性,因为它们通常包含相关的数据,减少了缓存失效的可能性,从而提高了程序的性能。

普通页的优势

  1. 灵活性: 普通物理页更加灵活,因为它们的大小通常比复合页小。这使得操作系统能够更好地适应不同大小的内存分配请求。
  2. 更好的内存分配粒度: 普通物理页可以更好地满足某些内存分配需求,特别是当应用程序需要较小的内存块时,使用普通物理页更为合适。
  3. 更好的隔离性: 普通物理页可以更好地实现内存的隔离,因为每个页都是独立的。这有助于防止一个应用程序的错误影响其他应用程序的内存。

前面提到复合页本质上是由多个连续的普通页拼接而成,第一个物理页称为首页(Head Page),其余的物理页均称为尾页(Tail Page)。先介绍首页然后再介绍尾页。

首页的 page 结构体中 flags 字段中 PG_head 位会被置成 1,表示该页是复合页的首页。首页存储着关于复合页的一些额外信息,下面是首页使用的结构体:

struct { /* 复合页面(compound page)的第一个页面 */ unsigned long compound_head; /* 第 0 位被置位 */ unsigned char compound_dtor; unsigned char compound_order; atomic_t compound_mapcount; atomic_t compound_pincount; #ifdef CONFIG_64BIT unsigned int compound_nr; /* 1 << compound_order */ #endif };

  • compound_head:用于存储复合页面的头部信息,第 0 位被置位。
  • compound_dtor:用于释放复合页的析构函数。
  • compound_order:复合页的分配阶 order,2 的几次幂。
  • compound_mapcount:复合页的反向映射个数,表示该复合页被映射到多少个页表中。
  • compound_pincount:复合页的引用计数。

所有尾页使用的结构体如下:

struct { /* 复合页面(compound page)的尾页面 */ unsigned long _compound_pad_1; /* compound_head */ unsigned long _compound_pad_2; /* 用于 global 和 memcg */ struct list_head deferred_list; };

  • _compound_pad_1:指向首页的 compound_head,所有尾页通过它与首页关联起来;
  • _compound_pad_2:用于填充的无符号长整型字段;
  • deferred_list:这是一个链表头结构体,用于管理全局和内存控制组(memcg)中的复合页面的延迟操作列表。链表通常用于管理一系列相关的数据结构。

Linux系统启动之后,物理内存的布局是怎么样的?  第16张

图10

5.2.4 网络页

网络收包时需要申请内存页来储存接收到的网络数据,然后做好 DMA 映射,网卡通过DMA 将数据写入网络页中。

struct { /* 由 netstack 使用的 page_pool */ /*@pp_magic: 用于避免回收非 page_pool 分配的页面。*/ unsigned long pp_magic; struct page_pool *pp; unsigned long _pp_mapping_pad; unsigned long dma_addr; union { /* dma_addr_upper:在 32 位体系结构上可能需要 64 位值。*/ unsigned long dma_addr_upper; /* 用于 frag 页面支持,在带有 64 位 DMA 的 32 位体系结构上不支持。*/ atomic_long_t pp_frag_count; }; };

5.2.5 页表页

页表也是有体积的,也需要真实物理内存来存储,那么存储页表结构的物理页称为页表页,一张页表会占满一整个页表页。

struct { /* 页表页 */ unsigned long _pt_pad_1; /* compound_head */ pgtable_t pmd_huge_pte; /* protected by page->ptl */ unsigned long _pt_pad_2; /* mapping */ union { struct mm_struct *pt_mm; /* x86 pgds only */ atomic_t pt_frag_refcount; /* powerpc */ }; #if ALLOC_SPLIT_PTLOCKS spinlock_t *ptl; #else spinlock_t ptl; #endif };

5.3 页标志

struct pageflags虽然只有 64 位,但是其包含了很多逻辑,每个 bit 在不同场景下含义可能发生变化,里面不仅包含了很多标志位,还根据不同内存模型和内核参数包含了 section、node id 和 zone不同的组合形式,在 include\linux\page-flags-layout.h 文件中描述了其主要 5 种形式。

5.3.1 五种形式

Linux系统启动之后,物理内存的布局是怎么样的?  第17张

图11

每种形式中都有ZONE,其长度是变长的,根据系统中区域类型的数量而定,取值由 0 到 3。代码如下:

#if MAX_NR_ZONES < 2 #define ZONES_SHIFT 0 #elif MAX_NR_ZONES <= 2 #define ZONES_SHIFT 1 #elif MAX_NR_ZONES <= 4 #define ZONES_SHIFT 2 #elif MAX_NR_ZONES <= 8 #define ZONES_SHIFT 3 #else #error ZONES_SHIFT "Too many zones conpd" #endif #define ZONES_WIDTH ZONES_SHIFT // 通常为 3

每种形式中也都有KASAN,用于内存监测。当开启了 CONFIG_KASAN_SW_TAGSCONFIG_KASAN_HW_TAGS 选项,那么KASAN为 8 位,否则 0 位。

#if defined(CONFIG_KASAN_SW_TAGS) || defined(CONFIG_KASAN_HW_TAGS) #define KASAN_TAG_WIDTH 8 #else #define KASAN_TAG_WIDTH 0 #endif

KASAN(Kernel Address Sanitizer)是一种用于检测操作系统内核中的内存错误的工具。具体来说,KASAN旨在帮助发现和修复内核代码中的内存访问问题,如缓冲区溢出、使用未初始化的内存、释放后再次访问内存等。KASAN是 Linux 内核中的一个重要工具,它有助于提高内核代码的稳定性和安全性。

KASAN的工作原理是在内存分配和释放操作中,为每个分配的内存块添加特殊的标签影子内存。这些标签与实际数据存储在一起,并用于跟踪内存访问。当内核代码尝试访问分配的内存时,KASAN会检查相应的标签,以查看是否存在任何错误或违规访问。如果发现问题,KASAN将生成相应的错误报告,帮助开发人员找到和修复问题。

除了第 5 种极端形式没有 NODE,其他 4 种都有 NODE,其长度可配置,x86_64 系统默认为 6 位,取值范围 1~10 位,如果 ZONES_WIDTH + SECTIONS_WIDTH + NODES_SHIFT <= BITS_PER_LONG - NR_PAGEFLAGS,那么 NODE 为 0 位,代码如下:

config NODES_SHIFT int "Maximum NUMA Nodes (as a power of 2)" if !MAXSMP range 1 10 default "10" if MAXSMP default "6" if X86_64 default "3" depends on NUMA help Specify the maximum number of NUMA Nodes available on the target system. Increases memory reserved to accommodate various tables. #ifdef CONFIG_NODES_SHIFT #define NODES_SHIFT CONFIG_NODES_SHIFT // default "6" if X86_64 #else #define NODES_SHIFT 0 #endif #if ZONES_WIDTH + SECTIONS_WIDTH + NODES_SHIFT <= BITS_PER_LONG - NR_PAGEFLAGS #define NODES_WIDTH NODES_SHIFT // default "6" if X86_64 #elif defined(CONFIG_SPARSEMEM_VMEMMAP) #error "Vmemmap: No space for nodes field in page flags" #else #define NODES_WIDTH 0 #endif

1、非sparse稀疏内存模型或sparse vmemmap的稀疏内存模型

Linux系统启动之后,物理内存的布局是怎么样的?  第18张

图12

NODE 在 NUMA 架构中表示该 page 所属的 Node 节点的 id,如果是非 NUMA 系统则为 0,ZONE 表示该 page 所属的内存区域(zone)。KASAN用于内存监测,低位为众多 FLAGS 标志位,中间剩余部分为保留位。

2、在 1 基础上开启 LAST_CPUPID

Linux系统启动之后,物理内存的布局是怎么样的?  第19张

图13

#ifdef CONFIG_NUMA_BALANCING #define LAST__PID_SHIFT 8 #define LAST__PID_MASK ((1 << LAST__PID_SHIFT)-1) #define LAST__CPU_SHIFT NR_CPUS_BITS #define LAST__CPU_MASK ((1 << LAST__CPU_SHIFT)-1) #define LAST_CPUPID_SHIFT (LAST__PID_SHIFT+LAST__CPU_SHIFT) #else #define LAST_CPUPID_SHIFT 0 #endif #if ZONES_WIDTH + SECTIONS_WIDTH + NODES_WIDTH + KASAN_TAG_WIDTH + LAST_CPUPID_SHIFT \ <= BITS_PER_LONG - NR_PAGEFLAGS #define LAST_CPUPID_WIDTH LAST_CPUPID_SHIFT #else #define LAST_CPUPID_WIDTH 0 #endif

在 1 基础上增加了 LAST_CPUPID 字段,表示上一次访问的 CPU 和 PID。如果其他字段太长,就关闭 LAST_CPUPID 字段。

3、非sparse vmemmap的稀疏内存模型

Linux系统启动之后,物理内存的布局是怎么样的?  第20张

图14

#ifdef CONFIG_SPARSEMEM #include <asm/sparsemem.h> #define SECTIONS_SHIFT (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS) //46 - 27 = 19 #else #define SECTIONS_SHIFT 0 #endif #if defined(CONFIG_SPARSEMEM) && !defined(CONFIG_SPARSEMEM_VMEMMAP) #define SECTIONS_WIDTH SECTIONS_SHIFT // 19 #else #define SECTIONS_WIDTH 0 #endif

增加 SECTION 字段表示该 page 所在的 mem_section 段。前面介绍过的page_to_section函数就是通过 page 中的 flags 获取段号的:

static inline unsigned long page_to_section(const struct page *page) { return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK; }

4、在 3 基础上开启 LAST_CPUPID

Linux系统启动之后,物理内存的布局是怎么样的?  第21张

图15

在 3 基础上增加了 LAST_CPUPID 字段,表示上一次访问的 CPU 和 PID。

5、稀疏内存模型不支持 NUMA

Linux系统启动之后,物理内存的布局是怎么样的?  第22张

图16

在 4 的基础上去掉了 NODE。

5.3.2 低位标志

/* * Don't use the pageflags directly. Use the PageFoo macros. * * The page flags field is split into two parts, the main flags area * which extends from the low bits upwards, and the fields area which * extends from the high bits downwards. * * | FIELD | ... | FLAGS | * N-1 ^ 0 * (NR_PAGEFLAGS) * * The fields area is reserved for fields mapping zone, node (for NUMA) and * SPARSEMEM section (for variants of SPARSEMEM that require section ids like * SPARSEMEM_EXTREME with !SPARSEMEM_VMEMMAP). */ enum pageflags { PG_locked, /* Page is locked. Don't touch. */ PG_referenced, PG_uptodate, PG_dirty, PG_lru, PG_active, PG_workingset, PG_waiters, /* Page has waiters, check its waitqueue. Must be bit #7 and in the same byte as "PG_locked" */ PG_error, PG_slab, PG_owner_priv_1, /* Owner use. If pagecache, fs may use*/ PG_arch_1, PG_reserved, PG_private, /* If pagecache, has fs-private data */ PG_private_2, /* If pagecache, has fs aux data */ PG_writeback, /* Page is under writeback */ PG_head, /* A head page */ PG_mappedtodisk, /* Has blocks allocated on-disk */ PG_reclaim, /* To be reclaimed asap */ PG_swapbacked, /* Page is backed by RAM/swap */ PG_unevictable, /* Page is "unevictable" */ #ifdef CONFIG_MMU PG_mlocked, /* Page is vma mlocked */ #endif #ifdef CONFIG_ARCH_USES_PG_UNCACHED PG_uncached, /* Page has been mapped as uncached */ #endif #ifdef CONFIG_MEMORY_FAILURE PG_hwpoison, /* hardware poisoned page. Don't touch */ #endif #if defined(CONFIG_PAGE_IDLE_FLAG) && defined(CONFIG_64BIT) PG_young, PG_idle, #endif #ifdef CONFIG_ARCH_USES_PG_ARCH_X PG_arch_2, PG_arch_3, #endif __NR_PAGEFLAGS, PG_readahead = PG_reclaim, PG_anon_exclusive = PG_mappedtodisk, /* Filesystems */ PG_checked = PG_owner_priv_1, /* SwapBacked */ PG_swapcache = PG_owner_priv_1, /* Swap page: swp_entry_t in private */ PG_fscache = PG_private_2, /* page backed by cache */ /* XEN */ /* Pinned in Xen as a read-only pagetable page. */ PG_pinned = PG_owner_priv_1, /* Pinned as part of domain save (see xen_mm_pin_all()). */ PG_savepinned = PG_dirty, /* Has a grant mapping of another (foreign) domain's page. */ PG_foreign = PG_owner_priv_1, /* Remapped by swiotlb-xen. */ PG_xen_remapped = PG_owner_priv_1, #ifdef CONFIG_MEMORY_FAILURE PG_has_hwpoisoned = PG_error, #endif /* non-lru isolated movable page */ PG_isolated = PG_reclaim, /* Only valid for buddy pages. Used to track pages that are reported */ PG_reported = PG_uptodate, #ifdef CONFIG_MEMORY_HOTPLUG /* For self-hosted memmap pages */ PG_vmemmap_self_hosted = PG_owner_priv_1, #endif }; #define PAGEFLAGS_MASK ((1UL << NR_PAGEFLAGS) - 1)

下面表格对上面代码中的字段一一做了解释:

标志位说明
PG_locked页面已锁定,不可被访问。通常表明有进程在进行硬盘 I/O 操作。
PG_referenced表示该页面刚刚被访问过,用于页面回收。
PG_uptodate页面的数据已经是最新的,无需更新。
PG_dirty页面的数据已被修改,需要写回到磁盘。
PG_lru页面在 LRU(Least Recently Used,最近最少使用)链表中。
PG_active表示该页在 active 链表上。PG_referenced 和 PG_active 共同控制了该页的活跃程度,在内存回收提供重要依据。
PG_workingset用于工作集管理,与页面活动性有关。
PG_waiters页面有等待者,检查等待队列。
PG_error页面发生了I/O错误。
PG_slab表示该页属于 slab 分配器,用于内核对象分配。
PG_owner_priv_1属于所有者使用的私有标志1。具体用途由所有者定义。
PG_arch_1架构特定的页面状态位1。
PG_reserved页面已保留,通常用于特殊页面,如内核映像、BIOS等。
PG_private如果是页缓存,表示该 struct page 的 private 指向了具体的对象。
PG_private_2如果是页缓存,包含文件系统辅助数据。
PG_writeback表示该页正在被内核的 pdflush 线程回写到硬盘中。
PG_head作为复合页面(compound page)的头部。
PG_mappedtodisk页面在磁盘上有分配的块。
PG_reclaim表示该页已经被内核选中即将被回收。
PG_swapbacked页面使用交换空间作为后备存储。
PG_unevictable页面是 "unevictable",不会被换出。
PG_mlocked表示该页被进程通过 mlock 系统调用锁定在 VMA(虚拟内存区域),不会被换出。
PG_uncached页面已映射为无缓存。
PG_hwpoison页面被硬件损坏,不安全访问。
PG_young页面被访问过。
PG_idle页面处于空闲状态。
PG_arch_2架构特定的页面状态位2。
PG_arch_3架构特定的页面状态位3。
__NR_PAGEFLAGS页面标志的总数。
PG_readahead当进程顺序访问文件时,内核会预读若干相邻文件页数据到物理页中,该位表示该页是一个正在被内核预读的页。
PG_anon_exclusive用于匿名页面,表示页面是独占的。
PG_checked用于文件系统,表示页面已经被检查。
PG_swapcache用于交换空间,表示该物理内存页处于 swap cache 中。 struct page 的 private 指向 swap_entry_t 。
PG_fscache用于文件系统缓存,表示页面由缓存支持。
PG_pinned用于Xen虚拟化,表示页面被锁定为只读页表页。
PG_savepinned用于Xen虚拟化,表示页面在域保存期间被锁定。
PG_foreign用于Xen虚拟化,表示页面有另一个(外部)域的授权映射。
PG_xen_remapped用于Xen虚拟化,表示页面已被swiotlb-xen重新映射。
PG_has_hwpoisoned用于复合页面,表示至少有一个子页面在THP中被硬件污染。
PG_isolated用于非LRU孤立可移动页面。
PG_reported仅对伙伴页面有效,用于跟踪已报告的页面。
PG_vmemmap_self_hosted用于自托管的memmap页面。

5.4 页回收

内核中有 5 个链表,分别是匿名页的 active/inactive 链表、文件页的 active/inactive 链表和 mlock 链表,管理物理页的活跃度,用于页面回收。

$ cat /proc/zoneinfo ··· Node 0, zone Normal pages free 329518 min 8346 low 10432 high 12518 spanned 786432 present 786432 managed 751697 protection: (0, 0, 0, 0, 0) nr_free_pages 329518 nr_zone_inactive_anon 109932 //匿名页的 inactive 链表的长度 nr_zone_active_anon 17338 //匿名页的 active 链表的长度 nr_zone_inactive_file 141904 //文件页的 inactive 链表的长度 nr_zone_active_file 119306 //文件页的 active 链表的长度 nr_mlock 0 // mlock 链表的长度 ···

active 类型链表保存访问频繁的物理页(热页),inactive 类型链表保存访问不频繁的物理页(冷页),mlock 链表保存进程通过 mlock() 系统调用锁定在内存空间里物理页,不能被置换出去,比如为了安全或者性能考量,物理页存储敏感信息不可以置换到硬盘造成泄密,或者已知频繁访问的物理页常驻内存提升访问性能。

stuct pagestruct list_head lru字段表示该物理页挂在上面 5 个链表中的唯一一个链表上,通过源码发现只有匿名页和文件页结构体里面有lru字段,也就只有它们涉及挂在链表上的回收形式,其他类型页有自己的回收形式。stuct pageatomic_t _refcount字段表示该物理页被引用的次数,值越大表示该物理页越活跃。

文件页和匿名页在链表中的行为略有不同:

  • 文件页第一次被访问时会被挂在 inactive 链表的头部;
    • 如果它继续被访问,则会被提升至 active 链表的尾部;
    • 如果它没有被访问,则随着别的文件页加入头部,它会被推到 inactive 链表尾部,如果再次被访问,则会直接被提升到 active 链表的头部;
  • 匿名页第一次被访问时会被挂在 active 链表的尾部,因为匿名页换出成本高;
    • 如果它再次被访问,则会被提升至 active 链表的头部;

当内存紧张时,内核先从 active 链表的尾部开始扫描,将一些不活跃的物理页降级挂到 inactive 链表头部,然后回收 inactive 链表尾部的物理页。

这里的回收类型,对文件页和匿名页来说是不同的:

  • 文件页是把脏数据写回硬盘,然后回收物理页,没有脏数据直接回收;
  • 匿名页回收逻辑是将物理页换出(Swap)到硬盘,然后回收物理页。

内核引入swappiness参数来控制页面置换 Swap 的积极程度,swappiness 取值范围为 0 到 100,默认为 60,通过下面命令查看:

[root@929533e6b321 /]# cat /proc/sys/vm/swappiness 60

  • swappiness数值越大,Swap 的积极程度越高,越倾向回收匿名页;
  • swappiness数值越小,Swap 的积极程度越低,越倾向回收文件页,因为不倾向回收匿名页,只能回收文件页;

当内存压力非常大时,即使swappiness设置为 0,也还会发生 Swap。

可以通过下面命令动态修改swappiness

sysctl -w vm.swappiness=80

六、常用命令

6.1 物理内存布局

物理内存布局每个真实的物理机器输出的结果略有不同,下面是在我的 docker 容器中执行的结果。

[root@929533e6b321 test]# cat /proc/iomem 00000000-7fffffffff : PCI Bus 0000:00 // 512GB 00000000-00000fff : Reserved // 4KB 00001000-0009fbff : System RAM // 635KB 000f0000-000fffff : System ROM // 64KB 00100000-bfffffff : System RAM // 2.99GB c0000000-c0001fff : 0000:00:01.0 // 8KB c0002000-c0003fff : 0000:00:02.0 // 8KB c0004000-c0005fff : 0000:00:03.0 // 8KB c0006000-c0007fff : 0000:00:04.0 // 8KB c0008000-c00087ff : 0000:00:01.0 // 2KB c0008800-c0008fff : 0000:00:02.0 // 2KB c0009000-c00097ff : 0000:00:03.0 // 2KB c0009800-c0009fff : 0000:00:04.0 // 2KB c000a000-c000a7ff : 0000:00:1f.0 // 2KB e0000000-efffffff : PCI MMCONFIG 0000 [bus 00-ff] // 256MB e0000000-efffffff : pnp 00:00 // 256MB fec00000-fec003ff : IOAPIC 0 // 1KB fed00000-fed003ff : HPET 0 // 1KB fed00000-fed003ff : PNP0103:00 // 1KB fee00000-fee00fff : Local APIC // 4KB 100000000-1bfffffff : System RAM // 3GB 11a000000-11ae03316 : Kernel code // 14.01MB 11b000000-11b36bfff : Kernel rodata // 3.42MB 11b400000-11b641cbf : Kernel data // 144.45KB 11bb2c000-11bdfffff : Kernel bss // 2.83MB

下图是对上面输出结果的形象化,有一个 PCI Bus 总线,编号是 000:00,寻址范围 512GB,但是我的容器没有那么大真实物理内存,只有 7GB,其他的地址没有用上。内核代码段只占了 28MB,与机器、内核版本有关,这里的地址都不是绝对的,我是了多个容器,发现内核代码段可能处在不同的位置。提供这个示例的目的是对物理内存布局有一个直观的认识。

Linux系统启动之后,物理内存的布局是怎么样的?  第23张

图17

6.2 进程页表项

/proc/[pid]/pagemap 文件包含了与指定进程的虚拟内存页面映射相关的信息。每个条目都是一个页表项,对应一个物理页,其中包含有关该页面的信息,如物理页框号等。

/proc/[pid]/pagemap是二进制文件,直接使用cat命令输出会显示乱码,下面提供了三个脚本用来解析二进制内容,一个是 shell 脚本,另外两个是不同 Python 版本的脚本。

  • Shell 脚本

#!/bin/bash if [ "$#" -ne 1 ]; then echo "用法: $0 <PID>" exit 1 fi pid="$1" # Read the pagemap file in binary and convert to hex using xxd xxd -p -c 8 -g 8 "/proc/$pid/pagemap" | while read entry; do echo "Page Entry: 0x$entry" done

  1. 创建 read_pagemap.sh 文件,然后将上面的代码粘贴到里面;
  2. 执行 sh read_pagemap.sh pid 命令即可看到输出结果。
  • Python 3.x 版本

#!/usr/bin/env python3 import os import sys def read_pagemap(pid): with open("/proc/{}/pagemap".format(pid), "rb") as f: while True: data = f.read(8) if not data: break entry = int(data.hex(), 16) print("Page Entry: 0x{:016x}".format(entry)) if __name__ == "__main__": pid = int(sys.argv[1]) # 获取命令行参数作为PID read_pagemap(pid)

  • Python 2.7 版本

#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys def read_pagemap(pid): with open("/proc/{}/pagemap".format(pid), "rb") as f: while True: data = f.read(8) if not data: break entry = int(data.encode('hex'), 16) print("Page Entry: 0x{:016x}".format(entry)) if __name__ == "__main__": pid = int(sys.argv[1]) # 获取命令行参数作为PID read_pagemap(pid)

  1. 创建 read_pagemap.py 文件,然后将 2.x 或 3.x 版本的代码粘贴到里面;
  2. 执行 python read_pagemap.py pid 命令即可看到输出结果。

上面三个脚本输出结果中可能输出很多 Page Entry: 0x0000000000000000,可以通过 grep -v 0x0000000000000000对结果过滤,输出结果示例如下:

[root@929533e6b321 test]# sh read_pagemap.sh 123 |grep -v 0x0000000000000000 Page Entry: 0x74ce1600000000a1 Page Entry: 0x7a59170000000081 Page Entry: 0x0156170000000081 Page Entry: 0x2462170000000081

每一行输出都是一条页表项,每一项具体含义在后面的页表章节中会详细解释。

6.3 内核页面计数

cat /proc/kpagecount 6

需要 root 权限,用于跟踪内核页面(kernel pages)的计数,以便监视和诊断内核内存的使用情况。监视/proc/kpagecount可以帮助系统管理员或开发人员了解内核内存的使用情况,识别内核内存泄漏或者其他与内核内存相关的问题。

6.4 内核页面状态

$ cat /proc/kpageflags

同样需要 root 权限,用于提供有关内核页面(kernel pages)的标志信息。这些标志用于描述内核页面的不同属性和状态,就是 1.5.2.2 中的一系列标志,包括页面是否被使用、是否脏(已被修改但未写回磁盘)等。返回值是乱码,需要工具来查看。

6.5 内核页控制组

$ cat /proc/kpagecgroup

同样需要 root 权限,用于提供关于内核页面(kernel pages)和内存控制组(cgroup)之间的关联信息。内存控制组是Linux内核中的一种机制,用于限制和管理进程或任务组的内存使用。返回值是乱码,需要工具来查看。

七、内存硬件

内存硬件的正式名字叫随机访问存储器(RAM,Random Access Memory)。RAM 分为两类:

  • 静态 RAM,也称 SRAM,主要用于 CPU 内的高速缓存 L1/L2/L3 Cache。优点是访问速度快(1~30 个时钟周期,L1:1~3,L2:4~10,L3:10~30),缺点是容量小价格高;
  • 动态 RAM,也称 DRAM,主要用于主存,也就是常说的内存条,优点是容量大价格低,缺点是访问速度通常在几十个时钟周期以上,甚至更多。

Linux系统启动之后,物理内存的布局是怎么样的?  第7张

图18

缓存部分在 CPU 内部,不是在本节讨论范围内,缓存相关的知识已经在《十年码农内功:缓存篇》讲解过了,下面主要关注主存,也就是狭义上的内存。

一个 DDR5 内存条正反面上一共有 16 个 DRAM 芯片,双通道,每个通道 8 个 DRAM。

Linux系统启动之后,物理内存的布局是怎么样的?  第25张

图19(来源网络)

Linux系统启动之后,物理内存的布局是怎么样的?  第26张

图20(来源网络)

每个 DRAM 内部是一个二维矩阵,通过行地址(RAS,row access strobe)和列地址(CAS,column access strobe)定位一个 8 bits 数据,也就是每个 DRAM 一次读写 8 bits 数据,一个通道一次就可以读写 64 bits 数据。

Linux系统启动之后,物理内存的布局是怎么样的?  第27张

图21

八、参考

  • [一步一图带你深入理解 Linux 虚拟内存管理](mp.weixin.qq.com/s/uWad)
  • [一步一图带你深入理解 Linux 物理内存管理](mp.weixin.qq.com/s/Cn-o)
  • [23 | 物理内存管理(上):会议室管理员如何分配会议室?](time.geekbang.org/colum)
  • [24 | 物理内存管理(下):会议室管理员如何分配会议室?](time.geekbang.org/colum)
  • [17 | NUMA:非均匀访存带来了哪些提升与挑战?](time.geekbang.org/colum)
  • [Linux内存管理:SPARSEMEM模型](zhuanlan.zhihu.com/p/22)
  • [Linux内核API page_address](deepinout.com/linux-ker)
  • [利用/proc/pid/pagemap将虚拟地址转换为物理地址](cnblogs.com/pengdonglin)
  • [【计算机体系结构】NUMA架构详解](houmin.cc/posts/b893097)
  • [Linux NUMA Optimization - 1](oliveryang.net/2016/02/)
  • [快速通道互联](zh.wikipedia.org/wiki/%)
  • [linux内核那些事之struct page](zhuanlan.zhihu.com/p/57)
  • [Linux内存管理:SPARSEMEM模型](zhuanlan.zhihu.com/p/22)
  • [RISC-V Linux SPARSEMEM 介绍与分析](tinylab.org/riscv-spars)
  • [物理内存模型](kernel.org/doc/html/lat)
  • [从pfn_to_page/page_to_pfn看linux SPARSEMEM内存模型](cnblogs.com/liuhailong0)
  • [sparse内存模型之__pfn_to_page分析](zhuanlan.zhihu.com/p/59)

创作不易,您的 点赞、分享、收藏 是我创作的最大动力!

关注微信公众号(科英)、知乎(科英)获取最新技术文章。

联系方式:微信公众号(科英)
END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

欢迎 发表评论:

请填写验证码