物理内存¶
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_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
,并在具有 16 GB RAM 的 arm64 机器上以 movablecore=80%
参数启动,则在两个节点之间平均分配,则节点 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[<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_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()
用于操作node_size_lock
而无需检查CONFIG_MEMORY_HOTPLUG
或CONFIG_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 统计信息。
区域¶
存根
此部分不完整。请列出并描述相应的字段。
页面¶
存根
此部分不完整。请列出并描述相应的字段。
页框¶
存根
此部分不完整。请列出并描述相应的字段。
初始化¶
存根
此部分不完整。请列出并描述相应的字段。