英语

物理内存

Linux 可用于各种架构,因此需要一个与架构无关的抽象来表示物理内存。本章介绍用于管理运行系统中物理内存的结构。

内存管理中首要的概念是非统一内存访问 (NUMA)。对于多核和多插槽机器,内存可能被组织成多个内存库,根据与处理器的“距离”,访问这些内存库会产生不同的成本。例如,可能会有一个分配给每个 CPU 的内存库,或者一个非常适合外围设备附近 DMA 的内存库。

每个内存库都称为一个节点,在 Linux 中,即使架构是 UMA,也由 struct pglist_data 表示这个概念。这个结构总是通过其 typedef pg_data_t 引用。特定节点的 pg_data_t 结构可以通过 NODE_DATA(nid) 宏来引用,其中 nid 是该节点的 ID。

对于 NUMA 架构,节点结构由架构特定的代码在启动早期分配。通常,这些结构在它们所代表的内存库上本地分配。对于 UMA 架构,只使用一个静态的 pg_data_t 结构,称为 contig_page_data。节点将在后面的 节点 部分进一步讨论

整个物理地址空间被划分为一个或多个称为区域的块,这些区域表示内存中的范围。这些范围通常由访问物理内存的架构约束决定。节点内与特定区域对应的内存范围由 struct zone 描述,该结构被 typedef 为 zone_t。每个区域都有以下类型之一。

  • ZONE_DMAZONE_DMA32 在历史上表示适合外围设备 DMA 的内存,这些设备无法访问所有可寻址的内存。多年来,有更好的更强大的接口来获取具有 DMA 特定要求的内存(使用通用设备的动态 DMA 映射),但 ZONE_DMAZONE_DMA32 仍然表示对其访问方式有限制的内存范围。根据架构,可以使用 CONFIG_ZONE_DMACONFIG_ZONE_DMA32 配置选项在构建时禁用这些区域类型中的任何一个甚至两者。一些 64 位平台可能需要这两个区域,因为它们支持具有不同 DMA 寻址限制的外围设备。

  • ZONE_NORMAL 用于内核始终可以访问的普通内存。如果 DMA 设备支持传输到所有可寻址内存,则可以在此区域的页面上执行 DMA 操作。ZONE_NORMAL 始终启用。

  • ZONE_HIGHMEM 是物理内存的一部分,内核页表中没有永久映射覆盖。内核只能使用临时映射访问此区域中的内存。此区域仅在某些 32 位架构上可用,并且通过 CONFIG_HIGHMEM 启用。

  • ZONE_MOVABLE 用于普通的、可访问的内存,就像 ZONE_NORMAL 一样。区别在于 ZONE_MOVABLE 中大多数页面的内容是可移动的。这意味着,虽然这些页面的虚拟地址不变,但它们的内容可能会在不同的物理页面之间移动。通常,ZONE_MOVABLE 在内存热插拔期间填充,但也可能在启动时使用 kernelcoremovablecoremovable_node 内核命令行参数之一来填充。有关更多详细信息,请参阅 页面迁移内存热插拔

  • ZONE_DEVICE 表示驻留在 PMEM 和 GPU 等设备上的内存。它具有与 RAM 区域类型不同的特性,它的存在是为了为设备驱动程序识别的物理地址范围提供 struct page 和内存映射服务。ZONE_DEVICE 通过配置选项 CONFIG_ZONE_DEVICE 启用。

请务必注意,许多内核操作只能使用 ZONE_NORMAL 进行,因此它是性能最关键的区域。区域将在后面的 区域 部分进一步讨论。

节点和区域范围之间的关系取决于固件报告的物理内存映射、内存寻址的架构约束以及内核命令行中的某些参数。

例如,在具有 2 GB RAM 的 x86 UMA 机器上使用 32 位内核,整个内存将在节点 0 上,并且将有三个区域:ZONE_DMAZONE_NORMALZONE_HIGHMEM

0                                                            2G
+-------------------------------------------------------------+
|                            node 0                           |
+-------------------------------------------------------------+

0         16M                    896M                        2G
+----------+-----------------------+--------------------------+
| ZONE_DMA |      ZONE_NORMAL      |       ZONE_HIGHMEM       |
+----------+-----------------------+--------------------------+

如果构建的内核禁用了 ZONE_DMA 并启用了 ZONE_DMA32,并在具有 16 GB RAM 的 arm64 机器上以 movablecore=80% 参数启动,则在两个节点之间平均分配,则节点 0 上将有 ZONE_DMA32ZONE_NORMALZONE_MOVABLE,节点 1 上将有 ZONE_NORMALZONE_MOVABLE

1G                                9G                         17G
+--------------------------------+ +--------------------------+
|              node 0            | |          node 1          |
+--------------------------------+ +--------------------------+

1G       4G        4200M          9G          9320M          17G
+---------+----------+-----------+ +------------+-------------+
|  DMA32  |  NORMAL  |  MOVABLE  | |   NORMAL   |   MOVABLE   |
+---------+----------+-----------+ +------------+-------------+

内存库可能属于交错的节点。在下面的示例中,x86 机器在 4 个内存库中有 16 GB RAM,偶数内存库属于节点 0,奇数内存库属于节点 1

0              4G              8G             12G            16G
+-------------+ +-------------+ +-------------+ +-------------+
|    node 0   | |    node 1   | |    node 0   | |    node 1   |
+-------------+ +-------------+ +-------------+ +-------------+

0   16M      4G
+-----+-------+ +-------------+ +-------------+ +-------------+
| DMA | DMA32 | |    NORMAL   | |    NORMAL   | |    NORMAL   |
+-----+-------+ +-------------+ +-------------+ +-------------+

在这种情况下,节点 0 将从 0 到 12 GB,节点 1 将从 4 到 16 GB。

节点

正如我们已经提到的,内存中的每个节点都由 pg_data_t 描述,它是 struct pglist_data 的 typedef。在分配页面时,默认情况下,Linux 使用节点本地分配策略从最接近运行 CPU 的节点分配内存。由于进程倾向于在同一 CPU 上运行,因此很可能会使用来自当前节点的内存。用户可以控制分配策略,如 NUMA 内存策略 中所述。

大多数 NUMA 架构都维护一个指向节点结构的指针数组。实际的结构在启动早期分配,此时架构特定的代码会解析固件报告的物理内存映射。节点初始化的大部分发生在启动过程稍后的 free_area_init() 函数中,稍后将在 初始化 部分中描述。

除了节点结构之外,内核还维护一个名为 node_statesnodemask_t 位掩码数组。此数组中的每个位掩码都表示一组具有特定属性的节点,如 enum node_states 定义的那样

N_POSSIBLE

该节点在某个时候可能会变为在线。

N_ONLINE

该节点在线。

N_NORMAL_MEMORY

该节点具有常规内存。

N_HIGH_MEMORY

该节点具有常规内存或高位内存。当 CONFIG_HIGHMEM 禁用时,别名为 N_NORMAL_MEMORY

N_MEMORY

该节点具有内存(常规内存、高位内存、可移动内存)

N_CPU

该节点具有一个或多个 CPU

对于具有上述属性的每个节点,将在 node_states[<property>] 位掩码中设置与节点 ID 对应的位。

例如,对于具有普通内存和 CPU 的节点 2,将在

node_states[N_POSSIBLE]
node_states[N_ONLINE]
node_states[N_NORMAL_MEMORY]
node_states[N_HIGH_MEMORY]
node_states[N_MEMORY]
node_states[N_CPU]

有关 nodemask 的各种操作,请参阅 include/linux/nodemask.h

除此之外,nodemask 还用于为节点遍历提供宏,即 for_each_node()for_each_online_node()

例如,要为每个在线节点调用函数 foo()

for_each_online_node(nid) {
        pg_data_t *pgdat = NODE_DATA(nid);

        foo(pgdat);
}

节点结构

节点结构 struct pglist_datainclude/linux/mmzone.h 中声明。在此,我们简要介绍此结构的字段

常规

node_zones

此节点的区域。并非所有区域都可能被填充,但它是完整列表。它由此节点的 node_zonelists 以及其他节点的 node_zonelists 引用。

node_zonelists

所有节点中所有区域的列表。此列表定义了首选分配区域的顺序。node_zonelistsmm/page_alloc.c 中的 build_zonelists() 在核心内存管理结构初始化期间设置。

nr_zones

此节点中已填充的区域数量。

node_mem_map

对于使用 FLATMEM 内存模型的 UMA 系统,节点 0 的 node_mem_map 是一个 struct page 数组,表示每个物理帧。

node_page_ext

对于使用 FLATMEM 内存模型的 UMA 系统,节点 0 的 node_page_ext 是 struct page 扩展的数组。仅在启用了 CONFIG_PAGE_EXTENSION 构建的内核中可用。

node_start_pfn

此节点中起始页帧的页帧号。

node_present_pages

此节点中存在的物理页总数。

node_spanned_pages

物理页范围的总大小,包括空洞。

node_size_lock

一个保护定义节点范围的字段的锁。仅当启用了 CONFIG_MEMORY_HOTPLUGCONFIG_DEFERRED_STRUCT_PAGE_INIT 配置选项中的至少一个时才定义。pgdat_resize_lock()pgdat_resize_unlock() 用于操作 node_size_lock 而无需检查 CONFIG_MEMORY_HOTPLUGCONFIG_DEFERRED_STRUCT_PAGE_INIT

node_id

节点的节点 ID (NID),从 0 开始。

totalreserve_pages

这是每个节点的保留页数,用户空间分配不可用。

first_deferred_pfn

如果大型机器上的内存初始化被延迟,那么这是需要初始化的第一个 PFN。仅当启用 CONFIG_DEFERRED_STRUCT_PAGE_INIT 时定义

deferred_split_queue

每个节点的大页面队列,其拆分被延迟。仅当启用 CONFIG_TRANSPARENT_HUGEPAGE 时定义。

__lruvec

每个节点的 lruvec 保存 LRU 列表和相关参数。仅当禁用内存 cgroup 时使用。不应直接访问它,而是使用 mem_cgroup_lruvec() 来查找 lruvec。

回收控制

另请参阅 页面回收

kswapd

kswapd 内核线程的每个节点实例。

kswapd_wait, pfmemalloc_wait, reclaim_wait

用于同步内存回收任务的工作队列

nr_writeback_throttled

由于等待脏页被清理而受到限制的任务数。

nr_reclaim_start

在回收受到回写限制的情况下写入的页数。

kswapd_order

控制 kswapd 尝试回收的顺序

kswapd_highest_zoneidx

kswapd 要回收的最高区域索引

kswapd_failures

kswapd 无法回收任何页面的运行次数

min_unmapped_pages

无法回收的最小未映射文件支持页面数。由 vm.min_unmapped_ratio sysctl 确定。仅当启用 CONFIG_NUMA 时定义。

min_slab_pages

无法回收的最小 SLAB 页数。由 vm.min_slab_ratio sysctl 确定。仅当启用 CONFIG_NUMA 时定义

flags

控制回收行为的标志。

压缩控制

kcompactd_max_order

kcompactd 应尝试达到的页面顺序。

kcompactd_highest_zoneidx

kcompactd 要压缩的最高区域索引。

kcompactd_wait

用于同步内存压缩任务的工作队列。

kcompactd

kcompactd 内核线程的每个节点实例。

proactive_compact_trigger

确定是否启用主动压缩。由 vm.compaction_proactiveness sysctl 控制。

统计信息

per_cpu_nodestats

节点的每个 CPU VM 统计信息

vm_stat

节点的 VM 统计信息。

区域

存根

此部分不完整。请列出并描述相应的字段。

页面

存根

此部分不完整。请列出并描述相应的字段。

页框

存根

此部分不完整。请列出并描述相应的字段。

初始化

存根

此部分不完整。请列出并描述相应的字段。