物理内存¶
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。
回收控制¶
另请参阅 页面回收。
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 统计信息。
区域¶
正如我们提到的,内存中的每个区域都由 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_pfn
zone的起始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_pages
zone跨越的总页数,包括空洞,计算公式为:
spanned_pages
=zone_end_pfn
-zone_start_pfn
。 它由calculate_node_totalpages()
初始化。present_pages
zone中存在的物理页面数量,计算公式为:
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_pages
zone中存在的、位于早期启动时可用的内存上的页面数量,不包括热插拔的内存。仅当启用
CONFIG_MEMORY_HOTPLUG
时定义,并由calculate_node_totalpages()
初始化。cma_pages
为 CMA 使用而保留的页面。当这些页面不用于 CMA 时,它们的行为类似于
ZONE_MOVABLE
。仅当启用CONFIG_CMA
时定义。name
zone的名称。它是指向
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
时定义。flags
zone 标志。 使用最少的三个位,并由
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_defer_shift
。 在defer_compaction()
中增加 1。 当直接压缩导致页面分配成功时,在compaction_defer_reset()
中重置。 它的最大值是COMPACT_MAX_DEFER_SHIFT
。compact_order_failed
最小的压缩失败 order。 当压缩成功时,在
compaction_defer_reset()
中设置,当压缩未能导致页面分配成功时,在defer_compaction()
中设置。compact_blockskip_flush
当压缩迁移扫描器和空闲扫描器相遇时设置为 true,这意味着应清除
PB_migrate_skip
位。contiguous
当zone是连续的时设置为 true(换句话说,没有空洞)。
统计信息¶
vm_stat
zone的 VM 统计信息。 跟踪的项目由
enum zone_stat_item
定义。vm_numa_event
zone的 VM NUMA 事件统计信息。 跟踪的项目由
enum numa_stat_item
定义。per_cpu_zonestats
zone的每个 CPU 的 VM 统计信息。 它记录每个 CPU 的 VM 统计信息和 VM NUMA 事件统计信息。 它减少了对zone的全局
vm_stat
和vm_numa_event
字段的更新,以提高性能。
页面¶
存根
本节不完整。请列出并描述相应的字段。
Folios¶
存根
本节不完整。请列出并描述相应的字段。
初始化¶
存根
本节不完整。请列出并描述相应的字段。