物理内存¶
Linux 可用于各种架构,因此需要一种架构无关的抽象来表示物理内存。 本章介绍用于管理运行系统中的物理内存的结构。
内存管理中流行的第一个主要概念是 非一致性内存访问 (NUMA)。 对于多核和多插槽机器,内存可以排列成存储体,这些存储体的访问成本因与处理器的“距离”而异。 例如,可能有一个分配给每个 CPU 的内存库,或者一个非常适合外围设备附近 DMA 的内存库。
每个存储体称为一个节点,该概念在 Linux 下由 struct pglist_data 表示,即使该架构是 UMA。 此结构始终由其 typedef pg_data_t 引用。 特定节点的 pg_data_t 结构可以通过 NODE_DATA(nid) 宏引用,其中 nid 是该节点的 ID。
对于 NUMA 架构,节点结构由架构特定代码在启动早期分配。 通常,这些结构在它们代表的内存库上本地分配。 对于 UMA 架构,仅使用一个静态 pg_data_t 结构,称为 contig_page_data。 将在第 节点 节中进一步讨论节点
整个物理地址空间被划分为一个或多个块,称为区域,这些区域表示内存中的范围。 这些范围通常由访问物理内存的架构约束决定。 节点内对应于特定区域的内存范围由 struct zone 描述。 每个区域都有以下描述的类型之一。
ZONE_DMA 和 ZONE_DMA32 历史上代表了适合无法访问所有可寻址内存的外围设备进行 DMA 的内存。 多年来,有更好更强大的接口来获取具有 DMA 特定要求的内存(使用通用设备的动态 DMA 映射),但 ZONE_DMA 和 ZONE_DMA32 仍然代表对如何访问它们有限制的内存范围。 根据架构,可以使用 CONFIG_ZONE_DMA 和 CONFIG_ZONE_DMA32 配置选项在构建时禁用这些区域类型中的一种,甚至两种。 某些 64 位平台可能需要这两个区域,因为它们支持具有不同 DMA 寻址限制的外围设备。
ZONE_NORMAL 用于内核始终可以访问的普通内存。 如果 DMA 设备支持传输到所有可寻址内存,则可以在此区域中的页面上执行 DMA 操作。 ZONE_NORMAL 始终启用。
ZONE_HIGHMEM 是内核页表中未被永久映射覆盖的物理内存部分。 内核只能使用临时映射访问此区域中的内存。 此区域仅在某些 32 位架构上可用,并通过 CONFIG_HIGHMEM 启用。
ZONE_MOVABLE 用于正常可访问的内存,就像 ZONE_NORMAL 一样。 区别在于 ZONE_MOVABLE 中大多数页面的内容是可移动的。 这意味着,虽然这些页面的虚拟地址不会改变,但它们的内容可能会在不同的物理页面之间移动。 通常,在内存热插拔期间会填充 ZONE_MOVABLE,但也可以使用 kernelcore、movablecore 和 movable_node 内核命令行参数在启动时填充。 有关更多详细信息,请参阅 页面迁移 和 内存热(卸)插拔。
ZONE_DEVICE 表示驻留在 PMEM 和 GPU 等设备上的内存。 它具有与 RAM 区域类型不同的特性,它的存在是为了为设备驱动程序识别的物理地址范围提供 struct page 和内存映射服务。ZONE_DEVICE 通过配置选项 CONFIG_ZONE_DEVICE 启用。
重要的是要注意,许多内核操作只能使用 ZONE_NORMAL 进行,因此它是性能最关键的区域。 区域将在第 区域 节中进一步讨论。
节点和区域范围之间的关系由固件报告的物理内存映射、内存寻址的架构约束和内核命令行中的某些参数决定。
例如,在具有 2 GB RAM 的 x86 UMA 机器上的 32 位内核上,整个内存将在节点 0 上,并且将有三个区域:ZONE_DMA、ZONE_NORMAL 和 ZONE_HIGHMEM
0 2G
+-------------------------------------------------------------+
| node 0 |
+-------------------------------------------------------------+
0 16M 896M 2G
+----------+-----------------------+--------------------------+
| ZONE_DMA | ZONE_NORMAL | ZONE_HIGHMEM |
+----------+-----------------------+--------------------------+
使用禁用了 ZONE_DMA 并且启用了 ZONE_DMA32 且使用 arm64 机器上的 movablecore=80% 参数启动的内核,其中 16 GB 的 RAM 在两个节点之间平均分配,节点 0 上将有 ZONE_DMA32、ZONE_NORMAL 和 ZONE_MOVABLE,节点 1 上将有 ZONE_NORMAL 和 ZONE_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_states 的 nodemask_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[<属性>] 位掩码中设置对应于节点 ID 的位。
例如,对于具有常规内存和 CPU 的节点 2,将在以下位置设置位 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_data 在 include/linux/mmzone.h 中声明。 在这里,我们简要介绍一下此结构的字段
常规¶
node_zones此节点的区域。 并非所有区域都可能已填充,但它是完整列表。 它由此节点的 node_zonelists 以及其他节点的 node_zonelists 引用。
node_zonelists所有节点中所有区域的列表。 此列表定义了分配首选的区域顺序。 在核心内存管理结构的初始化期间,node_zonelists 由 mm/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_HOTPLUG 或 CONFIG_DEFERRED_STRUCT_PAGE_INIT 配置选项中的至少一个时才定义。pgdat_resize_lock() 和 pgdat_resize_unlock() 用于在不检查 CONFIG_MEMORY_HOTPLUG 或 CONFIG_DEFERRED_STRUCT_PAGE_INIT 的情况下操作 node_size_lock。
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。
回收控制¶
另请参阅 页面回收。
kswapdkswapd 内核线程的每个节点实例。
kswapd_wait、pfmemalloc_wait、reclaim_wait用于同步内存回收工作的工作队列
nr_writeback_throttled由于等待脏页面清理而被限制的任务数。
nr_reclaim_start在回收受到限制等待写回时写入的页面数。
kswapd_order控制 kswapd 尝试回收的顺序
kswapd_highest_zoneidxkswapd 要回收的最高区域索引
kswapd_failureskswapd 无法回收任何页面的运行次数
min_unmapped_pages无法回收的最小未映射文件支持页面数。 由 vm.min_unmapped_ratio sysctl 确定。 仅当启用 CONFIG_NUMA 时才定义。
min_slab_pages无法回收的最小 SLAB 页面数。 由 vm.min_slab_ratio sysctl 确定。 仅当启用 CONFIG_NUMA 时才定义
flags控制回收行为的标志。
压缩控制¶
kcompactd_max_orderkcompactd 应尝试实现的页面顺序。
kcompactd_highest_zoneidxkcompactd 要压缩的最高区域索引。
kcompactd_wait用于同步内存压缩任务的工作队列。
kcompactdkcompactd 内核线程的每个节点实例。
proactive_compact_trigger确定是否启用主动压缩。 由 vm.compaction_proactiveness sysctl 控制。
统计信息¶
per_cpu_nodestats节点的每个 CPU VM 统计信息
vm_stat节点的 VM 统计信息。
区域¶
正如我们提到的,内存中的每个区域都由 struct zone 描述,它是它所属节点的 node_zones 数组的一个元素。struct zone 是页面分配器的核心数据结构。 区域表示物理内存范围,并且可能存在空洞。
页面分配器使用 GFP 标志,请参阅 内存分配控制,由内存分配指定,以确定节点中内存分配可以从中分配内存的最高区域。 页面分配器首先从该区域分配内存,如果页面分配器无法从该区域分配请求的内存量,它将从节点中的下一个较低区域分配内存,该过程将继续到最低区域(包括最低区域)。 例如,如果一个节点包含 ZONE_DMA32、ZONE_NORMAL 和 ZONE_MOVABLE,并且内存分配的最高区域是 ZONE_MOVABLE,则页面分配器从中分配内存的区域顺序为 ZONE_MOVABLE > ZONE_NORMAL > ZONE_DMA32。
在运行时,区域中的空闲页面位于每个 CPU 页面集 (PCP) 或区域的空闲区域中。 每个 CPU 页面集是内核内存管理系统中的一个重要机制。 通过在每个 CPU 上本地处理最频繁的分配和释放,每个 CPU 页面集提高了性能和可扩展性,尤其是在具有多个内核的系统上。 内核中的页面分配器采用两步内存分配策略,首先从每个 CPU 页面集开始,然后回退到伙伴分配器。 页面在每个 CPU 页面集和全局空闲区域(由伙伴分配器管理)之间批量传输。 这最大限度地减少了与全局伙伴分配器频繁交互的开销。
架构特定代码调用 free_area_init() 来初始化区域。
区域结构¶
区域结构 struct zone 在 include/linux/mmzone.h 中定义。 在这里,我们简要介绍一下此结构的字段
常规¶
_watermark此区域的水印。 当区域中的空闲页面数低于最小水印时,将忽略提升,分配可能会触发直接回收和直接压缩,它也用于限制直接回收。 当区域中的空闲页面数低于低水印时,kswapd 会被唤醒。 当区域中的空闲页面数高于高水印时,当未设置 sysctl_numa_balancing_mode 的 NUMA_BALANCING_MEMORY_TIERING 位时,kswapd 会停止回收(区域已平衡)。 提升水印用于内存分层和 NUMA 平衡。 当区域中的空闲页面数高于提升水印时,当设置了 sysctl_numa_balancing_mode 的 NUMA_BALANCING_MEMORY_TIERING 位时,kswapd 会停止回收。 水印由 __setup_per_zone_wmarks() 设置。 最小水印根据 vm.min_free_kbytes sysctl 计算。 其他三个水印根据两个水印之间的距离设置。 距离本身是考虑到 vm.watermark_scale_factor sysctl 计算的。
watermark_boost用于提升水印以增加回收压力以减少未来回退的可能性并立即唤醒 kswapd 的页面数,因为节点可能总体上已平衡,并且 kswapd 不会自然唤醒。
nr_reserved_highatomic为高阶原子分配保留的页面数。
nr_free_highatomic保留的 highatomic 页面块中的空闲页面数
lowmem_reserve为内存分配在此区域中保留的内存量数组。 例如,如果内存分配可以从中分配内存的最高区域是 ZONE_MOVABLE,则当尝试从此区域分配内存时,为此分配保留在此区域中的内存量为 lowmem_reserve[ZONE_MOVABLE]。 这是页面分配器用于防止可以使用 highmem 的分配使用太多 lowmem 的一种机制。 对于 highmem 机器上的一些专门工作负载,内核允许从 lowmem 区域分配进程内存是危险的。 这是因为该内存随后可以通过 mlock() 系统调用或通过交换空间不可用来固定。vm.lowmem_reserve_ratio sysctl 确定内核在防御这些较低区域方面的积极程度。 如果 vm.lowmem_reserve_ratio sysctl 更改,此数组会在运行时由 setup_per_zone_lowmem_reserve() 重新计算。
node此区域所属节点的索引。 仅当启用 CONFIG_NUMA 时才可用,因为 UMA 系统中只有一个区域。
zone_pgdat指向此区域所属节点的 struct pglist_data 的指针。
per_cpu_pageset指向由 setup_zone_pageset() 分配和初始化的每个 CPU 页面集 (PCP) 的指针。 通过在每个 CPU 上本地处理最频繁的分配和释放,PCP 提高了具有多个内核的系统的性能和可扩展性。
pageset_high_min复制到每个 CPU 页面集的 high_min 以便更快地访问。
pageset_high_max复制到每个 CPU 页面集的 high_max 以便更快地访问。
pageset_batch复制到每个 CPU 页面集的 batch 以便更快地访问。 每个 CPU 页面集的 batch、high_min 和 high_max 用于计算每个 CPU 页面集在一次锁定保持下从伙伴分配器获取的元素数量以提高效率。 它们还用于确定每个 CPU 页面集是否在页面释放过程中将页面返回到伙伴分配器。
pageblock_flags指向zone中页面块标志的指针(标志列表请参考 include/linux/pageblock-flags.h)。内存分配在 setup_usemap() 中。每个页面块占用 NR_PAGEBLOCK_BITS 位。仅当启用 CONFIG_FLATMEM 时定义。当启用 CONFIG_SPARSEMEM 时,标志存储在 mem_section 中。
zone_start_pfnzone的起始pfn。它由 calculate_node_totalpages() 初始化。
managed_pages伙伴系统管理的实际页面数量,计算公式为:managed_pages = present_pages - reserved_pages,其中 reserved_pages 包括由 memblock 分配器分配的页面。页面分配器和 vm 扫描器应使用它来计算各种水位线和阈值。使用 atomic_long_xxx() 函数访问它。它在 free_area_init_core() 中初始化,然后在 memblock 分配器将页面释放到伙伴系统时重新初始化。
spanned_pageszone跨越的总页数,包括空洞,计算公式为:spanned_pages = zone_end_pfn - zone_start_pfn。 它由 calculate_node_totalpages() 初始化。
present_pageszone中存在的物理页面数量,计算公式为:present_pages = spanned_pages - absent_pages(空洞中的页面)。内存热插拔或内存电源管理逻辑可以使用它通过检查(present_pages - managed_pages)来找出未管理的页面。运行时对 present_pages 的写入访问应受到 mem_hotplug_begin/done() 的保护。任何不能容忍 present_pages 漂移的读取器都应使用 get_online_mems() 来获取稳定值。它由 calculate_node_totalpages() 初始化。
present_early_pageszone中存在的、位于早期启动时可用的内存上的页面数量,不包括热插拔的内存。仅当启用 CONFIG_MEMORY_HOTPLUG 时定义,并由 calculate_node_totalpages() 初始化。
cma_pages为 CMA 使用而保留的页面。当这些页面不用于 CMA 时,它们的行为类似于 ZONE_MOVABLE。仅当启用 CONFIG_CMA 时定义。
namezone的名称。它是指向 zone_names 数组中相应元素的指针。
nr_isolate_pageblock隔离的页面块的数量。 它用于解决由于竞争性地检索页面块的 migratetype 而导致的不正确的空闲页面计数问题。 受 zone->lock 保护。仅当启用 CONFIG_MEMORY_ISOLATION 时定义。
span_seqlock用于保护 zone_start_pfn 和 spanned_pages 的 seqlock。 它是一个 seqlock,因为它必须在 zone->lock 之外读取,并且在主分配器路径中完成。 但是,seqlock 的写入频率很低。 仅当启用 CONFIG_MEMORY_HOTPLUG 时定义。
initialized指示 zone 是否已初始化的标志。 由启动期间的 init_currently_empty_zone() 设置。
free_area空闲区域的数组,其中每个元素对应于特定的 order,即 2 的幂。伙伴分配器使用此结构来有效地管理空闲内存。 分配时,它尝试找到最小的足够块,如果最小的足够块大于请求的大小,它将递归地拆分为下一个较小的块,直到达到所需的大小。 释放页面后,它可以与其伙伴合并以形成更大的块。 它由 zone_init_free_lists() 初始化。
unaccepted_pages要接受的页面列表。列表上的所有页面都是 MAX_PAGE_ORDER。仅当启用 CONFIG_UNACCEPTED_MEMORY 时定义。
flagszone 标志。 使用最少的三个位,并由 enum zone_flags 定义。ZONE_BOOSTED_WATERMARK(bit 0):zone 最近提高了水位线。 当唤醒 kswapd 时清除。ZONE_RECLAIM_ACTIVE(bit 1):kswapd 可能正在扫描该zone。ZONE_BELOW_HIGH(bit 2):zone 低于高水位线。
lock主锁,用于保护特定于zone的页面分配器的内部数据结构,尤其是保护 free_area。
percpu_drift_mark当空闲页低于此点时,在读取空闲页数时会采取其他步骤,以避免每个CPU计数器漂移,从而允许突破水位线。 它在 refresh_zone_stat_thresholds() 中更新。
压缩控制¶
compact_cached_free_pfn下次扫描时压缩空闲扫描器应从哪里开始的PFN。
compact_cached_migrate_pfn下次扫描时压缩迁移扫描器应从哪里开始的PFN。 此数组有两个元素:第一个元素用于 MIGRATE_ASYNC 模式,另一个元素用于 MIGRATE_SYNC 模式。
compact_init_migrate_pfn初始迁移 PFN,在启动时初始化为 0,并在完整压缩完成后初始化为 zone 中具有可迁移页面的第一个页面块。 它用于检查扫描是否为整个zone扫描。
compact_init_free_pfn初始空闲 PFN,在启动时初始化为 0,并初始化为zone中具有空闲 MIGRATE_MOVABLE 页面的最后一个页面块。 它用于检查它是否是扫描的开始。
compact_considered自上次失败以来尝试的压缩次数。 当压缩未能导致页面分配成功时,会在 defer_compaction() 中重置。 当应跳过压缩时,会在 compaction_deferred() 中增加 1。在调用 compact_zone() 之前调用 compaction_deferred(),当 compact_zone() 返回 COMPACT_SUCCESS 时调用 compaction_defer_reset(),当 compact_zone() 返回 COMPACT_PARTIAL_SKIPPED 或 COMPACT_COMPLETE 时调用 defer_compaction()。
compact_defer_shift在再次尝试之前跳过的压缩次数是 1< compact_order_failed最小的压缩失败 order。 当压缩成功时,在 compaction_defer_reset() 中设置,当压缩未能导致页面分配成功时,在 defer_compaction() 中设置。 compact_blockskip_flush当压缩迁移扫描器和空闲扫描器相遇时设置为 true,这意味着应清除 PB_migrate_skip 位。 contiguous当zone是连续的时设置为 true(换句话说,没有空洞)。 统计信息¶ vm_statzone的 VM 统计信息。 跟踪的项目由 enum zone_stat_item 定义。 vm_numa_eventzone的 VM NUMA 事件统计信息。 跟踪的项目由 enum numa_stat_item 定义。 per_cpu_zonestatszone的每个 CPU 的 VM 统计信息。 它记录每个 CPU 的 VM 统计信息和 VM NUMA 事件统计信息。 它减少了对zone的全局 vm_stat 和 vm_numa_event 字段的更新,以提高性能。 页面¶ 存根 本节不完整。请列出并描述相应的字段。 Folios¶ 存根 本节不完整。请列出并描述相应的字段。 初始化¶ 存根 本节不完整。请列出并描述相应的字段。