物理内存¶
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_ratiosysctl 确定。 仅当启用- 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_proactivenesssysctl 控制。
统计信息¶
- 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_kbytessysctl 计算。 其他三个水印根据两个水印之间的距离设置。 距离本身是考虑到- vm.watermark_scale_factorsysctl 计算的。
- 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_ratiosysctl 确定内核在防御这些较低区域方面的积极程度。 如果- vm.lowmem_reserve_ratiosysctl 更改,此数组会在运行时由- 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¶
存根
本节不完整。请列出并描述相应的字段。
初始化¶
存根
本节不完整。请列出并描述相应的字段。